The IOLI Crackmes

About

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 :)

Tools

The only tool I used to solve this challenge was GDB (with GEF installed). Any similar interactive debugger such as radare2 will work.

Review

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.

Hints
Click here to open hints for each level
crackme0x00

The passcode for this level is stored as plaintext in the binary.

crackme0x01

The integer passcode is hard-coded and directly compared to user input.

crackme0x02

The majority of main() is obfuscation; solving this challenge is similar to crackme0x01.

crackme0x03

shift() decrypts the strings Password OK!!! :) and Invalid Password!, and the majority of main() is obfuscation.

crackme0x04

check() performs some mathematical operations on each character which the user inputs.

crackme0x05

An additional restriction is added to crackme0x04. Examine parell().

crackme0x06

An additional restriction is added to crackme0x05. Look at environment variables and dummy().

crackme0x07

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()?

crackme0x08

This challenge is the same as crackme0x07, without symbols stripped.

crackme0x09

The same solution is again valid.

Solutions
crackme0x00

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.

crackme0x01

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.

crackme0x02

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:

stack convention
crackme0x03

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.

crackme0x04

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.

crackme0x05

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.

crackme0x06

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

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.

crackme0x08

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

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.