Root Me: ELF-CrackPass cu Ghidra

  • 8 minute de citit
  • 8 exemple de cod
  • 6 imagini

Disponibil și în English

Pe această pagină

Salut!

Am jucat în ultima vreme puțin cu Ghidra, un instrument de reverse engineering care a fost recent publicat ca open source de NSA. Pe site-ul oficial, instrumentul este descris astfel:

O suită de instrumente pentru software reverse engineering (SRE), dezvoltată de Research Directorate al NSA în sprijinul misiunii de securitate cibernetică

Sunt la începutul carierei mele în reverse engineering, nu am făcut nimic avansat și nu știu ce funcționalități să aștept de la un instrument profesionist ca acesta. Dacă vrei să citești despre funcționalitățile avansate ale Ghidra, îmi pare rău. :/

În acest articol voi încerca să rezolv o provocare simplă de tip crackme, pe care am găsit-o pe site-ul root-me. Provocarea pe care o rezolv se numește ELF – CrackPass , iar dacă vrei să o încerci singur, atunci ar fi bine să nu citești acest articol, pentru că îți voi strica provocarea.

Să începem! Deschid Ghidra și creez un nou Project pe care îl numesc RootMe.

În Ghidra trebuie să creăm un Project, nu lucrăm cu fișiere individuale ca în alte disassemblere. Apoi import fișierul provocării trăgându-l în folderul proiectului; merg cu valorile implicite.

După ce apare o fereastră cu informații despre fișierul binar, apăs OK, selectez fișierul și fac dublu click pe el. Aceasta deschide utilitarul Code Browser al Ghidra și întreabă dacă vreau să analizez fișierul; apăs Yes și merg mai departe cu valorile implicite.

După ce importăm fișierul, primim niște informații despre fișierul binar. Apăs OK, închid această fereastră și apoi fac dublu click pe fișierul importat, ceea ce deschide utilitarul Code Browser al Ghidra. Aleg Yes când sunt întrebat dacă vreau să analizez fișierul binar și merg mai departe cu valorile implicite.

Code Browser este destul de convenabil. În panoul din stânga vedem vizualizarea disassembly, iar în panoul din dreapta vizualizarea decompile.

Ghidra ne arată direct informațiile din antetul ELF și punctul de intrare al fișierului binar; după ce fac dublu click pe punctul de intrare, vizualizarea disassembler sare la funcția de intrare. Acum putem identifica cu succes funcția main, pe care o redenumesc în main. Ar fi frumos ca instrumentul să încerce să detecteze automat funcția main și să o redenumească corespunzător.

Înainte de a analiza funcția main am vrut să îi schimb semnătura. Am schimbat tipul returnat în int și am corectat tipul și numele parametrilor. Această modificare a avut efect în vizualizarea decompile. Ceea ce este grozav!

Evidențierea unei linii în vizualizarea decompile evidențiază și linia corespunzătoare în vizualizarea de assembly.

Să explorăm funcția FUN_080485a5, pe care o voi redenumi în CheckPassword.

Codul funcției CheckPassword poate fi găsit mai jos. Am copiat codul direct din vizualizarea decompile a Ghidra, ceea ce este o funcționalitate utilă pe care multe instrumente de acest tip nu o au! Să poți copia assembly și cod este o funcționalitate plăcută, dar nu esențială!

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

După ce m-am uitat la cod, am ajuns la următoarele concluzii. Blocul cu if verifică dacă utilizatorul a furnizat o parolă și inspectează parola furnizată pentru a verifica dacă este un caracter valid sau ceva asemănător. Nu sunt sigur exact ce verifică, dar iată ce spune documentația funcției __ctype_b_loc():

The __ctype_b_loc() function shall return a pointer into an array of characters in the current locale that contains characteristics for each character in the current character set. The array shall contain a total of ```384``` characters, and can be indexed with any signed or unsigned char (i.e. with an index value between ```-128```and ```255```). If the application is multithreaded, the array shall be local to the current thread.<br></br>

Oricum, acel bloc de cod nu merită cu adevărat timpul, pentru că nu modifică parola în niciun fel, doar o verifică, iar noi putem sări peste acest tip de verificare.

Următoarea funcție apelată este FUN_080484f4. Privind codul ei, putem spune că este doar o implementare personalizată a memcopy. În loc să copiez codul C din vizualizarea decompile, am copiat codul assembly; da, asta este distractiv.

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

Iar cealaltă funcție, FUN_0804851c, generează parola din șirul „THEPASSWORDISEASYTOCRACK”. Privind vizualizarea decompile putem vedea aproximativ cum funcționează această funcție; dacă nu am avea-o, ar trebui să analizez manual fiecare instrucțiune assembly din funcție ca să înțeleg ce face.

Apoi comparăm parola generată anterior cu parola pe care am primit-o de la utilizator (primul argument, argv[1]). Dacă se potrivesc, programul spune bravo și o afișează, altfel afișează un mesaj de eroare.

Din această analiză de bază putem concluziona că, dacă modificăm programul în câteva locuri, îl putem face să afișeze parola fără să fie nevoie să facem reverse la vreo funcție C și să scriem cod. Modificarea programului înseamnă schimbarea unora dintre instrucțiunile lui.

Să vedem ce trebuie să modificăm:

La adresa 0x0804868c – Schimbăm instrucțiunea JNS într-un JMP. Și voilà, modificarea se reflectă în vizualizarea decompile. Verificarea rezultatului ptrace este ocolită.

{   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; }

La adresa 0x080485b8 – Schimbăm instrucțiunea JZ într-un JMP. Ocolim blocul de verificare a parolei pe care l-am văzut mai devreme.

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; }

La adresa 0x0804861e – Schimbăm JNZ în JZ. Aceasta inversează condiția if/else și, deoarece nu cunoaștem parola, vom trimite o parolă aleatorie care nu este egală cu cea generată, executând astfel printf din blocul else.

void CheckPassword(undefined4 param_1) {   int iVar1;   char local_108c [128];   char local_100c [4096];   CustomCopy(local_100c,param_1);   // construiește parola din șiruri și o stochează în   // local_108c    GeneratePassword(s_THEPASSWORDISEASYTOCRACK_08049960,local_108c);   iVar1 = strcmp(local_108c,local_100c);   if (iVar1 == 0) { // parolele sunt egale     puts("Is not the good password !");   }   else {     printf("Good work, the password is : \n\n%s\n",local_108c);   }   return; }

Atât!

Acum rulăm programul; în alte instrumente doar salvăm fișierul și funcționează, dar în Ghidra pare că trebuie să îl exportăm.

Ca să exportăm programul, mergem la File -> Export Program (O). Schimbăm formatul în binary și apăsăm OK.

Am primit programul exportat pe desktop, dar nu a funcționat; nu am reușit să rulez programul exportat. După ce am încercat să citesc antetul lui cu programul readelf -h, am primit următorul rezultat:

root@DESKTOP:/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

Păcat. Se pare că Ghidra a stricat antetul fișierului… iar acum nu vreau să repar manual antetele, așa că am pornit Binary Ninja, am aplicat aceleași modificări asupra fișierului, l-am salvat, l-am rulat cu un argument aleatoriu și am validat flag-ul.

Concluzii

Ghidra este un instrument bun, cu mult potențial; în starea sa actuală nu este chiar grozav, dar funcționează. De asemenea, am întâlnit un bug ciudat de derulare când l-am rulat pe laptopul meu.

Alternativele ar fi să plătești $$ pentru alte instrumente de acest tip, să îți faci propriile instrumente sau să lucrezi cu instrumente gratuite, dar mai puțin prietenoase cu utilizatorul.

Să sperăm că, odată ce codul va fi publicat, comunitatea va începe să facă remedieri și să îmbunătățească Ghidra.

Mulțumesc pentru lectură!