C) Steganographie – Daten in Bildern verstecken

Sie alle kennen vermutlich den Begriff Kryptographie. Bei der Kryptographie wird versucht, einen Text (oder binäre Daten) durch Verschlüsselung unleserlich zu machen. Dies könnte bspw. auf simpelste Art durch die Cäsar-Chiffre realisiert werden. Einen anderen Ansatz verfolgt die Steganographie. Bei der Steganographie werden die Daten nicht durch Verschlüsselung vor unbefugten Augen geschützt, sondern die geheimen Botschaften werden so getarnt bzw. versteckt, dass Dritte gar nicht wissen, dass es sich hierbei um Daten handelt. Dieses Kapitel beschäftigt sich mit dem Verstecken von Daten in Bildern.

Allgemeine Funktionsweise

Viele Leser, die bereits ein wenig Know-How besitzen, werden vermuten, dass die geheimen Daten einfach als Kommentar oder ähnliches im Bild versteckt werden würden. Natürlich bietet eine solche Methode keinen ausreichenden Schutz. Zum Einen fällt auf, wenn ein Bild plötzlich viel größer ist als vorher, und zum Anderen muss ein entsprechender Hacker nur die Bilddatei mit einem Texteditor öffnen – schon sieht er die Botschaft. Deshalb verfolge ich in diesem Kapitel einen anderen Ansatz!

Ein Bild hat Farbkanäle, in den meisten Fällen Rot, Grün und Blau (RGB). Jeder einzelne Pixel bekommt einen Rot-, Grün- und einen Blauwert zwischen 0 und 255 zugewiesen, was 16.777.216 verschiedene Farben pro Pixel ermöglicht. Natürlich besteht ein Bild aus sehr vielen Pixeln (Bildpunkte) – heutzutage sogar meistens aus Millionen und Milliarden unterschiedlichen Punkten. Falls Sie nun den Wert jedes Farbkanals eines Pixels um den Faktor eins erhöhen oder verringern, ändert sich die gesamte Farbe dieses einen Bildpunkts um 1/256 – also um 0,39%. Diese Änderung ist für das menschliche Auge nicht nachvollziehbar und folglich unsichtbar. Sie sehen, worauf ich hinaus möchte? Richtig! Die Nachricht wird im Bild versteckt, indem die Farbkanäle (das jeweils letzte Bit) manipuliert werden.

Detaillierte Funktionsweise

Sehen wir uns dieses Verfahren an einem Beispiel näher an. Angenommen, Sie möchten ein großes ‚A‘ in einem Bild verstecken. Nicht viel, aber zum Besseren Verständnis bestens geeignet. Sie wissen bereits aus dem Kapitel 02.11 ASCII, Unicode und Character, dass das ‚A‘ einer Zahl, dem ASCII-Wert, zugeordnet werden kann. Dies ist die 65. Wir benötigen diese Zahl jedoch in binärer Schreibweise: 01000001.

Falls Sie nicht (mehr) genau wissen, was es mit der binären Schreibweise auf sich hat, lesen Sie bitte kurz auf Wikipedia nach.

Auch wissen Sie, dass jedem Farbkanal ein Wert zwischen 0 und 255 zugewiesen werden kann, was genau einem Byte bzw. acht Bit entspricht. Der erste Pixel eines Bildes könnte z. B. einen Rotwert von 189 (10111101), einen Grünwert von 0 (00000000) und einen Blauwert von 255 (11111111) aufweisen. Der zweite Pixel hat bspw. einen Rotwert von 180 (10110100), einen Grünwert von 32 (00100000) und einen Blauwert von 241 (11110001). Der dritte Bildpunkt hat einen Rotwert von 173 (10101101), einen Grünwert von 34 (00100010) und einen Blauwert von 245 (11110101). Usw. usf..

Wenn wir dieses ‚A‘ (oder jedes andere Zeichen) nun in einem Bild verstecken möchten, überschreiben wir das jeweils letzte Bit in jedem Farbkanal eines jeden Pixels mit dem nächsten Bit im zu versteckenden Zeichen.

Konkrete, manuelle Steganographie

Das 1. Bit unseres ‚A‘ (01000001) ist eine 0. Diese 0 schreiben wir nun an die letzte Stelle des roten Farbkanals des ersten Pixel unseres Bildes (10111101):

Aus 10111101 (189) wird 10111100 (188)

Das 2. Bit unseres ‚A‘ ist eine 1. Diese 1 schreiben wir an die letzte Stelle des grünen Farbkanals des ersten Pixel unseres Bildes (00000000):

Aus 00000000 (0) wird 00000001 (1)

Das 3. Bit unseres Zeichens ist wieder eine 0. Diese schreiben wir abermals an die letzte Stelle des nächsten Farbkanals (blau) des ersten Pixel unseres Bildes (11111111):

Aus 11111111 (255) wird 11111110 (254)

Das 4. Bit von ‚A‘ ist abermals eine 0. Nachdem der erste Pixel keinen Farbkanal mehr hat, gehen wir zum zweiten Pixel unseres Bildes zum ersten (roten) Farbkanal (10110100) und manipulieren diesen. Da dort bereits die letzte Stelle eine 0 ist, muss der Farbkanal nicht weiter bearbeitet werden.

Aus 10110100 (180) wird 10110100 (180)

Dieses Verfahren wenden Sie auch für die letzten vier Bit des Zeichen ‚A‘ an. Ihre binären Farbkanäle sehen anschließend so aus:

Aus 00100000 (32) wird 00100000 (32)
Aus 11110001 (241) wird 11110000 (240)
Aus 10101101 (173) wird 10101100 (172)
Aus 00100010 (34) wird 00100011 (35)

Der letzte Farbkanal des dritten Pixels wird nicht mehr berücksichtigt, da bereits alle Bit unseres Zeichens versteckt wurden. Betrachten Sie nun noch einmal die neuen, manipulierten Farbkanäle nacheinander:

1. Pixel
10111100
00000001
11111110
2. Pixel
10110100
00100000
11110000
3. Pixel
10101100
00100011

Wenn Sie sich jetzt die markierten, letzten Bits eines jeden Farbkanals untereinander ansehen, erkennen Sie die Kombination 01000001, was unserem ‚A‘ entspricht. Das ‚A‘ wurde also erfolgreich im Bild versteckt und wieder ausgelesen. Dabei hat sich die Farbe des 1. Pixels um 0,39% (3/768), die Farbe des 2. Pixels um 0,13% (1/768) und die Farbe des 3. Pixels um 0,26% (2/768) geändert. Insgesamt hat sich die Farbgebung der ersten drei Pixel um 0,26% (6/2304 bzw. 2/768) verändert. Dies ist für das menschliche Auge nicht erkennbar und dennoch konnte eine (wenn auch noch sehr kurze) Nachricht in diesem scheinbar unverändertem Bild versteckt werden.

Ein Pixel wird also immer um maximal 0,39%, minimal um 0% (alle letzten Bit der Farbkanäle stimmen mit dem Soll-Bit überein), und im Durchschnitt um 0,2% verändert. Die Manipulation ist – wie bereits erwähnt – für das menschliche Auge nicht sichtbar.

Ich hoffe, Ihnen ist das Prinzip dieser Vorgehensweise nun klar!?

Farbechtheit

Bevor wir diesen Algorithmus nun in Java implementieren, muss Ihnen klar sein, dass die Bilder, die manipuliert werden sollen, farbecht sein müssen! Das Bild muss also verlustfrei gespeichert werden. Hierzu eignen sich vor allem die Formate png (komprimiert, aber verlustfrei) und bmp (unkomprimiert).

Implementierung in Java

Zuerst benötigen Sie eine kleine Helfer-Klasse in Form einer Enumeration. In dieser Enumeration werden die Farbkanäle gespeichert. Dies ist notwendig, da Sie bei der Manipulation Ihres Bildes mit einer Zahl arbeiten werden, die den kompletten RGB-Wert des aktuellen Pixels repräsentiert. Jedes Element kennt den nächsten Farbkanal (getNext), eine Zahl (shift), um wie viel Stellen ein RGB-Wert verschoben werden muss, um die Farbe des Farbcodes zu erhalten (getShift) und eine Maske (rgbManipulator), die alle Farb-Bit des aktuellen Farbkanals eines bestehenden RGB-Werts via and-Verknüpfung auf 0 setzt (getRGBManipulator).

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;
  }
}

Kümmern wir uns jetzt um die Methode, die einen Text in einem Bild speichert.

public BufferedImage hideMessage(BufferedImage img, String message) {}

Der übermittelten Nachricht hängen wir den ASCII-Wert 0 an, damit wir später beim Auslesen die Länge der Nachricht ermitteln können. Natürlich funktioniert auch jedes andere Zeichen bzw. Zeichenkombination. Es muss nur sichergestellt sein, dass dieses End-Zeichen nirgends im zu versteckenden Text vorkommt.

message += (char)0;

Da wir eine Zeichenkette, wie wir sie momentan vorliegen haben, so nicht weiter verarbeiten können, müssen wir diese in ein byte-Array umwandeln.

byte[] b = message.getBytes();

Bevor es jetzt an den eigentlichen Algorithmus geht, wird noch der erste Farbkanal in unserer Helfer-Klasse ausgewählt.

Color channel = Color.RED;

Da wir jedes einzelne Bit in unserem Bild unterbringen müssen, benötigen wir zwei verschachtelte for-Schleifen, die alle Bytes und alle Bits durchlaufen, wobei x und y den aktuellen Koordinaten im Bild entsprechen:

for (int i = 0, x = 0, y = 0; i < b.length; i++) {
  for (int j = 7; j > -1; j--) {
  }
}

In der Schleife sind nun folgende Operationen nötig:

1.) Auslesen des aktuellen Bits des aktuellen Zeichens, das durch die beiden Schleifen spezifiziert wird:

int bit = ((b[i]  & 0xFF) >> j) & 1;

Dabei wird das aktuelle Byte von signed (mit Vorzeichen zwischen -128 und +127) zu unsigned (ohne Vorzeichen von 0 bis 255) umgewandelt (b[i] & 0xFF), um j-Stellen (entsprechend der inneren for-Schleife) verschoben (>> j), und mit der 1 and-verknüpft (& 1). Sollte die Bit-Manipulation nicht mehr bei Ihnen im Gedächtnis liegen, frischen Sie Ihre Kenntnisse bitte kurz auf.

2.) Auslesen des RGB-Werts des Pixels und extrahieren des Farbwerts des aktuellen Kanals:

int rgb = img.getRGB(x, y);
int color = (rgb >> channel.getShift()) & 0xFF;

3.) Überprüfen, ob das letzte Bit verändert werden muss, falls ja, den RGB-Wert entsprechend manipulieren:

if ((color & 1) != bit) {
  rgb &= channel.getRGBManipulator();
  switch (bit) {
    case 1:
      color = color + 1;
      break;
    default:
      color = color - 1;
  }
  rgb |= color << channel.getShift();
  img.setRGB(x, y, rgb);
}

Falls das letzte Bit nicht mit dem aktuell zu setzenden Bit übereinstimmt, wird zuerst der aktuelle Farbkanal im RGB-Wert auf 0 gesetzt. Anschließend muss die aktuelle Farbe (je nach Bit) um eins inkrementiert (letztes Bit wird auf 1 gesetzt), oder dekrementiert (letztes Bit wird auf 0 gesetzt) werden. Jetzt kann der manipulierte Farbkanal in den RGB-Wert, und der RGB-Wert in das Bild, zurückgeschrieben werden.

Die eigentliche Logik ist nun implementiert. Am Ende dieser inneren Schleife muss lediglich noch alles für den nächsten Schleifendurchlauf vorbereitet werden. Das bedeutet, dass der nächste Farbkanal und Pixel im Bild ausgewählt wird, bzw. null zurückgegeben, falls noch Daten versteckt werden müssen, das Bild aber keine Pixel mehr hat. In letzterem Fall haben Sie natürlich die Möglichkeit, Ihre Daten auch über mehrere Bilder verteilt zu verstecken. Mein Code soll für Sie jedoch lediglich als Leitfaden dienen.

channel = channel.getNext();
if (channel.equals(Color.RED)) {
  x++;
  if (x >= img.getWidth()) {
    x = 0;
    y++;
    if (y >= img.getHeight()) {
      return null;
    }
  }
}

Auf der nächsten Seite finden Sie eine Zusammenfassung, und wie Sie die Werte wieder auslesen können.

Ähnliche Beiträge