Practical Binary Analysis – Walkthrough Crackme – Nivelul 2

  • 4 minute de citit
  • 8 exemple de cod

Disponibil și în English

Am terminat recent de citit cartea Practical Binary Analysis, pe care o consider o carte de ultimă generație (recenzia va apărea în curând), și aș vrea să îmi public soluția pentru crackme-ul din capitolul 5.

1. Un nou challenge CTF. Rezolvă noul challenge CTF deblocat de programul oracle! Poți rezolva întregul challenge folosind doar uneltele discutate în acest capitol și ce ai învățat în Capitolul 2. După ce termini challenge-ul, nu uita să dai flag-ul găsit oracle-ului ca să deblochezi următorul challenge

Ca să rezolvi crackme-ul și să vezi codul prezentat în carte nu trebuie să o cumperi, o poți descărca gratuit de pe site-ul oficial. Dacă nu folosești un sistem Linux, îți recomand să descarci VM-ul care vine cu cartea de aici:

În capitolul 5, autorul analizează primul nivel al crackme-ului folosind doar unelte Linux și explicând fiecare pas în detaliu; eu voi trece peste primul nivel și voi merge direct la nivelul 2.

Hai să începem: descarcă codul sau VM-ul și navighează în directorul chapter5. Dacă nu vezi binarul lvl2, îl poți debloca executând în terminal următoarea comandă:

./oracle 84b34c124b2ba5ca224af8e33b077e9e

Să vedem despre ce este vorba cu acest binar. Putem identifica, cu comanda file, că este un binar ELF. Știind că este ELF, putem rula readelf -a lvl2 > lvl2_readelf.txt asupra lui, ceea ce ne dă următorul output:

ELF Header:  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00   Class:                             ELF64  Data:                              2's complement, little endian  Version:                           1 (current)  OS/ABI:                            UNIX - System V  ABI Version:                       0  Type:                              EXEC (Executable file)  Machine:                           Advanced Micro Devices X86-64  Version:                           0x1  Entry point address:               0x400540 <- this is what we need.  Start of program headers:          64 (bytes into file)  Start of section headers:          4624 (bytes into file)  Flags:                             0x0  Size of this header:               64 (bytes)  Size of program headers:           56 (bytes)  Number of program headers:         9  Size of section headers:           64 (bytes)  Number of section headers:         29  Section header string table index: 28  ...

Dacă rulăm binarul primim doar un byte aleatoriu pe ecran. Să deschidem gdb și să setăm un breakpoint la adresa entry point-ului, scriind următoarele comenzi:

gdb lvl2b * 0x400540 r # Rulează programul. Se va opri acum la breakpoint-ul pe care l-am setat.set disassembly-flavor intel # Afișează disassembly-ul în sintaxa intel.disassemble # În mod normal, aceasta ar afișa disassembly-ul# În acest caz nu funcționează; putem vedea instrucțiunea la care suntem# folosind x/i $pc. $pc este registrul program counter.x/i $pc# => 0x400540:	xor    ebp,ebpdisplay/i $pc # Aceasta face ca GDB să afișeze automat următoarea instrucțiune de executat, de fiecare dată când programul se oprește.si step instructionsisi...q # Pare că nu funcționează; hai să ieșim și să vedem ce altceva putem face.

Dacă rulăm ltrace ./lvl2 putem vedea apelurile de bibliotecă pe care le face programul:

__libc_start_main(0x400500, 1, 0x7fff5c6b0ec8, 0x400640 <unfinished ...>time(0)                                                      = 1543667140srand(0x5c027dc4, 0x7fff5c6b0ec8, 0x7fff5c6b0ed8, 0)         = 0rand(0x7ff89538f620, 0x7fff5c6b0dac, 0x7ff89538f0a4, 0x7ff89538f11c) = 0x6e66a24aputs("d6"d6)                                                   = 3+++ exited (status 0) +++

Dacă căutăm __libc_start_main( pe Google, vom găsi:

int __libc_start_main(int *(main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end));

Funcția __libc_start_main() inițializează procesul, apelează funcția main cu argumentele potrivite și gestionează returnarea din main().

http://refspecs.linuxbase.org/LSB_3.0.0/LSB-PDA/LSB-PDA/baselib—libc-start-main-.html

Ceea ce înseamnă că prima adresă este pointerul către funcția main. Din apelul nostru ltrace avem: 0x400500

Dacă dezasamblăm binarul lvl2 cu: objdump -d -M intel lvl2 > lvl2_dasm.txt putem vedea că adresa pe care o căutăm se află la începutul secțiunii .text.

 0000000000400500 <.text>:  400500:	48 83 ec 08          	sub    rsp,0x8  400504:	31 ff                	xor    edi,edi  400506:	e8 c5 ff ff ff       	call   4004d0 <time@plt>  40050b:	89 c7                	mov    edi,eax  40050d:	e8 ae ff ff ff       	call   4004c0 <srand@plt>  400512:	e8 c9 ff ff ff       	call   4004e0 <rand@plt>  400517:	99                   	cdq      400518:	c1 ea 1c             	shr    edx,0x1c  40051b:	01 d0                	add    eax,edx  40051d:	83 e0 0f             	and    eax,0xf  400520:	29 d0                	sub    eax,edx  400522:	48 98                	cdqe     400524:	48 8b 3c c5 60 10 60 	mov    rdi,QWORD PTR [rax*8+0x601060]  40052b:	00   40052c:	e8 6f ff ff ff       	call   4004a0 <puts@plt>  400531:	31 c0                	xor    eax,eax  400533:	48 83 c4 08          	add    rsp,0x8  400537:	c3                   	ret      400538:	0f 1f 84 00 00 00 00 	nop    DWORD PTR [rax+rax*1+0x0]  40053f:	00 

Ce face binarul? Face un srand(time(0)) și apelează rand(), face niște calcule și afișează ceva din QWORD PTR [rax*8+0x601060]. Dacă ne întoarcem la debugger sau ne uităm din nou la output-ul readelf, putem identifica că 0x601060 nu este departe de secțiunea .data, care începe la 0x601040. Dacă descărcăm secțiunea obținem următoarele:

binary@binary-VirtualBox:~/code/chapter5$ objdump -s --section .data lvl2lvl2:     file format elf64-x86-64Contents of section .data: 601040 00000000 00000000 00000000 00000000  ................ 601050 00000000 00000000 00000000 00000000  ................# Aici ar trebui să ne uităm 601060 c4064000 00000000 c7064000 00000000  ..@.......@.....# Adresele sunt în hexazecimal, deci dacă adunăm 16 octeți (0x10 în hex) la 601060 obținem 601070; un octet este reprezentat prin două cifre hexazecimale alăturate, ca 0xCA 601070 ca064000 00000000 cd064000 00000000  ..@.......@..... 601080 d0064000 00000000 d3064000 00000000  ..@.......@..... 601090 d6064000 00000000 d9064000 00000000  ..@.......@..... 6010a0 dc064000 00000000 df064000 00000000  ..@.......@..... 6010b0 e2064000 00000000 e5064000 00000000  ..@.......@..... 6010c0 e8064000 00000000 eb064000 00000000  ..@.......@..... 6010d0 ee064000 00000000 f1064000 00000000  ..@.......@.....

Această secțiune pare să conțină pointeri către ceva urmat de un null: ca064000 00000000. Adresa ca064000 este în little endian; ca să o scriem în big endian o rescriem ca 004006ca, care este o adresă apropiată de secțiunea .rodata. Hai să o descărcăm:

[16] .rodata PROGBITS 00000000004006c0 000006c0 0000000000000034 0000000000000000 A 0 0 4

binary@binary-VirtualBox:~/code/chapter5$ objdump -s --section .rodata lvl2lvl2:     file format elf64-x86-64Contents of section .rodata: 4006c0 01000200 30330034 66006334 00663600  ....03.4f.c4.f6. 4006d0 61350033 36006632 00626600 37340066  a5.36.f2.bf.74.f 4006e0 38006436 00643300 38310036 63006466  8.d6.d3.81.6c.df 4006f0 00383800                             .88.

Și aceasta este cel mai probabil flag-ul pe care îl căutăm. Mulțumesc pentru lectură 😀