Hoe ik een simpele CrackMe-uitdaging oploste met de NSA's Ghidra

Hallo!

Ik speel de laatste tijd een beetje met Ghidra, een reverse engineering-tool die onlangs open source is gemaakt door de NSA. De officiële website beschrijft de tool als:

Een software reverse engineering (SRE) suite met tools ontwikkeld door NSA's Research Directorate ter ondersteuning van de Cybersecurity-missie.

Ik sta aan het begin van mijn reverse engineering-carrière, dus ik heb niets geavanceerds gedaan. Ik weet niet welke functies ik kan verwachten van een professionele tool als deze, als je meer wilt lezen over geavanceerde Ghidra-functies, dan is dit waarschijnlijk niet het artikel voor jou.

In dit artikel zal ik proberen een eenvoudige CrackMe-uitdaging op te lossen die ik op de website root-me heb gevonden. De uitdaging die ik aan het oplossen ben, heet ELF - CrackPass. Als je het zelf wilt proberen, overweeg dan om dit artikel niet te lezen, omdat het de uitdaging van je zal bederven.

Laten we beginnen! Ik open Ghidra en creëer een nieuw project dat ik RootMe noem.

Vervolgens importeer ik het challenge-bestand door het naar de projectmap te slepen. Ik ga met de standaardinstellingen.

Nadat ik wat informatie over het binaire bestand heb gekregen, druk ik op OK, selecteer het bestand en dubbelklik erop. Dit opent Ghidra's codebrowser en vraagt ​​of ik het bestand wil analyseren, dan druk ik op Ja en ga verder met de standaardinstellingen.

Nadat we het bestand hebben geïmporteerd, krijgen we wat informatie over het binaire bestand. Als we op OK drukken en dit venster sluiten en vervolgens dubbelklikken op het bestand dat we hebben geïmporteerd, wordt Ghidra's codebrowser geopend. Ik selecteer Ja als daarom wordt gevraagd om het binaire bestand te analyseren en door te gaan met de standaardinstellingen.

De codebrowser is best handig. In het linkerpaneel krijgen we de demontageweergave te zien en in het rechterpaneel de decompileerweergave.

Ghidra toont ons direct de ELF-headerinfo en het ingangspunt van het binaire bestand. Na dubbelklikken op het invoerpunt, springt de dissembler-weergave naar de invoerfunctie.

Nu kunnen we met succes de hoofdfunctie identificeren, die ik hernoem naar main. Het zou mooi zijn als de tool zou proberen om de hoofdfunctie automatisch te detecteren en dienovereenkomstig een andere naam te geven.

Voordat ik de hoofdfunctie analyseerde, wilde ik de handtekening ervan wijzigen. Ik heb het retourtype gewijzigd in int en het type en de naam van de parameters gecorrigeerd. Deze wijziging is van kracht geworden in de decompileerweergave, wat gaaf is! ?

Als u een regel markeert in de decompileerweergave, wordt deze ook gemarkeerd in de assembly-weergave.

Laten we de functie FUN_080485a5 verkennen, die ik zal hernoemen in CheckPassword.

De inhoud van de functie CheckPassword vindt u hieronder. Ik heb de code rechtstreeks uit Ghidra's decompileerweergave gekopieerd, wat een handige functie is die bij veel van dit type tools ontbreekt! In staat zijn om assembly en code te kopiëren is een leuke functie.

void CheckPassword(char *param_1) { ushort **ppuVar1; int iVar2; char *pcVar3; char cVar4; char local_108c [128]; char local_100c [4096]; cVar4 = param_1; if (cVar4 != 0) { ppuVar1 = __ctype_b_loc(); pcVar3 = param_1; do { if (((byte )(ppuVar1 + (int)cVar4) & 8) == 0) { puts("Bad password !"); /* WARNING: Subroutine does not return */ abort(); } cVar4 = pcVar3[1]; pcVar3 = pcVar3 + 1; } while (cVar4 != 0); } FUN_080484f4(local_100c,param_1); FUN_0804851c(s_THEPASSWORDISEASYTOCRACK_08049960,local_108c); iVar2 = strcmp(local_108c,local_100c); if (iVar2 == 0) { printf("Good work, the password is : \n\n%s\n",local_108c); } else { puts("Is not the good password !"); } return; }

Nadat ik de code heb bekeken, ben ik tot de volgende conclusies gekomen. Het blok met de ifcontroleert of de gebruiker een wachtwoord heeft opgegeven en inspecteert het opgegeven wachtwoord om te controleren of het een geldig teken is of zoiets. Ik weet niet precies waar het naar controleert, maar dit is wat de documentatie van __ctype_b_loc () zegt:

De functie __ctype_b_loc () zal een pointer teruggeven in een array van tekens in de huidige landinstelling die kenmerken bevat voor elk teken in de huidige tekenset. De array zal in totaal 384 tekens bevatten en kan worden geïndexeerd met elk teken of teken zonder teken (dwz met een indexwaarde tussen 128 en 255). Als de applicatie multi-threaded is, moet de array lokaal zijn voor de huidige thread.

Hoe dan ook, dat codeblok is de tijd niet echt waard, omdat het ons wachtwoord op geen enkele manier wijzigt, het verifieert het alleen. We kunnen dit soort verificatie dus overslaan.

De volgende functie die wordt aangeroepen, is FUN_080484f4. Als we naar de code kijken, kunnen we zien dat het slechts een aangepaste memcopy-implementatie is. In plaats van de C-code uit de decompiler-weergave te kopiëren, heb ik de assembly-code gekopieerd - ja, dit is leuk.

************************************************************* * FUNCTION ************************************************************* undefined FUN_080484f4 (undefined4 param_1 , undefined4 p undefined AL:1  undefined4 Stack[0x4]:4 param_1 XREF[1]: 080484f8 (R) undefined4 Stack[0x8]:4 param_2 XREF[1]: 080484fb (R) FUN_080484f4 XREF[1]: CheckPassword:080485f5 (c) 080484f4 55 PUSH EBP 080484f5 89 e5 MOV EBP ,ESP 080484f7 53 PUSH EBX 080484f8 8b 5d 08 MOV EBX ,dword ptr [EBP + param_1 ] 080484fb 8b 4d 0c MOV ECX ,dword ptr [EBP + param_2 ] 080484fe 0f b6 11 MOVZX EDX ,byte ptr [ECX ] 08048501 84 d2 TEST DL,DL 08048503 74 14 JZ LAB_08048519 08048505 b8 00 00 MOV EAX ,0x0 00 00 LAB_0804850a XREF[1]: 08048517 (j) 0804850a 88 14 03 MOV byte ptr [EBX + EAX *0x1 ],DL 0804850d 0f b6 54 MOVZX EDX ,byte ptr [ECX + EAX *0x1 + 0x1 ] 01 01 08048512 83 c0 01 ADD EAX ,0x1 08048515 84 d2 TEST DL,DL 08048517 75 f1 JNZ LAB_0804850a LAB_08048519 XREF[1]: 08048503 (j) 08048519 5b POP EBX 0804851a 5d POP EBP 0804851b c3 RETComment: param_1 is dest, param_2 is src. 08048501 checks if src is null and if it is it returns else it initializes EAX (index, current_character) with 0. The next instructions move bytes into EBX (dest) from EDX (src).The loop stops when EDX is null.

En de andere functie FUN_0804851c genereert het wachtwoord uit de "THEPASSWORDISEASYTOCRACK" string. Kijkend naar de gedecompileerde weergave. we kunnen ongeveer zien hoe deze functie werkt. Als we dat niet hadden, zouden we elke montage-instructie van de functie handmatig moeten analyseren om te begrijpen wat deze doet.

Vervolgens vergelijken we het eerder gegenereerde wachtwoord met het wachtwoord dat we van de gebruiker hebben gekregen (het eerste argument, argv [1]). Als het overeenkomt, zegt het programma goed werk en drukt het af, anders drukt het een foutmelding af.

Uit deze basisanalyse kunnen we concluderen dat als we het programma op verschillende plaatsen patchen, we het het wachtwoord kunnen laten spugen zonder dat we een C-functie hoeven om te keren en code te schrijven. Het programma patchen betekent dat u enkele van de instructies moet wijzigen.

Laten we eens kijken wat we moeten patchen:

Op adres 0x0804868c patchen we de JNS-instructie in een JMP. En voilà, de verandering wordt weerspiegeld in de decompiler-weergave. De ptrace-resultaatcontrole wordt omzeild.

{ ptrace(PTRACE_TRACEME,0,1,0); if (argc != 2) { puts("You must give a password for use this program !"); /* WARNING: Subroutine does not return */ abort(); } CheckPassword(argv[1]); return 0;}

Op adres 0x080485b8 patchen we de JZ-instructie in een JMP. We omzeilen dat wachtwoordverificatieblok dat we eerder zagen.

void CheckPassword(undefined4 param_1) { int iVar1; char local_108c [128]; char local_100c [4096]; CustomCopy(local_100c,param_1); GeneratePassword(s_THEPASSWORDISEASYTOCRACK_08049960,local_108c); iVar1 = strcmp(local_108c,local_100c); if (iVar1 == 0) { printf("Good work, the password is : \n\n%s\n",local_108c); } else { puts("Is not the good password !"); } return; }

Op adres 0x0804861e patchen we JNZ naar JZ. Dit keert de if / else-voorwaarde om. Omdat we het wachtwoord niet kennen, gaan we een willekeurig wachtwoord indienen dat niet gelijk is aan het gegenereerde wachtwoord, waardoor de printf op het else-blok wordt uitgevoerd.

void CheckPassword(undefined4 param_1) { int iVar1; char local_108c [128]; char local_100c [4096]; CustomCopy(local_100c,param_1); // constructs the password from the strings and stores it in // local_108c GeneratePassword(s_THEPASSWORDISEASYTOCRACK_08049960,local_108c); iVar1 = strcmp(local_108c,local_100c); if (iVar1 == 0) { // passwords are equal puts("Is not the good password !"); } else { printf("Good work, the password is : \n\n%s\n",local_108c); } return; }

Dat is alles!

Nu draaien we het programma. In andere tools slaan we het bestand gewoon op en het werkt, maar in Ghidra lijkt het erop dat we het moeten exporteren.

Om het programma te exporteren, gaan we naar Bestand -> Programma exporteren (O). We veranderen het formaat naar binair en klikken op OK.

Ik krijg het geëxporteerde programma op mijn bureaublad, maar het werkt niet - het lukte me niet om het geëxporteerde programma uit te voeren. Nadat ik geprobeerd heb de header te lezen met het readelf -h programma, krijg ik de volgende output:

[email protected]:/mnt/c/users/denis/Desktop# readelf -h Crack.bin ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8048440 Start of program headers: 52 (bytes into file) Start of section headers: 2848 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 7 Size of section headers: 40 (bytes) Number of section headers: 27 Section header string table index: 26 readelf: Error: Reading 1080 bytes extends past end of file for section headers

Shame. It looks like Ghidra has messed up the file header… and, right now I don’t want to manually fix headers. So I fired up another tool and applied the same patches to the file, saved it, ran it with a random argument and validated the flag.

Conclusions

Ghidra is a nice tool with a lot of potential. In its current state, it’s not that great but it works. I’ve also encountered a weird scrolling bug while running it on my laptop.

The alternatives would be to pay $$ for other tools of this kind, make your own tools, or work with free but not so user friendly tools.

Let’s hope that once the code is released, the community will start doing fixes and improve Ghidra.

Thanks for reading!