Description :
Le clef est le mot de passe du programme Linux suivant: crackme
Ok, the key is the password of the given crackme. We start with our binary crackme-02, let’s discover some information about it
$ file crackme-02 crackme-02: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.6.32, dynamically linked (uses shared libs), stripped $ readelf -h crackme-02 En-tête ELF: Magique: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Classe: ELF32 Données: complément à 2, système à octets de poids faible d'abord (little endian) Version: 1 (current) OS/ABI: UNIX - System V Version ABI: 0 Type: EXEC (fichier exécutable) Machine: Intel 80386 Version: 0x1 Adresse du point d'entrée: 0x8048390 Début des en-têtes de programme: 52 (octets dans le fichier) Début des en-têtes de section: 0 (octets dans le fichier) Fanions: 0x0 Taille de cet en-tête: 52 (bytes) Taille de l'en-tête du programme: 32 (bytes) Nombre d'en-tête du programme: 8 Taille des en-têtes de section: 40 (bytes) Nombre d'en-têtes de section: 0 Table d'indexes des chaînes d'en-tête de section: 0
It’s a stripped binary, meaning there are no symbols we can use. We need to find the entry point in order to break at the beginning of the binary execution flow. readelf command makes it easy to find it (as show above). We then load our binary into gdb and break on the EP (Entry Point)
$ gdb crackme-02 (gdb) break *0x8048390
Since we don’t have any symbols, we can’t output main function disassembly. We need to use this command in order to print assembly code :
x/10i $pc
This command displays the 1st ten instructions starting at $pc (Program Counter, equivalent of EIP register)
(gdb) r Starting program: /home/betezed/cm2 Breakpoint 1, 0x08048390 in ?? () (gdb) x/14i $pc => 0x8048390: xor %ebp,%ebp 0x8048392: pop %esi 0x8048393: mov %esp,%ecx 0x8048395: and $0xfffffff0,%esp 0x8048398: push %eax 0x8048399: push %esp 0x804839a: push %edx 0x804839b: push $0x8048680 0x80483a0: push $0x8048610 0x80483a5: push %ecx 0x80483a6: push %esi 0x80483a7: push $0x8048505 (1) 0x80483ac: call 0x8048370 (2) 0x80483b1: hlt
We find ourselves at the very beginning of our binary execution flow. These lines prepare __libc_start_main call. In the man page, we can see that the first argument of this function is a pointer to main function. That’s why we can see our main function address in (1) , last pushed on the stack (since it’s the first argument of __libc_start_main), then __libc_start_main is called in (2). We now know that main starts at 0x8048505. We break on this address, keep on executing the binary, and display main instructions
(gdb) b *0x8048505 Breakpoint 2 at 0x8048505 (gdb) c Continuing. Breakpoint 2, 0x08048505 in ?? () (gdb) set disassembly-flavor intel (gdb) x/40i $pc => 0x8048505: mov ebp,esp 0x8048507: sub esp,0xc 0x804850a: and esp,0xfffffff0 0x804850d: add esp,0xc 0x8048510: push 0x0 0x8048512: push 0x1 0x8048514: push 0x0 0x8048516: call 0x8048380 0x804851b: mov esp,ebp 0x804851d: call 0x8048522 0x8048522: pop ebx 0x8048523: add ebx,0x7 0x8048526: jmp ebx 0x8048528: call 0xce0bfa6d 0x804852d: add eax,0x8048595 0x8048532: nop 0x8048533: mov ebp,esp 0x8048535: sub esp,0x4 0x8048538: and esp,0xfffffff0 0x804853b: add esp,0x4 0x804853e: push 0x8048490 0x8048543: call 0x8048340 // printf("Enter password:\n") (1) 0x8048548: mov esp,ebp 0x804854a: mov ebp,esp 0x804854c: sub esp,0xc 0x804854f: and esp,0xfffffff0 0x8048552: add esp,0xc 0x8048555: push 0xb 0x8048557: push 0x80484a1 0x804855c: push 0x0 0x804855e: call 0x8048330 // read(0, 0x80484a1, 11) (2) 0x8048563: mov esp,ebp 0x8048565: mov edx,eax 0x8048567: dec edx 0x8048568: mov BYTE PTR [eax+0x80484a0],0x0 0x804856f: mov esi,0x80485a4 0x8048574: mov edi,esi 0x8048576: mov ecx,0x15 0x804857b: lods al,BYTE PTR ds:[esi] // LOOP : Loop start (3) 0x804857c: xor al,0xaa 0x804857e: call 0x8048583 0x8048583: pop ebx 0x8048584: add ebx,0x7 0x8048587: jmp ebx 0x8048589: call 0x8d9cc30e 0x804858e: add al,0x8 0x8048590: int3 0x8048591: jne 0x8048595 0x8048593: inc al 0x8048595: stos BYTE PTR es:[edi],al 0x8048596: loop 0x804857b // Loop to LOOP 0x8048598: mov esi,0x80484a1 0x804859d: mov edi,0x80485ae 0x80485a2: mov ecx,edx 0x80485a4: push es 0x80485a5: xchg BYTE PTR [eax+0x4880df04],ch 0x80485ab: push edx 0x80485ac: inc ecx 0x80485ad: mov eax,ds:0xcefedefb 0x80485b2: out dx,eax 0x80485b3: cdq 0x80485b4: int3 0x80485b5: retf 0x80485b6: cwde 0x80485b7: enter 0x89aa,0xe5 // Start of call to /bin/sh 0x80485bb: sub esp,0x4 0x80485be: and esp,0xfffffff0 0x80485c1: add esp,0x4 0x80485c4: push 0x80485ef 0x80485c9: call 0x8048350 0x80485ce: mov esp,ebp 0x80485d0: xor eax,eax 0x80485d2: jmp 0x80485ee // End of call to /bin/sh 0x80485d4: mov ebp,esp // Start of call to printf "Bad Password\n" 0x80485d6: sub esp,0x4 0x80485d9: and esp,0xfffffff0 0x80485dc: add esp,0x4 0x80485df: push 0x80485f7 0x80485e4: call 0x8048340 0x80485e9: mov esp,ebp 0x80485eb: xor eax,eax 0x80485ed: inc eax 0x80485ee: ret // End of call to printf "Bad Password\n"
We switch to intel syntax, since it cleaner. We see that (1) Call to printf function :
0x804853e: push 0x8048490 0x8048543: call 0x8048340
which prints
(gdb) x/s 0x8048490 0x8048490: "Enter password:\n"
(2) Call to read function :
0x8048555: push 0xb 0x8048557: push 0x80484a1 0x804855c: push 0x0 0x804855e: call 0x8048330 // read call, 0xb is password length
Our buffer sent to read will be saved in memory at address 0x080484a1. The argument 0x0 pushed on the stack is the file descriptor (stdin) If we break after the read function and display our buffer (we sent some ‘AAA…’), we can find them at expected address.
(gdb) x/s 0x80484a1 0x8 0484a1: 'A'
So far, so good. Later on, we have different bloc of instructions
0x80485b9: mov ebp,esp 0x80485bb: sub esp,0x4 0x80485be: and esp,0xfffffff0 0x80485c1: add esp,0x4 0x80485c4: push 0x80485ef 0x80485c9: call 0x8048350 0x80485ce: mov esp,ebp 0x80485d0: xor eax,eax 0x80485d2: jmp 0x80485ee
Which calls /bin/sh
(gdb) x/s 0x80485ef >> "/bin/sh"
And this bloc
0x80485d4: mov ebp,esp 0x80485d6: sub esp,0x4 0x80485d9: and esp,0xfffffff0 0x80485dc: add esp,0x4 0x80485df: push 0x80485f7 0x80485e4: call 0x8048340 0x80485e9: mov esp,ebp 0x80485eb: xor eax,eax 0x80485ed: inc eax 0x80485ee: ret
Which calls printf(‘Bad password\n’)
(gdb) x/s 0x80485f7 >> "Bad password\n"
The trick is to find where is the condition making the execution flow jumping to one or the other bloc. In (3), there is a loop, modifying following code. If we break after the loop, we find this :
0x8048598: mov esi,0x80484a1 0x804859d: mov edi,0x80485ae 0x80485a2: mov ecx,edx 0x80485a4: lods al,BYTE PTR ds:[esi] 0x80485a5: sub al,0x2 0x80485a7: scas al,BYTE PTR es:[edi] 0x80485a8: jne 0x80485d4 Pointer to printf('Bad password\n') 0x80485aa: loop 0x80485a4 Loop on our buffer caracters 0x80485ac: jmp 0x80485b9 If we didn't jump to "Bad password", we jump to '/bin/sh'
When we reach the beginning of the loop
(gdb) x/8i $pc => 0x80485a4: lods al,BYTE PTR ds:[esi] 0x80485a5: sub al,0x2 0x80485a7: scas al,BYTE PTR es:[edi] 0x80485a8: jne 0x80485d4 0x80485aa: loop 0x80485a4 0x80485ac: jmp 0x80485b9 0x80485ae: push ecx 0x80485af: je 0x8048605 (gdb) i r $ecx ecx 0xa 10
ECX is the loop counter, it equals 10 (11 is the null byte). It means that each character will be loaded in al for each loop, 0x2 will be subtracted from it, and then it will be compared from the corresponding character stored in EDI. If we check EDI’s content
(gdb) x/s $edi 0x80485ae: "QtTdE3fa2b"
We then guess that the password is the content of EDI, but each letter must be incremented by 2 FLAG : SvVfG5hc4d
Bypass ptrace protection
In order to bypass segfault encountered in debug in gdb, we can create our ptrace function, always returning 0, and then load it (by giving it to LD_PRELOAD environment variable)
$ vi ptrace.c #include #include int ptrace(int i, int j, int k, int l) { return 0; } $ sudo gcc -m32 -shared ptrace.c -o ptrace.so $ gdb crackme-02 (gdb) set environment LD_PRELOAD ./ptrace.so (gdb) run
And binary will be executed without any problem.