Artificial truth

The more you see, the less you believe.

[archives] [latest] | [homepage] | [atom/rss]

Defeating crackme03 with radare2
Fri 08 November 2013 — download

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
  1. ebp = 0
  2. ebp = ebp +1 +1
  3. eax = 0x8010
  4. eax = ebp * eax
  5. eax = eax - [0x10014] = eax - 0x140C
  6. goto badboy if eax != 0

Note: mul ebp is equivalent to mul eax, ebp.

  1. esi = eax = 0x8010 * 2
  2. esi = esi - edx = esi - 0x140c
  3. si = si ^ 0xec14
  4. 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 ;)