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 give it a try) ).
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'm using
- radare2 for static analysis
- gdb for dynamic one
- python 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 run, every registers from 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 classic 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
:
0x8048285 + 0x1 = 0x8048286
0x8048221 + 0x2 = 0x8048223
0x8048223
is pushed last, so it will get pop'ed 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 theSIGTRAP
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 debuggers are also using SIGTRAP
to manage breakpoints.
One more ret
, and 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
,
which is the int3
instruction, to 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!
[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 an environment 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, it seems like our supposition might be 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 environment-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 straightforward.
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 really pleasant. Thanks crp- !
Ho, btw, this blogpost was accepted as a solution on crackmes.de.
Radare2 instead of gdb?
I was 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 with it yourself:
dcu <tab>
will run until your selection function. You can usedcu entry0
to break at the entrypointds
to single-steppd
to print disassemblydr=
to highlight changed registers