The IOLI Crackmes consist of a series of 10 challenges, each of which requires supplying the correct password to a binary file. Binaries are provided here for Win32, PocketPC, and Linux.
IOLI Crackme Level 0x00
Password: ******
Password OK :)
The only tool I used to solve this challenge was GDB (with GEF installed). Any similar interactive debugger such as radare2 will work.
I found these challenges to be a great refresher in x86 Assembly, GDB, and the CDECL calling convention. They require knowledge of assembly but no knowledge of exploitation techniques and are a good beginner's introduction to working with binary files or a new binary analysis tool. Crackmes 0x00 through 0x05 are interesting and fun, but each remaining challenge extends 0x05 minimally and the same solution is often valid.
The passcode for this level is stored as plaintext in the binary.
The integer passcode is hard-coded and directly compared to user input.
The majority of main()
is obfuscation; solving this challenge is similar to crackme0x01
.
shift()
decrypts the strings Password OK!!! :)
and Invalid Password!
, and the majority of main()
is obfuscation.
check()
performs some mathematical operations on each character which the user inputs.
An additional restriction is added to crackme0x04
. Examine parell()
.
An additional restriction is added to crackme0x05
. Look at environment variables and dummy()
.
Symbols have been stripped. For more information, try info file
and look at the .text section. If you're still stuck, what's the first argument passed to __libc_start_main()
?
This challenge is the same as crackme0x07
, without symbols stripped.
The same solution is again valid.
One solution requires disassembling the binary, for example using disassemble main
within GDB. The last few lines of the disassembled main()
function (with my comments) are:
0x0804845e <+74>: mov DWORD PTR [esp+0x4],0x804858f; put address of string on stack
0x08048466 <+82>: mov DWORD PTR [esp],eax ; place other argument on stack
0x08048469 <+85>: call 0x8048350 <strcmp@plt> ; call strcmp()
0x0804846e <+90>: test eax,eax ; test comparison
0x08048470 <+92>: je 0x8048480 <main+108> ; branch based on comparison
0x08048472 <+94>: mov DWORD PTR [esp],0x8048596 ; get failure string
0x08048479 <+101>: call 0x8048340 <printf@plt> ; print failure string
0x0804847e <+106>: jmp 0x804848c <main+120> ; jump past printing success
0x08048480 <+108>: mov DWORD PTR [esp],0x80485a9 ; get success string
0x08048487 <+115>: call 0x8048340 <printf@plt> ; print success string
0x0804848c <+120>: mov eax,0x0 ; return 0
0x08048491 <+125>: leave
0x08048492 <+126>: ret
As you can see, line 1
will load the address 0x804858f
onto the stack right before strcmp()
is called on line 3
, comparing the user-entered string with the string at 0x804858f
. Using GDB, we can examine this address as a string to find:
gef> x/s 0x804858f
0x804858f: "250382"
The string 250382
is our solution. Note that since this string was stored in plaintext, it could also have been found using strings crackme0x00
.
Similar to the previous challenge, disassemble main
using GDB shows that directly before jumping to the success or failure strings, a comparison is made. This time, instead of comparing to the user-inputted string, it is compared to a hexidecimal value:
0x0804841f <+59>: mov DWORD PTR [esp],0x804854c
0x08048426 <+66>: call 0x804830c <scanf@plt>
0x0804842b <+71>: cmp DWORD PTR [ebp-0x4],0x149a
0x08048432 <+78>: je 0x8048442 <main+94>
0x08048434 <+80>: mov DWORD PTR [esp],0x804854f
0x0804843b <+87>: call 0x804831c <printf@plt>
0x08048440 <+92>: jmp 0x804844e <main+106>
0x08048442 <+94>: mov DWORD PTR [esp],0x8048562
0x08048449 <+101>: call 0x804831c <printf@plt>
0x0804844e <+106>: mov eax,0x0
0x08048453 <+111>: leave
0x08048454 <+112>: ret
Line 1
loads the format string passed to scanf()
onto the stack. By examining this string, (x/s 0x804854c
) we can see that user input is stored in decimal format ("%d"
). This user input is compared to the value 0x149a
on line 3
. By calling python print(0x149a)
from within GDB, we convert this hexadecimal value to the decimal answer 5274
.
The beginning and end of main
are identical to earlier challenges, but now there are some confusing operations in the middle. Let's examine them in more detail:
0x08048400 <+28>: mov DWORD PTR [esp],0x8048548 ; load "IOLI Crackme..." string
0x08048407 <+35>: call 0x804831c <printf@plt> ; print it
0x0804840c <+40>: mov DWORD PTR [esp],0x8048561 ; load "Password:" string
0x08048413 <+47>: call 0x804831c <printf@plt> ; print it
0x08048418 <+52>: lea eax,[ebp-0x4] ; store address of var1
0x0804841b <+55>: mov DWORD PTR [esp+0x4],eax ; put on stack
0x0804841f <+59>: mov DWORD PTR [esp],0x804856c ; load "%d" string
0x08048426 <+66>: call 0x804830c <scanf@plt> ; put user input in var1
0x0804842b <+71>: mov DWORD PTR [ebp-0x8],0x5a ; var2 = 0x5a
0x08048432 <+78>: mov DWORD PTR [ebp-0xc],0x1ec ; var3 = 0x1ec
0x08048439 <+85>: mov edx,DWORD PTR [ebp-0xc] ; edx = var3
0x0804843c <+88>: lea eax,[ebp-0x8] ; eax = \&var2
0x0804843f <+91>: add DWORD PTR [eax],edx ; *eax += edx (var2 = 0x246)
0x08048441 <+93>: mov eax,DWORD PTR [ebp-0x8] ; eax = var2
0x08048444 <+96>: imul eax,DWORD PTR [ebp-0x8] ; eax *= var2
0x08048448 <+100>: mov DWORD PTR [ebp-0xc],eax ; var3 = eax = 0x52b24
0x0804844b <+103>: mov eax,DWORD PTR [ebp-0x4] ; eax = var1
0x0804844e <+106>: cmp eax,DWORD PTR [ebp-0xc] ; test if var1 == 0x52b4
0x08048451 <+109>: jne 0x8048461 <main+125> ; if not, jump
0x08048453 <+111>: mov DWORD PTR [esp],0x804856f ; else print "Password OK"
0x0804845a <+118>: call 0x804831c <printf@plt>
0x0804845f <+123>: jmp 0x804846d <main+137> ; jump to end
0x08048461 <+125>: mov DWORD PTR [esp],0x804857f ; print "Password Invalid!"
0x08048468 <+132>: call 0x804831c <printf@plt>
0x0804846d <+137>: mov eax,0x0 ; return 0
0x08048472 <+142>: leave
0x08048473 <+143>: ret
Lines 11-18
are simply used to obscure the value which is ultimately compared to the user input on line 21
when test
is called. By carefully following each operation, we can determine that the answer is 0x52b24
, or 338724
. Alternatively, we can set a breakpoint directly before this comparison with break *0x804844e
and check the value of var3
to obtain the same answer.
gef> x/xw (int*)($ebp-0xc)
0xffffd39c: 0x00052b24
I have chosen to use the names var1, var2, var3
in this write-up to refer to [ebp-0x4], [ebp-0x8], [ebp-0xc]
because local variables are stored at a negative offset from ebp
, and in this case have a width of 4 bytes. For reference, the CDECL calling convention is shown below:
In GDB, disass main
shows that function test
is called directly before returning:
0x0804850c <+116>: call 0x804846e <test>
0x08048511 <+121>: mov eax,0x0
0x08048516 <+126>: leave
0x08048517 <+127>: ret
Calling disassemble test
shows that shift
is called with argument 0x80485fe
if test's arg1 == arg2
, and 0x80485ec
otherwise:
0x0804846e <+0>: push ebp
0x0804846f <+1>: mov ebp,esp
0x08048471 <+3>: sub esp,0x8
0x08048474 <+6>: mov eax,DWORD PTR [ebp+0x8]
0x08048477 <+9>: cmp eax,DWORD PTR [ebp+0xc]
0x0804847a <+12>: je 0x804848a <test+28>
0x0804847c <+14>: mov DWORD PTR [esp],0x80485ec
0x08048483 <+21>: call 0x8048414 <shift>
0x08048488 <+26>: jmp 0x8048496 <test+40>
0x0804848a <+28>: mov DWORD PTR [esp],0x80485fe
0x08048491 <+35>: call 0x8048414 <shift>
0x08048496 <+40>: leave
0x08048497 <+41>: ret
Let's examine these two strings:
gef> x/s 0x80485ec
0x80485ec: "Lqydolg#Sdvvzrug$"
gef> x/s 0x80485fe
0x80485fe: "Sdvvzrug#RN$$$#=,"
After disassembling and briefly examining shift
, it is clear that the function iterates over each character in the input string, modifying it with a series of operations before printing the result. Therefore, our two previous strings are probably just the encrypted success and failure strings, and we need to ensure that test
is called with the correct arguments. Re-examining main
:
0x080484b4 <+28>: mov DWORD PTR [esp],0x8048610 ; load "IOLI Crackme..." string
0x080484bb <+35>: call 0x8048350 <printf@plt> ; print it
0x080484c0 <+40>: mov DWORD PTR [esp],0x8048629 ; load "Password:" string
0x080484c7 <+47>: call 0x8048350 <printf@plt> ; print it
0x080484cc <+52>: lea eax,[ebp-0x4] ; store address of var1
0x080484cf <+55>: mov DWORD PTR [esp+0x4],eax ; put on stack
0x080484d3 <+59>: mov DWORD PTR [esp],0x8048634 ; load "%d" string
0x080484da <+66>: call 0x8048330 <scanf@plt> ; put user input in var1
0x080484df <+71>: mov DWORD PTR [ebp-0x8],0x5a ; var2 = 0x5a
0x080484e6 <+78>: mov DWORD PTR [ebp-0xc],0x1ec ; var3 = 0x1ec
0x080484ed <+85>: mov edx,DWORD PTR [ebp-0xc] ; edx = var3
0x080484f0 <+88>: lea eax,[ebp-0x8] ; eax = \&var2
0x080484f3 <+91>: add DWORD PTR [eax],edx ; *eax += edx (var2 = 0x246)
0x080484f5 <+93>: mov eax,DWORD PTR [ebp-0x8] ; eax = var2
0x080484f8 <+96>: imul eax,DWORD PTR [ebp-0x8] ; eax = var2*var2 = 0x52b24
0x080484fc <+100>: mov DWORD PTR [ebp-0xc],eax ; var3 = 0x52b24
0x080484ff <+103>: mov eax,DWORD PTR [ebp-0xc] ; eax = 0x52b24
0x08048502 <+106>: mov DWORD PTR [esp+0x4],eax ; put 0x52b24 on stack
0x08048506 <+110>: mov eax,DWORD PTR [ebp-0x4] ; put user val1 on stack
0x08048509 <+113>: mov DWORD PTR [esp],eax
0x0804850c <+116>: call 0x804846e <test> ; call test
0x08048511 <+121>: mov eax,0x0 ; return 0
0x08048516 <+126>: leave
0x08048517 <+127>: ret
Lines 11-17
are simply used to obscure the value which is ultimately compared to the user input on line 24
when test
is called. By carefully following each operation, we can determine that the answer is 0x52b24
, or 338724
.
This time, disassembling main
reveals that the only changes of note are that user input is taken as a string and stored at ebp-0x78
, and is passed to a new function called check
:
0x08048540 <+55>: lea eax,[ebp-0x78]
0x08048543 <+58>: mov DWORD PTR [esp+0x4],eax
0x08048547 <+62>: mov DWORD PTR [esp],0x8048682
0x0804854e <+69>: call 0x8048374 <scanf@plt>
0x08048553 <+74>: lea eax,[ebp-0x78]
0x08048556 <+77>: mov DWORD PTR [esp],eax ; arg1(check) = \&input
0x08048559 <+80>: call 0x8048484 <check>
The check
function deserves some more attention:
0x0804848a <+6>: mov DWORD PTR [ebp-0x8],0x0 ; var2 = 0
0x08048491 <+13>: mov DWORD PTR [ebp-0xc],0x0 ; var3 = 0
0x08048498 <+20>: mov eax,DWORD PTR [ebp+0x8] ;start: arg1(strlen) = \&input
0x0804849b <+23>: mov DWORD PTR [esp],eax ; |
0x0804849e <+26>: call 0x8048384 <strlen@plt> ; call strlen
0x080484a3 <+31>: cmp DWORD PTR [ebp-0xc],eax ; if var3 >= len(input) fail
0x080484a6 <+34>: jae 0x80484fb <check+119> ; |
0x080484a8 <+36>: mov eax,DWORD PTR [ebp-0xc] ; var4 = char(input[var3])
0x080484ab <+39>: add eax,DWORD PTR [ebp+0x8] ; |
0x080484ae <+42>: movzx eax,BYTE PTR [eax] ; |
0x080484b1 <+45>: mov BYTE PTR [ebp-0xd],al ; |
0x080484b4 <+48>: lea eax,[ebp-0x4] ; arg3(sscanf) = \&var1
0x080484b7 <+51>: mov DWORD PTR [esp+0x8],eax ; |
0x080484bb <+55>: mov DWORD PTR [esp+0x4],0x8048638; arg2(sscanf) = "%d"
0x080484c3 <+63>: lea eax,[ebp-0xd] ; arg1(sscanf) = \&var4
0x080484c6 <+66>: mov DWORD PTR [esp],eax ; |
0x080484c9 <+69>: call 0x80483a4 <sscanf@plt> ; call sscanf, var1 = atoi(input[var3])
0x080484ce <+74>: mov edx,DWORD PTR [ebp-0x4] ; var2 += var1
0x080484d1 <+77>: lea eax,[ebp-0x8] ; |
0x080484d4 <+80>: add DWORD PTR [eax],edx ; |
0x080484d6 <+82>: cmp DWORD PTR [ebp-0x8],0xf ; compare var2 and 15
0x080484da <+86>: jne 0x80484f4 <check+112> ; not equal, goto cont
0x080484dc <+88>: mov DWORD PTR [esp],0x804863b ; win
0x080484e3 <+95>: call 0x8048394 <printf@plt> ; |
0x080484e8 <+100>: mov DWORD PTR [esp],0x0 ; |
0x080484ef <+107>: call 0x80483b4 <exit@plt> ; exit
0x080484f4 <+112>: lea eax,[ebp-0xc] ;cont: var3++
0x080484f7 <+115>: inc DWORD PTR [eax] ; |
0x080484f9 <+117>: jmp 0x8048498 <check+20> ; goto start
0x080484fb <+119>: mov DWORD PTR [esp],0x8048649 ; fail
0x08048502 <+126>: call 0x8048394 <printf@plt> ; |
0x08048507 <+131>: leave ; |
0x08048508 <+132>: ret ; exit
After looking more closely at the assembly, we can see that check
considers a single character of the input string at a time, converts it to an integer, and adds the integer to a running total. If the sum of this total is ever 15, the challenge is passed. Therefore, any string such as 12345
or 22222221
will work.
As with the previous challenge, user input is accepted as a string and passed to check()
, which is disassembled below:
0x080484ce <+6>: mov DWORD PTR [ebp-0x8],0x0 ; arg1(check) = input
0x080484d5 <+13>: mov DWORD PTR [ebp-0xc],0x0 ; var2 = var3 = 0
0x080484dc <+20>: mov eax,DWORD PTR [ebp+0x8] ;start:
0x080484df <+23>: mov DWORD PTR [esp],eax ; arg1(strlen) = arg1(check)
0x080484e2 <+26>: call 0x8048384 <strlen@plt> ; call strlen
0x080484e7 <+31>: cmp DWORD PTR [ebp-0xc],eax ; var3 ?> strlen(input)
0x080484ea <+34>: jae 0x8048532 <check+106> ; if so, fail
0x080484ec <+36>: mov eax,DWORD PTR [ebp-0xc] ; var4 = char(input[var3])
0x080484ef <+39>: add eax,DWORD PTR [ebp+0x8] ; |
0x080484f2 <+42>: movzx eax,BYTE PTR [eax] ; |
0x080484f5 <+45>: mov BYTE PTR [ebp-0xd],al ; |
0x080484f8 <+48>: lea eax,[ebp-0x4] ; arg3(sscanf) = \&var1
0x080484fb <+51>: mov DWORD PTR [esp+0x8],eax ; |
0x080484ff <+55>: mov DWORD PTR [esp+0x4],0x8048668; arg2(sscanf) = "%d"
0x08048507 <+63>: lea eax,[ebp-0xd] ; |
0x0804850a <+66>: mov DWORD PTR [esp],eax ; arg1(sscanf) = \&var4
0x0804850d <+69>: call 0x80483a4 <sscanf@plt> ; call sscanf, var1 = atoi(var4)
0x08048512 <+74>: mov edx,DWORD PTR [ebp-0x4] ; var2 += var1
0x08048515 <+77>: lea eax,[ebp-0x8] ; |
0x08048518 <+80>: add DWORD PTR [eax],edx ; |
0x0804851a <+82>: cmp DWORD PTR [ebp-0x8],0x10 ; var2 ?= 16
0x0804851e <+86>: jne 0x804852b <check+99> ; goto cont
0x08048520 <+88>: mov eax,DWORD PTR [ebp+0x8] ; arg1(parell) = input
0x08048523 <+91>: mov DWORD PTR [esp],eax ; |
0x08048526 <+94>: call 0x8048484 <parell> ; call parell
0x0804852b <+99>: lea eax,[ebp-0xc] ;cont: var3++
0x0804852e <+102>: inc DWORD PTR [eax] ; |
0x08048530 <+104>: jmp 0x80484dc <check+20> ; goto start
0x08048532 <+106>: mov DWORD PTR [esp],0x8048679 ; fail
0x08048539 <+113>: call 0x8048394 <printf@plt> ; |
0x0804853e <+118>: leave ; |
0x0804853f <+119>: ret ; exit
As with the previous challenge, check
iterates over the user-inputted string and records the sum of each input character as an integer. If this sum is ever equal to 16, the function parell
is called. A disassembly of parell
shows:
0x0804848a <+6>: lea eax,[ebp-0x4] ; arg3(sscanf) = \&input
0x0804848d <+9>: mov DWORD PTR [esp+0x8],eax ; |
0x08048491 <+13>: mov DWORD PTR [esp+0x4],0x8048668; arg2(sscanf) = "%d"
0x08048499 <+21>: mov eax,DWORD PTR [ebp+0x8] ; arg1(sscanf) = \& var1
0x0804849c <+24>: mov DWORD PTR [esp],eax ; |
0x0804849f <+27>: call 0x80483a4 <sscanf@plt> ; call sscanf, var1 = atoi(input)
0x080484a4 <+32>: mov eax,DWORD PTR [ebp-0x4] ; var1 ?= even
0x080484a7 <+35>: and eax,0x1 ; |
0x080484aa <+38>: test eax,eax ; |
0x080484ac <+40>: jne 0x80484c6 <parell+66> ; if not, return
0x080484ae <+42>: mov DWORD PTR [esp],0x804866b ; win
0x080484b5 <+49>: call 0x8048394 <printf@plt> ; |
0x080484ba <+54>: mov DWORD PTR [esp],0x0 ; exit program
0x080484c1 <+61>: call 0x80483b4 <exit@plt> ; |
0x080484c6 <+66>: leave ; return
0x080484c7 <+67>: ret ; |
A password is accepted as long as the input is an even integer. Note that parell
considers the entire input string, and not each character individually. Combined with the restriction from check
, we must enter an even integer whose digits sum to 16.
This time, main
calls check
with some different arguments:
0x08048651 <+74>: mov eax,DWORD PTR [ebp+0x10] ; arg2(check) = arg3(main) = envp
0x08048654 <+77>: mov DWORD PTR [esp+0x4],eax ; |
0x08048658 <+81>: lea eax,[ebp-0x78] ; arg1(check) = \&input
0x0804865b <+84>: mov DWORD PTR [esp],eax ; |
0x0804865e <+87>: call 0x8048588 <check> ; call check
0x08048663 <+92>: mov eax,0x0 ; return 0
0x08048668 <+97>: leave ; |
0x08048669 <+98>: ret ; |
When programming in C, the full function header for main()
is int main(int argc, char* argv[], char* envp[]);
. argc
is the "argument count". argv
is a pointer to an array of strings (terminated by a NULL
pointer) containing each of these arguments. envp
is a frequently omitted parameter which points to an array of strings defining all of the current environment variables. In BASH, this list can be printed with env
or printenv
. Typically, these variables are of the format VAR=value
, and are used either in scripts or from the command line. Let's examine how check
uses envp
:
0x08048588 <+0>: push ebp ; set up new stack frame
0x08048589 <+1>: mov ebp,esp ; |
0x0804858b <+3>: sub esp,0x28 ; |
0x0804858e <+6>: mov DWORD PTR [ebp-0x8],0x0 ; var2 = 0
0x08048595 <+13>: mov DWORD PTR [ebp-0xc],0x0 ; var3 = 0
0x0804859c <+20>: mov eax,DWORD PTR [ebp+0x8] ;start: arg1(strlen) = \&input
0x0804859f <+23>: mov DWORD PTR [esp],eax ; |
0x080485a2 <+26>: call 0x80483a8 <strlen@plt> ; call strlen
0x080485a7 <+31>: cmp DWORD PTR [ebp-0xc],eax ; var3 ?> strlen(input)
0x080485aa <+34>: jae 0x80485f9 <check+113> ; if so, fail
0x080485ac <+36>: mov eax,DWORD PTR [ebp-0xc] ; var4 = char(input[var3])
0x080485af <+39>: add eax,DWORD PTR [ebp+0x8] ; |
0x080485b2 <+42>: movzx eax,BYTE PTR [eax] ; |
0x080485b5 <+45>: mov BYTE PTR [ebp-0xd],al ; |
0x080485b8 <+48>: lea eax,[ebp-0x4] ; arg3(sscanf) = \&var1
0x080485bb <+51>: mov DWORD PTR [esp+0x8],eax ; |
0x080485bf <+55>: mov DWORD PTR [esp+0x4],0x804873d; arg2(sscanf) = "%d"
0x080485c7 <+63>: lea eax,[ebp-0xd] ; arg1(sscanf) = \&var4
0x080485ca <+66>: mov DWORD PTR [esp],eax ; |
0x080485cd <+69>: call 0x80483c8 <sscanf@plt> ; call sscanf, var1 = atoi(var4)
0x080485d2 <+74>: mov edx,DWORD PTR [ebp-0x4] ; var2 += var1
0x080485d5 <+77>: lea eax,[ebp-0x8] ; |
0x080485d8 <+80>: add DWORD PTR [eax],edx ; |
0x080485da <+82>: cmp DWORD PTR [ebp-0x8],0x10 ; var2 ?= 16
0x080485de <+86>: jne 0x80485f2 <check+106> ; if not, goto cont
0x080485e0 <+88>: mov eax,DWORD PTR [ebp+0xc] ; arg2(parell) = envp
0x080485e3 <+91>: mov DWORD PTR [esp+0x4],eax ; |
0x080485e7 <+95>: mov eax,DWORD PTR [ebp+0x8] ; arg1(parell) = \&input
0x080485ea <+98>: mov DWORD PTR [esp],eax ; |
0x080485ed <+101>: call 0x804851a <parell> ; call parell
0x080485f2 <+106>: lea eax,[ebp-0xc] ;cont: var3++
0x080485f5 <+109>: inc DWORD PTR [eax] ; |
0x080485f7 <+111>: jmp 0x804859c <check+20> ; goto start
0x080485f9 <+113>: mov DWORD PTR [esp],0x804874e ; fail
0x08048600 <+120>: call 0x80483b8 <printf@plt> ; |
0x08048605 <+125>: leave ; exit
0x08048606 <+126>: ret ; |
Nothing new here, we must supply a string of digits which sum to 16 and it just passes envp
on to parell
this time:
0x08048520 <+6>: lea eax,[ebp-0x4] ; arg3(sscanf) = \&var1
0x08048523 <+9>: mov DWORD PTR [esp+0x8],eax ; |
0x08048527 <+13>: mov DWORD PTR [esp+0x4],0x804873d; arg2(sscanf) = "%d"
0x0804852f <+21>: mov eax,DWORD PTR [ebp+0x8] ; arg1(sscanf) = \&input
0x08048532 <+24>: mov DWORD PTR [esp],eax ; |
0x08048535 <+27>: call 0x80483c8 <sscanf@plt> ; call sscanf, var1 = atoi(input)
0x0804853a <+32>: mov eax,DWORD PTR [ebp+0xc] ; arg2(dummy) = envp
0x0804853d <+35>: mov DWORD PTR [esp+0x4],eax ; |
0x08048541 <+39>: mov eax,DWORD PTR [ebp-0x4] ; arg1(dummy) = var1
0x08048544 <+42>: mov DWORD PTR [esp],eax ; |
0x08048547 <+45>: call 0x80484b4 <dummy> ; call dummy
0x0804854c <+50>: test eax,eax ; eax ?= 0
0x0804854e <+52>: je 0x8048586 <parell+108> ; if so, return 0
0x08048550 <+54>: mov DWORD PTR [ebp-0x8],0x0 ; var2 = 0
0x08048557 <+61>: cmp DWORD PTR [ebp-0x8],0x9 ;start: var2 ?> 9
0x0804855b <+65>: jg 0x8048586 <parell+108> ; if so, return
0x0804855d <+67>: mov eax,DWORD PTR [ebp-0x4] ; get parity of var1
0x08048560 <+70>: and eax,0x1 ; |
0x08048563 <+73>: test eax,eax ; test if even
0x08048565 <+75>: jne 0x804857f <parell+101> ; if not, goto cont
0x08048567 <+77>: mov DWORD PTR [esp],0x8048740 ; win
0x0804856e <+84>: call 0x80483b8 <printf@plt> ; |
0x08048573 <+89>: mov DWORD PTR [esp],0x0 ; exit
0x0804857a <+96>: call 0x80483e8 <exit@plt> ; |
0x0804857f <+101>: lea eax,[ebp-0x8] ; cont: var2++
0x08048582 <+104>: inc DWORD PTR [eax] ; |
0x08048584 <+106>: jmp 0x8048557 <parell+61> ; goto start
0x08048586 <+108>: leave ; return 0
0x08048587 <+109>: ret ; |
Based on lines 21-24
, it appears that we only need our input string to be an even number in order to print the success message. We must, however, first consider the fact that the dummy
function is passed envp
and may perform additional checks.
0x080484ba <+6>: mov DWORD PTR [ebp-0x4],0x0 ; var1 = 0
0x080484c1 <+13>: mov eax,DWORD PTR [ebp-0x4] ;start: eax = var1
0x080484c4 <+16>: lea edx,[eax*4+0x0] ; edx = 4*eax
0x080484cb <+23>: mov eax,DWORD PTR [ebp+0xc] ; envp[var1] ?= NULL
0x080484ce <+26>: cmp DWORD PTR [edx+eax*1],0x0 ; |
0x080484d2 <+30>: je 0x804850e <dummy+90> ; if so, return 0
0x080484d4 <+32>: mov eax,DWORD PTR [ebp-0x4] ; eax = var1
0x080484d7 <+35>: lea ecx,[eax*4+0x0] ; ecx = 4*eax
0x080484de <+42>: mov edx,DWORD PTR [ebp+0xc] ; edx = envp
0x080484e1 <+45>: lea eax,[ebp-0x4] ; var1++
0x080484e4 <+48>: inc DWORD PTR [eax] ; |
0x080484e6 <+50>: mov DWORD PTR [esp+0x8],0x3 ; arg3(strncmp) = 3
0x080484ee <+58>: mov DWORD PTR [esp+0x4],0x8048738; arg2(strncmp) = "LOLO"
0x080484f6 <+66>: mov eax,DWORD PTR [ecx+edx*1] ; arg1(strncmp) = envp[var1]
0x080484f9 <+69>: mov DWORD PTR [esp],eax ; |
0x080484fc <+72>: call 0x80483d8 <strncmp@plt> ; call strncmp
0x08048501 <+77>: test eax,eax ; were strings equal?
0x08048503 <+79>: jne 0x80484c1 <dummy+13> ; if not, goto start
0x08048505 <+81>: mov DWORD PTR [ebp-0x8],0x1 ; eax = 1
0x0804850c <+88>: jmp 0x8048515 <dummy+97> ; goto return
0x0804850e <+90>: mov DWORD PTR [ebp-0x8],0x0 ; eax = 0
0x08048515 <+97>: mov eax,DWORD PTR [ebp-0x8] ;return:
0x08048518 <+100>: leave ; |
0x08048519 <+101>: ret ; |
The dummy
function searches for an environment variable whose name begins with LOL
. Thus, we must enter a string of digits which sum to 16, the last digit is even, and the environemnt variable LOL exists. We can do this by entering LOLOL='hi' ./crackme0x06
and then entering 88
.
crackme0x07
features a stripped binary with the same solution as crackme0x06
. Although the solution is identical, it's useful to learn how to use GDB when symbol information is unavailable.
gef> disassemble main
No symbol table is loaded. Use the "file" command.
Let's see what information we do have on this file:
gef> info file
Local exec file:
`/home/tim/crackme0x07', file type elf32-i386.
Entry point: 0x8048400
0x08048154 - 0x08048167 is .interp
0x08048168 - 0x08048188 is .note.ABI-tag
0x08048188 - 0x080481c4 is .hash
0x080481c4 - 0x080481e4 is .gnu.hash
0x080481e4 - 0x08048284 is .dynsym
0x08048284 - 0x080482eb is .dynstr
0x080482ec - 0x08048300 is .gnu.version
0x08048300 - 0x08048320 is .gnu.version_r
0x08048320 - 0x08048328 is .rel.dyn
0x08048328 - 0x08048360 is .rel.plt
0x08048360 - 0x08048377 is .init
0x08048378 - 0x080483f8 is .plt
0x08048400 - 0x08048784 is .text
0x08048784 - 0x0804879e is .fini
0x080487a0 - 0x08048800 is .rodata
0x08048800 - 0x08048804 is .eh_frame
0x08049f0c - 0x08049f14 is .ctors
0x08049f14 - 0x08049f1c is .dtors
0x08049f1c - 0x08049f20 is .jcr
0x08049f20 - 0x08049ff0 is .dynamic
0x08049ff0 - 0x08049ff4 is .got
0x08049ff4 - 0x0804a01c is .got.plt
0x0804a01c - 0x0804a028 is .data
Most interesting for us is probably the entry point, since the .text segment contains all executable instructions. We can force GDB to disassemble instructions between two addresses as follows:
gef> disass 0x8048400, 0x8048784
The output of this command is hundreds of lines long, but the first few are:
0x08048400: xor ebp,ebp
0x08048402: pop esi
0x08048403: mov ecx,esp
0x08048405: and esp,0xfffffff0
0x08048408: push eax
0x08048409: push esp
0x0804840a: push edx
0x0804840b: push 0x8048750
0x08048410: push 0x80486e0
0x08048415: push ecx
0x08048416: push esi
0x08048417: push 0x804867d
0x0804841c: call 0x8048388 <__libc_start_main@plt>
0x08048421: hlt
We can see that the address 0x804867d
is passed as an argument to __libc_start_main
, so that's a probably a good place to look next:
0x0804867d: push ebp ; set up new frame
0x0804867e: mov ebp,esp
0x08048680: sub esp,0x88
0x08048686: and esp,0xfffffff0
0x08048689: mov eax,0x0
0x0804868e: add eax,0xf
0x08048691: add eax,0xf
0x08048694: shr eax,0x4
0x08048697: shl eax,0x4
0x0804869a: sub esp,eax
0x0804869c: mov DWORD PTR [esp],0x80487d9; print "IOLI Crackme Level 0x07"
0x080486a3: call 0x80483b8 <printf@plt>
0x080486a8: mov DWORD PTR [esp],0x80487f2; print "Password:"
0x080486af: call 0x80483b8 <printf@plt>
0x080486b4: lea eax,[ebp-0x78] ; get user input
0x080486b7: mov DWORD PTR [esp+0x4],eax
0x080486bb: mov DWORD PTR [esp],0x80487fd
0x080486c2: call 0x8048398 <scanf@plt>
0x080486c7: mov eax,DWORD PTR [ebp+0x10]
0x080486ca: mov DWORD PTR [esp+0x4],eax
0x080486ce: lea eax,[ebp-0x78] ; pass to "check"
0x080486d1: mov DWORD PTR [esp],eax
0x080486d4: call 0x80485b9
0x080486d9: mov eax,0x0
0x080486de: leave
0x080486df: ret
Here we can see our familiar main
function which displays a prompt to the user, accepts input, and validates it using a helper function previously called check
. The same solution to crackme0x06
will be accepted.
The disassembled main
function is from crackme0x06
, and calls check(&input, envp)
, shown below:
0x080485b9 <+0>: push ebp ; set up new stack frame
0x080485ba <+1>: mov ebp,esp ; |
0x080485bc <+3>: sub esp,0x28 ; |
0x080485bf <+6>: mov DWORD PTR [ebp-0x8],0x0 ; var2 = var3 = 0
0x080485c6 <+13>: mov DWORD PTR [ebp-0xc],0x0 ; |
0x080485cd <+20>: mov eax,DWORD PTR [ebp+0x8] ;start: strlen(&input)
0x080485d0 <+23>: mov DWORD PTR [esp],eax ; |
0x080485d3 <+26>: call 0x80483a8 <strlen@plt> ; |
0x080485d8 <+31>: cmp DWORD PTR [ebp-0xc],eax ; var3 ?> strlen(&input)
0x080485db <+34>: jae 0x804862a <check+113> ; if so, goto che
0x080485dd <+36>: mov eax,DWORD PTR [ebp-0xc] ; var4 = char(input[var3])
0x080485e0 <+39>: add eax,DWORD PTR [ebp+0x8] ; |
0x080485e3 <+42>: movzx eax,BYTE PTR [eax] ; |
0x080485e6 <+45>: mov BYTE PTR [ebp-0xd],al ; |
0x080485e9 <+48>: lea eax,[ebp-0x4] ; sscanf(&var4, "%d", &var1)
0x080485ec <+51>: mov DWORD PTR [esp+0x8],eax ; |
0x080485f0 <+55>: mov DWORD PTR [esp+0x4],0x80487c2; |
0x080485f8 <+63>: lea eax,[ebp-0xd] ; |
0x080485fb <+66>: mov DWORD PTR [esp],eax ; |
0x080485fe <+69>: call 0x80483c8 <sscanf@plt> ; |
0x08048603 <+74>: mov edx,DWORD PTR [ebp-0x4] ; var2 += var1
0x08048606 <+77>: lea eax,[ebp-0x8] ; |
0x08048609 <+80>: add DWORD PTR [eax],edx ; |
0x0804860b <+82>: cmp DWORD PTR [ebp-0x8],0x10 ; var2 ?= 16
0x0804860f <+86>: jne 0x8048623 <check+106> ; if not, goto cont:
0x08048611 <+88>: mov eax,DWORD PTR [ebp+0xc] ; parell(\&input, envp)
0x08048614 <+91>: mov DWORD PTR [esp+0x4],eax ; |
0x08048618 <+95>: mov eax,DWORD PTR [ebp+0x8] ; |
0x0804861b <+98>: mov DWORD PTR [esp],eax ; |
0x0804861e <+101>: call 0x8048542 <parell> ; |
0x08048623 <+106>: lea eax,[ebp-0xc] ;cont: var3++
0x08048626 <+109>: inc DWORD PTR [eax] ; |
0x08048628 <+111>: jmp 0x80485cd <check+20> ; goto start
0x0804862a <+113>: call 0x8048524 <che> ;che: che() exits
0x0804862f <+118>: mov eax,DWORD PTR [ebp+0xc] ; dummy(var1, envp)
0x08048632 <+121>: mov DWORD PTR [esp+0x4],eax ; |
0x08048636 <+125>: mov eax,DWORD PTR [ebp-0x4] ; |
0x08048639 <+128>: mov DWORD PTR [esp],eax ; |
x0804863c <+131>: call 0x80484b4 <dummy> ; |
0x08048641 <+136>: test eax,eax ; eax ?= 0
0x08048643 <+138>: je 0x804867b <check+194> ; if so, return
0x08048645 <+140>: mov DWORD PTR [ebp-0xc],0x0 ; var3 = 0
0x0804864c <+147>: cmp DWORD PTR [ebp-0xc],0x9 ;start2: var3 ?> 9
0x08048650 <+151>: jg 0x804867b <check+194> ; if so, return
0x08048652 <+153>: mov eax,DWORD PTR [ebp-0x4] ; var1 even?
0x08048655 <+156>: and eax,0x1 ; |
0x08048658 <+159>: test eax,eax ; |
0x0804865a <+161>: jne 0x8048674 <check+187> ; if not, goto cont2:
0x0804865c <+163>: mov DWORD PTR [esp],0x80487d3 ; printf("wtf?")
0x08048663 <+170>: call 0x80483b8 <printf@plt> ; |
0x08048668 <+175>: mov DWORD PTR [esp],0x0 ; exit(0)
0x0804866f <+182>: call 0x80483e8 <exit@plt> ; |
0x08048674 <+187>: lea eax,[ebp-0xc] ;cont2: var3++
0x08048677 <+190>: inc DWORD PTR [eax] ; |
0x08048679 <+192>: jmp 0x804864c <check+147> ; goto start2
0x0804867b <+194>: leave ; return
0x0804867c <+195>: ret ; |
Near the bottom, we see that wtf?
is printed, but this state does not appear to be reachable. Additionally, check
now calls che()
whenever an invalid condition is encountered. What is this che
function?
0x08048524 <+0>: push ebp ; set up stack frame
0x08048525 <+1>: mov ebp,esp ; |
0x08048527 <+3>: sub esp,0x8 ; |
0x0804852a <+6>: mov DWORD PTR [esp],0x80487ad; printf("Password Incorrect")
0x08048531 <+13>: call 0x80483b8 <printf@plt> ; |
0x08048536 <+18>: mov DWORD PTR [esp],0x0 ; exit 0
0x0804853d <+25>: call 0x80483e8 <exit@plt> ; |
che
simply prints Password Incorrect!
and exits the program. parell
acts the same as in crackme0x06
, checking that our input is an even number after calling dummy
to perform additional validation. The dummy
function hasn't changed either, but will now modify a value in the data segment upon failure. As with crackme0x06
, our solution is LOL="" ./crackme0x08
and entering 88
.
crackme0x09
features a stripped binary, where the strings printed are accessed at offsets relative to the base pointer. Nothing else changes, however, and the solution is identical to crackme0x08
.