Intro
I've been busy recently :) and was looking for a challenge, so I took this crackme on since it appeared to have been packed by UPX (messages in the exe) but procdump wouldnt unpack it. Ah, I thought it is time to do some manual unpacking but woe betide, I disassembled it and found it wasnt packed. Hehe, but then it's a bit lame to use someones packer unless you have made modifications to it, or it is your own packer :). Still, nice trick to put people off anyway.
Target
The crackme asks for a name, valid serial and keygen. The crackme appears to be pretty straightforward although the algorithm is uses doesnt look reversible to me, so we will take a brute force approach to serial generation for it in the keygen. The dialog box it presents looks a bit funny on my screen, which must either due to my screen size, or some of its properties. One of the buttons is obscured by a bitmap but it's still usable.
Analysis
OK, we are presented with a dialog box with two text boxes into which we can type a name and a serial, and a checkit button. So we need to put in two things and get the correct registration message. The disassembly of the program shows some entry code, easily traced through and a standard dialog box routine. So I'm just going to start within this dialog routine, at the point where the text inputs are read into strings, and take it from there. Following is the disassembly of the relevant part of the program.
;**************************************************************************************** ; This is the main part of the dialog which deals with processing the two input strings ; and checking whether they make a valid serial-key pair or not. ;**************************************************************************************** 1000:00401340 6a40 push 40h 1000:00401342 6830314000 push offset string1 1000:00401347 ff35c3304000 push dword ptr [4030c3h] 1000:0040134d e8a6030000 call _GetWindowTextA ; first get both strings 1000:00401352 83f804 cmp eax, 04h ; string1=name 1000:00401355 0f8ee9000000 jle failed 1000:0040135b 6a40 push 40h 1000:0040135d 6870314000 push offset string2 1000:00401362 68b90b0000 push bb9h 1000:00401367 ff7508 push dword ptr [ebp+08h] 1000:0040136a e877030000 call _GetDlgItemTextA ; from the dialog box 1000:0040136f 83f804 cmp eax, 04h ; string2=serial 1000:00401372 0f8ecc000000 jle failed 1000:00401378 a3bf304000 mov [string2_length], eax ; save string2 length 1000:0040137d ff35c3304000 push dword ptr [4030c3h] 1000:00401383 e8ac030000 call _SetFocus 1000:00401388 bf30314000 mov edi, offset string1 1000:0040138d be30314000 mov esi, offset string1 ; XREFS First: 1000:0040139a Number : 1 1000:00401392 ac lodsb ; turn string1 1000:00401393 0c00 or al, 00h ; into lowercase 1000:00401395 7405 jz 40139ch 1000:00401397 0c20 or al, 20h 1000:00401399 aa stosb 1000:0040139a ebf6 jmp 401392h ; XREFS First: 1000:00401395 Number : 1 1000:0040139c bf70314000 mov edi, offset string2 1000:004013a1 be70314000 mov esi, offset string2 1000:004013a6 8d1d30314000 lea ebx, [string1] ; XREFS First: 1000:004013b9 Number : 1 1000:004013ac ac lodsb 1000:004013ad 0c00 or al, 00h ; now 'subtract' 1000:004013af 740a jz 4013bbh ; string1 from string2 1000:004013b1 8a13 mov dl, [ebx] 1000:004013b3 2ad0 sub dl, al 1000:004013b5 8ac2 mov al, dl 1000:004013b7 aa stosb ; now string2=string1-string2 1000:004013b8 43 inc ebx 1000:004013b9 ebf1 jmp 4013ach ; XREFS First: 1000:004013af Number : 1 1000:004013bb 8b0dbf304000 mov ecx, [string2_length] ; init counter ; XREFS First: 1000:00401424 Number : 1 1000:004013c1 80c900 or cl, 00h 1000:004013c4 7460 jz 401426h 1000:004013c6 51 push ecx 1000:004013c7 6870314000 push offset string2 1000:004013cc e883020000 call 401654h ; converts ascii to number 1000:004013d1 f7e1 mul ecx ; times counter 1000:004013d3 68b0314000 push offset temp_string 1000:004013d8 50 push eax 1000:004013d9 e8be020000 call 40169ch ; convert it back to ascii 1000:004013de bfb0314000 mov edi, offset temp_string 1000:004013e3 beb0314000 mov esi, offset temp_string ; XREFS First: 1000:004013f1 Number : 1 1000:004013e8 ac lodsb 1000:004013e9 0c00 or al, 00h 1000:004013eb 7406 jz 4013f3h 1000:004013ed 83e00f and eax, 0fh ; bottom bits only of ascii digits 1000:004013f0 aa stosb 1000:004013f1 ebf5 jmp 4013e8h ; XREFS First: 1000:004013eb Number : 1 1000:004013f3 8b0dbf304000 mov ecx, [string2_length] ; divides string in 2 1000:004013f9 d1e9 shr ecx, 1h 1000:004013fb bff0304000 mov edi, offset enc_string 1000:00401400 beb0314000 mov esi, offset temp_string 1000:00401405 8d99b0314000 lea ebx, [ecx+temp_string] ; XREFS First: 1000:00401420 Number : 1 1000:0040140b 80c900 or cl, 00h 1000:0040140e 7412 jz 401422h 1000:00401410 ac lodsb ; calculate 1000:00401411 33d2 xor edx, edx ; enc_string+= 1000:00401413 8a13 mov dl, [ebx] ; temp_string (first half) 1000:00401415 02c2 add al, dl ; +temp_string (second half) 1000:00401417 8a17 mov dl, [edi] 1000:00401419 02c2 add al, dl 1000:0040141b 240f and al, 0fh ; mod 16 1000:0040141d aa stosb 1000:0040141e 49 dec ecx 1000:0040141f 43 inc ebx 1000:00401420 ebe9 jmp 40140bh ; XREFS First: 1000:0040140e Number : 1 1000:00401422 59 pop ecx ; loop over counter 1000:00401423 49 dec ecx 1000:00401424 eb9b jmp 4013c1h ; XREFS First: 1000:004013c4 Number : 1 1000:00401426 bef0304000 mov esi, offset enc_string 1000:0040142b 8b1dbf304000 mov ebx, [string2_length] ; final string must start with string2 length/2 as chars 1000:00401431 8bcb mov ecx, ebx ; eg length 6 becomes '333' 1000:00401433 d1e9 shr ecx, 1h ; (hex, not ascii) ; XREFS First: 1000:00401442 Number : 1 1000:00401435 8a06 mov al, [esi] 1000:00401437 80c900 or cl, 00h 1000:0040143a 7439 jz passed 1000:0040143c 38d8 cmp al, bl 1000:0040143e 7504 jnz failed 1000:00401440 46 inc esi 1000:00401441 49 dec ecx 1000:00401442 ebf1 jmp 401435h failed: ; XREFS First: 1000:00401355 Number : 3 1000:00401444 6800200000 push 2000h 1000:00401449 6818304000 push offset s________________Error 1000:0040144e 6841304000 push offset s____Sorry_Cracker__wrong_ 1000:00401453 ff7508 push dword ptr [ebp+08h] 1000:00401456 e8bb020000 call _MessageBoxA 1000:0040145b 6a40 push 40h 1000:0040145d 68f0304000 push offset enc_string 1000:00401462 e80f030000 call _RtlZeroMemory 1000:00401467 6a40 push 40h 1000:00401469 68b0314000 push offset temp_string 1000:0040146e e803030000 call _RtlZeroMemory 1000:00401473 eb2f jmp 4014a4h passed: ; XREFS First: 1000:0040143a Number : 1 1000:00401475 6800200000 push 2000h 1000:0040147a 682d304000 push offset s_________Registered_ 1000:0040147f 685a304000 push offset s_____________You_did_it_ 1000:00401484 ff7508 push dword ptr [ebp+08h] 1000:00401487 e88a020000 call _MessageBoxA 1000:0040148c 6a40 push 40h 1000:0040148e 68f0304000 push offset enc_string 1000:00401493 e8de020000 call _RtlZeroMemory 1000:00401498 6a40 push 40h 1000:0040149a 68b0314000 push offset temp_string 1000:0040149f e8d2020000 call _RtlZeroMemory ; XREFS First: 1000:00401473 Number : 1 1000:004014a4 eb0e jmp 4014b4h ;**************************************************************************************** ; end of our checking routine ;**************************************************************************************** |
Now thats quite a large chunk of code to get our heads around. Some of the string processing is straightforward, ie check the lengths are greater than or equal to 4. At the end we have the message printing parts, the well done you passed, or the bad luck you failed type messages. In the middle there are several loops over various strings, including simple 'convert to lowercase loops' and more complex loops with additions of various strings. Since we will need to write a keygen the next step is to convert this into some kind of easier to read format. I chose to decompile it by hand, into C. |
Converting code like the above into C isnt really that difficult if you take it a small chunk at a time. Sometimes I will take a single instruction and produce some C code for it, other times I will take a whole algorithm. The most difficult part is ensuring that you don't lose something in translation. The asm code above is best read by printing the table below with my reverse engineered C algorithm and referring between the two to see exactly what I mean, and see how it all works.......
// simple length checks if(strlen(string1)<=4) { return 0; } if(strlen(string2)<=4) { return 0; } string2_length=strlen(string2); i=0; // convert string1 to lowercase while(string1[i]) { string1[i]|=0x20; i++; } i=0; // compute differences while(string2[i]) { string2[i]=string1[i]-string2[i]; i++; } count=string2_length; // main crypt routine while(count) { r=atolnew(string2)*count; // ascii to long wsprintf(temp_string,"%lu",r); i=0; while(temp_string[i]) { temp_string[i]&=0x0f; i++; } r=string2_length/2; i=r; l=0; while(r) { enc_string[l]+=(temp_string[l]+temp_string[i]); enc_string[l]&=0x0f; l++; i++; r--; } count--; } i=0; r=string2_length/2; // finally check the answer while(r) { if(enc_string[i]!=string2_length) return 0; i++; r--; } return 1; |
Notice that I have changed the two calls into named routines. We will see that the atol routine isnt quite standard and so I have called it atolnew, and wsprintf is just a standard call although the program uses a subroutine to do this. Now its difficult to actually see how this could be reversed and so all I am going to do is use the above code in a keygen and bruteforce a serial number (I will use just a number rather than any letters). |
OK, adding some code to the above we arrive at the following keygen:
#include <string.h> #include <windows.h> #include <stdio.h> // slightly different atol, to take into account the // program version which allows non-digit characters long atolnew(unsigned char *str1) { long t; int i; unsigned char e; t=0; i=0; while(str1[i]) { e=str1[i]-48; t=t*10+e; i++; } return t; } // returns 0 for fail, 1 for success int checkit(char *string1,char *string2) { int string2_length,i,count,l; char temp_string[10],enc_string[10]; long r; i=0; // added zeroing of string for keygen - Cronos while(i<10) { temp_string[i]=0; enc_string[i]=0; i++; } // simple length checks if(strlen(string1)<=4) { return 0; } if(strlen(string2)<=4) { return 0; } string2_length=strlen(string2); i=0; // convert string1 to lowercase while(string1[i]) { string1[i]|=0x20; i++; } i=0; // compute differences while(string2[i]) { string2[i]=string1[i]-string2[i]; i++; } count=string2_length; // main crypt routine while(count) { r=atolnew(string2)*count; wsprintf(temp_string,"%lu",r); i=0; while(temp_string[i]) { temp_string[i]&=0x0f; i++; } r=string2_length/2; i=r; l=0; while(r) { enc_string[l]+=(temp_string[l]+temp_string[i]); enc_string[l]&=0x0f; l++; i++; r--; } count--; } i=0; r=string2_length/2; // finally check the answer while(r) { if(enc_string[i]!=string2_length) return 0; i++; r--; } return 1; } void main(int argc, char *argv[]) { char str1[10],str2[10]; int i,j; j=99999; printf("Keygen for czcrackme09 by Cronos\n"); printf("Usage: keygen name (name>4 chars)\n"); if(argc<2) return; do { j++; strcpy(str1,argv[1]); i=0; while(i<10) str2[i++]=0; wsprintf(str2,"%lu",j); if(!(j&0xffff)) printf("progress: %lu\r",j); } while(!checkit(str1,str2)); printf("\n%lu\n",j); } |
You will see in the above that I wrote a custom atol routine which allows non-digits in the computation, which is echoed elsewhere in the crackme if you want to look more closely. Running this give the following:
D:\uncracked crackmes\czcrackme09>keygen Cronos Keygen for czcrackme09 by Cronos Usage: keygen name (name>4 chars) 101045 |
And so there you have it, name:Cronos, serial:101045. Took less than a second.
Conclusions
Another interesting crackme bites the dust, although I wasnt able to reverse the algorithm. It was definitely worth doing for the experience and I would rate it 3/10 for difficulty, although I think some people might have difficulty with this.
{Cronos}