Couple of days ago I have received a crack.me_.32 binary to hack. This was my first crackme challenge and I would like to thank Geyslan (Geyslan G Bem) for giving me this task. Also I must to say Big Thanks to Vivek Ramachandran ‘s Linux Assembly Expert (SLAE) course that gave me great Assembly understanding.
The crackme binary itself is a 7.7K Linux executable file. When you run it, it asks for a password.
[arno@centos ~]$ ./crack.me.32
Please tell me my password: idk123
No! No! No! No! Try again.
Every attempt to debug it – fails
(gdb) run
Starting program: /home/arno/crack.me.32
I'm sorry GDB! You are not allowed!
Seems it has also tracing protection
[arno@centos ~]$ strace ./crack.me.32 2>&1 |tail -3
write(1, "Tracing is not allowed... Bye\n", 30Tracing is not allowed... Bye
) = 30
exit_group(1) = ?
Neither objdump nor ndisasm could help. The crackme binary has corrupted header.
[arno@centos ~]$ objdump -d ./crack.me.32 -M intel
./crack.me.32: file format elf32-i386
[arno@centos ~]$ file crack.me.32
crack.me.32: ELF 32-bit LSB executable, Intel 80386, invalid version (SYSV), for GNU/Linux 2.6.24, dynamically linked (uses shared libs), corrupted section header size
[arno@centos ~]$ readelf -h ./crack.me.32 |grep Entry
Entry point address: 0x8048530
[arno@centos ~]$ cat ./crack.me.32 |ndisasm -b32 -o 0x8048530 - |head -14
08048530 7F45 jg 0x8048577
08048532 4C dec esp
08048533 46 inc esi
08048534 0101 add [ecx],eax
08048536 0100 add [eax],eax
08048538 0000 add [eax],al
0804853A 0000 add [eax],al
0804853C 0000 add [eax],al
0804853E 0000 add [eax],al
08048540 0200 add al,[eax]
08048542 0300 add eax,[eax]
08048544 0000 add [eax],al
08048546 0000 add [eax],al
08048548 308504083400 xor [ebp+0x340804],al
Disassembly looks rubbish. The crackme binary’s entry point address is 0x8048530. To get proper disassembly, ndisasm needs correct offset of the loadable segment.
[arno@centos ~]$ readelf -l ./crack.me.32
Elf file type is EXEC (Executable file)
Entry point 0x8048530
There are 9 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00adc 0x00adc R E 0x1000
LOAD 0x000f04 0x08049f04 0x08049f04 0x00144 0x00184 RW 0x1000
DYNAMIC 0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW 0x4
NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4
GNU_EH_FRAME 0x00096c 0x0804896c 0x0804896c 0x0004c 0x0004c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x000f04 0x08049f04 0x08049f04 0x000fc 0x000fc R 0x1
The loadable segment address is 0x08048000 (PhysAddr of LOAD). Now we can calculate the needed offset: 0x8048530-0x08048000 = 0x530 0x530 = 1328 in decimal
With this, we can now see disassembly properly
[arno@centos ~]$ cat ./crack.me.32 |ndisasm -b32 -o 0x8048530 -e1328 - |head -14
08048530 31ED xor ebp,ebp
08048532 5E pop esi
08048533 89E1 mov ecx,esp
08048535 83E4F0 and esp,byte -0x10
08048538 50 push eax
08048539 54 push esp
0804853A 52 push edx
0804853B 6890880408 push dword 0x8048890
08048540 6820880408 push dword 0x8048820
08048545 51 push ecx
08048546 56 push esi
08048547 6834870408 push dword 0x8048734
0804854C E89FFFFFFF call dword 0x80484f0
08048551 F4 hlt
We can also notice a call instruction there. Ok, that seems good enough to continue, however I realized that disassembling with the ndisasm takes a lot of time. That is why I decided to break the anti-GDB and anti-ptrace protections.
In order to break anti-GDB protection, you can simply attach the GDB to already running process
[arno@centos ~]$ ps -ef|grep crack
arno 1347 1178 0 18:42 pts/0 00:00:00 ./crack.me.32
[arno@centos ~]$ sudo gdb -q ./crack.me.32 1347
Reading symbols from /home/arno/crack.me.32...
warning: no loadable sections found in added symbol-file /home/arno/crack.me.32
(no debugging symbols found)...done.
Attaching to program: /home/arno/crack.me.32, process 1347
ptrace: Operation not permitted.
/home/arno/1347: No such file or directory.
Unfortunately, ptrace operation was blocked by the crack.me.32 binary. To remedy this protection, I wrote fake ptrace function.
[arno@centos ~]$ cat ptrace.c
int ptrace(int i, int j, int k, int l)
{
printf("fake ptrace called\n");
}
[arno@centos ~]$ gcc -shared ptrace.c -o ptrace.so
[arno@centos ~]$ LD_PRELOAD=./ptrace.so ./crack.me.32
fake ptrace called
Please tell me my password:
It worked! The fake ptrace was called. Now I could try attaching GDB to a running process
[arno@centos ~]$ ps -ef|grep crack
arno 1414 1353 0 18:49 pts/1 00:00:00 ./crack.me.32
[arno@centos ~]$ sudo gdb -q ./crack.me.32 1414
Reading symbols from /home/arno/crack.me.32...
warning: no loadable sections found in added symbol-file /home/arno/crack.me.32
(no debugging symbols found)...done.
Attaching to program: /home/arno/crack.me.32, process 1414
Reading symbols from ./ptrace.so...(no debugging symbols found)...done.
Loaded symbols for ./ptrace.so
Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
0x00d0e424 in __kernel_vsyscall ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.80.el6_3.7.i686
Perfect, it worked. We hooked at the __kernel_vsyscall () !
Let’s see where we are now and how did we get there, running backtrace
(gdb) disassemble
Dump of assembler code for function __kernel_vsyscall:
0x00d0e414 <+0>: push %ecx
0x00d0e415 <+1>: push %edx
0x00d0e416 <+2>: push %ebp
0x00d0e417 <+3>: mov %esp,%ebp
0x00d0e419 <+5>: sysenter
0x00d0e41b <+7>: nop
0x00d0e41c <+8>: nop
0x00d0e41d <+9>: nop
0x00d0e41e <+10>: nop
0x00d0e41f <+11>: nop
0x00d0e420 <+12>: nop
0x00d0e421 <+13>: nop
0x00d0e422 <+14>: jmp 0xd0e417 <__kernel_vsyscall+3>
=> 0x00d0e424 <+16>: pop %ebp
0x00d0e425 <+17>: pop %edx
0x00d0e426 <+18>: pop %ecx
0x00d0e427 <+19>: ret
End of assembler dump.
(gdb) bt
#0 0x00d0e424 in __kernel_vsyscall ()
#1 0x001f22e3 in __read_nocancel () from /lib/libc.so.6
#2 0x0018c29b in _IO_new_file_underflow () from /lib/libc.so.6
#3 0x0018dfbb in _IO_default_uflow_internal () from /lib/libc.so.6
#4 0x0018f5ca in __uflow () from /lib/libc.so.6
#5 0x00181fbc in _IO_getline_info_internal () from /lib/libc.so.6
#6 0x00181f01 in _IO_getline_internal () from /lib/libc.so.6
#7 0x00180e3a in fgets () from /lib/libc.so.6
#8 0x0804878b in ?? ()
#9 0x00136ce6 in __libc_start_main () from /lib/libc.so.6
#10 0x08048551 in ?? ()
Ok, we are somewhere in the middle of fgets () function, because the crackme binary is waiting us to prompt the password.
While going further (stepi, enter, enter… in gdb), I realized that it can also take a lot of time. Thus, I set the break-pointer to an address that sits before the #7 0x00180e3a in fgets () and continue
(gdb) break *0x0804878b
Breakpoint 1 at 0x804878b
(gdb) continue
The GDB has just froze on “continue” without any response. Logically, it is waiting for the password prompt in the crackme. So for the password I use “test” and hit Enter
[arno@centos ~]$ LD_PRELOAD=./ptrace.so ./crack.me.32
fake ptrace called
Please tell me my password: test
Now I can see that GDB is continuing and stops at the next breakpoint (which I have just set above) – 0x0804878b
Continuing.
Breakpoint 1, 0x0804878b in ?? ()
(gdb) disassemble
No function contains program counter for selected frame.
(gdb) disassemble $eip, +25
Dump of assembler code from 0x804878b to 0x80487a4:
=> 0x0804878b: lea 0x14(%esp),%eax
0x0804878f: mov %eax,(%esp)
0x08048792: call 0x80486ae
0x08048797: movl $0x804a040,0x4(%esp)
0x0804879f: lea 0x14(%esp),%eax
0x080487a3: mov %eax,(%esp)
End of assembler dump.
Now, since we know that I entered “test” word as a password, it will be very interesting to see at what location in memory the “test” word is and what is going to happen with it? Logically the crackme has to compare my password with the real one.
(gdb) x/s $eax
0xbfbea0f4: "test\n"
(gdb) stepi
0x0804878f in ?? ()
(gdb) disassemble
No function contains program counter for selected frame.
(gdb) disassemble $eip, +25
Dump of assembler code from 0x804878f to 0x80487a8:
=> 0x0804878f: mov DWORD PTR [esp],eax
0x08048792: call 0x80486ae
0x08048797: mov DWORD PTR [esp+0x4],0x804a040
0x0804879f: lea eax,[esp+0x14]
0x080487a3: mov DWORD PTR [esp],eax
0x080487a6: call 0x80486e1
End of assembler dump.
Normally, EAX register keeps return value of most syscalls, that is why it was quite simple to find it. ($eax has a PTR 0xbfbea0f4 = “test\n”) Before continuing with GDB step-by-step, I will define hook-stop once, so that after each stepi we know where we are.
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>disassemble $eip, +25
>end
(gdb) stepi
Dump of assembler code from 0x8048792 to 0x80487ab:
=> 0x08048792: call 0x80486ae
0x08048797: mov DWORD PTR [esp+0x4],0x804a040
0x0804879f: lea eax,[esp+0x14]
0x080487a3: mov DWORD PTR [esp],eax
0x080487a6: call 0x80486e1
End of assembler dump.
0x08048792 in ?? ()
After several stepi’s I have noticed one very interesting loop function
(gdb) (Enter - repeats last stepi command)
Dump of assembler code from 0x80486bd to 0x80486d6:
=> 0x080486bd: mov edx,DWORD PTR [ebp-0x4]
0x080486c0: mov eax,DWORD PTR [ebp+0x8]
0x080486c3: add edx,eax
0x080486c5: mov ecx,DWORD PTR [ebp-0x4]
0x080486c8: mov eax,DWORD PTR [ebp+0x8]
0x080486cb: add eax,ecx
0x080486cd: movzx eax,BYTE PTR [eax]
0x080486d0: xor eax,0x6c
0x080486d3: mov BYTE PTR [edx],al
0x080486d5: add DWORD PTR [ebp-0x4],0x1
End of assembler dump.
0x080486bd in ?? ()
I will not insert here further output as it will take a lot of space here. But I will try to explain what I did. Basically I continued with ‘stepi’ and noticed that this function repeats on and on – it moves the data to EDX and EAX, then it does XOR EAX with 0x6c. (xor eax,0x6c) I found this interesting and enabled watchers for all registers. However it was enough just to watch EDX and EAX registers:
(gdb) stepi
7: x/s $edx 0x0: < Address 0x0 out of bounds>
4: x/s $eax 0xbfbea0f4: "test\n"
(gdb) stepi
7: x/s $edx 0xbfbea0f4: "test\n"
4: x/s $eax 0xbfbea0f4: "test\n"
# Then I noticed that EDX is changing
7: x/s $edx 0xbfbea0f4: "\030est\n"
# Then the result goes to EAX and EDX shifts
7: x/s $edx 0xbfbea0f5: "est\n"
4: x/s $eax 0xbfbea0f4: "\030est\n"
On and on... (stepi)
7: x/s $edx 0xbfbea0f5: "\tst\n"
4: x/s $eax 0xbfbea0f4: "\030est\n"
...
7: x/s $edx 0xbfbea0f6: "st\n"
4: x/s $eax 0xbfbea0f4: "\030\tst\n"
...
7: x/s $edx 0xbfbea0f6: "\037t\n"
...
4: x/s $eax 0xbfbea0f4: "\030\t\037t\n"
When EDX got empty, the crackme went further to a much more interesting function! It started to compare my encoded (XOR’ed with 0x6c) password “test” with, obviously, – the real one. :)
Moving forward with the gdb (stepi) we are finally getting to the key instruction that compares our key with the real key.
(gdb)
Dump of assembler code from 0x8048702 to 0x804871b:
=> 0x08048702: mov eax,DWORD PTR [ebp+0x8]
0x08048705: movzx edx,BYTE PTR [eax]
0x08048708: mov eax,DWORD PTR [ebp+0xc]
0x0804870b: movzx eax,BYTE PTR [eax]
0x0804870e: cmp dl,al
0x08048710: je 0x80486e6
0x08048712: mov eax,DWORD PTR [ebp+0x8]
0x08048715: movzx eax,BYTE PTR [eax]
0x08048718: test al,al
0x0804871a: jne 0x804872d
End of assembler dump.
0x08048702 in ?? ()
Just before 0x0804870e: cmp dl,al instruction start, I have found out the 1st XOR’ed letter of my word “test” and the 1st letter of the real key
(gdb)
Dump of assembler code from 0x804870e to 0x8048727:
=> 0x0804870e: cmp dl,al
0x08048710: je 0x80486e6
0x08048712: mov eax,DWORD PTR [ebp+0x8]
0x08048715: movzx eax,BYTE PTR [eax]
0x08048718: test al,al
0x0804871a: jne 0x804872d
0x0804871c: mov eax,DWORD PTR [ebp+0xc]
0x0804871f: movzx eax,BYTE PTR [eax]
0x08048722: test al,al
0x08048724: jne 0x804872d
0x08048726: mov eax,0x0
End of assembler dump.
0x0804870e in ?? ()
7: x/s $edx 0x18: < Address 0x18 out of bounds>
6: x/s $ecx 0x5: < Address 0x5 out of bounds>
5: x/s $ebx 0x2b1ff4: "|\035\031"
4: x/s $eax 0x1b: < Address 0x1b out of bounds>
3: x/s $esi 0x0: < Address 0x0 out of bounds>
2: x/s $esp 0xbfbea0d8: "\b\241\276\277\253\207\004\b\364\240\276\277@\240\004\b@$+"
1: x/s $ebp 0xbfbea0d8: "\b\241\276\277\253\207\004\b\364\240\276\277@\240\004\b@$+"
So you can see $EDX = 0×18, which is: 0×18 XOR 0x6c = 0×74 , and 0×74 is a “t” (first letter of my password “test”). Okay, so it obviously compares it with the 1st letter of the real key which is in $EAX = 0x1b. 0x1b XOR 0x6c = 0×77 and it is “w”.
Great, the 1st letter of the key has been discovered!
In order to continue, I have to set $EDX to 0x1b – otherwise comparing instruction (0x0804870e: cmp dl,al) will fail.
(gdb) set $edx = 0x1b
(gdb) print /x $edx
$9 = 0x1b
(gdb) print /x $eax
$10 = 0x1b
To make the password discovery process faster, I will set the break to the comparing instruction itself and continue watching EAX (key) and changing the EDX (my wrong key) registers.
(gdb) break *0x0804870e
Breakpoint 2 at 0x804870e
(gdb) c
Continuing.
Dump of assembler code from 0x804870e to 0x8048727:
=> 0x0804870e: cmp dl,al
0x08048710: je 0x80486e6
0x08048712: mov eax,DWORD PTR [ebp+0x8]
0x08048715: movzx eax,BYTE PTR [eax]
0x08048718: test al,al
0x0804871a: jne 0x804872d
0x0804871c: mov eax,DWORD PTR [ebp+0xc]
0x0804871f: movzx eax,BYTE PTR [eax]
0x08048722: test al,al
0x08048724: jne 0x804872d
0x08048726: mov eax,0x0
End of assembler dump.
Collecting the real password, byte-by-byte. NOTE: Each time you continue, don’t forget to substitute the next letter (EDX) of the wrong password (“test”) to the expected-discovered one (EAX).
7: x/s $edx 0x9: < Address 0x9 out of bounds>
4: x/s $eax 0x4: < Address 0x4 out of bounds>
>>> hex(0x6c^0x4) = '0x68' = h (The 2nd letter of the key decrypted !)
(gdb) set $edx = 0x4
(gdb) c
7: x/s $edx 0x1f: < Address 0x1f out of bounds>
4: x/s $eax 0x15: < Address 0x15 out of bounds>
>>> hex(0x6c^0x15) = '0x79' = y (3rd letter of the key)
(gdb) set $edx = 0x15
(gdb) c
7: x/s $edx 0x18: < Address 0x18 out of bounds>
4: x/s $eax 0x2: < Address 0x2 out of bounds>
>>> hex(0x6c^0x2) = '0x6e' = n
(gdb) set $edx = 0x2
(gdb) c
Continuing this way I’ve got the whole password which is “whyn0t” !
[arno@centos ~]$ ./crack.me.32
Please tell me my password: whyn0t
The password is correct!
Congratulations!!!