Skip to content

MP3Steganography

PebblesFTW edited this page Feb 11, 2021 · 22 revisions

Steganografie in MP3-Dateien

Die Bibliothek ist in der Lage .mp1-, .mp2- und .mp3-Dateien zu verarbeiten. Da heutzutage aber fast nur .mp3-Dateien genutzt werden, werden alle Dateitypen unter dem Namen "MP3-Datei" zusammengefasst.

1. Grobe Beschreibung des Algorithmus'

Um Nachrichten in MP3-Dateien zu verstecken steht die Klasse MP3Steganography zur Verfügung.

Instanziiert man die Klasse, so wird ein AudioOverlay erstellt. Dieses sucht sich alle Datenbytes (also Bytes, die nur die Tonspur(en) enthalten) aus der MP3-Datei heraus und stellt diese zur Verfügung. Nun kann eine Nachricht in der Datei versteckt werden. Dazu wird die Nachricht in eine Bitdarstellung umgewandelt und jeweils das letzte Bit (least significant bit = LSB) eines Datenbytes durch ein Bit der Nachricht ausgetauscht, bis die Nachricht vollständig versteckt ist.

Soll eine Nachricht aus einer MP3-Datei herausgelesen werden, so muss die MP3Steganography-Klasse auf dem selben Weg (wie beim Verstecken) instanziiert werden. Das liegt daran, dass das gleiche AudioOverlay verwendet werden muss. Verwendet man eine andere Art, werden die Datenbytes in einer anderen Reihenfolge zurückgegeben, weshalb die Nachricht dann nicht mehr auffindbar ist. Ist das Overlay erfolgreich erstellt, geht der Algorithmus beim Aufruf der Such-Methode die LSB's der MP3-Datei durch und speichert sich die Bits. Anschließend werden die gelesenen Bits in Bytes umgeformt und als Array zurückgegeben.

Genauere Informationen zu dem Algorithmus werden unter 4. Limitierungen des Algorithmus' erläutert.

2. Verwendung der Klasse MP3Steganography

2.1. Konstruktor

Der Konstruktor ermöglicht es, den zu nutzende Overlay festzulegen. Dazu dient die Enumeration MP3Overlays:

  • MP3Overlays.SEQUENCE_OVERLAY:
    • Gibt die Datenbytes in der gleichen Reihenfolge zurück, wie sie auch in der MP3-Datei stehen.
    • Wird beim Verstecken oder Suchen ein Seed angegeben, wird dieser ignoriert.
  • MP3Overlays.SHUFFLE_OVERLAY:
    • Gibt die Datenbytes in einer zufälligen aber dennoch deterministischen Reihenfolge zurück.
    • Die Reihenfolge hängt davon ab, welcher Seed bei Verstecken bzw. Suchen übergeben wird.

So kann die Klasse bespielsweise instanziiert werden:

Steganography steg = new MP3Steganography(MP3Overlays.SHUFFLE_OVERLAY);

Übergibt man kein Overlay, so wird der Standardwert MP3Overlays.SHUFFLE_OVERLAY genutzt.

2.2. Methoden

  • Pro Methodenaufruf ist ein neues Objekt der Klasse MP3Steganography zu erstellen! Ist dies nicht der Fall, kann es sein, dass die Randomisierung der Datenbytes nicht korrekt funktioniert und die Nachricht dementsprechend nicht mehr gelesen werden kann.
  • Bei jeder Methode kann zusätzlich ein Seed (ein long) als zusätzlicher Parameter angegeben werden. Dieser beeinflusst, wie genau die Nachricht in die MP3-Datei geschrieben bzw. wie aus der Datei gelesen wird. Um eine Nachricht erfolgreich auszulesen muss also das gleiche Overlay und der gleiche Seed genutzt werden (siehe Beispiele). Wird den Methoden kein Seed übergeben, nutzt die Klasse einen Standardseed.

2.2.1. Verstecken

Zum Verstecken von Nachrichten wird die Methode encode() genutzt.

Steganography steg = new MP3Steganography(); // Hier wird das Standardoverlay genutzt

byte[] mp3WithMessage = steg.encode(carrier, payload /*, optionalSeed */);

Hier wird das Byte-Array, das die Nachricht enthält (payload), in dem Byte-Array, welches die MP3-Datei enthält (carrier), versteckt. Wie genau die Nachricht versteckt wird, ist zum einen abhängig vom Overlay, dass dem Konstruktor übergeben wurde und zum anderen, welcher Seed verwendet wird. Anschließend wird die MP3-Datei mit der versteckten Nachricht als Bytearray zurückgegeben.

2.2.2. Suchen

Beim Suchen von Nachrichten in MP3-Dateien wird decode() genutzt.

Steganography steg = new MP3Steganography(MP3Overlays.SEQUENCE_OVERLAY);

byte[] message = steg.decode(mp3WithMessage, /* optionalSeed */);

Die Methode nimmt ein Byte-Array entgegen, welches eine MP3-Datei mit einer versteckten Nachricht enthält und probiert die Nachricht herauszulesen. Wie genau die Nachricht gelesen wird, ist wieder vom Overlay und vom Seed abhängig. Damit die Nachricht erfolgreich gelesen werden kann, müssen das gleiche Overlay und der gleiche Seed verwendet werden.

2.2.3. Überprüfung auf versteckte Nachrichten

Weiterhin stellt sie Klasse eine Methode zum Überprüfen auf versteckte Nachrichten zur Verfügung: isSteganographicData().

Steganography steg = new MP3Steganography(MP3Overlays.SHUFFLE_OVERLAY);

boolean hasMessage = steg.isSteganographicData(mp3WithMessage /*, optionalSeed */);

Bei dieser Methode wird nicht die Nachricht zurückgegeben, sondern ein Boolean. Dieser ist auf wahr gesetzt, wenn das Byte-Array eine MP3-Datei mit versteckter Nachricht enthält (mp3WithMessage). Auch bei dieser Methode muss das entsprechende Overlay und der entsprechende Seed genutzt werden, andernfalls wird die Nachricht nicht erkannt und false zurückgegeben.

Die Methode isSteganographicData bietet im Vergleich zur Methode decode() nur ein geringes Ressourcenersparnis, da trotzdem zuerst alle Datenbytes im Byte-Array gesucht werden müssen, bevor getestet werden kann, ob eine Nachricht versteckt ist. Wenn also nur wenige MP3-Dateien überprüft werden sollen, ist es performanter, direkt die Methode decode() zu nutzen und die entsprechenden Exceptions abzufangen. Erst, wenn viele Dateien ohne Nachricht überprüft werden, lohnt es sich die Methode isSteganographicData() zu nutzen.

3. Beispielcode zur Verwendung von MP3Steganography

3.1. Nutzung des Sequence-Overlays

// Variablen
byte[] mp3Bytes = Files.readAllBytes(new File("path/to/file.mp3").toPath());
byte[] messageBytes = "SecretMessage".getBytes(StandardCharsets.UTF_8);

// Erstellung des Objekts mit Sequence-Overlay
Steganography mp3Steg = new MP3Steganography(MP3Overlays.SEQUENCE_OVERLAY);

// Verstecken der Nachricht
byte[] carrierWithPayload = mp3Steg.encode(mp3Bytes, messageBytes);

// Erstellung des gleichen Objekts
mp3Steg = new MP3Steganography(MP3Overlays.SEQUENCE_OVERLAY);

// Lesen der Nachricht
byte[] messageFromCarrier = mp3Steg.decode(carrierWithPayload);

String message = new String(messageFromCarrier, StandardCharsets.UTF_8);  // enthält die ursprüngliche Nachricht "SecretMessage"

3.2. Beispiel mit Standardoverlay und benutzerdefiniertem Seed

// Variablen
byte[] carrier = Files.readAllBytes(new File("path/to/file.mp3").toPath());
byte[] payload = "SecretMessage".getBytes(StandardCharsets.UTF_8);
long seed = 1234L;

// Erstellung des Objekts
Steganography steg = new MP3Steganography();  // im weiteren Verlauf wird das Shuffle-Overlay genutzt

// Verstecken der Nachricht mit benutzerdefiniertem Seed
byte[] carrierWithPayload = steg.encode(carrier, payload, seed);

// Erstellung des gleichen Objekts
steg = new MP3Steganography();

// Lesen der Nachricht mit dem gleichen Seed
byte[] messageFromCarrier = steg.decode(carrierWithPayload, seed);

String message = new String(messageFromCarrier, StandardCharsets.UTF_8);  // enthält die ursprüngliche Nachricht "SecretMessage"

3.3. Beispiel mit Speicherung und Prüfung auf enthaltene Nachricht

Sender versteckt eine Nachricht in einer MP3-Datei:

// Variablen
byte[] carrier = Files.readAllBytes(new File("path/to/file.mp3").toPath());
byte[] payload = "SecretMessage".getBytes(StandardCharsets.UTF_8);
long seed = 987654L;

// Erstellung des Objekts mit einem Shuffle-Overlay
Steganography steg = new MP3Steganography(MP3Overlays.SHUFFLE_OVERLAY);

// Verstecken der Nachricht mit benutzerdefiniertem Seed
byte[] carrierWithPayload = steg.encode(carrier, payload, seed);

// Speichern der modifizierten MP3-Datei
Files.write(new File("path/to/fileWithMessage.mp3").toPath(), carrierWithPayload);

Nun kann die erstellte Datei dem Empfänger (einschließlich des Seeds und der Information über das genutzte Overlay) übermittelt werden.

Empfänger möchte die Nachricht lesen:

// Variablen
byte[] carrierWithPossibleMessage = Files.readAllBytes(new File("path/to/fileWithMessage.mp3").toPath());
long seedFromSender = 987654L;

// Erstellung des Objekts mit dem gleichen Overlay
Steganography mp3Steg = new MP3Steganography(MP3Overlays.SHUFFLE_OVERLAY);

// Lesen der Nachricht, sofern in der Datei eine Nachricht enthalten ist
if (mp3Steg.isSteganographicData(carrierWithPossibleMessage, seedFromSender)) {
        // Erstellung des gleichen Objekts
        mp3Steg = new MP3Steganography(MP3Overlays.SHUFFLE_OVERLAY);

	byte[] messageFromCarrier = mp3Steg.decode(carrierWithPossibleMessage, seedFromSender);

	// Umwandeln des byte[] in einen String
	String message = new String(messageFromCarrier, StandardCharsets.UTF_8);  // enthält die ursprüngliche Nachricht "SecretMessage"
}

4. Limitierungen des Algorithmus'

Der MPEG-Standards, auf denen MP1-, MP2- und MP3-Dateien basieren, sind nicht öffentlich zugänglich und müssen käuflich erworben werden. Im Rahmen dieses Projekts wurde sich daher nur auf öffentliche Quellen bezogen. Aus diesen lässt sich zwar der grobe Aufbau der Audiodateien ableiten, aber genauere Informationen fehlen.

Die Hauptquelle als Basis für den Algorithmus ist http://mpgedit.org/mpgedit/mpeg_format/MP3Format.html

4.1 Aufbau von MP1-, MP2- und MP3-Dateien

Alle drei Formate haben den gleichen Grundaufbau, sie unterscheiden sich lediglich durch z.B. Bitraten, Samplingraten, Slotanzahl und Slotgröße:

1. (Optional) Metadaten - MPEG Audio Tag ID3v1

Zu Beginn gibt es einen optionalen Teil, der Metadaten (z.B. Titel, Interpret oder Genre) enthalten kann.

2. Audiodaten - Frames

  • Frame-Header (32 Bit)
  • (Optional) Header-Prüfsumme (16 Bit)
  • Frame-Daten

Danach folgt der für den Algorithmus interessante Teil, die eigentlichen Audiodaten. Diese sind in Frames aufgeteilt, wobei jeder Frame aus einem 32 Bit langen Header, einer optionalen 16 Bit langen Prüfsumme und den Nutzdaten besteht.

3. (Optional) Metadaten - MPEG Audio Tag ID3v2

Nach den Audiodaten gibt es einen weiteren Metadatenteil, der später in den Standard eingeflossen ist. Hier sind zusätzliche Metadaten (z.B. Titelbilder) enthalten.

4.2 Umsetzung im Bezug auf den Aufbau

Der Algorithmus durchsucht beim Erstellen des Overlays die übergebene Datei nach den Datenbytes. Dazu werden die Frame-Header gesucht und dekodiert. Durch die Dekodierung ist nun bekannt, welche der Bytes modifizierbar sind und diese werden dem Overlay gegeben. Das Overlay sortiert die Datenbytes nun entsprechend. Zum Beispiel wird die Reihenfolge beim Shuffle-Overlay anhand des übergebenen Seeds randomisiert.

Die encode()-Methode ruft nun wiederholt die next()-Methode des Overlays auf. Bei diesem Aufruf wird jeweils das nächste Datenbyte übergeben und davon das letzte Bit (das Least Significant Bit) entsprechend der Nachricht verändert. Dies wird solange wiederholt, bis die Nachricht komplett verschlüsselt ist.

4.3 Limitierungen

Da in den öffentlichen Quellen nur die Header erklärt sind, kann nur geraten werden, ob Datenbytes verändert werden können oder nicht. Das Resultat ist, dass die Veränderungen als "Knacken" hörbar werden. Wenn man die genaue Nutzung der Datenbytes kennt, ist es vielleicht möglich, die Datenbytes so zu verändern, dass die Änderungen nicht mehr hörbar sind.

Damit das "Knacken" so leise wie möglich ist, wird die Nutzung des Shuffle-Overlays mit benutzerdefiniertem Seed und das Verschlüsseln von möglichst kleinen Nachrichten (wenige Bytes) empfohlen.