From aldeid
Jump to navigation Jump to search




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!":



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 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 loc_40124C:
CODE:0040124C                 call    f_good
CODE:00401251                 jmp     short loc_4011E6
CODE:00401251 WndProc         endp ; sp-analysis failed

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 my_username     = dword ptr  4
CODE:0040137E                 mov     esi, [esp+my_username] ; esi = my_username
CODE:00401382                 push    esi
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 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 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 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 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 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 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 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 loc_40124C:
CODE:0040124C                 call    f_good



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


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]
    print "Serial: %s" % keygen(sys.argv[1].upper())

Here is an example:

$ ./keygen.py aldeid
Serial: 17903







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 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 my_password     = dword ptr  4
CODE:00401365                 mov     ds:password_len, 0
CODE:0040136C                 mov     esi, [esp+my_password]
CODE:00401370                 push    esi
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 loc_401385:
CODE:00401385                 inc     esi
CODE:00401386                 jmp     short loc_401371
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 loc_401390:
CODE:00401390                 pop     esi
CODE:00401391                 call    f_encrypt_mypassword_
CODE:00401396                 jmp     short $+2
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 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 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 my_password_encrypted= dword ptr  4
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 good_boy:
CODE:0040124A                 call    f_good
CODE:0040124F                 jmp     short loc_4011E6
CODE:0040124F WndProc         endp ; sp-analysis failed


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)])

Let's validate:






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 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 my_serial       = dword ptr  4
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 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 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 my_serial_xored    = dword ptr  4
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:

Cracked Uncracked
CODE:00401346 f_upd_window_CRACKED proc near
CODE:00401346 window_name     = dword ptr  4
CODE:00401346                 mov     esi, [esp+window_name]
CODE:0040134A                 add     esi, 0Dh
CODE:0040134D                 mov     dword ptr [esi], 43202D20h ; ' - C'
CODE:00401353                 mov     dword ptr [esi+4], 6B636172h ; 'rack'
CODE:0040135A                 mov     dword ptr [esi+8], 21216465h ; 'ed!!'
CODE:00401361                 retn
CODE:00401361 f_upd_window_CRACKED endp
CODE:004012F5 f_upd_window_UNCRACKED proc near
CODE:004012F5 window_name           = dword ptr  4
CODE:004012F5                 mov     esi, [esp+window_name]
CODE:004012F9                 add     esi, 0Dh
CODE:004012FC                 mov     dword ptr [esi], 55202D20h ; ' - U'
CODE:00401302                 mov     dword ptr [esi+4], 6172636Eh ; 'ncra'
CODE:00401309                 mov     dword ptr [esi+8], 64656B63h ; 'cked'
CODE:00401310                 retn
CODE:00401310 f_upd_window_UNCRACKED endp
Write-up-Cruehead-CrackMes-crackme3-window-title-cracked.png Write-up-Cruehead-CrackMes-crackme3-window-title-uncracked.png


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 my_key_xored    = dword ptr  4
CODE:00401362 cracked_by      = dword ptr  8
CODE:00401362 now_try_next    = dword ptr  0Ch
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



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


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
    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]
    if len(sys.argv[1]) > 14:
        print "Max length: 14"
    print "CRACKME3.KEY generated"

Here is the output:

$ ./keygen.py aldeid
CRACKME3.KEY generated
00000000  20 2e 27 21 2c 22 47 48  49 4a 4b 4c 4d 4e 1b 54  | .'!,"GHIJKLMN.T|
00000010  34 12                                             |4.|


And here is the validation evidence:



Keywords: crackme reverse-engineering cruehead keygen