Zephyrnet-Logo

So verwenden Sie dynamisches Reverse Engineering für eingebettete Geräte | TechTarget

Datum:

Die Verbreitung des IoT geht mit einer Zunahme von Sicherheitslücken einher. Wenn sie nicht kontrolliert werden, können böswillige Angreifer diese Schwachstellen ausnutzen, um in die Systeme von Unternehmen einzudringen.

Lang , seit langem als bewährte Sicherheitsmethode anerkannt, hilft Sicherheitsteams dabei, Schwachstellen und Schwachstellen in eingebetteten Geräten zu identifizieren und zu mindern. Viele Organisationen beschränken Pen-Tests jedoch auf Netzwerke untersuchen und Infrastruktur – IoT-Geräte werden oft übersehen.

Um Sicherheitsteams mit Pen-Tests für eingebettete Geräte vertraut zu machen, schrieb Jean-Georges Valle, Senior Vice President bei Kroll, einem Beratungsunternehmen für Cyberrisiken und Finanzdienstleistungen Praktisches Hardware-Pentesting: Lernen Sie Angriffs- und Verteidigungstechniken für eingebettete Systeme im IoT und anderen Geräten.

Im folgenden Auszug aus Kapitel 10 beschreibt Valle, wie Penetrationstester dynamisches Reverse Engineering nutzen können, um zu sehen, wie sich Code während der Ausführung auf eingebetteten Geräten verhält. Valle liefert ein Beispiel für dynamisches Reverse Engineering, um Pentestern die Herausforderungen zu zeigen, die bei der Beobachtung des Codeverhaltens auftreten können.

Anmerkung der Redaktion: Der folgende Auszug stammt aus einer Early-Access-Version von Praktisches Hardware-Pentesting, XNUMX. Auflage und kann sich ändern.

Verwendung von dynamischem Reverse Engineering – ein Beispiel

Ich habe eine Variante des vorherigen Beispiels vorbereitet, die uns vor einige Herausforderungen stellen wird. Ich zeige Ihnen, wie Sie diese Herausforderungen sowohl statisch als auch dynamisch bewältigen können, damit Sie den Aufwand in beiden Fällen vergleichen können.

Als Faustregel beim Vergleich dynamischer und statischer Ansätze gilt, dass dynamische Ansätze in 99 % der Fälle einfach einfacher sind und nach Möglichkeit Vorrang haben sollten (vergessen Sie nicht, dass Sie möglicherweise keinen Zugriff auf JTAG/SWD oder andere haben). On-Chip-Debugging-Protokolle).

In diesem Abschnitt erfahren wir auch, wie man an der gewünschten Stelle Pausen macht, den Speicher mit GDB überprüft und all diese guten Dinge!

Das Zielprogramm befindet sich hier im Ordner, den Sie geklont haben, im Ordner ch12.

Beginnen wir zunächst damit, es in Ghidra zu laden und es oberflächlich zu untersuchen. Achten Sie darauf, die richtige Architektur und Basisadresse im Ladefenster von Ghidra einzustellen (lesen Sie das vorherige Kapitel, wenn Sie sich nicht erinnern, wie das geht oder welchen Wert die Basisadresse hat).

Erste Ghidra-Inspektion

Auf den ersten Blick sieht die Hauptfunktion der Hauptfunktion im vorherigen Kapitel sehr ähnlich. Wir können den Verweis auf die Hauptfunktion finden, indem wir wie im vorherigen Kapitel eine PASSWORD-Zeichenfolge durchsuchen und uns mit der Analyse ihrer Struktur befassen.

Ich lasse Sie an den Fähigkeiten arbeiten, die Sie im vorherigen Kapitel erworben haben, um die verschiedenen Funktionen zu finden. In dieser ausführbaren Datei finden Sie erneut Folgendes:

  • Ein großer während (wahr) Schleife, die als Hauptereignisschleife fungiert und die LED des Bluepills blinken lässt, während sie auf die Eingabe eines Passworts reagiert
  • Eine Funktion zum Initialisieren der Uhr
  • Eine Funktion zum Initialisieren der GPIOs
  • Eine Funktion zum Initialisieren des UART
  • Ein Wert, der von der eindeutigen Kennung des Chips abhängt, wird auf fast die gleiche Weise erneut berechnet (berechne diesen Wert für deinen Chip und notiere diesen Wert).
  • Eine Funktion validiert das Passwort (kurz vor einem großen if das löst entweder den Druck von aus DU GEWINNST or NEIN)
  • Eine Funktion entschlüsselt die Gewinnerzeichenfolge, wenn die Validierungsfunktion eine zurückgibt (uint16_t) 0 Wert.

Die Ähnlichkeit der Struktur ist beabsichtigt, da dies Ihr erstes Mal ist. Wenn ich genau die gleichen Schritte wie im vorherigen Kapitel wiederholen würde, gäbe es für Sie nichts Neues zu lernen, oder?

Lassen Sie uns nun mehrere Methoden zur Umgehung dieser Passwortvalidierung durch dynamische Interaktion mit dem System durchgehen. Wir gehen vom Komplexesten zum Einfachsten über, damit Sie fokussiert bleiben und sich Know-how aneignen können (wenn Sie so sind wie ich, wenn es einen einfachen Weg gibt, etwas zu umgehen, warum sollten Sie dann den schwierigen Weg wählen?).

Umkehren des erwarteten Passworts

Als Erstes versuchen wir herauszufinden, wie das Passwort validiert wird, um zu verstehen, wie man ein Passwort generiert, das die Tests besteht.

Werfen wir einen Blick auf den der Validierungsfunktion entsprechenden C-Code, der von Ghidra ausgegeben wird:

Screenshot of Ghidra output of decompiled code
Abbildung 12.2 – Die dekompilierte Validierungsfunktion macht tatsächlich nicht das, was Sie denken!

Humm... das hat nichts direkt mit den Parametern zu tun. Dabei wird der Inhalt eines kopiert 0x47 (71) ein langes statisches Array von Bytes in den RAM (und NICHT) und ruft es dann als Funktion auf.

Das ist merkwürdig.

Oder ist es?

Dies ist eine sehr verbreitete Technik zur Tarnung von Code (natürlich eine sehr einfache Version davon). Wenn in der .bin-Datei (und damit nicht im Flash der MCU) keine klare Version des Opcodes vorhanden ist, kann ein Reverse-Engineering-Tool wie Ghidra nicht erkennen, dass es sich um Code handelt! Hier haben wir zwei mögliche Ansätze:

  • Entweder extrahieren wir den Inhalt des Puffers manuell aus der .bin-Datei, entschlüsseln ihn (hier erfolgt die Verschlüsselung NICHT Byte für Byte, das ist absichtlich trivial) und lassen ihn von Ghidra dekompilieren.
  • Oder da wir JTAG-Zugriff auf den Chip haben, können wir einfach einen Haltepunkt an der richtigen Adresse im Speicher setzen und die MCU die harte Arbeit für uns erledigen lassen.

Die erste Lösung überlasse ich Ihnen zur Umsetzung als Übung. Für eine solch einfache Aufgabe sollten etwa 10 Zeilen Python- oder C-Code erforderlich sein! Du willst Hacker werden? Hack los!

Mich? Ich bin ein fauler Kerl. Wenn ein Computer für mich funktionieren kann, dann... So sei es! Ich werde mich für die zweite Lösung entscheiden.

Starten wir zunächst eine Bildschirmsitzung in einem Terminal, damit wir Passwörter eingeben und sehen können, wie es reagiert:

Bildschirm /dev/ttyUSB0 115200

Lassen Sie uns OpenOCD und GDB in einem zweiten Terminal starten, wie wir es zu Beginn des Kapitels getan haben, und stöbern wir herum:

openocd -f ./ftdi2232h.cfg.tcl -f ./clone_CSK.cfg & gdb-multiarch -x ./gdbinit
#openocd startet
[...]
Ziel wurde aufgrund einer Debug-Anfrage angehalten, aktueller Modus: Thread xPSR: 0x01000000 PC: 0x080013b8 MSP: 0x20005000
[...]

Und... und verdammt! Es gibt mir nicht die Kontrolle zurück! Kein Problem, wenn Ihnen das passiert – ein bisschen Ctrl + C gibt Ihnen sofort die Kontrolle zurück:

^C
Programmempfangssignal SIGINT, Interrupt.
0x080003aa im ?? ()
(GDB)

Nach unserem Ctrl + C (^c), gdb teilt uns mit, dass die Ausführung an der Adresse gestoppt wird 0x080003aa in einer unbekannten Funktion (??).

Abhängig von Ihrem Bundesstaat können Sie an einer anderen Adresse anhalten.

Keine Panik – setzen Sie Ihren Denkhut auf und nehmen Sie (immer) Ihr Handtuch mit.

Das ist kein Problem. Die Wahrscheinlichkeit ist groß, dass Sie in unmittelbarer Nähe dieser Adresse abbrechen, da sie sich in der Warteschleife befindet, in der die LED blinkt und auf den Empfang eines Passworts auf der seriellen Schnittstelle wartet.

Werfen wir zunächst einen Blick auf unsere Register:

(gdb) ir
r0 0x0 0
r1 0x8001a1d 134224413
r2 0x5b8d7f 5999999
r3 0x335d7 210391
r4 0x20004f88 536891272
r5 0x8001a74 134224500
r6 0x0 0
r7 0x20004f88 536891272
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0xf 15
sp 0x20004f88 0x20004f88
lr 0x80003bf 134218687
PC 0x80003aa 0x80003aa
xPSR 0x81000000 -2130706432
msp 0x20004f88 0x20004f88
[...]

Wir sehen, dass der PC tatsächlich dort ist, wo er sein soll, alles sieht gut und gut aus. Versuchen wir nun, ein Passwort einzugeben.

Und… im Fenster der seriellen Schnittstelle funktioniert nichts! Denken Sie daran: GDB blockiert tatsächlich die Ausführung des Codes; die serielle Schnittstelle reagiert nicht auf Ihre Eingaben. Das ist normal.

Lassen wir also zu, dass es weitergeht (fortsetzen or c der gdb Fenster) und prüfen Sie, ob die Seriennummer jetzt funktioniert. Ja tut es. Lassen Sie uns es noch einmal unterbrechen und einen Haltepunkt für die Adresse der Passwort-Validierungsfunktion setzen, oder?

In Ghidra können wir sehen, dass die Adresse der ersten Anweisung der Funktion ist 0x080002b0:

Screenshot of finding a function address in Ghidra
Abbildung 12.3 – Finden einer Funktionsadresse in Ghidra

Lassen Sie uns dort einen Haltepunkt setzen gdb Setzen Sie die Ausführung fort und geben Sie ein Dummy-Passwort ein:

(gdb) b * 0x080002b0
#1
Haltepunkt 1 bei 0x80002b0
#2
(gdb) c
#3
Auch weiterhin.
Hinweis: Automatische Verwendung von Hardware-Haltepunkten für schreibgeschützte Adressen.
#4
[Geben Sie „aaa“ in die serielle Konsole ein und geben Sie ein.]
Haltepunkt 1, 0x080002b0 in ?? ()
#5
(GDB)

Lassen Sie uns das analysieren:

  • b * 0x080002b0 fragt gdb um einen Haltepunkt für die unter Adresse gespeicherte Anweisung zu setzen 0x080002b0. Überprüfen Sie Ihre Hinweise.
  • gdb sagt mir: Okay, ich habe dort einen Haltepunkt gesetzt.
  • Setzen Sie bitte die Hinrichtung fort, meine Liebe gdb und es heißt, dass es das gerne tut.
  • ABER es benachrichtigt mich, dass es nicht an die Adresse schreiben kann 0x080002b0 (Es ist in Flash und Flash kann nicht einfach so geschrieben werden; es muss entsperrt und Stück für Stück geschrieben werden). Um so viel Hin und Her zu vermeiden, sind ARM-Chips mit einigen internen Debug-Systemen ausgestattet, die es ermöglichen, dass der PC kaputt geht, wenn er auf bestimmte Adressen trifft, auf die nicht einfach geschrieben werden kann.
  • Bumm! Der Haltepunkt wurde erreicht! Die Ausführung wird gestoppt, nachdem ich ein Dummy-Passwort eingebe.

Okay, was können wir jetzt damit machen?

Das Wichtigste zuerst: Wenn Sie sich an den Code der Validierungsfunktion erinnern, wurden ihre Argumente direkt an den dekodierten Code übergeben. Werfen wir einen Blick darauf, was sie sein können (denken Sie an die Aufrufkonvention für Funktionen: Argumente sind in r0-3):

(gdb) p/x $r0
$2 = 0x20000028
(gdb) p/x $r1
$3 = 0x2169

Das erste Argument ist etwas im RAM und das zweite ist ein Wert. (Das ist der umgewandelte UUID-Wert für Ihren Chip, den Sie notiert haben, oder?)

Was ist nun unter dieser ersten Adresse gespeichert? Lass es uns untersuchen:

(gdb) x/x 0x20000028
0x20000028: 0x00616161
(gdb) x/s 0x20000028
0x20000028: „aaa“

Ah! Ah! Ah! (Sehen Sie, was ich dort gemacht habe?) Das ist unser Passwort. Bitte beachten Sie die Verwendung des Formatmodifikators für den x-Befehl.

Dies wird also erwartet.

Schauen wir uns nun den entschlüsselten Code an.

Ghidra sagt uns, dass die Anweisung, die den Decodierungsschleifen folgt, bei ist 0x080002f0. Lassen Sie uns dort brechen:

(gdb) b * 0x080002f0
Haltepunkt 2 bei 0x80002f0
(gdb) c
Auch weiterhin.
Haltepunkt 2, 0x080002f0 in ?? ()
(gdb) c
(gdb) x/4i $pc
=> 0x80002f0: movs r0, #0
   0x80002f2: blx r3
   0x80002f4: mov r3, r0
   0x80002f6: mov r0, r3

Die Adresse des entschlüsselten Codes befindet sich also in r3. Wir haben gesehen, dass der Puffer vorhanden war 0x47 (71) lang. Wir sind im Daumenmodus (also Anleitung für Größe 2). Dies sollte 47/2 sein: etwa 35 Anweisungen. Das letzte Bit der Adresse ist für den Modus; wir können das loswerden:

(gdb) x/35i ($r3 & (~1))
   0x20000128: Drücken Sie {r4, r5, r6, r7, lr}
   0x2000012a: eors r4, r4
   0x2000012c: eors r3, r3
   0x2000012e: eors r5, r5
   0x20000130: ldrb r5, [r1, r4]
   0x20000132: mov r8, r5
   0x20000134: mov r6, r8
   0x20000136: lsrs r6, r6, #4
   0x20000138: lsls r5, r5, #4
   0x2000013a: orrs r5, r6
   0x2000013c: movs r6, #255 ; 0xff
   0x2000013e: unds r5, r6
   0x20000140: movs r6, #15
   0x20000142: mov r8, r4
   0x20000144: mov r7, r8
   0x20000146: unds r7, r6
   0x20000148: r6, pc, #16 hinzufügen; (adr r6, 0x2000015c) #1
   0x2000014a: ldrb r6, [r6, r7]
   0x2000014c: eors r5, r6
   0x2000014e: Fügt r0, r0, r5 hinzu
   0x20000150: fügt r4, #1 hinzu
   0x20000152: ldrb r5, [r1, r4]
   0x20000154: cmp r5, r3
   0x20000156: bgt.n 0x20000132
   0x20000158: eors r0, r2
   0x2000015a: pop {r4, r5, r6, r7, pc}
   0x2000015c: str r5, [r4, #36] ; 0x24
   0x2000015e: ldrb r4, [r6, #5]
   0x20000160: ldr r7, [r6, #32]
   0x20000162: subs r2, #55 ; 0x37
   0x20000164: ldr r4, [r2, r5]
   0x20000166: ldr r5, [r1, #100] ; 0x64
   0x20000168: r3, r12 hinzufügen
   0x2000016a: fügt r4, #68 hinzu; 0x44
   0x2000016c: vqadd.u8 q0, q8,

Das ist eher so! Wir sehen einen normalen Funktionsauftakt (Speichern von funktionsinternen Registern im Stapel), einige Verarbeitungsvorgänge und eine Funktionsrückgabe. Aber GDB warnt uns vor illegalen Befehlsparametern (0x2000016c).

Wenn wir uns die Auflistung ansehen, sehen wir, dass GDB die Nutzung eines PC-relativen Datenelements angibt:

#1: kommentiert: adr r6, 0x2000015c)

Dies wird sehr häufig zum Speichern von Daten in einem Assemblerprogramm verwendet. adr ist eine Pseudoanweisung, die dem Assembler mitteilt, dass er den Offset zu einer Beschriftung (einer benannten Position) im Code hinzufügen soll.

Schauen wir uns an, was dort gespeichert ist:

(gdb) x/4wx 0x2000015c
0x2000015c: 0x79746265 0x3a376a37 0x6e4d5954 0x34444463
(gdb) x/s 0x2000015c
0x2000015c: "ebty7j7:TYMncDD4"

Dies ist tatsächlich eine Zeichenfolge, die im Prozess irgendwie verwendet wird.

Lassen Sie uns die ersten Anweisungen Schritt für Schritt durchgehen, als Beispiel dafür, wie man einem Ausführungsablauf folgt. Wir werden zunächst einrichten gdb Es zeigt uns also die interessanten Register und Inhalte zu jedem Schritt:

(gdb) disp/x $r0
1: /x $r0 = 0x20000028
(gdb) disp/x $r1
2: /x $r1 = 0x20000028
(gdb) disp/x $r2
3: /x $r2 = 0x2169
(gdb) disp/x $r3
4: /x $r3 = 0x20000129
(gdb) disp/x $r4
5: /x $r4 = 0x20004f88
(gdb) disp/x $r5
6: /x $r5 = 0x8001a74
(gdb) disp/x $r6
7: /x $r6 = 0x0
(gdb) disp/x $r7
8: /x $r7 = 0x20004f70
(gdb) disp/x $r8
9: /x $r8 = 0x2
(gdb) disp/i $pc
10: x/i $pc
=> 0x80002f0: movs r0, #0
=> 0x80002f2: blx r3

Jetzt sind wir einsatzbereit Schritt (Schrittanweisung), um zu sehen, was los ist:

0x2000012b: eors r4, r4
0x2000012d: eors r3, r3
0x2000012f: eors r5, r5

Das sind Nullen r4, r3 und r5 (x^x = 0):

0x20000130: ldrb r5, [r1, r4]
0x20000132: mov r8, r5
0x20000134: mov r6, r8

Dadurch wird das erste Zeichen der Passwortzeichenfolge geladen r5 (r1 ist die Adresse und r4 ist zu diesem Zeitpunkt auf Null gesetzt) ​​und kopiert es nach r8 und r6:

0x20000136: lsrs r6, r6, #4
0x20000138: lsls r5, r5, #4
0x2000013a: orrs r5, r6
0x2000013c: movs r6, #255 ; 0xff
0x2000013e: unds r5, r6

Das verschiebt sich r6 4 Bit nach rechts, r5 4 Bits nach links und fügt deren OR-Wert ein r4. Anschließend wird das ODER-Ergebnis mit ausgeblendet 0xff, im Grunde werden die 4 niedrigeren und 4 höheren Bits des Passwortzeichens ausgetauscht und die überschüssigen Bits gelöscht!

0x20000140: movs r6, #15
0x20000142: mov r8, r4
0x20000144: mov r7, r8
0x20000146: unds r7, r6

Dies bewegt sich um 15 Zoll r6, Kopien r4 in r8 und r7, und Masken r7 mit 15. Aber warum? An dieser Stelle, r4 ist 0! Dies kann später verwendet werden – da wir das gesehen haben r4 wurde als Offset beim Laden des Passwortzeichens verwendet, r4 ist wahrscheinlich ein Zähler! Wenn das der Fall ist, kann diese Maskierung als eine Art Modulo verwendet werden ... (es ist sehr üblich, die Maskierung für Modulo einer Zweierpotenz -1 zu verwenden):

0x20000148: r6, pc, #16 hinzufügen; (adr r6, 0x2000015c)
0x2000014a: ldrb r6, [r6, r7]

Dadurch wird das erste Zeichen der Zeichenfolge geladen, in der versteckt war r6 und verwendet r7 und ein Offset! r4 ist definitiv ein Zähler hier und r7 eine modulierte Version davon. Dies ist eine sehr typische Programmiermethode, um dies zu erreichen:

0x2000014c: eors r5, r6
0x2000014e: Fügt r0, r0, r5 hinzu
0x20000150: fügt r4, #1 hinzu

Dabei wird der Wert des Bit-vertauschten Passwortzeichens mit den aktuellen Rängen der seltsamen Zeichenfolge XOR-verknüpft und zu dieser addiert r0 und Erhöhen der r4 Schalter:

0x20000152: ldrb r5, [r1, r4]
0x20000154: cmp r5, r3
0x20000156: bgt.n 0x20000132

Dadurch wird ein neues Passwortzeichen mit dem neuen Offset geladen r5. r3 ist 0, also prüft der cmp r5-r3 und warte … bgt.n? Was ist das? Erinnern Sie sich, was zu tun ist, wenn Sie Zweifel haben? Lesen Sie die Dokumentation hier: https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/condition-codes-1-condition-flags-and-codes.

Es springt also, wenn r5 > r3. Und r3 is 0, Also? Dies ist ein Test für a 0 beendeter String!

Dies ist die Hauptlogikschleife der Validierung!

Sobald dies erledigt ist, geschieht Folgendes:

0x20000158: eors r0, r2
0x2000015a: pop {r4, r5, r6, r7, pc}

Abhängig vom berechneten Wert wird diese Summe mit der UUID XOR-verknüpft, die Aufruferregisterwerte wiederhergestellt und dieser Wert zurückgegeben. Der C-Code prüft dann, ob dieser Wert null ist, um tatsächlich die Gewinnerzeichenfolge anzuzeigen. Wir müssen es dann nur noch so anordnen, dass unsere Summe dem UUID-abhängigen Wert entspricht, damit das XOR null ist!

Wir haben die ganze Logik!

spot_img

Neueste Intelligenz

spot_img