[0x2:Crackme] Basic Reversing with Radare2 and GDB Peda.
Post was written on 28th June, but crackme0x09
was completed only on 01/10
after I was crushed at two ctfs.
Note: I don’t have a complete solution for 0x09, and I received help in the form of reading WereW’s writeup
CrossCTF: 10th Qualifiying. 14th Finals. HackIt CTF: 82nd.
We didn’t perform very well, and its apparent that my skills are severly lacking in all aspects. My current documentation with my progress using this blog is also, quite frankly, terrible. I will look forward to updating the blog to be more readable and easier to work with, as well as continue to refine my skillsets in the RE, Pwn and Web areas.
Crackme0x04
Hmm.
JAE
. We haven’t seen that one before.
JAE
Jump if Above or Equal (unsigned comparison)
jae loc
CF = 0 or ZF = 1
Remember that back when cmp
was first introduced to us, we know that it performs a bitwise AND operation with the two operands.
It will modify the flags ZF=1
iff the result of the operation is 0 (i.e, the operands are equal).
We don’t want to take the jump, so we just need to set our argument to whatever dword [ebp - local_ch]
is NOT.
That seems to work fine, however we see later on that there is a cmp dword [ebp - local_8h], 0xf
.
Do we actually control that local variable?
At this point you can actually enter GDB and realise that what the program really is doing is just adding each digit together, and all combinations that reach 16 as a sum would work. But lets look at what the instructions are doing.
| 0x080484a8 8b45f4 mov eax, dword [ebp - local_ch] // Moving Local variable _CH into eax. (turns out to be zero)
| 0x080484ab 034508 add eax, dword [ebp + arg_8h] // Then we add our argument to it. !This means we're offsetting it by 0!
| 0x080484ae 0fb600 movzx eax, byte [eax] // movzx is basically moving with zero padded.
// So we the byte size of [eax] into eax. This means the first digit.
| 0x080484b1 8845f3 mov byte [ebp - local_dh], al // Then we move the lowest 1 byte into variable _DH. (Also first digit) >--+
| 0x080484b4 8d45fc lea eax, [ebp - local_4h] // And load the address of variable _4H into eax. |
| 0x080484b7 89442408 mov dword [esp + local_8h_2], eax // Which is then moved into local variable _8H_2. |
| ; [0x8048638:4]=0x50006425 |
| ; "%d" |
| 0x080484bb c74424043886. mov dword [esp + local_4h_2], ,0x8048638 // just '%d" |
| 0x080484c3 8d45f3 lea eax, [ebp - local_dh] // Looks like we're now putting the address of our first Digit here. <------+
| 0x080484c6 890424 mov dword [esp], eax // Then mov-ing it to [esp]? Preparing for a function call huh.
| 0x080484c9 e8d6feffff call sym.imp.sscanf ;[ge] // What this scanf call does is to write the first digit into the address.
// The address it writes to belong to local variable _4H.
| 0x080484ce 8b55fc mov edx, dword [ebp - local_4h] // We should expect edx to hold the first digit now.
| 0x080484d1 8d45f8 lea eax, [ebp - local_8h] // Effective address of local variable _8H loaded into eax.
| 0x080484d4 0110 add dword [eax], edx // Then added by edx (first digit)
| ; [0xf:4]=0x3000200
| 0x080484d6 837df80f cmp dword [ebp - local_8h], 0xf // and its value is compared to 15.
| 0x080484da 7518 jne 0x80484f4 ;[gf] // We need the value to be 15!
But we can’t enter 15 as a single digit right? We need at least 2 digits, like “87”.
What happens when the jump is taken?
|0x080484f4 8d45f4 lea eax, [ebp - local_ch] // Effective address of local variable _CH loaded into eax. Its 0.
|0x080484f7 ff00 inc dword [eax] // Now local variable _CH is incremented to 1.
|0x080484f9 eb9d jmp 0x8048498 ;[gd] // We then jump back to the start.
We loop back again, but this time the offset is 1! This means we’re going to be looking at the second digit. Remember we still have our first digit in variable _8H. After the sscanf, we see that our second digit will be added. This means we’re doing a digit-by-digit sum!
Also, realise that the block of function with STRLEN
is just trying to make sure we don’t read past the number of digits.
And that each digit must be bigger than 0.
Crackme0x05
Basically the same thing, just that the sum needs to be 16.
At this point, I was getting kind of bored. So I decided to skip all the way to the final level. :x
Crackme0x09
We see that this is a stripped binary - meaning we can’t do pdf @ sym.main
here.
In stripped binaries, the debugging symbols are removed from the binary. The primary benefit to this is to save memory, but it also makes the binary more difficult to disassemble or reverse.
But hey, we can still see main
.
So lets do pdf @ main
.
[0x08048420]> pdf @ main
/ (fcn) main 120
| main (int arg_10h);
| ; var int local_78h @ ebp-0x78
| ; var int local_4h_2 @ ebp-0x4
| ; arg int arg_10h @ ebp+0x10
| ; var int local_4h @ esp+0x4
| ; DATA XREF from 0x08048437 (entry0)
| 0x080486ee 55 push ebp
| 0x080486ef 89e5 mov ebp, esp
| 0x080486f1 53 push ebx
| 0x080486f2 81ec84000000 sub esp, 0x84
| 0x080486f8 e869000000 call fcn.08048766
| 0x080486fd 81c3f7180000 add ebx, 0x18f7
| 0x08048703 83e4f0 and esp, 0xfffffff0
| 0x08048706 b800000000 mov eax, 0
| 0x0804870b 83c00f add eax, 0xf
| 0x0804870e 83c00f add eax, 0xf
| 0x08048711 c1e804 shr eax, 4
| 0x08048714 c1e004 shl eax, 4
| 0x08048717 29c4 sub esp, eax
| 0x08048719 8d8375e8ffff lea eax, [ebx - 0x178b]
| 0x0804871f 890424 mov dword [esp], eax
| 0x08048722 e8b9fcffff call sym.imp.printf ; int printf(const char *format)
| 0x08048727 8d838ee8ffff lea eax, [ebx - 0x1772]
| 0x0804872d 890424 mov dword [esp], eax
| 0x08048730 e8abfcffff call sym.imp.printf ; int printf(const char *format)
| 0x08048735 8d4588 lea eax, [local_78h]
| 0x08048738 89442404 mov dword [local_4h], eax
| 0x0804873c 8d8399e8ffff lea eax, [ebx - 0x1767]
| 0x08048742 890424 mov dword [esp], eax
| 0x08048745 e876fcffff call sym.imp.scanf ; int scanf(const char *format)
| 0x0804874a 8b4510 mov eax, dword [arg_10h] ; [0x10:4]=-1 ; 16
| 0x0804874d 89442404 mov dword [local_4h], eax
| 0x08048751 8d4588 lea eax, [local_78h]
| 0x08048754 890424 mov dword [esp], eax
| 0x08048757 e8bafeffff call sub.strlen_616 ; size_t strlen(const char *s)
| 0x0804875c b800000000 mov eax, 0
| 0x08048761 8b5dfc mov ebx, dword [local_4h_2]
| 0x08048764 c9 leave
\ 0x08048765 c3 ret
The rest of the functions are not so lucky - we see them as fcn.*
.
The main
function can be located in a stripped ELF binary is straightforward, no symbol information is required.
The prototype for __libc_start_main is
int __libc_start_main(int (*main) (int, char**, char**),
int argc,
char *__unbounded *__unbounded ubp_av,
void (*init) (void),
void (*fini) (void),
void (*rtld_fini) (void),
void (*__unbounded stack_end));
The runtime memory address of main()
is the argument int
, which also means that it will be the last memory addressed saved on runtime stack prior to calling __libc_start_main is the memory address of main(), since arguments are pushed onto the runtime stack in the reverse order.
We can also see an interesting function call -
[0x08048420]> afn do_something fcn.08048616
[0x08048420]> pdf @ do_something
afn
renames the function to do_something in radare.
Lets look at that function.
| .-> 0x08048636 8b4508 mov eax, dword [userInput] ; [0x8:4]=-1 ; 8
| | 0x08048639 890424 mov dword [esp], eax
| | 0x0804863c e88ffdffff call sym.imp.strlen ; size_t strlen(const char *s)
| | 0x08048641 3945f0 cmp dword [count2], eax ; [0x13:4]=-1 ; 19
| ,==< 0x08048644 734f jae 0x8048695
| || 0x08048646 8b45f0 mov eax, dword [count2]
| || 0x08048649 034508 add eax, dword [userInput]
| || 0x0804864c 0fb600 movzx eax, byte [eax]
| || 0x0804864f 8845ef mov byte [local_11h], al
| || 0x08048652 8d45f8 lea eax, [local_8h]
| || 0x08048655 89442408 mov dword [local_8h_2], eax
| || 0x08048659 8d835ee8ffff lea eax, [ebx - 0x17a2]
| || 0x0804865f 89442404 mov dword [local_4h], eax
| || 0x08048663 8d45ef lea eax, [local_11h]
| || 0x08048666 890424 mov dword [esp], eax
| || 0x08048669 e882fdffff call sym.imp.sscanf ; int sscanf(const char *s,
| || 0x0804866e 8b55f8 mov edx, dword [local_8h]
| || 0x08048671 8d45f4 lea eax, [count1]
| || 0x08048674 0110 add dword [eax], edx
| || 0x08048676 837df410 cmp dword [count1], 0x10 ; [0x10:4]=-1 ; 16
| ,===< 0x0804867a 7512 jne 0x804868e
| ||| 0x0804867c 8b450c mov eax, dword [arg_ch] ; [0xc:4]=-1 ; 12
| ||| 0x0804867f 89442404 mov dword [local_4h], eax
| ||| 0x08048683 8b4508 mov eax, dword [userInput] ; [0x8:4]=-1 ; 8
| ||| 0x08048686 890424 mov dword [esp], eax
| ||| 0x08048689 e8fbfeffff call sub.sscanf_589 ; int sscanf(const char *s,
| ||! ; JMP XREF from 0x0804867a (do_something)
| `---> 0x0804868e 8d45f0 lea eax, [count2]
| || 0x08048691 ff00 inc dword [eax]
| |`=< 0x08048693 eba1 jmp 0x8048636
| | ; JMP XREF from 0x08048644 (do_something)
| `--> 0x08048695 e8c3feffff call sub.printf_55d ; int printf(const char *format)
| 0x0804869a 8b450c mov eax, dword [arg_ch] ; [0xc:4]=-1 ; 12
| 0x0804869d 89442404 mov dword [local_4h], eax
| 0x080486a1 8b45f8 mov eax, dword [local_8h]
| 0x080486a4 890424 mov dword [esp], eax
| 0x080486a7 e828feffff call sub.strncmp_4d4 ; int strncmp(const char *s1, const char *s2, size_t n)
| 0x080486ac 85c0 test eax, eax
| ,=< 0x080486ae 7438 je 0x80486e8
| | 0x080486b0 c745f0000000. mov dword [count2], 0
| | ; JMP XREF from 0x080486e6 (do_something)
| .--> 0x080486b7 837df009 cmp dword [count2], 9 ; [0x9:4]=-1 ; 9
| ,===< 0x080486bb 7f2b jg 0x80486e8
| ||| 0x080486bd 8b45f8 mov eax, dword [local_8h]
| ||| 0x080486c0 83e001 and eax, 1
| ||| 0x080486c3 85c0 test eax, eax
| ,====< 0x080486c5 751a jne 0x80486e1
| |||| 0x080486c7 8d836fe8ffff lea eax, [ebx - 0x1791]
| |||| 0x080486cd 890424 mov dword [esp], eax
| |||| 0x080486d0 e80bfdffff call sym.imp.printf ; int printf(const char *format)
| |||| 0x080486d5 c70424000000. mov dword [esp], 0
| |||| 0x080486dc e82ffdffff call sym.imp.exit ; void exit(int status)
| ||!| ; JMP XREF from 0x080486c5 (do_something)
| `----> 0x080486e1 8d45f0 lea eax, [count2]
| ||| 0x080486e4 ff00 inc dword [eax]
| |`==< 0x080486e6 ebcf jmp 0x80486b7
| | | ; JMP XREF from 0x080486ae (do_something)
| | | ; JMP XREF from 0x080486bb (do_something)
| `-`-> 0x080486e8 83c424 add esp, 0x24 ; '$'
| 0x080486eb 5b pop ebx
| 0x080486ec 5d pop ebp
\ 0x080486ed c3 ret
We see two variables here at the start of the function.
I’ve renamed them to count1
and count2
using afvn <original_name> <new_name>
. (You need to seek to the function scope first.)
The arguement has also been renamed to userInput
.
Seeing the inc dword[eax]
here, and seeing that it only reaches that branch because of a previous jne
, we can guess that this is probably a for loop
.
From the use of userInput
, we see that the shortened 8 bits are used to add into the whatever has been added in already.
Hence, we deduce that count1
is the char by char summation, and we need count1
to be the sum of 16 - that sounds familiar!
Looks like our target is to reach call sub.sscanf_589
, now to see inside it:
[0x08048616]> pdf @ sub.sscanf_589
/ (fcn) sub.sscanf_589 141
| sub.sscanf_589 (int arg_8h, int arg_9h, int arg_ch);
| ; var int local_ch @ ebp-0xc
| ; var int local_8h @ ebp-0x8
| ; arg int arg_8h @ ebp+0x8
| ; arg int arg_9h @ ebp+0x9
| ; arg int arg_ch @ ebp+0xc
| ; CALL XREF from 0x08048689 (do_something)
| 0x08048589 55 push ebp
| 0x0804858a 89e5 mov ebp, esp
| 0x0804858c 53 push ebx
| 0x0804858d 83ec14 sub esp, 0x14
| 0x08048590 e8d1010000 call init_ebx
| 0x08048595 81c35f1a0000 add ebx, 0x1a5f
| 0x0804859b 8d45f8 lea eax, [local_8h]
| 0x0804859e 89442408 mov dword [esp + 8], eax
| 0x080485a2 8d835ee8ffff lea eax, [ebx - 0x17a2]
| 0x080485a8 89442404 mov dword [esp + 4], eax
| 0x080485ac 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8
| 0x080485af 890424 mov dword [esp], eax
| 0x080485b2 e839feffff call sym.imp.sscanf ; int sscanf(const char *s,
| 0x080485b7 8b450c mov eax, dword [arg_ch] ; [0xc:4]=-1 ; 12
| 0x080485ba 89442404 mov dword [esp + 4], eax
| 0x080485be 8b45f8 mov eax, dword [local_8h]
| 0x080485c1 890424 mov dword [esp], eax
| 0x080485c4 e80bffffff call sub.strncmp_4d4 ; int strncmp(const char *s1, const char *s2, size_t n)
| 0x080485c9 85c0 test eax, eax
| ,=< 0x080485cb 7443 je 0x8048610
| | 0x080485cd c745f4000000. mov dword [local_ch], 0
| | ; JMP XREF from 0x0804860e (sub.sscanf_589)
| .--> 0x080485d4 837df409 cmp dword [local_ch], 9 ; [0x9:4]=-1 ; 9
| ,===< 0x080485d8 7f36 jg 0x8048610
| ||| 0x080485da 8b45f8 mov eax, dword [local_8h]
| ||| 0x080485dd 83e001 and eax, 1
| ||| 0x080485e0 85c0 test eax, eax
| ,====< 0x080485e2 7525 jne 0x8048609
| |||| 0x080485e4 8b83fcffffff mov eax, dword [ebx - 4]
| |||| 0x080485ea 833801 cmp dword [eax], 1 ; [0x1:4]=-1 ; 1
| ,=====< 0x080485ed 750e jne 0x80485fd
| ||||| 0x080485ef 8d8361e8ffff lea eax, [ebx - 0x179f]
| ||||| 0x080485f5 890424 mov dword [esp], eax
| ||||| 0x080485f8 e8e3fdffff call sym.imp.printf ; int printf(const char *format)
| |||!| ; JMP XREF from 0x080485ed (sub.sscanf_589)
| `-----> 0x080485fd c70424000000. mov dword [esp], 0
| |||| 0x08048604 e807feffff call sym.imp.exit ; void exit(int status)
| ||!| ; JMP XREF from 0x080485e2 (sub.sscanf_589)
| `----> 0x08048609 8d45f4 lea eax, [local_ch]
| ||| 0x0804860c ff00 inc dword [eax]
| |`==< 0x0804860e ebc4 jmp 0x80485d4
| | | ; JMP XREF from 0x080485cb (sub.sscanf_589)
| | | ; JMP XREF from 0x080485d8 (sub.sscanf_589)
| `-`-> 0x08048610 83c414 add esp, 0x14
| 0x08048613 5b pop ebx
| 0x08048614 5d pop ebp
\ 0x08048615 c3 ret
From:
| 0x080485c4 e80bffffff call sub.strncmp_4d4 ; int strncmp(const char *s1, const char *s2, size_t n)
| 0x080485c9 85c0 test eax, eax
| 0x080485cb 7443 je 0x8048610
We see that whatever sub.strncmp_4d4 returns, if it equals to 0 (test eax,eax. Remember that TEST
sets the ZF
when the AND
is 0, and JE
jumps if ZF
is 0. Test does not save the result of the AND operation anywhere, but CMP
subtracts the second operand from the first operand.), it will jump to the end and exit out.
Lets take a look into it, and see what to supply so that the value it returns is not 0. In the lower half of the function,
| || 0x0804852a e8d1feffff call sym.imp.strncmp ; int strncmp(const char *s1, const char *s2, size_t n)
| || 0x0804852f 85c0 test eax, eax
| |`=< 0x08048531 75ba jne 0x80484ed
| | 0x08048533 8b83fcffffff mov eax, dword [ebx - 4]
| | 0x08048539 c70001000000 mov dword [eax], 1
| | 0x0804853f c745f4010000. mov dword [local_ch], 1
| |,=< 0x08048546 eb0c jmp 0x8048554
| || ; JMP XREF from 0x080484fe (sub.strncmp_4d4)
| `--> 0x08048548 c70424ffffff. mov dword [esp], 0xffffffff ; [0xffffffff:4]=-1 ; -1
| | 0x0804854f e8bcfeffff call sym.imp.exit ; void exit(int status)
| | ; JMP XREF from 0x08048546 (sub.strncmp_4d4)
| `-> 0x08048554 8b45f4 mov eax, dword [local_ch]
We spot this. So whatever sym.imp.strncmp
is for, if the result of this comparison is 0 (same) then we will get a value of 1
in eax
.
####At this point, I had a lot of help from WereW’s writeup. I’m not sure how he realized that there was an environment variable constraint, but the rest of his solution are as follows: https://werew.tk/article/11/ioli-crackme0x09
It saddens me to realize that I have a lot more to learn when I overestimated myself and jumped forword 5 levels. This blogpost was completed on 1st October, 3 months late due to University work and CTFs.