Write-up-Cruehead-CrackMes
CrackMe1
Description
Downloads
- Download link: http://www.woodmann.com/crackz/Archives/Crackmes.zip
- Mirror: https://github.com/sebastiendamaye/public/raw/master/66f573036f8b99863d75743eff84f15d
When run, the crackme displays a window with a menu. When you select Help > Register, there is a form that prompts for a Name and a Serial. If the serial is incorrect, a popup shows the message "No luck there, mate!":
Analysis
start function
Starting from the start function, WndProc is used as WndClass.lpfnWndProc parameter at offset 0x401027:
CODE:00401000 public start
CODE:00401000 start proc near
CODE:00401000 push 0 ; lpModuleName
CODE:00401002 call GetModuleHandleA
CODE:00401007 mov ds:hInstance, eax
CODE:0040100C push 0 ; lpWindowName
CODE:0040100E push offset ClassName ; "No need to disasm the code!"
CODE:00401013 call FindWindowA
CODE:00401018 or eax, eax
CODE:0040101A jz short loc_40101D
CODE:0040101C retn
CODE:0040101D ; ---------------------------------------------------------------------------
CODE:0040101D
CODE:0040101D loc_40101D:
CODE:0040101D mov ds:WndClass.style, 4003h
CODE:00401027 mov ds:WndClass.lpfnWndProc, offset WndProc
CODE:00401031 mov ds:WndClass.cbClsExtra, 0
CODE:0040103B mov ds:WndClass.cbWndExtra, 0
CODE:00401045 mov eax, ds:hInstance
CODE:0040104A mov ds:WndClass.hInstance, eax
CODE:0040104F push 64h ; lpIconName
CODE:00401051 push eax ; hInstance
CODE:00401052 call LoadIconA
CODE:00401057 mov ds:WndClass.hIcon, eax
CODE:0040105C push 7F00h ; lpCursorName
CODE:00401061 push 0 ; hInstance
CODE:00401063 call LoadCursorA
CODE:00401068 mov ds:WndClass.hCursor, eax
CODE:0040106D mov ds:WndClass.hbrBackground, 5
CODE:00401077 mov ds:WndClass.lpszMenuName, offset aMenu ; "MENU"
CODE:00401081 mov ds:WndClass.lpszClassName, offset ClassName ; "No need to disasm the code!"
CODE:0040108B push offset WndClass ; lpWndClass
CODE:00401090 call RegisterClassA
CODE:00401095 push 0 ; lpParam
CODE:00401097 push ds:hInstance ; hInstance
CODE:0040109D push 0 ; hMenu
CODE:0040109F push 0 ; hWndParent
CODE:004010A1 push 8000h ; nHeight
CODE:004010A6 push 8000h ; nWidth
CODE:004010AB push 6Eh ; Y
CODE:004010AD push 0B4h ; X
CODE:004010B2 push 0CF0000h ; dwStyle
CODE:004010B7 push offset WindowName ; "CrackMe v1.0"
CODE:004010BC push offset ClassName ; "No need to disasm the code!"
CODE:004010C1 push 0 ; dwExStyle
CODE:004010C3 call CreateWindowExA
CODE:004010C8 mov ds:hWnd, eax
CODE:004010CD push 1 ; nCmdShow
CODE:004010CF push ds:hWnd ; hWnd
CODE:004010D5 call ShowWindow
CODE:004010DA push ds:hWnd ; hWnd
CODE:004010E0 call UpdateWindow
CODE:004010E5 push 1 ; bErase
CODE:004010E7 push 0 ; lpRect
CODE:004010E9 push dword ptr [ebp+8] ; hWnd
CODE:004010EC call InvalidateRect
; ...[SNIP]...
WndProc function
At offset 0x401228, the username is pushed to the stack as argument to the sub_40137E function (renamed f_num_from_username) and the return value (EAX) is saved to the stack at offset 0x401232. It will be retrieved later, at offset 0x401240.
At offset 0x401228, the serial is pushed to the stack as argument to the sub_4013E2 function (renamed f_num_from_serial).
There is a comparison of the output of both functions at offset 0x401241 and if they match, the code jumps to the good boy.
CODE:00401209 loc_401209:
CODE:00401209 push 0 ; dwInitParam
CODE:0040120B push offset sub_401253 ; lpDialogFunc
CODE:00401210 push [ebp+hWnd] ; hWndParent
CODE:00401213 push offset aDlg_regis ; "DLG_REGIS"
CODE:00401218 push ds:hInstance ; hInstance
CODE:0040121E call DialogBoxParamA
CODE:00401223 cmp eax, 0
CODE:00401226 jz short loc_4011E6
CODE:00401228 push offset my_username
CODE:0040122D call f_num_from_username ; eax = sum(username characters) ^ 0x5678
CODE:00401232 push eax
CODE:00401233 push offset my_serial
CODE:00401238 call f_num_from_serial ; ebx = my_serial ^ 0x1234
CODE:0040123D add esp, 4
CODE:00401240 pop eax ; eax = sum(username characters) ^ 0x5678
CODE:00401241 cmp eax, ebx ; \ if num_from_serial == num_from_username
CODE:00401243 jz short loc_40124C ; / jump to good boy
CODE:00401245 call f_bad
CODE:0040124A jmp short loc_4011E6
CODE:0040124C ; ---------------------------------------------------------------------------
CODE:0040124C
CODE:0040124C loc_40124C:
CODE:0040124C call f_good
CODE:00401251 jmp short loc_4011E6
CODE:00401251 WndProc endp ; sp-analysis failed
CODE:00401251
sub_40137E (f_num_from_username)
This function takes the username as argument. It does the following:
- Validated that the username is only composed of letters ([a-zA-Z]). If not, it displays an error message (No luck there, mate!)
- Converts the username to upper cases
- Computes the sum of all characters (upper case). It then XORs the result with 0x5678.
- The final result is saved to EDI.
CODE:0040137E f_num_from_username proc near
CODE:0040137E
CODE:0040137E my_username = dword ptr 4
CODE:0040137E
CODE:0040137E mov esi, [esp+my_username] ; esi = my_username
CODE:00401382 push esi
CODE:00401383
CODE:00401383 loc_401383:
CODE:00401383 mov al, [esi] ; al = my_username[i]
CODE:00401385 test al, al ; last char?
CODE:00401387 jz short loc_40139C
CODE:00401389 cmp al, 41h ; 'A'
CODE:0040138B jb short loc_4013AC
CODE:0040138D cmp al, 5Ah ; 'Z'
CODE:0040138F jnb short loc_401394
CODE:00401391 inc esi
CODE:00401392 jmp short loc_401383
CODE:00401394 ; ---------------------------------------------------------------------------
CODE:00401394
CODE:00401394 loc_401394:
CODE:00401394 call f_toupper ; if lowercase, set username in uppercase
CODE:00401399 inc esi
CODE:0040139A jmp short loc_401383
CODE:0040139C ; ---------------------------------------------------------------------------
CODE:0040139C
CODE:0040139C loc_40139C:
CODE:0040139C pop esi
CODE:0040139D call f_sum_username ; edi = sum of all characters
CODE:0040139D ; in username
CODE:004013A2 xor edi, 5678h ; edi = sum(username characters) ^ 0x5678
CODE:004013A8 mov eax, edi
CODE:004013AA jmp short locret_4013C1
CODE:004013AC ; ---------------------------------------------------------------------------
CODE:004013AC
CODE:004013AC loc_4013AC:
CODE:004013AC pop esi
CODE:004013AD push 30h ; uType
CODE:004013AF push offset aNoLuck ; "No luck!"
CODE:004013B4 push offset aNoLuckThereMat ; "No luck there, mate!"
CODE:004013B9 push dword ptr [ebp+8] ; hWnd
CODE:004013BC call MessageBoxA
CODE:004013C1
CODE:004013C1 locret_4013C1:
CODE:004013C1 retn
CODE:004013C1 f_num_from_username endp
sub_4013D8 (f_num_from_serial)
This function computes the hexadecimal value of the serial and XORs it with 0x1234.
CODE:004013D8 f_num_from_serial proc near
CODE:004013D8
CODE:004013D8 my_password = dword ptr 4
CODE:004013D8 ; *** Function to convert to HEX ***
CODE:004013D8 xor eax, eax ; eax = 0
CODE:004013DA xor edi, edi ; edi = 0
CODE:004013DC xor ebx, ebx ; ebx = 0
CODE:004013DE mov esi, [esp+my_serial]
CODE:004013E2
CODE:004013E2 loc_4013E2:
CODE:004013E2 mov al, 0Ah ; al = 0xA
CODE:004013E4 mov bl, [esi] ; bl = my_serial[i]
CODE:004013E6 test bl, bl ; last character of serial?
CODE:004013E8 jz short loc_4013F5
CODE:004013EA sub bl, 30h
CODE:004013ED imul edi, eax
CODE:004013F0 add edi, ebx ; edi = edi * 0xA + (my_serial[i] - 0x30)
CODE:004013F2 inc esi
CODE:004013F3 jmp short loc_4013E2
CODE:004013F5 ; ---------------------------------------------------------------------------
CODE:004013F5
CODE:004013F5 loc_4013F5:
CODE:004013F5 xor edi, 1234h ; edi ^= 0x1234
CODE:004013FB mov ebx, edi ; ebx = edi
CODE:004013FD retn
CODE:004013FD f_num_from_password endp
Final check
The final check compares the output of both previous functions and jumps to the good boy if they match:
CODE:00401240 pop eax ; eax = sum(username characters) ^ 0x5678
CODE:00401241 cmp eax, ebx ; \ if num_from_password == num_from_username?
CODE:00401243 jz short loc_40124C ; / jumpt ot good boy
CODE:00401245 call f_bad
CODE:0040124A jmp short loc_4011E6
CODE:0040124C ; ---------------------------------------------------------------------------
CODE:0040124C
CODE:0040124C loc_40124C:
CODE:0040124C call f_good
Solution
Conclusion
To sum up:
- The 1st function computes the sum of all characters of the username and XORs the total with 0x5678
- The 2nd function XORs to serial with 0x1234
- The result of both functions should match
As a conclusion, the serial should be computed as follows:
serial = sum(all characters of username) ^ 0x5678 ^ 0x1234
which can be simplied as follows:
serial = sum(all characters of username) ^ 0x444c
Keygen
Below is the code of the keygen, in python:
#!/usr/bin/env python
import sys
def keygen(username):
s = 0
for i in username:
s += ord(i)
return s ^ 0x444c
if __name__ == '__main__':
if len(sys.argv) < 2:
print "Usage: %s <username>" % sys.argv[0]
sys.exit(1)
print "Serial: %s" % keygen(sys.argv[1].upper())
Here is an example:
$ ./keygen.py aldeid Serial: 17903
CrackMe2
Description
Downloads
- Download link: http://www.woodmann.com/crackz/Archives/Crackmes.zip
- Mirror: https://github.com/sebastiendamaye/public/raw/master/6b18ff1b277679035387edd1f8ead3fe
Analysis
WndProc function
This crackme has the same structure has for the 1st crackme. So we will directly jump to the interesting code in the WndProc function.
At offset 0x401228, the password provided in the form is pushed to the stack, as argument to the sub_401365 function (renamed f_encrypt_mypassword). As we will detail later, this function is actually encrypting the password. It then passes the resulting encrypted password to the sub_4013B8 function (renamed f_check_password) that will check that the encrypted password is the one expected. At offset 0x40123F, the counter (cl) is checked and if it is zero, the code jumps to the good boy.
CODE:00401228 push offset my_password
CODE:0040122D call f_encrypt_mypassword ; encrypt password + compute password length
CODE:00401232 push offset my_password ; my_password (encrypted)
CODE:00401237 call f_check_password ; check encrypted password (cl = len(mypassword) - number of correct char)
CODE:0040123C add esp, 4
CODE:0040123F test cl, cl ; \ if cl == 0 (all char of encrypted password are correct)
CODE:00401241 jz short good_boy ; / jump to good boy
CODE:00401243 call f_bad
CODE:00401248 jmp short loc_4011E6
CODE:0040124A ; ---------------------------------------------------------------------------
CODE:0040124A
CODE:0040124A good_boy:
CODE:0040124A call f_good
CODE:0040124F jmp short loc_4011E6
CODE:0040124F WndProc endp ; sp-analysis failed
sub_401365 (f_encrypt_mypassword)
This function sets the provided password to upper case and ensures that all characters are only letters ([a-zA-Z]). It also computes the password length, saved at memory location 0x402118 (renamed password_len). Then, at offset 0x401391, it calls sub_401399 (renamed f_encrypt_mypassword_).
CODE:00401365 f_encrypt_mypassword proc near
CODE:00401365
CODE:00401365 my_password = dword ptr 4
CODE:00401365
CODE:00401365 mov ds:password_len, 0
CODE:0040136C mov esi, [esp+my_password]
CODE:00401370 push esi
CODE:00401371
CODE:00401371 loc_401371:
CODE:00401371 mov al, [esi] ; al = my_password[i]
CODE:00401373 test al, al
CODE:00401375 jz short loc_401390
CODE:00401377 inc ds:password_len ; mem loc 0x402118
CODE:0040137D cmp al, 41h ; 'A'
CODE:0040137F jb short loc_401385
CODE:00401381 cmp al, 5Ah ; 'Z'
CODE:00401383 jnb short loc_401388
CODE:00401385
CODE:00401385 loc_401385:
CODE:00401385 inc esi
CODE:00401386 jmp short loc_401371
CODE:00401388 ; ---------------------------------------------------------------------------
CODE:00401388
CODE:00401388 loc_401388:
CODE:00401388 call f_to_upper
CODE:0040138D inc esi
CODE:0040138E jmp short loc_401371
CODE:00401390 ; ---------------------------------------------------------------------------
CODE:00401390
CODE:00401390 loc_401390:
CODE:00401390 pop esi
CODE:00401391 call f_encrypt_mypassword_
CODE:00401396 jmp short $+2
CODE:00401398 ; ---------------------------------------------------------------------------
CODE:00401398
CODE:00401398 locret_401398:
CODE:00401398 retn
CODE:00401398 f_encrypt_mypassword endp
sub_401399 (f_encrypt_mypassword_)
This function replaces the provided password with its encrypted form (XORed with the key Messing_in_bytes).
CODE:00401399 f_encrypt_mypassword_ proc near
CODE:00401399 xor ebx, ebx
CODE:0040139B xor edi, edi
CODE:0040139D
CODE:0040139D loc_40139D:
CODE:0040139D mov cl, byte ptr ds:aMessing_in_bytes[edi] ; "Messing_in_bytes"
CODE:004013A3 mov bl, [esi]
CODE:004013A5 test bl, bl
CODE:004013A7 jz short locret_4013B1
CODE:004013A9 xor bl, cl
CODE:004013AB mov [esi], bl
CODE:004013AD inc esi
CODE:004013AE inc edi
CODE:004013AF jmp short loc_40139D
CODE:004013B1 ; ---------------------------------------------------------------------------
CODE:004013B1
CODE:004013B1 locret_4013B1:
CODE:004013B1 retn
CODE:004013B1 f_encrypt_mypassword_ endp
; ...[SNIP]...
DATA:004021A3 aMessing_in_bytes db 'Messing_in_bytes',0
sub_4013B8 (f_check_password)
This function counts how many characters of the encrypted password are correct by decrementing a counter (cl), initially set to the length of the password. If all characters are correct, cl should be equal to zero.
CODE:004013B8 f_check_password proc near
CODE:004013B8
CODE:004013B8 my_password_encrypted= dword ptr 4
CODE:004013B8
CODE:004013B8 xor edi, edi
CODE:004013BA xor ecx, ecx
CODE:004013BC mov cl, ds:password_len ; cl = len(my_password)
CODE:004013C2 mov esi, [esp+my_password_encrypted]
CODE:004013C6 mov edi, offset password_encrypted ; mem loc 0x402150
CODE:004013CB repe cmpsb
CODE:004013CD retn
CODE:004013CD f_check_password endp
; ...[SNIP]...
DATA:00402150 password_encrypted db 1Fh, 2Ch, 37h, 36h, 3Bh, 3Dh, 28h, 19h, 3Dh, 26h, 1Ah
DATA:00402150 db 31h, 2Dh, 3Bh, 37h, 3Eh
Final check
As explained in the previous function, cl should be equal to zero if the encrypted password
CODE:0040123F test cl, cl ; \ if all character of encrypted password are correct (cl = 0?)
CODE:00401241 jz short good_boy ; / jump to good boy
CODE:00401243 call f_bad
CODE:00401248 jmp short loc_4011E6
CODE:0040124A ; ---------------------------------------------------------------------------
CODE:0040124A
CODE:0040124A good_boy:
CODE:0040124A call f_good
CODE:0040124F jmp short loc_4011E6
CODE:0040124F WndProc endp ; sp-analysis failed
Solution
The solution can be scripted as follows in python:
>>> s = [0x1F, 0x2C, 0x37, 0x36, 0x3B, 0x3D, 0x28, 0x19, 0x3D, 0x26, 0x1A, 0x31, 0x2D, 0x3B, 0x37, 0x3E] >>> k = "Messing_in_bytes" >>> print ''.join([chr(i ^ ord(k[c])) for c, i in enumerate(s)]) RIDERSOFTHESTORM
Let's validate:
CrackMe3
Description
Downloads
- Download link: http://www.woodmann.com/crackz/Archives/Crackmes.zip
- Mirror: https://github.com/sebastiendamaye/public/raw/master/16e916272055bf2d32d2ca9befbc5ef8
Analysis
start function
This crackme is a bit different than the 2 previous ones. Immediately in the start function, you notice that the program attempts to access a file named CRACKME3.KEY at offset 0x40102D. If the file doesn't exist, the program exits.
CODE:00401000 start proc near
CODE:00401000 push 0 ; lpModuleName
CODE:00401002 call GetModuleHandleA
CODE:00401007 mov ds:hInstance, eax
CODE:0040100C mov ds:checksum, 0
CODE:00401016 push 0 ; hTemplateFile
CODE:00401018 push 80h ; dwFlagsAndAttributes
CODE:0040101D push 3 ; dwCreationDisposition
CODE:0040101F push 0 ; lpSecurityAttributes
CODE:00401021 push 3 ; dwShareMode
CODE:00401023 push 0C0000000h ; dwDesiredAccess
CODE:00401028 push offset FileName ; "CRACKME3.KEY"
CODE:0040102D call CreateFileA
CODE:00401032 cmp eax, 0FFFFFFFFh
CODE:00401035 jnz short loc_401043
CODE:00401037
CODE:00401037 bad_boy:
CODE:00401037 push offset WindowName ; "CrackMe v3.0 "
CODE:0040103C call f_upd_window_UNCRACKED
CODE:00401041 jmp short loc_4010AE
Then, the code reads 18 bytes from the file. It also exits if the CRACKME3.KEY file contains less than 18 bytes.
CODE:00401043 mov ds:hFile, eax
CODE:00401048 mov eax, 12h ; eax = 0x12 (18 bytes)
CODE:0040104D mov ebx, offset my_serial
CODE:00401052 push 0 ; lpOverlapped
CODE:00401054 push offset NumberOfBytesRead ; lpNumberOfBytesRead
CODE:00401059 push eax ; nNumberOfBytesToRead (18 bytes)
CODE:0040105A push ebx ; lpBuffer
CODE:0040105B push ds:hFile ; hFile
CODE:00401061 call ReadFile
CODE:00401066 cmp ds:NumberOfBytesRead, 12h ; \ if len(serial) != 18
CODE:0040106D jnz short bad_boy ; / jump to bad boy
Then, it pushes the serial read from the file (my_serial) as argument to the sub_401311 function (renamed f_xor_serial) called at offset 0x401074. We will see later that this function XORs the serial and computes a checksum. This latest is XORed with 0x12345678 at offset 0x401079. At offset 0x401986, the XORed serial is pushed to the stack, as argument to sub_40133C (renamed f_my_serial_last_4_bytes). This function extracts the last 4 bytes of the serial and compares the value to the checksum at offset 0x401093. The serial is valid if both value match and the code will call sub_401346 (renamed f_upd_window_CRACKED) at offset 0x4010A6. Also notice that the low 8 bits of EAX (AL) are updated by the setz instruction at offset 0x401099. This value is saved to the stack (with the push instruction) and will be retrieved later in the code, at offset 0x401187.
CODE:0040106F push offset my_serial
CODE:00401074 call f_xor_serial ; XOR key and compute checksum
CODE:00401079 xor ds:checksum, 12345678h ; checksum ^= 0x12345678
CODE:00401083 add esp, 4
CODE:00401086 push offset my_serial ; my_serial_xored
CODE:0040108B call f_my_serial_last_4_bytes ; eax = last 4 bytes my_serial_xored
CODE:00401090 add esp, 4
CODE:00401093 cmp eax, ds:checksum ; checksum == last 4 bytes my_serial_xored?
CODE:00401099 setz al ; AL=1 if checksum == last 4 bytes my_serial_xored
CODE:0040109C push eax ; save last 4 bytes to stack
CODE:0040109C ; (eax will be retrieved at 0x401187)
CODE:0040109D test al, al ; \ if AL==0
CODE:0040109F jz short bad_boy ; / jump to bad boy
CODE:004010A1 push offset WindowName ; "CrackMe v3.0 "
CODE:004010A6 call f_upd_window_CRACKED
CODE:004010AB add esp, 4
sub_401311 (f_xor_serial)
This function will do several things:
- It XORs bytes of the serial with a key, starting from 0x41 and incremented by 1 at each character
- It computes a checksum, initially set to 0, that cumulates the values of the resulting XOR operation (we will see later that it actually corresponds to the sum of the characters of the username)
- It will stop if one of the following conditions is met:
- the incremented key reaches 0x4F
- the resulting XOR computation is equal to 0
CODE:00401311 f_xor_serial proc near
CODE:00401311
CODE:00401311 my_serial = dword ptr 4
CODE:00401311
CODE:00401311 xor ecx, ecx ; ecx = 0
CODE:00401313 xor eax, eax
CODE:00401315 mov esi, [esp+my_serial] ; esi = my_serial
CODE:00401319 mov bl, 41h ; key = 0x41
CODE:0040131B
CODE:0040131B loc_40131B:
CODE:0040131B mov al, [esi] ;
CODE:0040131B ; al = my_serial[i]
CODE:0040131D xor al, bl ; al = my_serial[i] ^ key
CODE:0040131F mov [esi], al ; update my_serial[i]
CODE:00401321 inc esi ; i += 1
CODE:00401322 inc bl ; key += 1
CODE:00401324 add ds:checksum, eax ; checksum += my_serial[i] ^ 0x41
CODE:0040132A cmp al, 0 ; will stop if XORed value = 0
CODE:0040132A ; (end of username)
CODE:0040132C jz short loc_401335
CODE:0040132E inc cl ; cl += 1
CODE:00401330 cmp bl, 4Fh ; \ if key != 0x4F
CODE:00401333 jnz short loc_40131B ; / continue (will loop 14 times max)
CODE:00401335
CODE:00401335 loc_401335:
CODE:00401335 mov ds:equ_to_14, ecx
CODE:0040133B retn
CODE:0040133B f_xor_serial endp
At this stage, we know that:
- the serial should be 18 bytes long
- the loop is only working on a maximum of 14 bytes (0x4F - 0x41)
- the loop will stop if the XORed computation equals 0
sub_40133C (f_my_serial_last_4_bytes)
This function extracts the last 4 bytes of the serial read from the file:
CODE:0040133C f_my_serial_last_4_bytes proc near
CODE:0040133C
CODE:0040133C my_serial_xored = dword ptr 4
CODE:0040133C
CODE:0040133C mov esi, [esp+my_serial_xored]
CODE:00401340 add esi, 0Eh ; extract last 4 bytes of my_serial
CODE:00401343 mov eax, [esi]
CODE:00401345 retn
CODE:00401345 f_my_serial_last_4_bytes endp
Window title
Depending on the results of the test at offset (0x401093), the window title will be updated as follows:
MessageBox
Later in the code (at offset 0x401187), the value of the EAX saved at offset 0x40109C is retrieved and the low 8 bits are compared to 1 at offset 0x401188. In case of correct serial (if {{{1}}}), the code will call sub_401362 (renamed f_messagebox_success) at offset 0x40119B.
CODE:00401099 setz al ; AL=1 if checksum == last 4 bytes my_serial_xored
CODE:0040109C push eax ; save EAX to stack (eax will be retrieved at 0x401187)
; ...[SNIP]...
CODE:00401187 pop eax
CODE:00401188 cmp al, 1 ; if AL=1, correct serial
CODE:0040118A jnz short loc_4011A3
CODE:0040118C push offset aNowTryTheNextC ; "Now try the next crackme!"
CODE:00401191 push offset Text ; "Cracked by: "
CODE:00401196 push offset my_serial
CODE:0040119B call f_messagebox_success
CODE:004011A0 add esp, 0Ch
Below is the code of the sub_401362 (f_messagebox_success) function. The function is displaying a messagebox with a message that concatenates the string "Cracked by:" with the username (14 first bytes of the serial).
CODE:00401362 f_messagebox_success proc near
CODE:00401362
CODE:00401362 my_key_xored = dword ptr 4
CODE:00401362 cracked_by = dword ptr 8
CODE:00401362 now_try_next = dword ptr 0Ch
CODE:00401362
CODE:00401362 mov ecx, ds:equ_to_14 ; ecx = 14
CODE:00401368 mov esi, [esp+my_key_xored]
CODE:0040136C mov edi, [esp+cracked_by]
CODE:00401370 add edi, 0Ch
CODE:00401373 rep movsb
CODE:00401375 mov word ptr [edi], 0D21h
CODE:0040137A add edi, 2
CODE:0040137D mov esi, [esp+now_try_next]
CODE:00401381 mov ecx, 1Ah
CODE:00401386 rep movsb
CODE:00401388 push 30h ; uType
CODE:0040138A push offset Caption ; "Good work cracker!"
CODE:0040138F push offset Text ; "Cracked by: "
CODE:00401394 push ds:hWnd ; hWnd
CODE:0040139A call MessageBoxA
CODE:0040139F retn
CODE:0040139F f_messagebox_success endp
Solution
Structure of the serial
Now, with everything we have learned, we know that the serial is composed of 2 parts:
- the first 14 bytes correspond to the username (with an optional padding in case the username has less than 14 characters)
- the last 4 bytes are reserved for the checksum
username padding checksum ┌─────────────────────────────────────────┐┌──────────────────────────────────────────────────────┐┌──────────────────────────┐ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │ a │ l │ d │ e │ i │ d │ │ │ │ │ │ │ │ │ │ │ │ │ │ 0x61 │ 0x6c │ 0x64 │ 0x65 │ 0x69 │ 0x64 │ │ │ │ │ │ │ │ │ │ │ │ │ ├──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┤ key │ 0x41 │ 0x42 │ 0x43 │ 0x44 │ 0x45 │ 0x46 │ 0x47 │ 0x48 │ 0x49 │ 0x4A │ 0x4B │ 0x4C │ 0x4D │ 0x4E │ │ │ │ │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ serial │ 0x20 │ 0x2e │ 0x27 │ 0x21 │ 0x2c │ 0x22 │ 0x47 │ 0x48 │ 0x49 │ 0x4A │ 0x4B │ 0x4C │ 0x4D │ 0x4E │ 0x1b │ 0x54 │ 0x34 │ 0x12 │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
In the above example, the serial should start with the following values:
- 'a' ^ 0x41 = 0x20
- 'l' ^ 0x42 = 0x2e
- 'd' ^ 0x43 = 0x27
- ...
Then, the padding should start with a resulting XORed value of 0 (remember that it was 1 of the exit conditions of the loop in the sub_401311 (f_xor_serial) function). Hence, the following values should be:
- 0x47 because 0x47 ^ 0x47 = 0
- 0x48 because 0x48 ^ 0x48 = 0
- ...
And the last 4 bytes should contain our checksum, XORed by 0x12345678. The checksum is the sum of all characters of the username. Still with our example, here is how the checksum is computed:
(0x61 + 0x6c + 0x64 + 0x65 + 0x69 + 0x64) ^ 0x12345678 = 0x1234541b
Keygen
Below is the code of my keygen. The keygen takes the username as argument and generates a valid key file.
#!/usr/bin/env python
import sys
def splitNumber (num):
"""
Code taken from following source
http://stackoverflow.com/questions/20955543/python-writing-binary
"""
lst = []
while num > 0:
lst.append(num & 0xFF)
num >>= 8
#return lst[::-1]
return lst
def make_serial(username):
b = []
# XOR serial
checksum = 0
key = 0x41
for i in username:
b.append(ord(i) ^ key)
checksum += ord(i)
key += 1
# padding (will only apply if len(username) < 14)
for i in range(14 - len(username)):
b.append(0x41 + len(username) + i)
# Generate file
with open('CRACKME3.KEY', 'wb') as f:
f.write(bytearray(i for i in b))
# checksum
f.write(bytearray(i for i in splitNumber(checksum ^ 0x12345678)))
if __name__ == '__main__':
if len(sys.argv) < 2:
print "Usage: %s <username>" % sys.argv[0]
sys.exit(1)
if len(sys.argv[1]) > 14:
print "Max length: 14"
sys.exit(2)
make_serial(sys.argv[1])
print "CRACKME3.KEY generated"
Here is the output:
$ ./keygen.py aldeid CRACKME3.KEY generated $ hd CRACKME3.KEY 00000000 20 2e 27 21 2c 22 47 48 49 4a 4b 4c 4d 4e 1b 54 | .'!,"GHIJKLMN.T| 00000010 34 12 |4.| 00000012
Validation
And here is the validation evidence:
Comments
Keywords: crackme reverse-engineering cruehead keygen