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:
- Eloi Vanderbéken @elvanderb
- Dominique Bongard @reversity
- 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); }