Artificial truth

The more you see, the less you believe.

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

Defeating crp-'s 888 with radare2
Wed 07 August 2013 — download

crp- is known for his amazing crackmes (See Tavis Ormandy's blogposts about them). I wanted to start with the easiest one : 888 (local mirror) (trace-q's difficulty is legendary, feel free to try it and fail ;) ).

Overview

$ ./888
NO

Okay.

$ strings ./888
4e X3Bf
104 ,$GOOD
15a x:^1
196 ^[Y^
1a7 Ht  Ht
1bf 5ARGSu
2da tI-OOPS

GOOD seems to be the objective.

The binary is likely hand-crafted asm. For this write up, I'll use:

  • radare2 for static analysis
  • GDB for dynamic one
  • bpython as a calculator/converter

Analysis

$ r2 ./888
[0x0804823f]> aa
[0x0804823f]> pdf
    0x0804823f     90               nop
    0x08048240     08c0             or al, al
    0x08048242     7403             jz 0x8048247
    0x08048244     7512             jnz 0x8048258
    0x08048246     e868759214       call dword 0x1c96f7b3

When a binary is ran, every registers for eax to edx, esi and edi are set to 0. The jump to 0x8048247 is taken.

[0x0804823f]> pd@0x8048247
    0x08048247     6875921418       push dword 0x18149275
    0x0804824c     812c2410101010   sub dword [esp], 0x10101010
    0x08048253     e801000000       call dword 0x8048259
[0x0804823f]> pd@0x8048259
    0x08048259     81042408000000   add dword [esp], 0x8
    0x08048260     c3               ret

Some esp-related magic also known as a push-ret trick?

>>> hex(int("18149275", 16) - int("10101010", 16))
0x8048265

0x8048265 is our next step!

[0x0804823f]> pd@0x8048265
    0x08048265     40               inc eax  ; eax = 1
    0x08048266     8f0578830408     pop dword [section_end.undefined]
    0x0804826c     85c0             test eax, eax
    0x0804826e     7415             jz 0x8048285  ; not taken
    0x08048270     6885820408       push dword 0x8048285
    0x08048275     ff0424           inc dword [esp]
    0x08048278     6821820408       push dword 0x8048221
    0x0804827d     81042402000000   add dword [esp], 0x2
    0x08048284     c3               ret

Two more push + ret trick:

  • 0x8048285 + 0x1 = 0x8048286
  • 0x8048221 + 0x2 = 0x8048223

0x8048223 is pushed in last, so it will be poped first:

[0x0804823f]> pd@0x8048223
    0x08048223     31c0             xor eax, eax
    0x08048225     b032             mov al, 0x32
    0x08048227     b902000000       mov ecx, 0x2
    0x0804822c     fec8             dec al
    0x0804822e     e2fc             loop 0x804822c
    0x08048230     29db             sub ebx, ebx
    0x08048232     b305             mov bl, 0x5
    0x08048234     b90b820408       mov ecx, 0x804820b
    0x08048239     49               dec ecx       ; ecx = 0x804820a
    0x0804823a     e936010000       jmp dword 0x8048375
[0x0804823f]> pd@0x8048375
    0x08048375     cd80             int 0x80
    0x08048377     c3               ret

int 80 is syscall. The loop instruction is a trick to set eax to 0x30 (0x32 - 0x2).

  • eax = 0x30
  • ebx = 0x5
  • ecx = 0x804820a

This can be seen as:

  • The 0x30th syscall is _sighandler_t signal(int signum, sighandler_t handler)
  • 0x5 is also known as the SIGTRAP signal, mostly used by debuggers.
  • 0x804820a is the handler's address

So, our function looks like:

signal(SIGTRAP, 0x804820a);

If you are using dynamic analysis beyond this point, you should keep in mind that our beloved GDB also uses SIGTRAP to manage breakpoints.

One more ret: we land at 0x8048286

[0x0804823f]> pd@0x8048286
    0x08048286     b8d2820400       mov eax, 0x482d2
    0x0804828b     01d8             add eax, ebx        ; eax = 0x482d2 + 0x5
    0x0804828d     29c3             sub ebx, eax        ; ebx = 0x5 - (0x482d2 + 0x5) = -0x482d2
    0x0804828f     01d8             add eax, ebx        ; eax = 0x482d2 + 0x5 + 0x5 - (0x482d2 + 0x5) = 0x5
    0x08048291     f7db             neg ebx             ; ebx = -(-0x482d20) = 0x482d2
    0x08048293     7905             jns 0x804829a       ; taken !
    0x08048295     e903000000       jmp dword 0x804829d
    0x0804829a     68aa820408       push dword 0x80482aa
    0x0804829f     c70584830408908. mov dword [0x8048384], 0xd4a08f90
    0x080482a9     c3               ret

[0x0804823f]> pd@0x80482aa
    0x080482aa     81cb00000008     or ebx, 0x8000000   ; ebx = 0x482d2 | 0x8000000 = 0x80482d2
    0x080482b0     43               inc ebx             ; ebx = 0x80482d3
    0x080482b1     31c3             xor ebx, eax
    0x080482b3     31d8             xor eax, ebx
    0x080482b5     31c3             xor ebx, eax
    0x080482b7     48               dec eax             ; eax = 0x80482d3 - 1 = 0x80482d2
    0x080482b8     53               push ebx
    0x080482b9     93               xchg ebx, eax
    0x080482ba     b033             mov al, 0x33        ; eax = 0x33
    0x080482bc     c0e002           shl al, 0x2         ; eax = 0x33 << 2 = 0xcc
    0x080482bf     8803             mov [ebx], al       ; [ebx] = 0xcc
    0x080482c1     93               xchg ebx, eax
    0x080482c2     5b               pop ebx             ; ebx = 0x5
    0x080482c3     48               dec eax             ; eax = 0x80482d2 - 1 = 0x80482d1
    0x080482c4     750a             jnz 0x80482d0       ; taken

The triple xor is a well known trick to swap variables. The instruction at 0x080482bf seems to be a self-modification trick: It replaces whatever was at 0x80482d2 with 0xCC, also known as the int3 instruction, that generates a SIGTRAP.

[0x0804823f]> pd@0x80482d0
    0x080482d0     ebf7             jmp 0x80482c9

[0x0804823f]> pd@0x80482c9
    0x080482c9     40               inc eax             ; eax = 0x80482d1 + 1 = 0x80482d2
    0x080482ca     7402             jz 0x80482ce        ; taken

[0x0804823f]> pd@0x080482ca
    0x080482ca     7402             jz 0x80482ce
    0x080482cc     50               push eax            ; push 0x80482d2
    0x080482cd     c3               ret

Remember that 0x80482d2 is now int3? This will trigger a SIGTRAP.

[0x0804823f]> pd@0x804820a
    0x0804820a     ff057c830408     inc dword [0x804837c]   ; looks like a global variable
    0x08048210     c7058083040803d. mov dword [0x8048380], 0x5b54d103 ; and another one
    0x0804821a     29c0             sub eax, eax          ; eax = 0
    0x0804821c     40               inc eax               ; eax = 1
    0x0804821d     f7d8             neg eax               ; eax = 0xffffffff
    0x0804821f     7802             js 0x8048223          ; taken

Your can rename global variables in radare2 with f.

f globvar.isdebuggerpresent @0x8048380

The global variables may be used to remember the presence (or absence) of a debugger. We already met 0x8048223 : this is where the signal handler for SIGTRAP is set. Since we've not seen any push-ret trick, this will simply resume after the int3, at 0x80482d3.

[0x0804823f]> pd@0x80482d3
        0x080482d3     a180830408       mov eax, [globvar.isdebuggerpresent] ; debugger-related global variable
    0x080482d8     85c0             test eax, eax
    0x080482da     7449             jz 0x8048325         ; jump to garbage
    0x080482dc     2d4f4f5053       sub eax, 0x53504f4f  ; eax = 0x5b54d103 - 0x53504f4f = 0x80481b4
    0x080482e1     ffd0             call eax
    0x080482e3     6866830408       push dword 0x8048366
    0x080482e8     6825820408       push dword 0x8048225
    0x080482ed     68a1810408       push dword 0x80481a1
    0x080482f2     68544c0508       push dword 0x8054c54
    0x080482f7     0fbae116         bt ecx, 0x16         ; copy ecx (=0) to CF
    0x080482fb     7301             jae 0x80482fe        ; taken !
    0x080482fd     7581             jnz 0x8048280
    0x080482ff     2c24             sub al, 0x24
    0x08048301     49               dec ecx
    0x08048302     cb               retf
    0x08048303     0000             add [eax], al
    0x08048305     8b442404         mov eax, [esp+0x4]
    0x08048309     48               dec eax
    0x0804830a     2d03000000       sub eax, 0x3
    0x0804830f     89442404         mov [esp+0x4], eax

[0x0804823f]> pd@0x80481b4
    0x080481b4     8f0588830408     pop dword [0x8048388]
    0x080481ba     58               pop eax              ; 0xBFFFF758
    0x080481bb     09c0             or eax, eax
    0x080481bd     7407             jz 0x80481c6
    0x080481bf     3541524753       xor eax, 0x53475241  ; eax == "ARGS"
    0x080481c4     75f4             jnz 0x80481ba
    0x080481c6     85c0             test eax, eax
    0x080481c8     7401             jz 0x80481cb         ; taken if eax == "ARGS"

This piece of code harvests the stack, looking for the ARGS value. What comes after ARGS? ENV of course (remember your C courses).

[0x0804823f]> pd@0x80481cb
    0x080481cb     8b1c24           mov ebx, [esp]
    0x080481ce     09db             or ebx, ebx
    0x080481d0     742e             jz 0x8048200
    0x080481d2     e87bfeffff       call dword 0x8048052
    0x080481d7     3d04000000       cmp eax, 0x4
    0x080481dc     7ced             jl 0x80481cb
    0x080481de     3d40000000       cmp eax, 0x40
    0x080481e3     7fe6             jg 0x80481cb
    0x080481e5     8b0b             mov ecx, [ebx]
    0x080481e7     81e1ffffff00     and ecx, 0xffffff
    0x080481ed     81f96b657900     cmp ecx, 0x79656b ; 0x79656b == "key"
    0x080481f3     75d6             jnz 0x80481cb
    0x080481f5     53               push ebx
    0x080481f6     68cb810408       push dword 0x80481cb
    0x080481fb     e93effffff       jmp dword 0x804813e

0x8048052 is called, and its return value must not be inferior to 4, nor superior to 0x40.

[0x0804823f]> pd@0x8048052
    0x08048052     877c2404         xchg [esp+0x4], edi
    0x08048056     30c0             xor al, al
    0x08048058     31c9             xor ecx, ecx
    0x0804805a     f7d1             not ecx
    0x0804805c     fc               cld
    0x0804805d     f2ae             repne scasb
    0x0804805f     f7d1             not ecx
    0x08048061     49               dec ecx
    0x08048062     89c8             mov eax, ecx
    0x08048064     877c2404         xchg [esp+0x4], edi
    0x08048068     c20400           ret 0x4

This looks like a strlen (hint: ask your search engine about repne scasb).

So, the previous piece of code is likely searching in env a environnement variable named key, with "key=bleh" not longer than 0x40, and not shorter than 0x4.

[0x0804823f]> pd@0x804813e
    0x0804813e     56               push esi
    0x0804813f     51               push ecx
    0x08048140     53               push ebx
    0x08048141     8b742410         mov esi, [esp+0x10]
    0x08048145     56               push esi
    0x08048146     fc               cld
    0x08048147     ac               lodsb
    0x08048148     3c3d             cmp al, 0x3d ; 0x3d == '='
    0x0804814a     7406             jz 0x8048152

Mh, a test for a = symbol. Seems like our intuition was right.

[0x0804823f]> pd@0x8048152
    0x08048152     56               push esi
    0x08048153     e86dffffff       call dword 0x80480c5  ; atoi()
    0x08048158     85c0             test eax, eax
    0x0804815a     783a             js 0x8048196
    0x0804815c     5e               pop esi
    0x0804815d     31c9             xor ecx, ecx
    0x0804815f     8a5e03           mov bl, [esi+0x3] ; esi+0x3, just after "key" ?
    0x08048162     80eb31           sub bl, 0x31      ; "key" + "1"
    0x08048165     741b             jz 0x8048182
    0x08048167     fecb             dec bl
    0x08048169     7405             jz 0x8048170      ; "key" + "2"
    0x0804816b     e927000000       jmp dword 0x8048197
    0x08048170     810d8c830408020. or dword [0x804838c], 0x2
    0x0804817a     81c104000000     add ecx, 0x4
    0x08048180     eb0a             jmp 0x804818c
    0x08048182     810d8c830408010. or dword [0x804838c], 0x1
    0x0804818c     81c190830408     add ecx, 0x8048390
    0x08048192     8901             mov [ecx], eax
    0x08048194     eb01             jmp 0x8048197
    0x08048196     5e               pop esi
    0x08048197     5b               pop ebx
    0x08048198     59               pop ecx
    0x08048199     5e               pop esi
    0x0804819a     c20400           ret 0x4

    f globvar.keyfound @0x804838c

We were wrong, the binary is looking for two variables, key1 and key2.

If it founds them, globvar.keyfound is set to 3, because 0 | 0x2 | 0x1 = 3.

[0x0804823f]> pd@0x80480c5
    0x080480c5     56               push esi
    0x080480c6     51               push ecx
    0x080480c7     53               push ebx
    0x080480c8     8b742410         mov esi, [esp+0x10]
    0x080480cc     31c0             xor eax, eax
    0x080480ce     31db             xor ebx, ebx
    0x080480d0     b90a000000       mov ecx, 0xa
    0x080480d5     ac               lodsb
    0x080480d6     08c0             or al, al
    0x080480d8     7412             jz 0x80480ec
    0x080480da     3c30             cmp al, 0x30 ; 0x30 = '0'
    0x080480dc     7c15             jl 0x80480f3
    0x080480de     3c39             cmp al, 0x39 ; 0x39 = '9'
    0x080480e0     7f11             jg 0x80480f3
    0x080480e2     2c30             sub al, 0x30
    0x080480e4     93               xchg ebx, eax
    0x080480e5     f7e1             mul ecx
    0x080480e7     93               xchg ebx, eax
    0x080480e8     01c3             add ebx, eax
    0x080480ea     ebe9             jmp 0x80480d5
    0x080480ec     89d8             mov eax, ebx
    0x080480ee     e904000000       jmp dword 0x80480f7
    0x080480f3     0fbae81f         bts eax, 0x1f
    0x080480f7     5b               pop ebx
    0x080480f8     59               pop ecx
    0x080480f9     5e               pop esi
    0x080480fa     c20400           ret 0x4

This seems like to be a converter ascii <-> integers.

Ok, where are we landing now ? At 0x080482e3, after the call eax

[0x0804823f]> pd@0x80482fe
    0x080482fe     812c2449cb0000   sub dword [esp], 0xcb49
    0x08048305     8b442404         mov eax, [esp+0x4]
    0x08048309     48               dec eax
    0x0804830a     2d03000000       sub eax, 0x3
    0x0804830f     89442404         mov [esp+0x4], eax
    0x08048313     8b442408         mov eax, [esp+0x8]
    0x08048317     b900010000       mov ecx, 0x100
    0x0804831c     40               inc eax
    0x0804831d     e2fd             loop 0x804831c
    0x0804831f     89442408         mov [esp+0x8], eax
    0x08048323     c3               ret

Since I'm pretty lazy, I used GDB from this point, until something cool poped-up.

We land at 0x8048375, our syscall call. GDB tells us:

  • eax: 0x30
  • ebx: 0x8
  • ecx: 0x08048070

another signal()?

signal(SIGFPE, 0x08048070);

SIGFPE is triggered by a wrong arithmetic operation, such as a division by zero, we may likely expect one during the next instructions :) After this, we lend on 0x804819d:

[0x0804823f]> pd@0x804819d
    0x0804819d     a18c830408       mov eax, [globvar.keyfound] ; global variable
    0x080481a2     2503000000       and eax, 0x3    ; eax = 3
    0x080481a7     48               dec eax         ; eax = 2
    0x080481a8     7409             jz 0x80481b3
    0x080481aa     48               dec eax         ; eax = 1
    0x080481ab     7406             jz 0x80481b3
    0x080481ad     48               dec eax         ; eax = 0
    0x080481ae     7503             jnz 0x80481b3
    0x080481b0     91               xchg ecx, eax   ; ecx = 0
    0x080481b1     f7f1             div ecx         ; division by 0 ! SIGFPE is triggered
    0x080481b3     c3               ret

Remember globvar.keyfound? This is where is stored our environnement-related check variable, set to 0x3 if they were found.

[0x0804823f]> pd@0x08048070
    0x08048070     57               push edi
    0x08048071     bfb1810408       mov edi, 0x80481b1
    0x08048076     b902000000       mov ecx, 0x2
    0x0804807b     b090             mov al, 0x90
    0x0804807d     f3aa             rep stosb
    0x0804807f     5f               pop edi
    0x08048080     a18c830408       mov eax, [globvar.keyfound]
    0x08048085     2503000000       and eax, 0x3
    0x0804808a     3d03000000       cmp eax, 0x3
    0x0804808f     7532             jnz 0x80480c3
    0x08048091     a190830408       mov eax, [0x8048390]
    0x08048096     8b0d94830408     mov ecx, [0x8048394]
    0x0804809c     81f900100000     cmp ecx, 0x1000  ; ecx < 4096 ?
    0x080480a2     7c1f             jl 0x80480c3
    0x080480a4     3d00100000       cmp eax, 0x1000  ; eax < 4096 ?
    0x080480a9     7c18             jl 0x80480c3
    0x080480ab     f7e1             mul ecx
    0x080480ad     b9               invalid
    0x080480ae     0100             add [eax], eax

Wtf is going on at 0x080480ad, invalid?! After some digging, it seems like it's an expected behavior from radare2.

[0x0804823f]> b
    0x40

[0x0804823f]> b 0x64

[0x0804823f]> pd@0x08048070
        0x08048070     57               push edi
        0x08048071     bfb1810408       mov edi, 0x80481b1
        0x08048076     b902000000       mov ecx, 0x2
        0x0804807b     b090             mov al, 0x90
        0x0804807d     f3aa             rep stosb
        0x0804807f     5f               pop edi
        0x08048080     a18c830408       mov eax, [globvar.keyfound]
        0x08048085     2503000000       and eax, 0x3
        0x0804808a     3d03000000       cmp eax, 0x3
        0x0804808f     7532             jnz 0x80480c3
        0x08048091     a190830408       mov eax, [0x8048390] ; this is our "key1" value
        0x08048096     8b0d94830408     mov ecx, [0x8048394] ; this is our "key2" value
        0x0804809c     81f900100000     cmp ecx, 0x1000
        0x080480a2     7c1f             jl 0x80480c3
        0x080480a4     3d00100000       cmp eax, 0x1000
        0x080480a9     7c18             jl 0x80480c3
        0x080480ab     f7e1             mul ecx
        0x080480ad     b901000100       mov ecx, 0x10001
        0x080480b2     f7f1             div ecx
        0x080480b4     89d0             mov eax, edx
        0x080480b6     48               dec eax
        0x080480b7     750a             jnz 0x80480c3
        0x080480b9     6875d0534c       push dword 0x4c53d075
        0x080480be     e940000000       jmp dword 0x8048103
        0x080480c3     c3               ret

Much better ! For 0x08048091 and 0x08048096, I used GDB.

[0x0804823f]> pd @0x8048103
    0x08048103     812c24474f4f44   sub dword [esp], 0x444f4f47 ; "GOOD"
    0x0804810a     c3               ret

[0x0804823f]> pd@0x80480c3
    0x080480c3     c3               ret

Pretty straigforward.

key1 < 4096
key2 < 4096
key1 * key2 % 0x10001 - 1 = 0

Here is the keygen in Python:

for key1 in range(4096):
    for key2 in range(4096):
        if key1 * key2 % 0x10001 == 1:
            print key1, key2

taviso was right: this crackme was realy pleasant. Thank you crp- !

Ho, btw, this blogpost was accepted as a solution on crackmes.de.

Radare2 instead of gdb?

I was trolled told that I could use radare2's debugger instead of GDB. But at this time, there is no signal() support in radare2 :/ Here are a few useful commands if you want to play:

  • dcu <tab> will run until your selection function. You can use dcu entry0 for breaking at entrypoint.
  • ds to single-step
  • pd to print disassembly
  • dr= to highligh changed registers