xvilka asked me to do a writeup with radare2, to serve as an example for the release of radare2 0.9.6. I took the one from hackingbits, since the previous ones where "relatively easy but not boring".
The crackme
So, here we go:
$ ./crackme.03.32
Try to find the string of success and make me print it.
Okay, doesn't seems fancy for now.
$ rabin2 ./crackme.03.32 -deilMRsSz
Warning: read (shdr) at 0xc0312ab3
Warning: Cannot initialize section headers
Warning: Cannot initialize strings table
[Sections]
idx=00 addr=0x00000000 off=0x00000000 sz=372 vsz=372 perm=-rwx name=undefined
1 sections
[Entrypoints]
addr=0x00010020 off=0x00000020 baddr=0x00010000
1 entrypoints
[Imports]
0 imports
[Symbols]
0 symbols
[strings]
0 strings
[Linked libraries]
0 libraries
[Relocations]
0 relocations
A small file, with no symbols, no strings, no sections, no imports, a weird header… Looks like a hand-crafted binary!
Analysis
$ r2 ./crackme.03.32
Warning: read (shdr) at 0xc0312ab3
Warning: Cannot initialize section headers
Warning: Cannot initialize strings table
[0x00010020]> aa
[0x0001005b]> pdf
╒ (fcn) entry0 424
│ 0x00010020 b32a mov bl, 0x2a
│ 0x00010022 31c0 xor eax, eax
│ 0x00010024 40 inc eax
│ ┌─< 0x00010025 eb12 jmp loc.00010039
│ │ 0x00010027 003400 add [eax+eax], dh
│ │ 0x0001002a 2000 and [eax], al
│ │ 0x0001002c 0100 add [eax], eax
│ │ 0x0001002e 0c14 or al, 0x14
│ │ 0x00010030 90 nop
│ │ 0x00010031 7c97 jl 0x10000ffca
│ │ 0x00010033 ad lodsd
│ │ 0x00010034 b6b6 mov dh, 0xb6
│ │ 0x00010036 c6c0bf mov al, 0xbf
│ └─> 0x00010039 29c9 sub ecx, ecx
│ 0x0001003b b900000100 mov ecx, 0x10000
│ 0x00010040 31d2 xor edx, edx
│ 0x00010042 31db xor ebx, ebx
│ ┌──> 0x00010044 8a19 mov bl, [ecx]
│ │ 0x00010046 01da add edx, ebx
│ │ 0x00010048 41 inc ecx
│ │ 0x00010049 81f92e000100 cmp ecx, 0x1002e
│ └──< 0x0001004f 75f3 jnz 0x100010044
│ 0x00010051 c1e202 shl edx, 0x2
│ 0x00010054 663b152e000. cmp dx, [0x1002e]
│ ┌───< 0x0001005b 7529 jnz 0x10086
│ │ 0x0001005d 31ed xor ebp, ebp
│ │ 0x0001005f 89d7 mov edi, edx
│ │ 0x00010061 45 inc ebp
│ │ 0x00010062 b810800000 mov eax, 0x8010
│ │ 0x00010067 45 inc ebp
│ │ 0x00010068 f7e5 mul ebp
│ │ 0x0001006a 96 xchg esi, eax
│ │ 0x0001006b 89f0 mov eax, esi
│ │ 0x0001006d 662b0514000. sub ax, [0x10014]
│ ┌────< 0x00010074 7510 jnz 0x10086
│ ││ 0x00010076 29fe sub esi, edi
│ ││ 0x00010078 6681f614ec xor si, 0xec14
│ ┌─────< 0x0001007d 7507 jnz 0x10086
│ ┌──────< 0x0001007f eb01 jmp fcn.00010082
It seems that the block between 0x00010025 and 0x00010039 is not used. We can hide it with:
[0x00010020]> s 0x00010027 # seek to 0x00010025
[0x00010027]> Ch 0x00010039 - 0x00010027 # hide code starting from 0x00010025 for 0x00010039 - 0x00010025 bytes
[0x00010027]> s- # seek back to the previous location
[0x00010020]> pdf
╒ (fcn) entry0 424
│ 0x00010020 b32a mov bl, 0x2a
│ 0x00010022 31c0 xor eax, eax
│ 0x00010024 40 inc eax
│ ┌─< 0x00010025 eb12 jmp 0x10039
│ │ 0x00010027 (18 bytes hidden)
│ └─> 0x00010039 29c9 sub ecx, ecx
│ 0x0001003b b900000100 mov ecx, 0x10000
│ 0x00010040 31d2 xor edx, edx
│ 0x00010042 31db xor ebx, ebx
│ ┌──> 0x00010044 8a19 mov bl, [ecx]
│ │ 0x00010046 01da add edx, ebx
│ │ 0x00010048 41 inc ecx
│ │ 0x00010049 81f92e000100 cmp ecx, 0x1002e
│ └──< 0x0001004f 75f3 jnz 0x100010044
│ 0x00010051 c1e202 shl edx, 0x2
│ 0x00010054 663b152e000. cmp dx, [0x1002e]
│ ┌───< 0x0001005b 7529 jnz 0x10086
│ │ 0x0001005d 31ed xor ebp, ebp
│ │ 0x0001005f 89d7 mov edi, edx
│ │ 0x00010061 45 inc ebp
│ │ 0x00010062 b810800000 mov eax, 0x8010
│ │ 0x00010067 45 inc ebp
│ │ 0x00010068 f7e5 mul ebp
│ │ 0x0001006a 96 xchg esi, eax
│ │ 0x0001006b 89f0 mov eax, esi
│ │ 0x0001006d 662b0514000. sub ax, [0x10014]
│ ┌────< 0x00010074 7510 jnz 0x10086
│ ││ 0x00010076 29fe sub esi, edi
│ ││ 0x00010078 6681f614ec xor si, 0xec14
│ ┌─────< 0x0001007d 7507 jnz 0x10086
│ ┌──────< 0x0001007f eb01 jmp fcn.00010082
Yay, way better !
First checksum
We can see a short loop starting at 0x00010044, that loads 0x10000. Since the headers is screwed to prevent loading in GNU tools, this is likely a checksum to prevent modifications. Further, they are 3 jumps to 0x10086. This may be a badboy
[0x0001005b]> pdb @0x10086
│ 0x00010086 29d2 sub edx, edx
│ 0x00010088 7438 jz 0x100c2
Classic trick to fool automatic analyzers. Of course edx - edx is always equal to zero, the jump is taken. If arrows are annoying you, feel free to turn them off with e asm.lines = false .
Badboy
[0x0001005b]> pd @0x100c2
0x000100c2 b804000000 mov eax, 0x4
0x000100c7 bb01000000 mov ebx, 0x1
0x000100cc b98a000100 mov ecx, 0x1008a
0x000100d1 ba38000000 mov edx, 0x38
0x000100d6 cd80 int 0x80
0x000100d8 b801000000 mov eax, 0x1
0x000100dd bb00000000 mov ebx, 0x0
0x000100e2 cd80 int 0x80
Time to take a look at the syscall reference (You can also check you local syscall.h). Looks like the first one is a write, and the second an exit. This is a badboy. Just to be sure, let's check what is printed. sys_write takes:
- Like every syscall, the call number in eax (4)
- The file descriptor in ebx (1, aka stdout),
- The buffer to print in ecx (0x1008a, this is an address.), and the lenght in edx (0x38).
What is at 0x1008a?
[0x0001005b]> ps 0x38 @0x1008a
Try to find the string of success and make me print it.
[0x0001005b]>
We should add a comment at 0x1008a:
[0x0001005b]> CCa 0x1008a BADBOY
We should focus on avoiding jumps to this location.
Reversing the checksum
Back to 0x00010082. Since no input/output operations occurs until this jump, you can bet that all this part was a checksum. Let's reverse the checksum, for fun (We'll likely patch the jumps anyway; there is no point to reverse it, but since there is some black magic involved…) If you're not familiar with assembly, setting e asm.pseudo = true may help.
[0x00010020]> pd @0x0001003b
│ 0x0001003b b900000100 mov ecx, 0x10000
│ 0x00010040 31d2 xor edx, edx
│ 0x00010042 31db xor ebx, ebx
│ ┌──> 0x00010044 8a19 mov bl, [ecx]
│ │ 0x00010046 01da add edx, ebx
│ │ 0x00010048 41 inc ecx
│ │ 0x00010049 81f92e000100 cmp ecx, 0x1002e
│ └──< 0x0001004f 75f3 jnz 0x100010044
│ 0x00010051 c1e202 shl edx, 0x2
│ 0x00010054 663b152e000. cmp dx, [0x1002e]
│ ┌───< 0x0001005b 7529 jnz 0x10086 ; badboy
This code will load the address 0x10000 (Pointing to the first byte of the binary) in ecx, ebx and ebx are set to zero, and the loop starts:
1. bl = *ecx
2. edx = edx + ebx
3. ecx++
4. goto 1. if ecx != 0x1002e
5. edx = edx * 0x2
6. goto badboy if edx != [0x1002e]
Did you notice that the loop is increasing ecx, and not the value "pointed" by ecx ? This loop will add every bytes between 0x10000 and 0x1002e, and the sum must be equal to the value at 0x1002e.
[0x0001005b]> pfw @0x1002e
0x0001002e = 0x140c
This is indeed a checksum to check the integrity of the header.
│ │ 0x0001005d 31ed xor ebp, ebp
│ │ 0x0001005f 89d7 mov edi, edx
│ │ 0x00010061 45 inc ebp
│ │ 0x00010062 b810800000 mov eax, 0x8010
│ │ 0x00010067 45 inc ebp
│ │ 0x00010068 f7e5 mul ebp
│ │ 0x0001006a 96 xchg esi, eax
│ │ 0x0001006b 89f0 mov eax, esi
│ │ 0x0001006d 662b0514000. sub ax, [0x10014]
│ ┌────< 0x00010074 7510 jnz 0x10086
│ ││ 0x00010076 29fe sub esi, edi
│ ││ 0x00010078 6681f614ec xor si, 0xec14
│ ┌─────< 0x0001007d 7507 jnz 0x10086
│ ┌──────< 0x0001007f eb01 jmp fcn.00010082
- ebp = 0
- ebp = ebp +1 +1
- eax = 0x8010
- eax = ebp * eax
- eax = eax - [0x10014] = eax - 0x140C
- goto badboy if eax != 0
Note: mul ebp is equivalent to mul eax, ebp.
- esi = eax = 0x8010 * 2
- esi = esi - edx = esi - 0x140c
- si = si ^ 0xec14
- goto badboy if si != 0
[0x0001005b]> pd @0x00010082
0x00010082 31c0 xor eax, eax
0x00010084 755e jnz 0x100e4
0x00010086 29d2 sub edx, edx
Ok, this is weird. The jump is never taken, and we'll end up in badboy. But if you take a look at the README, you'll notice that this more a patchme than a crackme. It seems that we should patch here. To load the file within radare2 in write mode, you can use the -w option. If the jump was taken, we'll land right after the badboy block.
Decryption
│ 0x000100e4 31d2 xor edx, edx
│ 0x000100e6 6839000100 push loc.00010039 ; 0x00010039
│ 0x000100eb 66832c240b sub word [esp], 0xb
│ 0x000100f0 5e pop esi
│ 0x000100f1 8d7601 lea esi, [esi+0x1]
│ 0x000100f4 29c9 sub ecx, ecx
│ 0x000100f6 75ec jnz 0x1000100e4
│ 0x000100f8 46 inc esi
│ 0x000100f9 eb01 jmp loc.000100fc
│ 0x000100fb c3 ret ; garbage instruction
│ 0x000100fc 8a16 mov dl, [esi]
│ 0x000100fe 88140c mov [esp+ecx], dl
│ 0x00010101 41 inc ecx
│ 0x00010102 83f909 cmp ecx, 0x9
│ 0x00010105 75f1 jnz 0x1000100f8
Fancy push/pop trick at 0x000100e6: 1. 0x00010039 is pushed on the stack 2. 0xb is substracted from [esp], which is indeed 0x00010039 3. The top of the stack (0x00010039 - 0xb) is poped into esi.
The routine looks rougly like: 1. esi = 0x00010039 2. esi = esi - 0xb + 1 + 1 = 0x00010030
It seems that 9 bytes are also pushed on the stack "manually", at 0x000100fe, in a small loop.
│ 0x00010107 29d2 sub edx, edx
│ 0x00010109 31c9 xor ecx, ecx
│ 0x0001010b 41 inc ecx
│ 0x0001010c 8a140c mov dl, [esp+ecx]
│ 0x0001010f 80ea09 sub dl, 0x9
│ 0x00010112 80f2ac xor dl, 0xac
│ 0x00010115 eb02 jmp loc.00010119
│ 0x00010117 e84132540c call 0xc55335d
│ 0x0001011c ff88140c83f9 dec dword [eax-0x67cf3ec]
│ 0x00010122 0875e6 or [ebp-0x1a], dh
│ 0x00010125 41 inc ecx
│ 0x00010126 c6040c0a mov byte [esp+ecx], 0xa
│ 0x0001012a 49 dec ecx
│ 0x0001012b 87d1 xchg ecx, edx
│ 0x0001012d 42 inc edx
│ 0x0001012e 44 inc esp
│ 0x0001012f eb01 jmp loc.00010132
This looks like a decryption one. Nothing complicated. 1. edx = 0 2. ecx = 0 3. ecx = ecx + 1 4. dl = esp + ecx 5. dl = dl - 0x9 6. dl = dl ^ 0xac 7. dl = dl ^ (esp + ecx - 1) 8. goto 3 if ecx != 8
Here is the corresponding code in Python:
array = [ 0x90, 0x7C, 0x97, 0xAD, 0xB6, 0xB6, 0xC6, 0xC0, 0xBF ]
for i in range(0,8):
array[i+1] = (array[i+1] - 9) ^ 0xac ^ array[i]
print ''.join([chr(i) for i in array])
This prints ?Omedetou, a Japanese word meaning Congratulations. The weird char on the front will likely be skip later. Looks like we're on the right track.
Another checksum
│0x00010132 b804000000 mov eax, 0x4
│0x00010137 bb01000000 mov ebx, 0x1
│ 0x00010136 00bb01000000 add [ebx+0x1], bh
│ 0x0001013c 89e1 mov ecx, esp
│ 0x0001013e 60 pushad
│ 0x0001013f 31c9 xor ecx, ecx
│ 0x00010141 51 push ecx
│ 0x00010142 b900000100 mov ecx, 0x10000
│ 0x00010147 5a pop edx
│ 0x00010148 89d3 mov ebx, edx
│ 0x0001014a 8a19 mov bl, [ecx]
│ 0x0001014c 01da add edx, ebx
│ 0x0001014e 41 inc ecx
│ 0x0001014f 81f972010100 cmp ecx, fcn.00010172 ; 0x7f6d
│ 0x00010155 75f3 jnz 0x10001014a
│ 0x00010157 eb01 jmp loc.0001015a
│ 0x00010159 cd66 int 0x66 ; garbage
│ 0x0001015b 3b1572010100 cmp edx, [fcn.00010172] ;checksum comparison
│ 0x00010161 0f851fffffff jnz 0x100010086
│ 0x00010167 61 popad
│ 0x00010168 eb01 jmp fcn.0001016b ; badboy
│ 0x0001016a c9 leave
│ 0x0001016b cd80 int 0x80
│ 0x0001016d e966ffffff jmp 0x1000100d8
[0x0001005b]> pd@0x000100d8
│ 0x000100d8 b801000000 mov eax, 0x1
│ 0x000100dd bb00000000 mov ebx, 0x0
│ 0x000100e2 cd80 int 0x8
[0x0001005b]> pfw @0x00010172
0x00010172 = 0x7f6d
Once again, a checksum (Reversing it is left as an exercice to the reader.), but right before, a pushad. Since the pushad opcode might not be obvious for everyone, let's ask radare2 about it:
[0x0001005b]> ?d pushad
push all general-purpose registers
This will push on the stack the following registers:
1. eax = 0x4
2. ebx = 0x1
3. ecx = the previously deciphered text + 1
4. edx = 0x8
After the checksum, registers are poped back, and a syscall (0x80) occurs. It's likely a call to sys_write ;) Then, a jump to the sys_exit of badboy.
Patching
To sum up, we need to: - invert the jump at 0x00010084 - bypass the final checksum at 0x00010161 (Since it will detect the modification at 0x00010084)
First patch:
[0x0001005b]>s 0x00010084
[0x0001005b]>pd 1
0x00010084 755e jnz 0x100e4
[0x0001005b]> wx 74
[0x0001005b]>pd 1
0x00010084 745e jz 0x100e4
If you open your intel manual, you'll see that the opcode for jnz is 75, and the one for jz is 74. But you may not know every correspondences. Fortunately, radare2 provides more convenients way. Second patch:
[0x0001005b]>s 0x00010161
[0x00010161]>pd 1
0x00010161 0f851fffffff jnz 0x100010086
[0x00010161]>wa jn 0x100010086
[0x00010161]>pd 1
0x00010161 0f841fffffff jz 0x100010086
Hurray ! Let's check that everything is working:
$ ./test.32
Omedetou
Conclusion
Thank you Geyslan for your nice crackme, and to pancake for r2 ;)