C) Steganographie – Daten in Bildern verstecken

Zusammenfassung

Mehr ist zum Speichern einer geheimen Botschaft in einem Bild nicht notwendig. Das Auslesen funktioniert selbstverständlich genau in die andere Richtung. Alle Bits der Farbkanäle der Pixel des Bildes werden zu kompletten Bytes aneinander gereiht. Diese extrahierten Bytes bilden die ursprüngliche Nachricht. Da diese Vorgehensweise nun sicherlich auch für Sie kein Hexenwerk mehr ist, verzichte ich an dieser Stelle auf eine detaillierte Beschreibung, sondern beschränke mich auf den zusammengefassten Code mitsamt Kommentaren.

package de.jbb.steganographie;

import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;

import javax.imageio.ImageIO;

public class HideInPicture {

  public BufferedImage hideMessage(BufferedImage img, String message) {

    // Zeichen für Message-Ende hinzufügen
    message += (char)0;
    // Text in Bytes umwandeln
    byte[] b = message.getBytes();
    // Farbkanal
    Color channel = Color.RED;
    // Alle Bytes durchlaufen
    for (int i = 0, x = 0, y = 0; i < b.length; i++) {
      // Alle Bits durchlaufen
      for (int j = 7; j > -1; j--) {

        // Wert des Bits auslesen
        int bit = ((b[i] & 0xFF) >> j) & 1;
        // Farbe an der aktuellen Position auslesen
        int rgb = img.getRGB(x, y);
        // Den aktuellen Farbkanal auslesen
        int color = (rgb >> channel.getShift()) & 0xFF;
        
        // Farbkanal manipulieren
        if ((color & 1) != bit) {
          // Den ausgelesenen Farbkanal der Farbe auf 0 setzen
          rgb &= channel.getRGBManipulator();
          switch (bit) {
            case 1:
              color = color + 1;
              break;
            default:
              color = color - 1;
          }
          // Farbkanal zurückschreiben
          rgb |= color << channel.getShift();
          img.setRGB(x, y, rgb);
        }

        // nächsten Farbkanal setzen
        channel = channel.getNext();
        // Falls Farbkanal = RED => X-Position verändern
        if (channel.equals(Color.RED)) {
          x++;
          // Falls x größer als Breite des Bildes => Y-Positon verändern
          if (x >= img.getWidth()) {
            x = 0;
            y++;
            // Falls y größer als Höhe des Bildes => Fehler
            if (y >= img.getHeight()) {
              return null;
            }
          }
        }
      }
    }
    return img;
  }

  public String extractMessage(BufferedImage img) {

    // ArrayList für gelesene Bytes
    ArrayList<Byte> bytes = new ArrayList<Byte>();
    // Alle horizontalen Pixel durchlaufen
    for (int y = 0, count = 7, value = 0; y < img.getHeight(); y++) {
      // Alle vertikalen Pixel durchlaufen
      for (int x = 0; x < img.getWidth(); x++) {
        // Aktuelle Farbe auslesen
        int rgb = img.getRGB(x, y);
        // Alle Farbkanäle durchlaufen
        for (Color c : Color.values()) {
          // Aktuelles Byte befüllen
          value |= (((rgb >> c.getShift()) & 0xFF) & 1) << count--;
          // Aktuelles Byte ist voll
          if (count == -1) {
            // Byte == 0 (Nachrichtende)
            if ((byte)value == 0) {
              // Nachricht in String umwandeln und zurückgeben
              return bytesToString(bytes.toArray(new Byte[0]));
            }
            // Byte-ArrayList das aktuelle Byte hinzufügen
            bytes.add((byte)value);
            // Zählvariablen zurücksetzen
            value = 0;
            count = 7;
          }
        }
      }
    }
    return bytesToString(bytes.toArray(new Byte[0]));
  }

  private String bytesToString(Byte[] barr) {

    byte[] barrPrim = new byte[barr.length];
    for (int i = 0; i < barr.length; i++) {
      barrPrim[i] = barr[i];
    }
    return new String(barrPrim);
  }
}

Damit Sie alles beisammen haben, finden Sie hier auch noch einmal den Code unserer Color-Enumeration.

package de.jbb.steganographie;

public enum Color {

  RED(16, ( 255 << 24 ) | ( 255 << 8 ) | ( 255 << 0 )), 
  GREEN(8, ( 255 << 24 ) | ( 255 << 16 ) | ( 255 << 0 )), 
  BLUE(0, ( 255 << 24 ) | ( 255 << 16 ) | ( 255 << 8 ));

  private int shift = 0;
  private int rgbManipulator;

  private Color(int shift, int rgbManipulator) {
    this.shift = shift;
    this.rgbManipulator = rgbManipulator;
  }

  public Color getNext() {

    if (this == RED) {
      return GREEN;
    }
    else if (this == GREEN) {
      return BLUE;
    }
    else {
      return RED;
    }
  }

  public int getShift() {
    return this.shift;
  }

  public int getRGBManipulator() {
    return this.rgbManipulator;
  }
}

Die Klasse könnten Sie bspw. so testen (Vorausgesetzt, dass auf „C:\“ ein Bild mit dem Namen „test.png“ liegt):

public static void main(String[] args) throws Exception {
  HideInPicture hip = new HideInPicture();
  BufferedImage img = hip.hideMessage(ImageIO.read(new File("C:/test.PNG")), "Abc23*ß");
  ImageIO.write(img, "png", new File("C:/test2.png"));
  System.out.println(hip.extractMessage(ImageIO.read(new File("C:/test2.PNG"))));
}

Sie können eine mögliche Implementierung von diesem Algorithmus auf SoftK.de als Opensource kostenfrei herunterladen.

Previous Article
Next Article

12 Replies to “C) Steganographie – Daten in Bildern verstecken”

  1. marcel

    Wieso gibt uns die zugefügte 0 das Ende der Nachricht an?
    Theoretisch hat doch die RGB Farbe(0,0,0) also Schwarz, auch die Binärdarstellung 0000000 also als Byte 0.

  2. Stefan Kiesel

    Hallo Marcel,

    drei Punkte:

    1.) In Java hat ein Farbkanal eines RGB-Wertes typischerweise einen Wert zwischen 0 und 255, also einen von 256 verschiedene Werte insgesamt. Da 256 = 2^8 ist, werden 8 Bit zur Darstellung eines einzelnen Farbkanals benötigt. Bei der RGB-Farbe (0,0,0) benötigt also jeder dieser drei Farbkanäle 8 Bit (da wir hier meistens von einem Integer mit 32 Bit = 4*8 Bit ausgehen gibt es idR noch einen zusätzlichen Alpha-Kanal). Es würde sich also um die Binärdarstellung (bei Annahme eines Alpha-Kanals: 00000000)000000000000000000000000 handeln.

    2.) Ein Zeichen wird nicht in einer einzelnen Farbe (die ja nur 3, bzw. max. 4 Farbkanäle hat) versteckt, sondern in mehreren Farben durch Veränderung des jeweils letzten Bits der Farbkanäle der Farben.

    3.) Die Zeichen werden sequenziell in das Bild geschrieben. Wir gehen davon aus, dass (char)0 in keiner gewöhnlichen Zeichenkette vorkommen wird (was durchaus realistisch ist). Deshalb gibt es keine 00000000-Kette im Bild (alle anderen Zeichen außer (char)0 haben ja eine andere Binärrepräsentation) bevor nicht das Ende der Zeichen durch unser angehängtes (char)0 erreicht ist/signalisiert wurde – unabhängig davon, welche Farben es davor oder danach im Bild gibt bzw. gab.

    Falls noch Fragen bestehen, einfach kommentieren.

    Grüße
    Stefan

  3. Eike Sebastian Böker

    Hallo,

    erst einmal vorab ein großes Lob an deine Arbeit!

    Nun zu meiner Frage:

    Könntest du beschreiben noch etwas genauer auf die Stelle eingehen an dem quasi der Kernalgorithmus abläuft, also die Verschiebung der Bytes und die Manipulation des letzten Bits. Mir ist nicht so ganz klar weswegen die einzelnen ungebrauchten Kanäle mit 1 & verknüpft werden und wie die if-Abfrage funktioniert in der „color“ mit „bit“ verglichen wird.

    Vielen Dank vorab.

    Gruß,
    Eike

  4. Stefan Kiesel

    Hallo Eike,

    wenn ich dich richtig verstanden habe, dann meinst du die Zeilen 25 und 32, oder?

    In der Zeile 25 wird das aktuell zu versteckende Bit ausgelesen. Hierzu wird das aktuelle Byte so weit nach rechts verschoben, bis das aktuell zu bearbeitende Bit an erster Stelle steht. Anschließend wird es mit & 1 verknüpft, damit die anderen Bits, die sich weiter links vom aktuell zu bearbeitenden Bit befinden, auf 0 gesetzt werden. Ein Beispiel:

    10010101 soll versteckt werden, wir befinden uns aktuell an der 2. Stelle, also die 1. 0 von rechts. Das Byte wird zuerst um den Faktor 1 nach rechts verschoben, so dass wir folgendes Byte haben: 1001010. Nun wird dieses Byte mit 1 & verknüpft. Die & Verknüpfung bewirkt, dass alle Bits bis auf Bit ganz rechts gelöscht werden. Das Ergebnis der Verknüpfung ist also 1001010 & 1 = 0 & 1 = 0. Nachdem die restlichen Operationen durchgelaufen sind, befinden wir uns nun an der 3. Stelle, also die 2. 1 von rechts des ursprünglichen Bytes. Das ursprüngliche Byte wird um den Faktor 2 nach rechts verschoben und wir erhalten 100101 als Ergebnis. Dieses Ergebnis wird wiederum mit 1 & verknüpft und wir erhalten wieder das letzte Bit als Ergebnis: 100101 & 1 = 1 & 1 = 1.

    Zeile 32 funktioniert ähnlich wie Zeile 25. Dort wird auf ähnliche Weise einfach überprüft, ob das zu manipulierende Bit im aktuellen Farbkanal sich vom in Zeile 25 ermittelten zu setzenden Bit unterscheidet. Falls beide Bits identisch sind muss gar nichts zur Kodierung der Nachricht getan werden. Sind die Bits jedoch verschieden muss das letzte Bit des Farbkanals invertiert werden.

    Ich hoffe das hat dir weitergeholfen!?

    Grüße
    Stefan

  5. Eike Sebastian Boeke

    Hallo Stefan,

    Dann bin ich ja beruhigt und ich habe alles richtig verstanden.
    Eine kleine Anmerkung haette ich aber noch. Und zwar veraendert sich bei der 1. Anwendung auf ein .png file die Dateigroesse, sie wird kleiner. Wenn nun ein neuerlicher Text in dem schon „bearbeiteten“ Bild versteckt wird veraendert sich die Groesse allerdings nicht. Woran kann das liegen?!

    An dieser Stelle nochmal ein riesen Lob fuer deine Seite und vor allem fuer diese Einsatzbereitschaft und schnelle, sowie kompetente Antwort! Das ist nicht selbstverstaendlich.

    Vielen Dank und mit freundlichem Gruss,
    Eike

  6. Stefan Kiesel

    Hallo Eike,

    das ist zufällig und liegt an der Codierung sowie der Farbgebung des Bildes. Ein PNG Bild bspw. wird beim Speichern komprimiert. Je nach Aufbau und Farben des Bildes kann es unterschiedlich gut komprimiert werden. Es kann deshalb sein, dass das Verstecken eines Wortes im PNG Bild eine höhere Komprimierung erlaubt. Es kann jedoch auch umgekehrt sein (und das sollte meistens der Fall sein): Das versteckte Wort verschlechtert die mögliche Komprimierung und die Datei wird größer. Es kann aber auch sein, dass das PNG vor der Bearbeitung mit einem schlechten Algorithmus komprimiert wurde. Java verwendet einen besseren Algorithmus und erzeugt so generell eine bessere Komprimierung.

    Verwendest du anstatt eines PNGs bspw. ein BMP, wirst du überhaupt keine Veränderung an der Dateigröße feststellen, da ein BMP immer unkomprimiert gespeichert wird und bei identischer Auflösung, DPI Zahl und Farbtiefe seine Größe nicht verändert, auch wenn das Bild verändert wurde.

    Einen Einstieg in das PNG-Format sowie weiterführende Quellen findest du auf Wikipedia.

    Grüße
    Stefan

  7. Eike Sebastian Boeke

    Das erklaert so einiges 😉

    Vielen herzlichen Dank und weiter so!

    Gruss,
    Eike

  8. TB

    Eine Frage zum Verwandeln des Bytes von (möglicherweise signed) zu unsigned:

    Character haben doch immer einen „positiven“ Binärwert? Wieso muss ich mich darum kümmern das signed Byte in ein unsigned Byte zu verwandeln wenn das Byte (wenn man es bitweise ausliest) eh immer positiv ist? Stehe etwas auf dem Schlauch 🙁

  9. The_S

    Hallo TB,

    in Java hat ein byte einen Wert zwischen -128 und 127. Wenn du bspw. die Zahl 128 in ein byte schreiben möchtest, wird wieder „hinten“ angefangen und die byte-Repräsentation dieser Zahl lautet -128. Deshalb kann das byte negativ sein.

    Grüße
    Stefan

  10. TB

    Ja sie könnten theoretisch, aber SIND sie es denn in irgendeinem Fall in dem man ein Char in ein Byte umwandelt? Ich habe bis jetzt kein Char gefunden bei dem das der Fall ist. (http://www.pcguide.com/res/tablesASCII-c.html)

    Hier ein wenig Testcode von mir – vielleicht habe ich auch einfach irgendwas nicht verstanden aber ich sehe nicht wo der Fehler ist:

    public class BitTestMain {
    
    	/**
    	 * @param args
    	 */
    	public static void main(String[] args) {
    
    		final String text = "abc;.+*&(§&AZ=@#";
    		
    		final byte[] textInByte = text.getBytes();
    		
    		for(int pos = 0; pos  -1; shift--){
    			
    				System.out.print((int)((currentByte >> shift) & 1));
    			}
    			
    			System.out.println();
    			
    			System.out.print("Char als Bits " + text.charAt(pos) + " unsigned: ");
    			
    			// ... nochmal ausgeben, diesmal unsigned?!
    			for(int shift = 7; shift > -1; shift--){
    						
    				System.out.print((int)(((currentByte & 0xFF) >> shift) & 1));
    			}
    			
    			System.out.println();
    		}
    	}
    }

    Ausgabe:
    Char als Bits a signed: 01100001
    Char als Bits a unsigned: 01100001
    Char als Bits b signed: 01100010
    Char als Bits b unsigned: 01100010
    Char als Bits c signed: 01100011
    Char als Bits c unsigned: 01100011
    Char als Bits ; signed: 00111011
    Char als Bits ; unsigned: 00111011
    Char als Bits . signed: 00101110
    Char als Bits . unsigned: 00101110
    Char als Bits + signed: 00101011
    Char als Bits + unsigned: 00101011
    Char als Bits * signed: 00101010
    Char als Bits * unsigned: 00101010
    Char als Bits & signed: 00100110
    Char als Bits & unsigned: 00100110
    Char als Bits ( signed: 00101000
    Char als Bits ( unsigned: 00101000
    Char als Bits § signed: 10100111
    Char als Bits § unsigned: 10100111
    Char als Bits & signed: 00100110
    Char als Bits & unsigned: 00100110
    Char als Bits A signed: 01000001
    Char als Bits A unsigned: 01000001
    Char als Bits Z signed: 01011010
    Char als Bits Z unsigned: 01011010
    Char als Bits = signed: 00111101
    Char als Bits = unsigned: 00111101
    Char als Bits @ signed: 01000000
    Char als Bits @ unsigned: 01000000
    Char als Bits # signed: 00100011
    Char als Bits # unsigned: 00100011

  11. The_S

    Um zumindest annähernd fehlerfreie Software zu produzieren, sollte man auch die theoretisch möglichen Fälle abfangen – zumal das hier kein großer Aufwand ist und der Lesbarkeit nicht schadet. Davon abgesehen hat bspw. ein ‚Ä‘ einen Wert von 196 und somit -60 als byte.

    Ein kleiner Tipp: Die char-Codes beim Casten in eine Zahl entsprechen nur bis zur Stelle 127 dem ASCII-Code, anschließend gibt es Abweichungen.

    Grüße
    Stefan

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.