ZDResearch Binary Challenge One Writeup

Screenshot of Binary App

Hi everybody!

Our second challenge (which was a reverse engineering one) came to its end, as well. This one was solver much faster than our XSS challenge, meaning that either our reversing experts are not as good as our XSS experts, or that reverse engineers out there are much better than XSS experts; or something else.

On this challenge, we required the participants to submit write-ups as well, because hackers are lazy, and we knew that they would only submit partial write-ups, and then we would do the other half (we’re hackers too!).

We had almost 186 participants, and here’s the list of three winners:

  1. Eloi Vanderbéken @elvanderb
  2. Dominique Bongard @reversity
  3. deroko @deroko_ (He actually solved it too fast, but arrived late! Also won the best writeup.)

Link to the challenge: https://zdresearch.com/challenges/binary1/

Writeups

In this section, we will provide you with write-ups provided by our winners, and finally our own write-up. On top of that, keygen codes are linked to the bottom of each writeup:

Writeup by Deroko of ARTeam:

The first step was to see how key is obtained. As usual breakpoint on
file apis leads us to proper place:

.text:00402005                 push    80h
.text:0040200A                 push    3
.text:0040200C                 push    edi
.text:0040200D                 push    1
.text:0040200F                 push    80000000h
.text:00402014                 lea     ecx, [ebp+szLicFileName]
.text:00402017                 push    ecx
.text:00402018                 call    [ebp+CreateFileA_or_allocated_buffer]
.text:0040201B                 mov     [ebp+hFile], eax
.text:0040201E                 cmp     eax, 0FFFFFFFFh

Right after that we will find interesting code which deals with strings
deofbsucation. No wonder why IDA can’t see them:

.text:00402027                 mov     ecx, [esi+OBJECT.InternalApisObject]
.text:0040202A                 mov     ebx, [ecx+InternalApisObject.GetModuleHandleA]
.text:0040202D                 mov     [ebp+var_C], 3C34443Eh
.text:00402034                 mov     [ebp+var_8], 0FF3Ch
.text:0040203A                 mov     edx, [ecx+InternalApisObject.lpVtbl] ; InternalObject->Vtbl
.text:0040203C                 mov     edx, [edx+InternalApisVtbl.fnDecryptString]
.text:0040203E                 push    6
.text:00402040                 lea     eax, [ebp+var_C]
.text:00402043                 push    eax
.text:00402044                 call    edx             ; 401cb0 - deobsfucation function
.text:00402046                 lea     eax, [ebp+var_C]

Function which decrypts strings is doing that by adding 0x30 to every character, so no need
to speen too much time explaining it. But for the sake of writeup here it is:

.text:00401CD7                 mov     eax, [ebp+var_4]
.text:00401CDA                 mov     ecx, [ebp+arg_0]
.text:00401CDD                 add     byte ptr [eax+ecx], 30h ; add 30h to every character to deobsfucate it
.text:00401CE1                 mov     edx, [ebp+arg_4]
.text:00401CE4                 inc     eax
.text:00401CE5                 mov     [ebp+var_4], eax
.text:00401CE8                 cmp     eax, edx
.text:00401CEA                 jb      short loc_401CC3

Nexts steps which follow are, getting file size, allocating memory, and reading filesize to the memory, and

then calling function which will split file properly, and pass it back to the function which will do the magic.
It’s very important to know that we are dealing with C++ code, and also to note that there is custom getprocaddress
function which uses this function as a hash (we will need it as a part of keygen to give it MessageBoxA as last
argument):

.text:00401DF0 HashFunGetProcAddress proc near
.text:00401DF7 xor eax, eax
.text:00401DF9 mov [ebp+var_4], eax
.text:00401DFC cmp [ecx], al
.text:00401DFE jz short loc_401E13
.text:00401E00
.text:00401E00 loc_401E00:
.text:00401E00 ror [ebp+var_4], 0Dh
.text:00401E04 movsx eax, byte ptr [ecx]
.text:00401E07 add [ebp+var_4], eax
.text:00401E0A inc ecx
.text:00401E0B cmp byte ptr [ecx], 0
.text:00401E0E jnz short loc_401E00
.text:00401E10 mov eax, [ebp+var_4]
.text:00401E16 HashFunGetProcAddress endp

Now we will dig a little bit into Objects here, as it’s important to know it’s layout to be able to properly handle

keygen.

There is GlobalObject which calls functions related to keygens. First member of object is yet another object which
is used to deal with windows APIs, so their lpVtbl can be seen here:

.rdata:00543728 g_InteralApisVtbl dd offset DeobsfucateFunction
.rdata:0054372C dd offset sub_401D10
.rdata:00543730 dd offset HashFunGetProcAddress
.rdata:00543734 dd offset sub_401E20
.rdata:00543738 dd offset PopulateWin32Apis
.rdata:0054373C dd offset unk_54B10C
.rdata:00543740 g_ObjectVtbl dd offset ObjectConstructorCallOpenFile
.rdata:00543744 dd offset OpenFileCheckKey
.rdata:00543748 dd offset SomeFunnyFunc0
.rdata:0054374C dd offset SomeFunnyFunc1
.rdata:00543750 dd offset msvcrt_time_wrapper
.rdata:00543754 dd offset FinalHashFunction
.rdata:00543758 dd offset do_the_dead

First function which is called is do_the_dead which will call OpenFileCheckKey, which in turn look for “lic.dat”
file. Once file is read, function which will process input file, and allocate it is SomeFunnyFunc1:

.text:004022E6 mov ebx, [ebp+pObject]
.text:004022E9 mov edi, [ebp+lic_buffer]
.text:004022EC mov ecx, [edi]
.text:004022EE mov [ebx+OBJECT.lic_buf_0_4bytes], ecx
.text:004022F1 mov edx, [edi+4]
.text:004022F4 movzx ecx, g_1E_byte
.text:004022FB mov [ebx+OBJECT.lic_buf_4_8bytes], edx
.text:004022FE mov eax, [edi+8]
.text:00402301 mov [ebx+OBJECT.lic_buff_8_Cbytes], eax
.text:00402304 mov esi, [ebx+OBJECT.lic_buf_0_4bytes]
.text:00402307 mov eax, [ebx+OBJECT.lic_buff_8_Cbytes]
.text:0040230A and [ebx+OBJECT.lic_buf_4_8bytes], 0FFFFFC1Fh
.text:00402311 mov edx, 2
.text:00402316 shl edx, cl
.text:00402318 movzx ecx, g_1E_byte
.text:0040231F add esi, esi ; 0-4
.text:00402321 sar esi, 1
.text:00402323 add esi, edx
.text:00402325 add eax, eax ; 8-0Ch
.text:00402327 mov edx, 2
.text:0040232C shl edx, cl
.text:0040232E sar eax, 1
.text:00402330 add edi, 0Ch
.text:00402333 add eax, edx
.text:00402335 mov [ebp+computed_val], eax
.text:00402338 mov eax, edi
.text:0040233A sub eax, [ebx+OBJECT.lic_buffer]
.text:0040233D cmp eax, [ebp+arg_dwSizeOfReadBytes]

 

There are many decoys here, so what this will do is to simply add 0x80000000 to 1st and 3rd dword
from input file. This is very important to note sar instruction, which might give us small headache
as theoretically what’s happening here is to give you back same number, as long as you don’t make
number to have 31st bit set. This is very important, as it’s needed for bypassing time checks.
Eg. value is caluclated as:

add eax, eax
sar eax, 1 <---- consider also 31st bit
add eax, 80000000h

Next code which comes is:

.text:00402355 mov eax, [ebx+OBJECT.ObjectVtbl]
.text:00402357 mov [ebp+pObject], eax
.text:0040235A mov eax, [eax+10h]
.text:0040235D push 5
.text:0040235F mov ecx, ebx
.text:00402361 call eax ; 402250 - calls to proc to get msvcrt.time()
.text:00402363 mov edx, [ebp+pObject]
.text:00402366 push eax
.text:00402367 mov eax, [edx+ObjectVtbl.SomeFunnyFunc0]
.text:0040236A mov ecx, ebx
.text:0040236C call eax ; 402150
.text:0040236E mov edx, [ebx]
.text:00402370 mov edx, [edx+ObjectVtbl.SomeFunnyFunc0]
.text:00402373 push 5
.text:00402375 push esi
.text:00402376 mov ecx, ebx
.text:00402378 mov [ebp+pObject], eax
.text:0040237B call edx ; 402150
.text:0040237D cmp eax, [ebp+pObject]
.text:00402380 ja __exit0 <--- 1st DWORD after computation with time
has to be smaller than time
.text:00402386 mov esi, [ebx+OBJECT.ObjectVtbl]
.text:00402388 mov eax, [esi+ObjectVtbl.msvcrt_time_wrapper]
.text:0040238B push 5
.text:0040238D mov ecx, ebx
.text:0040238F call eax ; 402250 - calls to proc to get msvcrt.time()
.text:00402391 mov edx, [esi+ObjectVtbl.SomeFunnyFunc0]
.text:00402394 push eax
.text:00402395 mov ecx, ebx
.text:00402397 call edx ; 402150
.text:00402399 mov ecx, [ebp+computed_val]
.text:0040239C mov esi, eax ; save computed time to esi
.text:0040239E mov eax, [ebx+OBJECT.ObjectVtbl]
.text:004023A0 mov edx, [eax+ObjectVtbl.SomeFunnyFunc0]
.text:004023A3 push 5
.text:004023A5 push ecx
.text:004023A6 mov ecx, ebx
.text:004023A8 call edx ; 402150
.text:004023AA cmp eax, esi ;<---- 4rd DWORD after computation must be bigger
.text:004023AC jb __exit0 than time

msvcrt_time_wrapper gets time() from msvcr.dll via custom getprocaddress, and returns back value. When this
value is parsed via SomeFunnyFunc0 becomes????:

.text:00402150 push ebp
.text:00402151 mov ebp, esp
.text:00402153 sub esp, 30h
.text:00402156 mov eax, ___security_cookie
.text:0040215B xor eax, ebp
.text:0040215D mov [ebp+cookie], eax
.text:00402160 push esi
.text:00402161 mov [ebp+init_var], 3Ch
.text:00402168 mov eax, [ebp+init_var]
.text:0040216B mov ecx, eax
.text:0040216D shl ecx, 4
.text:00402170 sub ecx, eax
.text:00402172 add ecx, ecx
.text:00402174 add ecx, ecx
.text:00402176 mov [ebp+var_24], ecx
.text:00402179 mov eax, [ebp+var_24]
.text:0040217C lea edx, [eax+eax*2]
.text:0040217F add edx, edx
.text:00402181 add edx, edx
.text:00402183 add edx, edx
.text:00402185 mov [ebp+var_28], edx
.text:00402188 mov eax, [ebp+var_28]
.text:0040218B mov ecx, eax
.text:0040218D shl ecx, 4
.text:00402190 sub ecx, eax
.text:00402192 add ecx, ecx
.text:00402194 mov [ebp+var_30], ecx
.text:00402197 mov edx, [ebp+var_30]
.text:0040219A mov [ebp+divisor], edx
.text:0040219D fild [ebp+divisor] ; mul by 12.13 and get divisor back
.text:004021A0 fmul ds:dbl_543760
.text:004021A6 call __ftol2_sse
.text:004021AB mov [ebp+divisor], eax
.text:004021AE mov esi, [ebp+divisor]
.text:004021B1 mov ecx, [ebp+arg_0]
.text:004021B4 xor edx, edx
.text:004021B6 mov eax, ecx
.text:004021B8 div esi
.text:004021BA xor edx, edx
.text:004021BC add eax, 7B2h

After deobsfucation of a function, what happens is that input arg is divided by: 0x1DFC040, and
0x7B2 is added to the code. Now to pass these checks we need to find numbers such that:

(1st DWORD + 0x80000000) / 0x1DFC040 < time() / 0x1DFC040
and
(3rd DWORD + 0x80000000) / 0x1DFC040 > time() / 0x1DFC040

time() should return atm something in form of 0x5xxxxxxx thus we will pass these checks if our
1st number is < 0x5xxxxxxx and 3rd dword > 0x5xxxxxxx, so after a little bit of caluclation, and
considering sar in computing values:

1st dword = 0x40000000 after computation gives us 0x40000000
3rd dword = 0x3fffffff after computation gives us 0xBFFFFFFF

So we passed these checks 🙂 Next interesting part is spliting of input file:

.text:004023AC jb __exit0
.text:004023B2 mov eax, [ebx+OBJECT.lic_buf_4_8bytes]
.text:004023B5 shl eax, 1Bh
.text:004023B8 sar eax, 1Bh
.text:004023BB cmp eax, 4 <--- hint how many lines should have
.text:004023BE jl __exit0 input file... (2d DWORD from input)
.text:004023C4 mov [ebp+pObject], 0
.text:004023CB test eax, eax
...
.text:004023E6 mov eax, [edi]
.text:004023E8 mov ecx, eax
.text:004023EA shl ecx, 0Eh
.text:004023ED sar ecx, 0Eh
.text:004023F0 add edi, 4
.text:004023F3 push ecx ; unsigned int
.text:004023F4 mov [esi], eax
.text:004023F6 mov [ebp+lic_buffer], edi
.text:004023F9 call ??_U@YAPAXI@Z ; operator new[](uint)
.text:004023FE mov edx, [esi]
.text:00402400 shl edx, 0Eh
.text:00402403 sar edx, 0Eh
.text:00402406 add esp, 4
.text:00402409 mov [esi+28h], eax
.text:0040240C mov [ebp+var_10], edx
.text:0040240F mov [ebp+var_14], eax
.text:00402412 mov esi, [ebp+lic_buffer]
.text:00402415 mov edi, [ebp+var_14]
.text:00402418 mov ecx, [ebp+var_10]
.text:0040241B rep movsb
.text:0040241D mov ecx, [ebp+computed_val]
.text:00402420 mov eax, [ecx]
.text:00402422 mov edx, [ebp+lic_buffer]
.text:00402425 mov esi, eax
.text:00402427 shl esi, 0Eh <---- note also how sizes are obtained!!!!
.text:0040242A sar esi, 0Eh <---- note also how sizes are obtained!!!!
.text:0040242D add edx, esi
.text:0040242F and eax, 0FE03FFFFh
.text:00402434 mov [ebp+lic_buffer], edx
.text:00402437 mov [ecx], eax
.text:00402439 sub edx, [ebx+OBJECT.lic_buffer]
.text:0040243C cmp edx, [ebp+arg_dwSizeOfReadBytes]
.text:0040243F ja short __exit

What happens now is that DWORD is read from buffer, and size is allocated to copy data following this
value. Thus if we look at 004023BB we see hint that we need at least 4 of these data. File layout looks
like:

[DWORD]

Now we will exit this function, aslo be aware that you can’t singlestep this code, as timers will catch you!!
Here is code:

...
.text:0040234C                 lea     edx, [ebp+var_28]
.text:0040234F                 push    edx
.text:00402350                 mov     [ebp+QueryPerformanceCounter], eax
.text:00402353                 call    eax             ; QueryPerformanceCounter
.text:00402355                 mov     eax, [ebx+OBJECT.ObjectVtbl]
...
and after copying of data is done:
.text:0040246D                 lea     edx, [ebp+var_20]
.text:00402470                 push    edx
.text:00402471                 mov     [ebx+8], esi
.text:00402474                 call    [ebp+QueryPerformanceCounter]
.text:00402477                 mov     eax, [ebp+var_20]
.text:0040247A                 sub     eax, [ebp+var_28]
.text:0040247D                 cmp     eax, 10000h

Ok now, once data is split we go back to do_the_dead function, at will handle all our data.

.text:00402523                 call    edx
.text:00402525                 or      eax, 0FFFFFFFFh
.text:00402528                 mov     [ebp+dwMustBeSet0], eax
.text:0040252B                 mov     [ebp+dwMustBeSet1], eax
.text:0040252E                 mov     [ebp+pObject], eax
.text:00402531                 mov     [ebp+dwNeededObjectIndex], eax
.text:00402534                 mov     eax, [ebx+OBJECT.lic_buf_4_8bytes]
.text:00402537                 shl     eax, 1Bh
.text:0040253A                 sar     eax, 1Bh
.text:0040253D                 mov     [ebp+dwChunkIndex], 0
.text:00402544                 mov     [ebp+dwNumberOfChunks], eax
.text:00402547                 test    eax, eax

Note 4 local variables which are set to -1, it’s very important!!!

.text:00402563                 cmp     [esi+OBJECT.ptrs], 0
.text:00402567                 jz      __exit0
.text:0040256D                 mov     edi, [esi]

When we exit from here it’s very important to note is that we case switch with only 4
posibilities, thus when existing this loop we need to have ecx = 0/1/2/3 this means
that we need [DATA_SIZE] part of every chunk to have certain data, values which we need
are: 0xB, 0x13, 0x1E, 0x31 thus [DATA_SIZE] for every chunk must be:

0x0B << 0x19  + chunk_real_size
0x13 << 0x19  + chunk_real_size
0x1E << 0x19  + chunk_real_size
0x31 << 0x19  + chunk_real_size

This will give us posibility to initialize all values on stack with 0,1,2,3

.text:00402591                 jmp     short __no_case ; jumptable 00402598 default case
.text:00402593 __go_switch:
.text:00402593                 cmp     ecx, 3          ; switch 4 cases
.text:00402596                 ja      short __no_case ; jumptable 00402598 default case
.text:00402598                 jmp     ds:case_offsets[ecx*4] ; switch jump
.text:0040259F case_0_set:
.text:0040259F                 mov     eax, [ebp+dwChunkIndex] ; jumptable 00402598 case 0
.text:004025A2                 mov     [ebp+dwMustBeSet0], eax

In FinalHashingFunction (key check function):

.text:00402846                 mov     ecx, [ebx+eax*4+18h] ;length of 2nd chunk
.text:0040284A                 shl     ecx, 0Eh
.text:0040284D                 cmp     ecx, 28000h     ; check if 2nd part of keygen is 0xA or more bytes
.text:00402853                 jl      __exit0
.text:00402859                 mov     ecx, [ebp+var_18]

2nd chunk must be 0xA bytes as code shows us later on:

Note that ecx/eax/edx depend on data how we filled in switch/case before (0,1,2,3)
.text:0040286F                 mov     edi, [ebx+ecx*4+40h] ; Key1 = username
.text:00402873                 mov     eax, [ebx+eax*4+40h] ; Key2 = key to be combined
.text:00402877                 mov     edx, [ebp+arg_8]
.text:0040287A                 mov     ecx, [ebx+edx*4+40h] ; key3 = 3rd part of key which has to match this function
.text:0040287E                 mov     [ebp+pKey3], ecx

Now 2nd chunks is obsfucated a little bit:

.text:00402881                 mov     cl, [eax]
.text:00402883                 xor     cl, 6
.text:00402886                 add     cl, cl
.text:00402888                 add     cl, cl
.text:0040288A                 mov     [ebp+szComputedHash9], cl
.text:0040288D                 mov     cl, [eax+1]
.text:00402890                 xor     cl, 0Bh
.text:00402893                 add     cl, cl
.text:00402895                 add     cl, cl
.text:00402897                 mov     [ebp+szComputedHash8], cl
.text:0040289A                 mov     cl, [eax+2]
.text:0040289D                 xor     cl, 0Ch
.text:004028A0                 add     cl, cl
.text:004028A2                 add     cl, cl
.text:004028A4                 mov     [ebp+szComputedHash7], cl
.text:004028A7                 mov     cl, [eax+3]
.text:004028AA                 xor     cl, 2
.text:004028AD                 add     cl, cl
.text:004028AF                 add     cl, cl
.text:004028B1                 mov     [ebp+szComputedHash6], cl
.text:004028B4                 mov     cl, [eax+4]
.text:004028B7                 xor     cl, 7
.text:004028BA                 add     cl, cl
.text:004028BC                 add     cl, cl
.text:004028BE                 mov     [ebp+szComputedHash5], cl
.text:004028C1                 mov     cl, [eax+5]
.text:004028C4                 add     cl, cl
.text:004028C6                 add     cl, cl
.text:004028C8                 mov     [ebp+szComputedHash4], cl
.text:004028CB                 mov     cl, [eax+6]
.text:004028CE                 xor     cl, 3
.text:004028D1                 add     cl, cl
.text:004028D3                 add     cl, cl
.text:004028D5                 mov     [ebp+szComputedHash3], cl
.text:004028D8                 mov     cl, [eax+7]
.text:004028DB                 xor     cl, 5
.text:004028DE                 add     cl, cl
.text:004028E0                 add     cl, cl
.text:004028E2                 mov     [ebp+szComputedHash2], cl
.text:004028E5                 mov     cl, [eax+8]
.text:004028E8                 mov     al, [eax+9]
.text:004028EB                 xor     cl, 1
.text:004028EE                 xor     al, 0Ah
.text:004028F0                 add     cl, cl
.text:004028F2                 add     al, al
.text:004028F4                 lea     esi, [esi+esi*4]
.text:004028F7                 add     esi, esi        ; esi = username_len * 0xA IMPORTANT!!!!!
.text:004028F9                 add     cl, cl
.text:004028FB                 add     al, al
.text:004028FD                 push    esi             ; unsigned int
.text:004028FE                 mov     [ebp+szComputedHash1], cl
.text:00402901                 mov     [ebp+szComputedHash0], al

This part of code we don’t need to understand, as it’s needed to compute keygen. So what we can do here
is to RIP it out of the code and use it. Now comes obsfucation of “username” (1st data chunk), in such way
that generated hash from above code is used to obsfucate every char from “username” (1st data chunk). Also
size of buffer where final key is generated length is username_len * 0xA.

.text:00402930                 movzx   eax, byte ptr [ecx+edi]
.text:00402934                 xor     al, [ebp+szComputedHash9]
.text:00402937                 inc     ecx
.text:00402938                 mov     [esi-1], al
.text:0040293B                 movzx   edx, byte ptr [ecx+edi-1]
.text:00402940                 xor     dl, [ebp+szComputedHash8]
.text:00402943                 add     esi, 0Ah
.text:00402946                 mov     [esi-0Ah], dl
.text:00402949                 movzx   eax, byte ptr [ecx+edi-1]
.text:0040294E                 xor     al, [ebp+szComputedHash7]
.text:00402951                 mov     [esi-9], al
.text:00402954                 movzx   edx, byte ptr [ecx+edi-1]
.text:00402959                 xor     dl, [ebp+szComputedHash6]
.text:0040295C                 mov     [esi-8], dl
.text:0040295F                 movzx   eax, byte ptr [ecx+edi-1]
.text:00402964                 xor     al, [ebp+szComputedHash5]
.text:00402967                 mov     [esi-7], al
.text:0040296A                 movzx   edx, byte ptr [ecx+edi-1]
.text:0040296F                 xor     dl, [ebp+szComputedHash4]
.text:00402972                 mov     [esi-6], dl
.text:00402975                 movzx   eax, byte ptr [ecx+edi-1]
.text:0040297A                 xor     al, [ebp+szComputedHash3]
.text:0040297D                 mov     [esi-5], al
.text:00402980                 movzx   edx, byte ptr [ecx+edi-1]
.text:00402985                 xor     dl, [ebp+szComputedHash2]
.text:00402988                 mov     [esi-4], dl
.text:0040298B                 movzx   eax, byte ptr [ecx+edi-1]
.text:00402990                 xor     al, [ebp+szComputedHash1]
.text:00402993                 mov     [esi-3], al
.text:00402996                 movzx   edx, byte ptr [ecx+edi-1]
.text:0040299B                 xor     dl, [ebp+szComputedHash0]
.text:0040299E                 mov     eax, [ebp+var_18]
.text:004029A1                 mov     [esi-2], dl
.text:004029A4                 mov     edx, [ebx+eax*4+18h]
.text:004029A8                 shl     edx, 0Eh
.text:004029AB                 sar     edx, 0Eh        ; ecx = counter
.text:004029AE                 cmp     ecx, edx        ; edx = lenght of username
.text:004029B0                 jl      __loop_combine_keys

As we may see, every char from username is obsfucated which characters from generated hash, or
in python/C it is:

        for (index = 0; index < strlen(username) + 1; index++){ //I use + 1 as I want 0 to be included for nicer user name display
                                                                //later when strings are put together
                for (jindex = 0; jindex < 0xA; jindex++){
                        buff[index * 0xA + jindex] = username[index] ^ hash2datareverse[jindex];
                }
        }

        buff = "";
        for x in username:
                for y in hash:
                        data = ord(x) ^ ord(y);
                        buff += chr(data);

Now comes final check:

.text:004029A4                 mov     edx, [ebx+eax*4+18h]
.text:004029A8                 shl     edx, 0Eh
.text:004029AB                 sar     edx, 0Eh        ; ecx = length of genreated obsfuacted username
                                                             = strlen(username) * 0xA
.text:004029AE                 cmp     ecx, edx        ; edx = lenght of 3rd chunk
.text:004029B0                 jl      __loop_combine_keys
.text:004029B6                 mov     esi, [ebp+var_14]
.text:004029B9                 mov     eax, [ebp+var_1C]
.text:004029BC
.text:004029BC __goon_no_2nd_buffer:
.text:004029BC                 mov     ecx, [ebp+arg_8]
.text:004029BF                 mov     edx, [ebx+ecx*4+18h]
.text:004029C3                 shl     edx, 0Eh
.text:004029C6                 sar     edx, 0Eh
.text:004029C9                 cmp     esi, edx
.text:004029CB                 jnz     short __exit0
.text:004029CD                 xor     edi, edi
.text:004029CF                 test    esi, esi
.text:004029D1                 jz      short loc_4029ED
.text:004029D3                 mov     ecx, [ebp+pKey3]
.text:004029D6                 mov     edx, eax
.text:004029D8                 sub     edx, ecx
.text:004029DA                 lea     ebx, [ebx+0]
.text:004029E0
.text:004029E0 loc_4029E0:
.text:004029E0                 mov     bl, [ecx]

If all went good we will get eax = 1 and we can go on:

.text:0040260F                 mov     ecx, [ebx+OBJECT.InternalApisObject]
.text:00402612                 mov     esi, [ecx+InternalApisObject.GetModuleHandleA]
.text:00402615                 mov     [ebp+buffer_data], 3C34443Eh
.text:0040261C                 mov     [ebp+var_8], 0FF3Ch
.text:00402622                 mov     edx, [ecx+InternalApisObject.lpVtbl]
.text:00402624                 mov     edx, [edx+InternalApisVtbl.fnDecryptString]
.text:00402626                 push    6
.text:00402628                 lea     eax, [ebp+buffer_data]
.text:0040262B                 push    eax
.text:0040262C                 call    edx

So what happens is that code uses NtQueryInformationProcess to query NoDebugInherit, if flag is set
code will use 0xCAFEBABE to locae API from user32.dll which doesn’t exist. Otherwise, if process is
not debugged, DWORD from chunk4 will be used to locate API and here we use MessageBoxA as chunk4:
0xBC4DA2A8

And if everything worked fine we are done with code and can have full keygen.

So full keygen is:

import  struct
import  os
import  sys
import  time

hash =  "\x28\x1C\x00\x38\x68\x74\x78\x60\xBC\x90";

if len(sys.argv) != 2:
        print("[X] give me username...");
username = sys.argv[1] + "\x00";

payload = "";

payload = struct.pack("I", 0x40000000);
payload += struct.pack("I", 0x4);
payload += struct.pack("I", 0x3FFFFFFF);
payload += struct.pack("I", len(username) + (0xB << 0x19) & 0xFFFFFFFF);
payload += username;
len = len(username);
len = len * 0xA;
payload += struct.pack("I", 0x0000000A + (0x13 << 0x19) & 0xFFFFFFFF);
payload += struct.pack("I", 0xCCCCCCCC);
payload += struct.pack("I", 0xDDDDDDDD);
payload += struct.pack("H", 0xEEEE);
payload += struct.pack("I", (len + (0x1E << 0x19)) & 0xFFFFFFFF);

buff = "";
for x in username:
        for y in hash:
                data = ord(x) ^ ord(y);
                buff += chr(data);

payload += buff;

payload += struct.pack("I", 0x00000004 + (0x31 << 0x19) & 0xFFFFFFFF);
payload += struct.pack("I", 0xBC4DA2A8);

try:
        os.unlink("lic.dat");
except:
        pass;
f = open("lic.dat", "wb");
f.write(payload);
f.close();
 unsigned       char    hash2datareverse[] = {
 0x28, 0x1C, 0x00, 0x38, 0x68, 0x74, 0x78, 0x60, 0xBC, 0x90};

int __cdecl main(int    argc, char **argv){
        HANDLE  hFile;
        size_t  len;
        unsigned char *username;
        unsigned char *buff;
        ULONG   index;
        ULONG   jindex;
        ULONG   dwWritten;
        ULONG   dword;

        if (argc != 2){
                printf("[X] Needs username as argument\n");
                return 1;
        }

        hFile = CreateFile("lic.dat", GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, 0,0);
        if (hFile == INVALID_HANDLE_VALUE){
                printf("[X] Failed to create : lic.dat\n");
                return 1;
        }

        username = argv[1];
        len = strlen(username) + 1;
        len = len * 0xA;

        buff = malloc(len+1);
        memset(buff, 0, len+1);
        for (index = 0; index < strlen(username) + 1; index++){
                for (jindex = 0; jindex < 0xA; jindex++){
                        buff[index * 0xA + jindex] = username[index] ^ hash2datareverse[jindex];
                }
        }
        //build key...
        dword   = 0x40000000;
        WriteFile(hFile, &dword, sizeof(dword), &dwWritten, 0);
        dword   = 0x4;
        WriteFile(hFile, &dword, sizeof(dword), &dwWritten, 0);
        dword   = 0x3FFFFFFF;
        WriteFile(hFile, &dword, sizeof(dword), &dwWritten, 0);
        dword   = (0xb << 0x19) + strlen(username) + 1;
        WriteFile(hFile, &dword, sizeof(dword), &dwWritten, 0);
        WriteFile(hFile, username, strlen(username) + 1, &dwWritten, 0);
        dword = (0x13 << 0x19) + 0xA;
        WriteFile(hFile, &dword, sizeof(dword), &dwWritten, 0);
        WriteFile(hFile, "\xCC\xCC\xCC\xCC\xDD\xDD\xDD\xDD\xEE\xEE", 0xA, &dwWritten, 0);
        dword = len + (0x1E << 0x19);
        WriteFile(hFile, &dword, sizeof(dword), &dwWritten, 0);
        WriteFile(hFile, buff, len, &dwWritten, 0);
        dword = 0x4 + (0x31 << 0x19);
        WriteFile(hFile, &dword, sizeof(dword), &dwWritten, 0);
        dword = 0xBC4DA2A8;
        WriteFile(hFile, &dword, sizeof(dword), &dwWritten, 0);
        FlushFileBuffers(hFile);
        CloseHandle(hFile);

}

Writeup by Eloi:

1] First steps…We launch the crack-me, no name/serial boxes : must be a keyfile.
We launch the crack-me under olly with the Phantom plugin[1] to avoid basic anti-debuggers (I don’t want to lose time on that…). We set a BP on CreateFileW, we click the check button : Break! It’s trying to read a lic.dat file!
Quick tracing, there is some anti-debuggers (PEB flags tricks @ 0040207C), if they detect a debugger, file handle is modified so subsequent use of the handle will fail.
The file is read in allocated memory, we put an HBP on the buffer and we resume the crakme.

2] Breaking the first part.
The HBP is fired in the function located at 004022C0. There is a very basic obfuscation :

004022D3    B8 01000000     MOV EAX,1
004022D8    83F8 01         CMP EAX,1
004022DB    75 02           JNZ SHORT ZDKeygen.004022DF  12 :
0040233A    2B43 08         SUB EAX,DWORD PTR DS:[EBX+8]
0040233D    3B45 08         CMP EAX,DWORD PTR SS:[EBP+8]
00402340    0F87 61010000   JA ZDKeygen.004024A7

will be used to detect tracing :
00402350    8945 E8         MOV DWORD PTR SS:[EBP-18],EAX
00402353    FFD0            CALL EAX                                 ; kernel32.QueryPerformanceCounter
[...]
0040246D    8D55 E0         LEA EDX,DWORD PTR SS:[EBP-20]
00402470    52              PUSH EDX
00402471    8973 08         MOV DWORD PTR DS:[EBX+8],ESI
00402474    FF55 E8         CALL DWORD PTR SS:[EBP-18]
00402477    8B45 E0         MOV EAX,DWORD PTR SS:[EBP-20]
0040247A    2B45 D8         SUB EAX,DWORD PTR SS:[EBP-28]
0040247D    3D 00000100     CMP EAX,10000                            ; UNICODE "=::=::\"
00402482    76 23           JBE SHORT ZDKeygen.004024A7

We can see that some API address are found by loading library (DLL names are “deciphered” at 00401CB0) and by locating export by their name hash (GetProcAddr fun @ 00401E20 and hash function @ 00401DF0).
Still the same basic obfuscation trick.

msvcrt.time is called by using this trick @ 00402361. Result is passed to the function @ 00402150. This function is quite ugly but actually pretty simple. It just extracts the year from the time return value (year(X) = X / 31440960 + 1970).

Serial is then treated like this : d1 | d2 | d3 | XXXX
First check is OK if year((2 * d1 >> 1) + 0x80000000) <= year(time()) <= year((2 * d3 >> 1) + 0x80000000)

To pass the first check, all we have to do is to generate a lic using the following code :

import struct
import time

date1 = int(time.time())               # date of the day
date2 = int(time.time()) + 10*31440960 # date of the day + 10 years

f = file("lic.dat", "wb")

f.write(struct.pack("<I", (date1 - 0x80000000) & 0xFFFFFFFF))
f.write(struct.pack("<I", 0x12345678) # we don't know what it is :)
f.write(struct.pack("<I", (date2 - 0x80000000) & 0xFFFFFFFF))

Next part of the check is a little bit trickier. The XXX part is a list of binary strings, d2 is actually the number of strings (and must be >= 4 and < 16).
Strings are encoded as follow :

d_1 | str_1 | d_2 | str_2 | ...

Where the d_i are little endian dword d_1 & 0x3ffff = len(str_1) (the rest of the dword will be used later 🙂 )
(btw, you can specify arbitrary len and crash the crackme).
String and d_i are stored in a list.

Then the high parts of d_is are check. It’s a little bit tricky to explain but to solve this part, all you have to do is to set those bits to particular (and constant) values. Here is the code that does this :


Then you find and other call that, from the 2 first provided strings, calculate a buffer of bytes. If the 3rd string is equal to this buffer then you pass the third check. We didn’t reverse the code (it seems boring …), we just dumped the buffer just before the memcmp :

004029C9    3BF2            CMP ESI,EDX
004029CB    75 45           JNZ SHORT ZDKeygen.00402A12

We just have to modify our source to incorporate the dumped key :

strings = ["Eloi\x00", "Vanderbeken\x00", "05EDCDDDCD8DC1C5EDF92CC4E4F4E4A4E8ECC4D02FC7E7F7E7A7EBEFC7D329C1E1F1E1A1EDE9C1D540A8889888C88480A8BC".decode("hex"), struct.pack("<I", 0xBC4DA2A8)]

An other little anti-dbg …

00402648    8D4D A0         LEA ECX,DWORD PTR SS:[EBP-60]
0040264B    51              PUSH ECX
0040264C    6A 1F           PUSH 1F
0040264E    6A FF           PUSH -1
00402650    C745 A0 0000000>MOV DWORD PTR SS:[EBP-60],0
00402657    FFD0            CALL EAX                                 ; ntdll.ZwQueryInformationProcess

ZwQueryInformationProcess + ProcessDebugFlags

The last check is not really a check ; the last DWORD is used to find an API in user32. We guess it’s MessageBoxA. All we have to do is to find the hash for MessageBoxA, to find it we just change the argument when the hash function is called to point on a “MessageBoxA” we previously put in memory (wherever there is a place). And this hash is 0xBC4DA2A8.

All the pieces together :

import struct
import time

d1 = int(time.time())               # date of the day
d2 = int(time.time()) + 10*31440960 # date of the day + 10 years

f = file("lic.dat", "wb")

strings = ["Eloi\x00", "Vanderbeken\x00", "05EDCDDDCD8DC1C5EDF92CC4E4F4E4A4E8ECC4D02FC7E7F7E7A7EBEFC7D329C1E1F1E1A1EDE9C1D540A8889888C88480A8BC".decode("hex"), struct.pack("<I", 0xBC4DA2A8)]
codes = [0xB, 0x13, 0x1E, 0x31]

f.write(struct.pack("<I", (d1 - 0x80000000) & 0xFFFFFFFF))
assert(4 <= len(strings) < 16)
f.write(struct.pack("<I", len(strings)))
f.write(struct.pack("<I", (d2 - 0x80000000) & 0xFFFFFFFF))

for c, s in zip(codes, strings) :
	f.write(struct.pack("<I", len(s) | (c << 25)))
	f.write(s)

Writeup by Dominique

Here is a code for binary one that should work in most cases (no error checks).

I first removed the ASLR flag from the binary.
I then I found the address of the check routine with procmon.

Then I reversed the function to find:

The license has several sections, the number of sections is in the 5th byte.
Each section starts with the following bytes {length, 0,0, (tag<<1)}.
The license must have 4 sections with the tags 8, 0x13,0x1e,0x31.

Section 8 contains the user name
Section 0x13 can be anything
Section 0x1e contains a sort of hash of the user name which length is 10* user name length
Section 0x31 contains a 32 bytes value that is a sort of hash of the string MessageBoxA (used to find the function that will display the congratulations message).

There were probably a few other checks, but they don’t matter once you have a working “license skeleton” by trial and error.

The anti-debugging was trivial so no need to talk about it (I attached instead of starting the binary in the debugger, and to avoid the timechecks, just don’t single step around them).

#include <stdio.h>
#include <memory.h>
#include <stdlib.h>
#include <Windows.h>

unsigned int findApiHash(char* api)
{
       unsigned __int32 hash = 0;

              int i = 0;

              while (api[i] != 0)
              {
                     __asm ror hash,0x0d;
                     hash = hash+ api[i];
                     i++;
              }

       return hash;
}

void hash( char *in, unsigned char * out, int length)
{
       unsigned char key[] = { 0x10,0x24,0x38,0x00,0x14,0x08,0x04,0x1c,0x0c,0x20};

       for (int x = 0; x < length; x++)
       {
              for (int i = 0 ; i < 10; i++)
              {
                     out[x*10 + i] = in[x] ^ key[i];
              }
       }
}

void writeSection(FILE *file,unsigned char tag, int length, unsigned char *data)
{
       fputc(length,file);
       fputc(0,file);
       fputc(0,file);
       fputc(tag<<1,file);
       fwrite(data,length,1,file);
}

int main(int argc, char **argv)
{
       // header[3] must be < 0x51 >= 0x04 (derived from time)
       unsigned char header[] = {02,0x62,0x62,0x41,4,0x32,0x32,01,01,01,01,01}; // 4 = number of sections
    unsigned char data[0x10];
       unsigned char *shash;
       int nameLen;

    char name[0x100];

       memset(name,0,0x100);

  printf("Enter your name: ");
  scanf("%s", name);
  nameLen = strlen(name)+1;

    FILE *f = fopen("lic.dat","wb");
       fwrite(header,12,1,f);
       writeSection(f, 11,nameLen, (unsigned char *) name);
       memset(data,0x42,0x10);
       writeSection(f, 19,0x10, data);
       shash = (unsigned char *) malloc(nameLen*10);
       hash(name,shash,nameLen+1);
       writeSection(f, 30,nameLen*10, shash);
       memset(data,0x44,0x100);
       unsigned int apiHash = findApiHash("MessageBoxA");
       writeSection(f, 49,4, (unsigned char*) &apiHash);
       fclose(f);
    return 0;
}

ZDResearch Keygen

/*
ZDResearch Binary Challenge One Keygen
CopyRight (c) ZDResearch.com
*/
#include <stdio.h>
#include <Windows.h>

typedef DWORD (*Typetime)(DWORD* timer);
DWORD timestamp()
{
	HMODULE hmodule = LoadLibraryA("msvcrt");
	Typetime time =  (Typetime)GetProcAddress(hmodule, "time");
	return time(0);
}

typedef struct StructHeader
{
	int start_time:31;
	int numTags:5;
	int junk:5;
	int end_time:31;
}Header;

typedef struct StructTag
{
  int size:18;
  int junk:7;
  int sig:7;

}Tag;

volatile int shl=30;

void WriteLicense(char * name, BYTE * key, DWORD keySize)
{

	FILE * fp = fopen("lic.dat","wb");
	int start_timestamp = timestamp()-60*60*24*30*6, end_timestamp = timestamp()+(2<<26);
	Header h = {0};
	h.start_time = start_timestamp-(2<<shl);
	h.end_time = end_timestamp- (2<<shl);
	h.numTags = 4;
	fwrite(&h, 1, sizeof(Header), fp);

	Tag t[4];
	for(int i = 0, k=8, j = 3; i<4; i++)
	{

		t[i].sig =  j+k;
		int t = k;
		k+= j;
		j = t;
	}

	t[0].size = strlen(name);
	//BYTE name[] = "shahin";

	t[1].size = keySize;
	//BYTE key[] = { 0x32, 0x12, 0x42, 0x52, 0xff, 0x32, 0x12, 0x42, 0x52, 0xff, 0x42, 0x52, 0xff };

	t[2].size = 10*strlen((char*)name);

	BYTE zdr[] = {0x5a-0x30, 0x44-0x30, 0x52-0x30, 0x65-0x30, 0x73-0x30, 0x65-0x30, 0x61-0x30, 0x72-0x30, 0x63-0x30, 0x68-0x30};
	BYTE chl[] = {0x43-0x40, 0x68-0x40, 0x61-0x40, 0x6c-0x40, 0x6c-0x40, 0x65-0x40, 0x6e-0x40, 0x67-0x40, 0x65-0x40};

	for(int i=0; i < 10; i++)
	{
		zdr[i] = (zdr[i] + 0x30 ) ^ (chl[i%9]+0x40);
	}

	for(int i=0; i<10; i++) 	{ 		zdr[i] = ((zdr[i] >> 2)^key[i%10]) << 2;
	}

	DWORD sizeCipher = t[2].size;

	BYTE * cipher = new BYTE[sizeCipher];

	for(int i = 0, c = 0; i< t[0].size; i++)
	{
		for(int j = 0; j < 10; j++)
		{
			cipher[c++] = name[i] ^ zdr[j];
		}
	}

	t[3].size = 4;
	BYTE hash[] = {0xa8, 0xa2, 0x4d, 0xbc } ;

	for(int i=0; i < h.numTags; i++) 	{		 		fwrite(&t[i], 1, sizeof(Tag), fp);	 		if(i == 1) 		{ 			fwrite(key, 1, t[i].size, fp);	 		} 		else if(i==2) 		{ 			fwrite(cipher, 1, t[i].size, fp);	 		} 		else if(i==3) 		{ 			fwrite(hash, 1, t[i].size, fp);	 		} 		else 		{fwrite(name, 1, t[i].size, fp);	} 	} 	fclose(fp); 	delete cipher; } int main(int argc, char ** argv) { 	if(argc != 2) 	{ 		printf("[*] www.ZDResearch.com [*]\n"); 		printf("[!] usage: > challenge0keygen.exe ");
		return -1;
	}
	BYTE key[] = { 0x32, 0x12, 0x42, 0x52, 0xff, 0x32, 0x12, 0x42, 0x52, 0xff, 0x42, 0x52, 0xff, 0x42, 0x52, 0xff, 0x32 };
	WriteLicense(argv[1], key, 17);

}

Leave a Reply

Your email address will not be published. Required fields are marked *