Zephyrnet-logo

Dynamische reverse engineering gebruiken voor embedded apparaten | TechTarget

Datum:

De verspreiding van het internet der dingen is gepaard gegaan met een toename van beveiligingsproblemen. Als dit niet wordt gecontroleerd, kunnen kwaadwillende aanvallers deze zwakheden gebruiken om de systemen van organisaties te infiltreren.

Normaal penetratietesten, al lang erkend als best practice op het gebied van beveiliging, helpen beveiligingsteams kwetsbaarheden en zwakheden in embedded apparaten te identificeren en te verminderen. Veel organisaties beperken pentesten echter tot netwerken onderzoeken en infrastructuur — IoT-apparaten worden vaak over het hoofd gezien.

Jean-Georges Valle, senior vice president bij Kroll, een adviesbureau op het gebied van cyberrisico's en financiële diensten, schreef om beveiligingsteams op de hoogte te brengen van pentesten van embedded apparaten Praktische hardware-pentesting: leer aanvals- en verdedigingstechnieken voor ingebedde systemen in IoT en andere apparaten.

In het volgende fragment uit hoofdstuk 10 beschrijft Valle hoe pentesters dynamische reverse engineering kunnen gebruiken om te zien hoe code zich gedraagt ​​tijdens uitvoering op ingebedde apparaten. Valle geeft een voorbeeld van dynamische reverse engineering om pentesters de uitdagingen te laten zien die zich kunnen voordoen bij het observeren van hoe code zich gedraagt.

Noot van de redactie: Het volgende fragment komt uit een versie met vroege toegang van Praktische hardware pentesting, tweede editie en is onderhevig aan verandering.

Dynamische reverse engineering gebruiken — een voorbeeld

Ik heb een variant van het vorige voorbeeld voorbereid die ons voor een aantal uitdagingen zal stellen. Ik zal u laten zien hoe u deze uitdagingen zowel statisch als dynamisch kunt overwinnen, zodat u de benodigde inspanning in beide gevallen kunt vergelijken.

De vuistregel bij het vergelijken van dynamische en statische benaderingen is dat dynamische benaderingen in 99% van de gevallen gewoon eenvoudiger zijn en indien mogelijk prioriteit moeten krijgen (vergeet niet dat u mogelijk geen toegang kunt krijgen tot JTAG/SWD of andere foutopsporingsprotocollen op de chip).

In deze sectie leren we ook hoe we kunnen breken waar we willen, geheugen inspecteren met GDB en al deze goede dingen!

Het doelprogramma bevindt zich hier in de map die u hebt gekloond, in de map ch12.

Laten we eerst beginnen met het in Ghidra te laden en het oppervlakkig te inspecteren. Besteed aandacht aan het instellen van de juiste architectuur en het basisadres in het laadvenster van Ghidra (raadpleeg het vorige hoofdstuk als u niet meer weet hoe u dat moet doen of wat de basisadreswaarde is).

Eerste Ghidra-inspectie

Op het eerste gezicht lijkt de hoofdfunctie sterk op de hoofdfunctie in het vorige hoofdstuk. We kunnen de verwijzing naar de hoofdfunctie vinden door een PASSWORD-reeks te doorzoeken, net als in het vorige hoofdstuk, en de structuur ervan te analyseren.

Ik laat je werken aan de vaardigheden die je in het vorige hoofdstuk hebt opgedaan om de verschillende functies te vinden. In dit uitvoerbare bestand vindt u het volgende terug:

  • Een grote terwijl (waar) lus die fungeert als de hoofdgebeurtenislus en de LED van de bluepill laat knipperen terwijl hij reageert op een wachtwoord dat wordt ingevoerd
  • Een functie om de klok te initialiseren
  • Een functie om de GPIO's te initialiseren
  • Een functie om de UART te initialiseren
  • Een waarde die afhangt van de unieke identificatie van de chip wordt op bijna dezelfde manier opnieuw berekend (bereken deze waarde voor uw chip en noteer deze waarde)
  • Een functie valideert het wachtwoord (net voor een big if dat activeert ofwel het afdrukken van JIJ WINT or NEE)
  • Een functie decodeert de winnende string als de validatiefunctie een retourneert (uint16_t) 0 waarde.

De gelijkenis van de structuur is opzettelijk, aangezien dit je eerste keer is. Als ik precies dezelfde stappen zou herhalen als in het vorige hoofdstuk, zou je niets nieuws leren, toch?

Laten we nu eens kijken naar meerdere methoden om deze wachtwoordvalidatie te omzeilen door dynamische interactie met het systeem. We gaan van de meest complexe naar de eenvoudigste om u gefocust te houden en kennis op te doen (als u op mij lijkt, als er een gemakkelijke manier is om iets te omzeilen, waarom zou u dan voor de moeilijke weg gaan?).

Het verwachte wachtwoord terugdraaien

Het eerste dat we gaan doen, is proberen te zien hoe het wachtwoord wordt gevalideerd om te begrijpen hoe een wachtwoord kan worden gegenereerd dat de tests doorstaat.

Laten we eens kijken naar de equivalente C-code van de validatiefunctie die wordt uitgevoerd door Ghidra:

Screenshot of Ghidra output of decompiled code
Afbeelding 12.2 — De gedecompileerde validatiefunctie doet niet echt wat u denkt!

Humm… dit doet niets direct met de parameters. Dit is het kopiëren van de inhoud van een 0x47 (71) lange statische array van bytes naar RAM (en NIET) en roept het dan op als een functie.

Dit is raar.

Of is het?

Dit is een veel voorkomende techniek om code te camoufleren (natuurlijk een heel eenvoudige versie ervan). Als er geen duidelijke versie van de opcode aanwezig is in het .bin-bestand (en dus niet in de flash van de MCU), kan een reverse engineering-tool zoals Ghidra niet detecteren dat het code is! Hier hebben we twee mogelijke benaderingen:

  • Of we halen handmatig de inhoud van de buffer uit het .bin-bestand, ontcijferen het (hier is het cijfer gewoon NIET byte voor byte, het is met opzet triviaal), en laten dit decompileren door Ghidra.
  • Of, aangezien we JTAG-toegang tot de chip hebben, kunnen we gewoon een breekpunt op het juiste adres in het geheugen plaatsen en de MCU het harde werk voor ons laten doen.

Ik laat de eerste oplossing voor u om te implementeren als een oefening. Er zijn ongeveer 10 regels Python- of C-code nodig voor zo'n eenvoudige taak! Wil je hacker worden? Hack weg!

Mij? Ik ben een luie jongen. Als een computer voor mij kan werken, nou... Het zij zo! Ik ga voor de tweede oplossing.

Laten we eerst een schermsessie starten in een terminal, zodat we wachtwoorden kunnen invoeren en zien hoe deze reageert:

scherm /dev/ttyUSB0 115200

Laten we OpenOCD en GDB starten in een tweede terminal, zoals we aan het begin van het hoofdstuk deden, en rondneuzen:

openocd -f ./ftdi2232h.cfg.tcl -f ./clone_CSK.cfg & gdb-multiarch -x ./gdbinit
#openocd gelanceerd
[...]
doel gestopt vanwege foutopsporingsverzoek, huidige modus: thread xPSR: 0x01000000 pc: 0x080013b8 msp: 0x20005000
[...]

En... en verdomme! Het geeft me geen controle terug! Geen probleem als dat jou overkomt - een beetje CTRL + C geeft je direct de controle terug:

^C
Programma ontvangen signaal SIGINT, Interrupt.
0x080003aa in ?? ()
(GDB)

Na onze CTRL + C (^c), gdb vertelt ons dat de uitvoering is gestopt op adres 0x080003aa in een onbekende functie (??).

Afhankelijk van uw specifieke staat, kunt u op een ander adres breken.

Geen paniek - zet je denkhoed op en neem je handdoek (altijd) mee.

Dit is geen probleem. De kans is groot dat u heel dicht bij dit adres inbreekt, aangezien het zich in de wachtende lus bevindt die de LED laat knipperen, wachtend op een wachtwoord dat op de seriële interface wordt ontvangen.

Laten we eerst eens kijken naar onze registers:

(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
[...]

We zien dat de pc inderdaad is waar hij hoort te zijn, alles ziet er prima en dandy uit. Laten we nu proberen een wachtwoord in te voeren.

En... niets werkt op het seriële interfacevenster! Denkhoed op... GDB blokkeert eigenlijk de uitvoering van de code; de seriële interface reageert niet op uw invoer. Dit is normaal.

Laten we het dus laten doorgaan (voortzetten or c in de gdb venster) en kijk of het serienummer nu werkt. Ja dat doet het. Laten we het nog een keer onderbreken en een breekpunt plaatsen op het adres van de wachtwoordvalidatiefunctie, zullen we?

In Ghidra kunnen we zien dat het adres van de eerste instructie van de functie is 0x080002b0:

Screenshot of finding a function address in Ghidra
Afbeelding 12.3 — Een functieadres zoeken in Ghidra

Laten we daar een breekpunt plaatsen, laten we gdb hervat de uitvoering en voer een dummy-wachtwoord in:

(gdb)b * 0x080002b0
#1
Breekpunt 1 bij 0x80002b0
#2
(gdb) c
#3
Doorgaan.
Opmerking: automatisch hardware-onderbrekingspunten gebruiken voor alleen-lezen adressen.
#4
[voer 'aaa' in de seriële console in en voer in]
Breekpunt 1, 0x080002b0 in ?? ()
#5
(GDB)

Laten we dat eens ontleden:

  • b * 0x080002b0 vraagt gdb om een ​​breekpunt te plaatsen op de instructie die is opgeslagen op het adres 0x080002b0. Controleer uw aanwijzingen.
  • gdb vertelt me, oké, ik heb daar een breekpunt geplaatst.
  • Ga alsjeblieft door met de executie, mijn liefste gdb en het zegt dat het blij is om dat te doen.
  • MAAR het meldt me dat het niet op adres kan schrijven 0x080002b0 (het is in flash en flash kan niet zomaar worden geschreven; het moet worden ontgrendeld en stuk voor stuk geschreven). Om te voorkomen dat je zoveel heen en weer moet doen, worden ARM-chips geleverd met een aantal interne foutopsporingssystemen die het mogelijk maken om te breken wanneer de pc specifieke adressen raakt waarnaar niet gemakkelijk kan worden geschreven).
  • Bam! Het breekpunt is bereikt! De uitvoering wordt gestopt nadat ik een dummy-wachtwoord heb ingevoerd.

Oké, wat kunnen we daar nu mee?

Allereerst, als u zich de code van de validatiefunctie herinnert, werden de argumenten rechtstreeks doorgegeven aan de gedecodeerde code. Laten we eens kijken wat ze kunnen zijn (denk aan de aanroepconventie voor functies: argumenten zijn in r0-3):

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

Het eerste argument is iets in RAM en het tweede is een soort waarde. (Dit is de getransformeerde UUID-waarde voor uw chip, die u hebt genoteerd, toch?)

Wat wordt er op dit eerste adres opgeslagen? Laten we het onderzoeken:

(gdb) x/x 0x20000028
0x20000028: 0x00616161
(gdb) x/s 0x20000028
0x20000028: "aa"

Ah! Ah! Ah! (Zie je wat ik daar deed?) Dit is ons wachtwoord. Let op het gebruik van de formaatmodificator voor het x-commando.

Dit wordt dus verwacht.

Laten we nu eens kijken naar de ontcijferde code.

Ghidra vertelt ons dat de instructie die volgt op de decoderingslussen is 0x080002f0. Laten we daar pauzeren:

(gdb)b * 0x080002f0
Breekpunt 2 op 0x80002f0
(gdb) c
Doorgaan.
Breekpunt 2, 0x080002f0 in ?? ()
(gdb) c
(gdb) x/4i $st
=> 0x80002f0: bewegingen r0, #0
   0x80002f2: bxr3
   0x80002f4: mov r3, r0
   0x80002f6: mov r0, r3

Het adres van de ontcijferde code staat dus in r3. We zagen dat de buffer was 0x47 (71) lang. We staan ​​in duimmodus (dus maat 2 instructies). Dit zou 47/2 moeten zijn: ongeveer 35 instructies. Het laatste stukje van het adres is voor de modus; daar kunnen we vanaf komen:

(gdb) x/35i ($r3 & (~1))
   0x20000128: druk {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: ofrs r5, r6
   0x2000013c: bewegingen r6, #255; 0xff
   0x2000013e: ands r5, r6
   0x20000140: bewegingen r6, #15
   0x20000142: mov r8, r4
   0x20000144: mov r7, r8
   0x20000146: ands r7, r6
   0x20000148: voeg r6, pc, #16 toe; (adr r6, 0x2000015c) #1
   0x2000014a: ldrb r6, [r6, r7]
   0x2000014c: eors r5, r6
   0x2000014e: voegt r0, r0, r5 toe
   0x20000150: voegt r4, #1 toe
   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: ondertitels r2, #55; 0x37
   0x20000164: ldr r4, [r2, r5]
   0x20000166: ldr r5, [r1, #100] ; 0x64
   0x20000168: voeg r3, r12 toe
   0x2000016a: voegt r4, #68 toe; 0x44
   0x2000016c: vqadd.u8 q0, q8,

Dat lijkt er meer op! We zien een normale functieprelude (opslaan van intra-functieregisters in de stapel), enige verwerking en een functieterugkeer. Maar GDB waarschuwt ons voor illegale instructieparameters (0x2000016c).

Als we naar de lijst kijken, zien we dat GDB het gebruik van een pc-relatief stuk gegevens aangeeft:

#1: commentaar: adr r6, 0x2000015c)

Dit wordt heel vaak gebruikt om gegevens op te slaan in een assemblageprogramma. adr is een pseudo-instructie die de assembler vertelt om de offset toe te voegen aan een label (een benoemde positie) in de code.

Laten we eens kijken wat daar is opgeslagen:

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

Dit is inderdaad een tekenreeks die op de een of andere manier in het proces wordt gebruikt.

Laten we de eerste instructies doorlopen, als voorbeeld van hoe een uitvoeringsstroom moet worden gevolgd. We gaan eerst opzetten gdb dus het toont ons de interessante registers, inhoud op elke stap:

(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 $st
=> 0x80002f0: bewegingen r0, #0
=> 0x80002f2: blx r3

Nu zijn we klaar voor gebruik stapi (stap instructie) om te zien wat er aan de hand is:

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

Deze nullen r4, r3 en r5 (x^x = 0):

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

Hiermee wordt het eerste teken van de wachtwoordreeks geladen r5 (r1 is het adres en r4 wordt op dit punt op nul gezet) en kopieert het naar r8 en r6:

0x20000136: lsrs r6, r6, #4
0x20000138: lsls r5, r5, #4
0x2000013a: ofrs r5, r6
0x2000013c: bewegingen r6, #255; 0xff
0x2000013e: ands r5, r6

Dit verschuift r6 4 bits naar rechts, r5 4 bits naar links en voert hun ORed-waarde in r4. Vervolgens maskeert het het ORed-resultaat met 0xff, in feite de 4 lagere en 4 hogere bits van het wachtwoordteken uitwisselen en de overtollige bits opschonen!

0x20000140: bewegingen r6, #15
0x20000142: mov r8, r4
0x20000144: mov r7, r8
0x20000146: ands r7, r6

Dit beweegt 15 inch r6, kopieën r4 in r8 en r7, en maskers r7 met 15. Maar waarom? Op dit punt, r4 is 0! Dit kan later worden gebruikt - aangezien we dat hebben gezien r4 werd gebruikt als compensatie voor het laden van het wachtwoordteken, r4 is waarschijnlijk een teller! Als dat het geval is, kan deze maskering gebruikt worden als een soort modulo... (het is heel gebruikelijk om maskering te gebruiken voor modulo een macht van twee -1):

0x20000148: voeg r6, pc, #16 toe; (adr r6, 0x2000015c)
0x2000014a: ldrb r6, [r6, r7]

Dit laadt het eerste teken van de tekenreeks waarin was verborgen r6 en gebruikt r7 en een tegenprestatie! r4 is zeker een teller hier en r7 een gemoduleerde versie ervan. Dit is een heel typische programmeermanier om dit te benaderen:

0x2000014c: eors r5, r6
0x2000014e: voegt r0, r0, r5 toe
0x20000150: voegt r4, #1 toe

Dit is XORing van de waarde van het bit-verwisselde wachtwoordteken met de huidige rangen van de vreemde tekenreeks, en voegt dit toe aan r0 en het verhogen van de r4 balie:

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

Dit laadt een nieuw wachtwoordteken met de nieuwe offset r5. r3 is 0 dus de cmp controleert r5-r3 en wacht … bgt.n? Wat is dat? Weet jij nog wat je moet doen als je twijfelt? Lees hier de documentatie: https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/condition-codes-1-condition-flags-and-codes.

Dus, het springt als r5 > r3. En r3 is 0, Dus? Dit is testen voor een 0 beëindigde tekenreeks!

Dit is de belangrijkste logische lus voor validatie!

Zodra dit is gebeurd, doet het dit:

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

Het XORs deze som met de UUID, afhankelijk van de berekende waarde, herstelt de registerwaarden van de beller en retourneert deze waarde. De C-code controleert vervolgens of deze waarde null is om de winnende reeks daadwerkelijk weer te geven. We hoeven het dan alleen maar zo te regelen dat onze som gelijk is aan de UUID-afhankelijke waarde om de XOR nul te laten zijn!

We hebben de hele logica!

spot_img

Laatste intelligentie

spot_img