Phase 3: Reverse Engineering

Native Libraries mit Ghidra analysieren und Schlüssel extrahieren

Lernziel

Nach dieser Seite weißt du, wie du eine native Android-Bibliothek (.so-Datei) in Ghidra öffnest, JNI-Exportfunktionen als Einstiegspunkte nutzt, das FindCrypt-Plugin zur Erkennung von Kryptoalgorithmen einsetzt und hartcodierte Schlüssel in der .data-Section einer ARM64-ELF-Datei lokalisierst. Als konkretes Beispiel extrahierst du den AES-128-Schlüssel aus der libAES.so der Sonhomay LED Glasses.

Was ist Reverse Engineering einer nativen Bibliothek?

Viele Android-Apps lagern sicherheitskritischen Code in native Bibliotheken aus — Dateien mit der Endung .so (Shared Object). Das ist dasselbe Format wie .dll unter Windows oder .dylib unter macOS. Diese Bibliotheken sind kompilierter Maschinencode für einen bestimmten Prozessortyp (hier: ARM64) und enthalten keinen lesbaren Quelltext.

Stell dir eine verschlossene Metalldose vor, deren Inhalt du nicht direkt sehen kannst. Ghidra ist das Werkzeug, das die Dose öffnet: Es übersetzt die binären Maschinenbefehle zurück in pseudo-C-Code (Decompiler) und zeigt dir, was das Programm tatsächlich macht. Diese Übersetzung ist nicht perfekt, aber gut genug, um Schlüssel, Algorithmen und Protokolllogik zu erkennen.

Phase 1 hat gezeigt, dass die App Krypto verwendet und auf libAES.so delegiert. Phase 3 öffnet jetzt diese Bibliothek und liest heraus, welcher Schlüssel tatsächlich drin steckt.

ARM64 vs. x86

Android-Geräte nutzen ARM-Prozessoren. Ghidra unterstützt ARM64 (AArch64) nativ — du musst keinen Prozessortyp manuell auswählen, Ghidra erkennt ihn automatisch beim Import einer ELF-Datei.

Was ist JNI und warum ist es der Schlüssel zum Einstieg?

JNI steht für Java Native Interface — die Brücke zwischen Java/Kotlin-Code einer App und nativem C/C++-Code in einer .so-Datei. Wenn Java-Code eine native Funktion aufruft, muss diese Funktion in der Bibliothek einen ganz bestimmten Namen haben:

Java_<Paketname>_<Klassenname>_<Methodenname>

Punkte im Paketnamen werden durch Unterstriche ersetzt. Das ist eine verbindliche Konvention des Android-Betriebssystems, die du dir zunutze machen kannst: Du musst nicht den gesamten Binärcode durchsuchen — du suchst einfach nach Funktionen, die mit Java_ beginnen.

Beispiel aus der LED-Brille:

In Phase 1 hast du in JADX die Klasse com.pinkysinye.funkyglassesplus.model.data.Agreement mit einer nativen Methode gesehen. Die zugehörige JNI-Exportfunktion in libAES.so heißt daher:

Java_csh_tiro_cc_aes_keyExpansionDefault

Dieser Name ist der Einstiegspunkt für die gesamte AES-Implementierung.

Sonderzeichen in Paketnamen

Enthält ein Paketname Sonderzeichen oder Ziffern, werden diese nach dem JNI-Encoding-Schema mit _0 ersetzt (z. B. _1 für $). Wenn eine Suche nach dem erwarteten Namen scheitert, prüfe auf solche Kodierungen.

Ghidra einrichten für ARM64-ELF-Analyse

Systemvoraussetzungen:

  • Ghidra 11.4.3 (aktuellste stabile Version)
  • Java 17 oder neuer (JDK, nicht nur JRE)
  • 8 GB RAM empfohlen; für große Bibliotheken mehr

Installation:

# Java-Version prüfen
java -version
# Muss: openjdk 17 oder höher

# Ghidra herunterladen: https://ghidra-sre.org/
# ZIP entpacken, kein Installer nötig

cd ~/tools/ghidra_11.4.3_PUBLIC
./ghidraRun

FindCrypt-Plugin installieren:

Das FindCrypt-Plugin erkennt bekannte kryptographische Konstanten (AES-S-Box, SHA-Konstanten, u. a.) automatisch im Binärcode. Das erspart stundenlange manuelle Suche.

# Ghidra öffnen -> Script Manager (Fenster -> Script Manager)
# Oder direkt: Tools -> Run Script

# Alternative: FindCrypt als Extension installieren
# Script: FindCrypt.py aus dem Ghidra-Repository oder community builds
# Ablage: ~/ghidra_scripts/FindCrypt.py

# Im Script Manager: "FindCrypt" suchen, Skript auswählen, Run

Decompiler-Timeout bei großen Funktionen

Manche AES-Implementierungen haben große, unrollte Funktionen (>500 Zeilen Assembler). Der Ghidra-Decompiler bricht standardmäßig nach 60 Sekunden ab. Erhöhe den Timeout unter Edit -> Tool Options -> Decompiler -> Analysis -> Timeout auf 300 Sekunden, bevor du mit der Analyse beginnst.

Schritt 1: libAES.so in Ghidra importieren

Du hast die .so-Datei bereits in Phase 1 aus der APK extrahiert. Sie liegt im APK-Archiv unter lib/arm64-v8a/libAES.so.

1. Ghidra öffnen -> Neues Projekt anlegen (File -> New Project)
2. File -> Import File -> libAES.so auswählen
3. Ghidra erkennt automatisch: ELF, ARM Cortex, 64-bit, little-endian
4. "OK" klicken -> "Analyze" im folgenden Dialog bestätigen
5. Analyse dauert 10–30 Sekunden (Fortschrittsbalken unten)

Nach der Analyse öffnet sich der Code Browser. Links siehst du den Symbol Tree — eine Liste aller Funktionen, Exports und Daten.

Schritt 2: JNI-Exportfunktion finden

Im Symbol Tree unter Exports findest du alle öffentlichen Funktionen der Bibliothek. JNI-Exporte beginnen mit Java_.

Symbol Tree -> Exports -> Java_csh_tiro_cc_aes_keyExpansionDefault
Adresse: 0x00101278

Doppelklick auf die Funktion öffnet den Disassembler und den Decompiler gleichzeitig. Im Decompiler-Fenster (rechts) siehst du pseudo-C-Code — dieser ist für die Analyse entscheidend.

Was sind Exports?

Eine ELF-Shared-Library "exportiert" Funktionen, die von außen aufrufbar sind — ähnlich wie eine öffentliche API. Nur exportierte Funktionen sind von Java aus via JNI aufrufbar. Private (interne) Hilfsfunktionen sind nicht exportiert.

Schritt 3: FindCrypt ausführen

Bevor du den Decompiler-Code liest, lass FindCrypt laufen. Das Skript markiert alle Stellen im Binärcode, an denen bekannte kryptographische Konstanten vorkommen.

Window -> Script Manager -> "FindCrypt" eingeben -> Run

FindCrypt zeigt nach dem Durchlauf eine Tabelle mit Treffern. Für libAES.so der LED-Brille findest du:

Adresse       Algorithmus   Beschreibung
0x00102000    AES           AES S-Box (Forward)
0x00102100    AES           AES S-Box (Inverse)
0x00102200    AES           Round Constants

Diese Adressen zeigen dir, wo die AES-Implementierung ihre Tabellen gespeichert hat. Doppelklick auf einen Eintrag springt direkt dorthin.

Schritt 4: GOT-Referenzen und Schlüssel verfolgen

Jetzt gehst du zurück zur JNI-Funktion bei 0x00101278 und liest den Decompiler-Code. Du wirst zwei markante Zeilen sehen:

// Ghidra Decompiler-Ausgabe (vereinfacht):
___bss_start__  = *PTR_key_00112ff0;          // Key Bytes 0–7
_DAT_00113038   = *(PTR_key_00112ff0 + 8);    // Key Bytes 8–15

Was bedeutet das?

PTR_key_00112ff0 ist ein Eintrag in der GOT — der Global Offset Table. Die GOT ist eine Tabelle in ELF-Binärdateien, die Adressen zu globalen Variablen und Funktionen speichert. Der Name PTR_key_ wurde von Ghidra automatisch vergeben, weil es erkennt, dass hier ein Schlüssel referenziert wird.

Der Pointer in der GOT zeigt auf die eigentlichen Schlüsseldaten in der .data-Section bei Adresse 0x00113020.

Decompiler-Artefakt: Zwei Ladeoperationen

Ghidra stellt den 128-Bit-AES-Schlüssel als zwei separate 64-Bit-Ladeoperationen dar. Das ist ein bekanntes Decompiler-Artefakt. Im echten ARM64-Assembler wird der Schlüssel durch eine einzige ldr q0, [x1]-Instruktion in ein 128-Bit-NEON-Register geladen. Die zweigeteilte Darstellung täuscht — es ist ein einziger zusammenhängender 16-Byte-Schlüssel.

Schritt 5: Schlüssel in der .data-Section lesen

Navigiere zu Adresse 0x00113020:

Navigation -> Go To Address -> 0x00113020 -> Enter

Im Listing-Fenster siehst du rohe Bytes. Der AES-128-Schlüssel der LED-Brille:

Adresse: 0x00113020  (.data-Section)

34 52 2A 5B  7A 6E 49 2C  08 09 0A 9D  8D 2A 23 F8

Das sind genau 16 Bytes (128 Bit) — die Schlüsselgröße für AES-128. Dieser Schlüssel ist statisch und hardcodiert: Er ist in allen weltweit verkauften Exemplaren der LED-Brille identisch.

Verifizierung: Der extrahierte Schlüssel kann sofort überprüft werden. Das Klartext-/Chiffretext-Paar aus dem PCAP-Capture (Phase 2) liefert den Testfall:

from Crypto.Cipher import AES

key = bytes([0x34, 0x52, 0x2A, 0x5B, 0x7A, 0x6E, 0x49, 0x2C,
             0x08, 0x09, 0x0A, 0x9D, 0x8D, 0x2A, 0x23, 0xF8])

# DATS-Command aus dem PCAP-Capture
plaintext = bytes([0x07, 0x44, 0x41, 0x54, 0x53, 0x01, 0x00, 0x26,
                   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])

cipher = AES.new(key, AES.MODE_ECB)
result = cipher.encrypt(plaintext).hex()
# Ergebnis: e703dd68d7c0bb9ca75f0140df55e219
# Stimmt mit dem erfassten PCAP-Paket überein — Schlüssel korrekt!

Häufiger Fehler: Falsche .so-Architektur

Android-Apps liefern oft mehrere .so-Versionen für verschiedene CPU-Architekturen: armeabi-v7a (32-Bit ARM), arm64-v8a (64-Bit ARM), x86, x86_64. Lade immer die passende Version für dein Analyseziel. Für moderne Android-Geräte ist arm64-v8a die richtige Wahl. Die Speicheradressen unterscheiden sich je nach Architektur.

Warum ECB-Modus ein Problem ist

Der extrahierte Schlüssel enthüllt nicht nur das Geheimnis — er zeigt auch den verwendeten AES-Modus: ECB (Electronic Codebook). ECB ist der einfachste AES-Modus, hat aber eine fundamentale Schwäche:

Stell dir vor, du verschlüsselst ein Bild. Im ECB-Modus bleibt jeder identische 16-Byte-Block im Klartext auch ein identischer Block im Chiffretext. Das Muster des Originalbildes bleibt erkennbar — selbst nach der Verschlüsselung. Für die LED-Brille bedeutet das: Identische Protokollbefehle erzeugen immer exakt denselben Chiffretext. Ein Angreifer kann Muster erkennen und Befehle replizieren, ohne den Schlüssel zu kennen.

Sichere Alternativen

Statt AES-ECB sollte AES-GCM (Galois/Counter Mode) verwendet werden. GCM bietet zusätzlich zur Verschlüsselung auch Integritätsschutz (Authenticated Encryption). Noch besser: BLE LE Secure Connections auf Link-Layer-Ebene aktivieren — das erledigt die gesamte Schlüsselaushandlung sicher.

Interaktive Checkliste: Phase 3

Phase 3: Native Library Reverse Engineering

0/12

Zeitaufwand und Ergebnisse

Phase 3 dauert typisch 4–8 Stunden, abhängig von der Komplexität der Bibliothek. Größere Bibliotheken mit mehreren JNI-Funktionen und verschachtelten Hilfsfunktionen brauchen mehr Zeit.

Das Hauptergebnis dieser Phase ist der extrahierte Schlüssel — ein konkretes, verifizierbares Artefakt, das die Sicherheitslücke beweist. Dieser Schlüssel fließt direkt in Phase 5 (PoC-Entwicklung) ein.

Sekundäres Ergebnis: Du verstehst jetzt, wie die Verschlüsselung implementiert ist — welcher AES-Modus, welche Paketstruktur, welche Padding-Strategie. Diese Information ist für Phase 4 (Protokollrekonstruktion) unverzichtbar.

Zusammenfassung

  • Native .so-Bibliotheken enthalten oft hartcodierten Kryptoschlüssel in der .data-Section.
  • JNI-Funktionen (Präfix Java_) sind der Einstiegspunkt in der nativen Bibliothek — sie müssen diesen Namensschema folgen.
  • Das FindCrypt-Plugin erkennt AES- und andere Kryptoalgorithmen automatisch anhand ihrer mathematischen Konstanten.
  • GOT-Einträge (Global Offset Table) zeigen auf die eigentlichen Schlüsseldaten — verfolge den Zeiger von der JNI-Funktion bis zur .data-Section.
  • Der gefundene 16-Byte-Schlüssel 34 52 2A 5B 7A 6E 49 2C 08 09 0A 9D 8D 2A 23 F8 ist in allen Geräten identisch — ein globaler Schlüssel ohne gerätespezifische Variation.

Warum sind JNI-Exportfunktionen (Präfix Java_) so nützlich als Einstiegspunkt in der Ghidra-Analyse?