This writeup demonstrates the use of two tools, Radare2 and GDB with Pwndbg, in a couple of different uses. For instructions on how to set up these tools, see How to Configure Your Environment for instructions on setting up Kali and installation of radare2, pwndbg, and more.
First, let's download and extract the IOLI CrackMe challenge binaries:
root@kali:~# wget http://pof.eslack.org/tmp/IOLI-crackme.tar.gz
root@kali:~# tar -xvzf IOLI-crackme.tar.gz
root@kali:~# cd IOLI-crackme/bin-linux
I can definitely relate. Check out my Radare2 cheat sheet, but you'll also want to refer to teh Radare2 book.
For this writeup, we'll be focusing on the simplest one -- crackme0x00. Run it to get a feel for what it does.
root@kali:~# ./crackme0x00
IOLI Crackme Level 0x00
Password: test
Invalid Password!
root@kali:~#
It asks you for a password, and apparently does some kind of check. Let's find a way around the password check.
Refer to the Radare2 tutorial for a quick walkthrough of the commands and hotkeys you need to know.
Before we dig in, take a moment to review the assembly instructions of the main
function:
root@kali:~/IOLI-crackme/bin-linux# r2 -d ./crackme0x00
[0xf77d8ac0]> aaaa
[0xf77d8ac0]> pdf @ main
n:
/ (fcn) sym.main 127
| sym.main ();
| ; var int local_18h @ ebp-0x18
| ; var int local_4h @ esp+0x4
| ; JMP XREF from 0x08048377 (entry0)
| ; DATA XREF from 0x08048377 (entry0)
| 0x08048414 55 push ebp
| 0x08048415 89e5 mov ebp, esp
| 0x08048417 83ec28 sub esp, 0x28 ; '('
| 0x0804841a 83e4f0 and esp, 0xfffffff0
| 0x0804841d b800000000 mov eax, 0
| 0x08048422 83c00f add eax, 0xf
| 0x08048425 83c00f add eax, 0xf
| 0x08048428 c1e804 shr eax, 4
| 0x0804842b c1e004 shl eax, 4
| 0x0804842e 29c4 sub esp, eax
| 0x08048430 c70424688504. mov dword [esp], str.IOLI_Crackme_Level_0x00_n ; [0x8048568:4]=0x494c4f49 LEA str.IOLI_Crackme_Level_0x00_n ; "IOLI Crackme Level 0x00." @ 0x8048568
| 0x08048437 e804ffffff call sym.imp.printf
| 0x0804843c c70424818504. mov dword [esp], str.Password: ; [0x8048581:4]=0x73736150 LEA str.Password: ; "Password: " @ 0x8048581
| 0x08048443 e8f8feffff call sym.imp.printf
| 0x08048448 8d45e8 lea eax, [ebp - local_18h]
| 0x0804844b 89442404 mov dword [esp + local_4h], eax
| 0x0804844f c704248c8504. mov dword [esp], 0x804858c ; [0x804858c:4]=0x32007325
| 0x08048456 e8d5feffff call sym.imp.scanf
| 0x0804845b 8d45e8 lea eax, [ebp - local_18h]
| 0x0804845e c74424048f85. mov dword [esp + local_4h], str.250382 ; [0x804858f:4]=0x33303532 LEA str.250382 ; "250382" @ 0x804858f
| 0x08048466 890424 mov dword [esp], eax
| 0x08048469 e8e2feffff call sym.imp.strcmp
| 0x0804846e 85c0 test eax, eax
| ,=< 0x08048470 740e je 0x8048480
| | 0x08048472 c70424968504. mov dword [esp], str.Invalid_Password__n ; [0x8048596:4]=0x61766e49 LEA str.Invalid_Password__n ; "Invalid Password!." @ 0x8048596
| | 0x08048479 e8c2feffff call sym.imp.printf
| ,==< 0x0804847e eb0c jmp 0x804848c
| |`-> 0x08048480 c70424a98504. mov dword [esp], str.Password_OK_:__n ; [0x80485a9:4]=0x73736150 LEA str.Password_OK_:__n ; "Password OK :)." @ 0x80485a9
| | 0x08048487 e8b4feffff call sym.imp.printf
| | ; JMP XREF from 0x0804847e (sym.main)
| `--> 0x0804848c b800000000 mov eax, 0
| 0x08048491 c9 leave
\ 0x08048492 c3 ret
There are a few instructions above that are of interest. Feel free to reference back, or just read through to get a feel for how the program works:
0x08048443
: A password prompt is printed via printf().0x08048456
: The password is read in from user input via scanf().0x08048469
: The user-input password is compared to the correct password via strcmp().0x0804846e
: The result of strcmp() is returned. By convention, this return value is passed via the "EAX" CPU register.0x0804846e
: The EAX register is tested. This instruction will affect another CPU register, EFLAGS. One of the bits in the EFLAGS register will tell us if EAX is equal to zero (the zero flag).0x08048470
: This instruction will jump to0x8048480
if EAX was equal to zero (which will happen if the strcmp() password check succeeds). If the password check fails, the next instruction will be executed (0x08048472).0x08048472
: The "Invalid Password" sequence of instructions begin here. This looks like a bad place.0x08048480
: The "Password OK" sequence of instructions begin here. We need to redirect the flow of the program to end up here.
There are three immediately apparent (to me) ways to solve this challenge:
- Find the password string: Find the correct password string, stored within the file. Since this is a simple challenge, you could guess at what this might be, simply by feeding the file to the
strings
command, but let's be certain by using good tools. - Modify the return value: Debug the binary, set a breakpoint where the binary returns from the
strcmp()
. By modifying the return value (stored inEAX
), we can trick the rest of the program into thinking that the our (incorrect) password and the correct password string are a perfect match. - Patch the binary: A conditional jump instruction (JE), seen at 0x08048470, is the last part of the verification that the strings match. If we change this instruction to be an unconditional jump instruction (JMP), the password check will always succeed.
To that end, I've solved this simple challenge six different times. The three above techniquies are demonstrated with two different tools, Radare and GDB+pwndbg, albeit slightly out of order:
- Solution #1: Using Radare2 to modify the return value from strcmp()
- Solution #2: Using Radare2 to find the password string inside the binary
- Solution #3: Using Radare2 to patch the binary in memory, bypassing the check of the strcmp() return value
- Solution #4: Using GDB+pwndbg to find the password string inside the binary
- Solution #5: Using GDB+pwndbg to modify the return value from strcmp()
- Solution #6: Using GDB+pwndbg to patch the binary in memory, bypassing the check of the strcmp() return value
root@kali:~/Desktop/IOLI-crackme/bin-linux# r2 -d ./crackme0x00
[0xf77d8ac0]> aaaa
[0xf77d8ac0]> s main
[0x08048414]> pdf
;-- main:
/ (fcn) sym.main 127
| sym.main ();
| ; var int local_18h @ ebp-0x18
| ; var int local_4h @ esp+0x4
| ; JMP XREF from 0x08048377 (entry0)
| ; DATA XREF from 0x08048377 (entry0)
| 0x08048414 55 push ebp
| 0x08048415 89e5 mov ebp, esp
| 0x08048417 83ec28 sub esp, 0x28 ; '('
| 0x0804841a 83e4f0 and esp, 0xfffffff0
| 0x0804841d b800000000 mov eax, 0
| 0x08048422 83c00f add eax, 0xf
| 0x08048425 83c00f add eax, 0xf
| 0x08048428 c1e804 shr eax, 4
| 0x0804842b c1e004 shl eax, 4
| 0x0804842e 29c4 sub esp, eax
| 0x08048430 c70424688504. mov dword [esp], str.IOLI_Crackme_Level_0x00_n ; [0x8048568:4]=0x494c4f49 LEA str.IOLI_Crackme_Level_0x00_n ; "IOLI Crackme Level 0x00." @ 0x8048568
| 0x08048437 e804ffffff call sym.imp.printf
| 0x0804843c c70424818504. mov dword [esp], str.Password: ; [0x8048581:4]=0x73736150 LEA str.Password: ; "Password: " @ 0x8048581
| 0x08048443 e8f8feffff call sym.imp.printf
| 0x08048448 8d45e8 lea eax, [ebp - local_18h]
| 0x0804844b 89442404 mov dword [esp + local_4h], eax
| 0x0804844f c704248c8504. mov dword [esp], 0x804858c ; [0x804858c:4]=0x32007325
| 0x08048456 e8d5feffff call sym.imp.scanf
| 0x0804845b 8d45e8 lea eax, [ebp - local_18h]
| 0x0804845e c74424048f85. mov dword [esp + local_4h], str.250382 ; [0x804858f:4]=0x33303532 LEA str.250382 ; "250382" @ 0x804858f
| 0x08048466 890424 mov dword [esp], eax
| 0x08048469 e8e2feffff call sym.imp.strcmp
| 0x0804846e 85c0 test eax, eax
| ,=< 0x08048470 740e je 0x8048480
| | 0x08048472 c70424968504. mov dword [esp], str.Invalid_Password__n ; [0x8048596:4]=0x61766e49 LEA str.Invalid_Password__n ; "Invalid Password!." @ 0x8048596
| | 0x08048479 e8c2feffff call sym.imp.printf
| ,==< 0x0804847e eb0c jmp 0x804848c
| |`-> 0x08048480 c70424a98504. mov dword [esp], str.Password_OK_:__n ; [0x80485a9:4]=0x73736150 LEA str.Password_OK_:__n ; "Password OK :)." @ 0x80485a9
| | 0x08048487 e8b4feffff call sym.imp.printf
| | ; JMP XREF from 0x0804847e (sym.main)
| `--> 0x0804848c b800000000 mov eax, 0
| 0x08048491 c9 leave
\ 0x08048492 c3 ret
[0x08048414]> db 0x0804846e # Set a breakpoint right after strcmp()
[0x08048414]> dc # Begin execution, should break after strcmp()
IOLI Crackme Level 0x00
Password: test
hit breakpoint at: 804846e
= attach 35135 1
[0x0804846e]> dr~eax # Show the regsiters, specifically the return value from strcmp()
oeax = 0x00000001
eax = 0xffffffff
[0x0804846e]> dr eax=0 # Change the return value from 1 to 0
[0x0804846e]> dr~eax # Confirm that the change worked
oeax = 0xffffffff
eax = 0x00000000
[0x0804846e]> dc # Continue execution
Password OK :)
PTRACE_EVENT_EXIT pid=35067, status=0
[0xf77a1d49]> # SUCCESS!
Work smarter, not harder.
In our disassembly above, the instruction at 0x0804845e, where we are moving a pointer to a string "250382" into [esp + local_4h]. Based on that comparison, we've got a good guess at the password:
root@kali:~/Desktop/IOLI-crackme/bin-linux# ./crackme0x00
IOLI Crackme Level 0x00
Password: 250382
Password OK :)
Solution #3: Using Radare2 to patch the binary in memory, bypassing the check of the strcmp() return value
Password? We don't need no stinkin' password!
Let's modify the instructions themselves to make the binary always work, no matter what password we give it!
root@kali:~/Desktop/IOLI-crackme/bin-linux# r2 -d ./crackme0x00
There's an instruction at 0x08048470 (je 0x8048480) that is altering the flow of the program based on the result of the strcmp(). If we change this from a conditional jump (JE) to an unconditional jump (JMP), the password check will always succeeed.
For this to work, we have to launch radare2 with -d
(debug) in addition to -w
to enable writing (modifying) the executable:
root@kali:~/IOLI-crackme/bin-linux# r2 -N -d -w crackme0x00
Process with PID 37590 started...
= attach 37590 37590
bin.baddr 0x08048000
Assuming filepath /root/IOLI-crackme/bin-linux/crackme0x00
asm.bits 32
[0xf77bfac0]> s 0x08048470 # Seek to the JE instruction
[0x08048470]> pd 1 # Output a single line of disassembly
0x08048470 740e je 0x8048480
[0x08048470]> wa jmp 0x8048480
Written 2 bytes (jmp 0x8048480) = wx eb0e
[0x08048470]> pd 1 # Output a single line of disassembly. Note the 0x74 is now 0xEB.
0x08048470 eb0e jmp 0x8048480
[0x08048470]> dc # Run the program to test our changes.
IOLI Crackme Level 0x00
Password: test
Password OK :)
PTRACE_EVENT_EXIT pid=37590, status=0
= attach 37590 1
[0xf77bdd49]> quit # SUCCESS!
Do you want to quit? (Y/n)
Do you want to kill the process? (Y/n)
Now, in theory, we patched the binary on disk, but it doesn't work! :-(
root@kali:~/IOLI-crackme/bin-linux# ./crackme0x00
IOLI Crackme Level 0x00
Password: test
Invalid Password!
If you want to patch the binary on disk, we have to repeat these steps, but without a debugger. The r2 command is just a shortcut for radare2, with -N
saying "don't load my ~/.radare2rc" and -w
saying "enable read-write" node on the file. Note the lack of -d
for debugging means we won't have the d
series of commands.
root@kali:~/IOLI-crackme/bin-linux# r2 -N -w crackme0x00
[0x08048360]> oo+ # Turns out this is apparently unnecessary.
File crackme0x00 reopened in read-write mode
[0x08048360]> s 0x08048470; wa jmp 0x8048480; pd 1 # Patch the binary in one line
Written 2 bytes (jmp 0x8048480) = wx eb0e
0x08048470 eb0e jmp 0x8048480
[0x08048470]> quit
root@kali:~/IOLI-crackme/bin-linux# ./crackme0x00
IOLI Crackme Level 0x00
Password: test
Password OK :)
root@kali:~/IOLI-crackme/bin-linux# # SUCCESS!
root@kali:~/Desktop/IOLI-crackme/bin-linux# gdb ./crackme0x00
GNU gdb (Debian 7.11.1-2) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Loaded 94 commands. Type pwndbg [filter] for a list.
Reading symbols from ./crackme0x00...(no debugging symbols found)...done.
pwndbg> x/40i main # Look at the main function
0x8048414 <main>: push %ebp
0x8048415 <main+1>: mov %esp,%ebp
0x8048417 <main+3>: sub $0x28,%esp
0x804841a <main+6>: and $0xfffffff0,%esp
0x804841d <main+9>: mov $0x0,%eax
0x8048422 <main+14>: add $0xf,%eax
0x8048425 <main+17>: add $0xf,%eax
0x8048428 <main+20>: shr $0x4,%eax
0x804842b <main+23>: shl $0x4,%eax
0x804842e <main+26>: sub %eax,%esp
0x8048430 <main+28>: movl $0x8048568,(%esp)
0x8048437 <main+35>: call 0x8048340 <printf@plt>
0x804843c <main+40>: movl $0x8048581,(%esp)
0x8048443 <main+47>: call 0x8048340 <printf@plt>
0x8048448 <main+52>: lea -0x18(%ebp),%eax
0x804844b <main+55>: mov %eax,0x4(%esp)
0x804844f <main+59>: movl $0x804858c,(%esp)
0x8048456 <main+66>: call 0x8048330 <scanf@plt>
0x804845b <main+71>: lea -0x18(%ebp),%eax
0x804845e <main+74>: movl $0x804858f,0x4(%esp)
0x8048466 <main+82>: mov %eax,(%esp)
0x8048469 <main+85>: call 0x8048350 <strcmp@plt> # <---- There's our string comparison!
0x804846e <main+90>: test %eax,%eax
0x8048470 <main+92>: je 0x8048480 <main+108>
0x8048472 <main+94>: movl $0x8048596,(%esp)
0x8048479 <main+101>: call 0x8048340 <printf@plt>
0x804847e <main+106>: jmp 0x804848c <main+120>
0x8048480 <main+108>: movl $0x80485a9,(%esp)
0x8048487 <main+115>: call 0x8048340 <printf@plt>
0x804848c <main+120>: mov $0x0,%eax
0x8048491 <main+125>: leave
0x8048492 <main+126>: ret
0x8048493: nop
0x8048494: nop
0x8048495: nop
0x8048496: nop
0x8048497: nop
0x8048498: nop
0x8048499: nop
0x804849a: nop
pwndbg> break *main+85 # Set a breakpoint right before the string comparison
Breakpoint 1 at 0x8048469
pwndbg> run
Starting program: /root/Desktop/IOLI-crackme/bin-linux/crackme0x00
IOLI Crackme Level 0x00
Password: test
Breakpoint 1, 0x08048469 in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
[────────────────────────────────────────────────────────────────────────────────────────────────────REGISTERS────────────────────────────────────────────────────────────────────────────────────────────────────]
*EAX 0xffffd390 ◂— 'test'
EBX 0x0
*ECX 0xf7fae000 ◂— 0x1b2db0
*EDX 0xf7faf87c ◂— 0x0
*EDI 0xf7fae000 ◂— 0x1b2db0
*ESI 0x1
*EBP 0xffffd3a8 ◂— 0x0
*ESP 0xffffd370 —▸ 0xffffd390 ◂— 'test'
*EIP 0x8048469 (main+85) —▸ 0xfffee2e8 ◂— 0x0
[──────────────────────────────────────────────────────────────────────────────────────────────────────CODE───────────────────────────────────────────────────────────────────────────────────────────────────────]
► 0x8048469 <main+85> call strcmp@plt <0x8048350>
s1: 0xffffd390 ◂— 'test'
s2: 0x804858f ◂— '250382'
0x804846e <main+90> test eax, eax
0x8048470 <main+92> je main+108 <0x8048480>
0x8048472 <main+94> mov dword ptr [esp], 0x8048596
0x8048479 <main+101> call printf@plt <0x8048340>
0x804847e <main+106> jmp main+120 <0x804848c>
↓
0x804848c <main+120> mov eax, 0
0x8048491 <main+125> leave
0x8048492 <main+126> ret
0x8048493 nop
0x8048494 nop
[──────────────────────────────────────────────────────────────────────────────────────────────────────STACK──────────────────────────────────────────────────────────────────────────────────────────────────────]
00:0000│ esp 0xffffd370 —▸ 0xffffd390 ◂— 'test'
01:0004│ 0xffffd374 —▸ 0x804858f ◂— xor dh, byte ptr [0x32383330] /* '250382' */
02:0008│ 0xffffd378 —▸ 0xffffd3a8 ◂— 0x0
03:000c│ 0xffffd37c —▸ 0x80484bb (__libc_csu_init+27) ◂— lea eax, [ebx - 0xe8]
04:0010│ 0xffffd380 ◂— 0x1
05:0014│ 0xffffd384 —▸ 0xf7fae000 ◂— 0x1b2db0
06:0018│ 0xffffd388 ◂— 0x0
07:001c│ 0xffffd38c —▸ 0xf7e29a2b (__cxa_atexit+27) ◂— add esp, 0x10
[────────────────────────────────────────────────────────────────────────────────────────────────────BACKTRACE────────────────────────────────────────────────────────────────────────────────────────────────────]
► f 0 8048469 main+85
f 1 f7e13276 __libc_start_main+246
Breakpoint *main+85
And there's our string, handily parsed out by pwndbg as s2 (the second of two strings to be compared within strcmp().
Start as above, but now:
pwndbg> stepover # Used to execute the instruction we're pointing at, without having GDB follow into the function.
pwndbg> set $eax=0 # Modify the return value of strcmp() to SUCCESS (0)
pwndbg> continue # Continue execution of the program.
Continuing.
Password OK :)
[Inferior 1 (process 35543) exited normally]
pwndbg> # SUCCESS!
Solution #6: Using GDB+pwndbg to patch the binary in memory, bypassing the check of the strcmp() return value
We'll find the address of the conditional jump instruction (JE) and modify a single byte to change it to an unconditional jump
instruction (JMP). Since GDB doesn't have a handy way to modify an instruction, we have to know that the byte that represents
an unconditional short jump in the Intel x86 instruction set is 0xeb
.
pwndbg> break *main+92
pwndbg> run
Starting program: /root/Desktop/IOLI-crackme/bin-linux/crackme0x00
IOLI Crackme Level 0x00
Password: test
Breakpoint 1, 0x08048470 in main ()
[ ... snip ... ]
pwndbg> x/i main+92 # DISASSEMBLE THE INSTRUCTION AT main() + 92 BYTES
0x8048470 <main+92>: je 0x8048480 <main+108>
pwndbg> set *(char*)(main+92)=0xeb # SET A SINGLE BYTE (CHAR*) TO 0xEB
pwndbg> x/i main+92 # VERIFY OUR WORK
0x8048470 <main+92>: jmp 0x8048480 <main+108>
pwndbg> continue
Continuing.
Password OK :)
[Inferior 1 (process 35637) exited normally]