Write-up-MaxXor-KeygenMe-V6
Description
This crackme is available here: http://crackmes.de/users/maxxor/keygenme_v6/ and is rated "2 - Needs a little brain (or luck)". I had a lot of fun resolving it.
When run, it shows a form with 2 fields respectively for the username and the serial, as follows:
Analysis
start (sub_401AAB)
The start function calls load_funcs (sub_4012E8) at offset 0x401AB4
.text:00401AAB start proc near
.text:00401AAB
.text:00401AAB var_5C = WNDCLASSEXA ptr -5Ch
.text:00401AAB Msg = MSG ptr -2Ch
.text:00401AAB Rect = tagRECT ptr -10h
.text:00401AAB hInstance = dword ptr 8
.text:00401AAB
.text:00401AAB push ebp
.text:00401AAC mov ebp, esp
.text:00401AAE sub esp, 5Ch
.text:00401AB1 push ebx
.text:00401AB2 push esi
.text:00401AB3 push edi
.text:00401AB4 call f_load_funcs
Then, the f_check (sub_4013B1 function is called as lpfnWndProc parameter to the LoadCursorA function:
.text:00401B15 mov [ebp+var_5C.hIconSm], eax
.text:00401B18 mov [ebp+var_5C.hInstance], ebx
.text:00401B1B mov [ebp+var_5C.lpszClassName], edi
.text:00401B1E mov [ebp+var_5C.lpfnWndProc], offset f_check
.text:00401B25 mov [ebp+var_5C.style], 8
.text:00401B2C mov [ebp+var_5C.cbSize], 30h
.text:00401B33 call ds:LoadCursorA
f_load_funcs (sub_4012E8)
f_load_funcs (sub_4012E8) actually loads several functions (strlen, sprintf, strcmp, memset, isdigit, atoi) to memory locations that will be called later in the code. The purpose is to make the calls to these functions less obvious.
.text:004012E8 f_load_funcs proc near
.text:004012E8 push esi
.text:004012E9 push edi
.text:004012EA push offset ModuleName ; "ntdll.dll"
.text:004012EF call ds:GetModuleHandleA
.text:004012F5 mov esi, ds:GetProcAddress
.text:004012FB mov edi, eax
.text:004012FD push offset ProcName ; "strlen"
.text:00401302 push edi ; hModule
.text:00401303 call esi ; GetProcAddress
.text:00401305 push offset aSprintf ; "sprintf"
.text:0040130A push edi ; hModule
.text:0040130B mov strlen, eax ; mem loc 0x402000
.text:00401310 call esi ; GetProcAddress
.text:00401312 push offset aStrncmp ; "strncmp"
.text:00401317 push edi ; hModule
.text:00401318 mov sprintf, eax ; mem loc 0x402004
.text:0040131D call esi ; GetProcAddress
.text:0040131F push offset aMemset ; "memset"
.text:00401324 push edi ; hModule
.text:00401325 mov strncmp, eax ; mem loc 0x402008
.text:0040132A call esi ; GetProcAddress
.text:0040132C push offset aIsdigit ; "isdigit"
.text:00401331 push edi ; hModule
.text:00401332 mov memset, eax ; mem loc 0x40200C
.text:00401337 call esi ; GetProcAddress
.text:00401339 push offset aAtoi ; "atoi"
.text:0040133E push edi ; hModule
.text:0040133F mov isdigit, eax ; mem loc 0x402010
.text:00401344 call esi ; GetProcAddress
.text:00401346 pop edi
.text:00401347 mov atoi, eax ; mem loc 0x402014
.text:0040134C pop esi
.text:0040134D retn
.text:0040134D f_load_funcs endp
; ...[SNIP]...
.data:00402000 strlen dd ?
.data:00402004 sprintf dd ?
.data:00402008 strncmp dd ?
.data:0040200C memset dd ?
.data:00402010 isdigit dd ?
.data:00402014 atoi dd ?
f_check (sub_4013B1)
Graph overview
Below is the graph overview of the function:
Check username length
Starting from offset 0x401426, the function checks whether the username is 4 characters long.
.text:00401426 push hWnd ; hWnd
.text:0040142C call ds:GetWindowTextLengthA
.text:00401432 cmp eax, 4 ; len(my_username) = 4
.text:00401435 jnz loc_40174A
Check serial length
Starting from offset 0x40143B, the function then checks whether the serial is 20 characters long.
.text:0040143B push dword_402020 ; hWnd
.text:00401441 call ds:GetWindowTextLengthA
.text:00401447 cmp eax, 14h ; len(my_serial) = 20
.text:0040144A jnz loc_40174A
Check username format
Starting from offset 0x4014BA, the function starts by checking the length of the username, which should be 4 digits.
.text:004014BA loc_4014BA:
.text:004014BA mov eax, [ebp+counter_i]
.text:004014BD inc eax
.text:004014BE mov [ebp+counter_i], eax ; i+=1
.text:004014C1
.text:004014C1 loc_4014C1:
.text:004014C1 cmp [ebp+counter_i], 4 ;
.text:004014C1 ; while (i<=4)
.text:004014C5 jnb short loc_4014FC
.text:004014C7 mov eax, [ebp+counter_i]
.text:004014CA movsx eax, [ebp+eax+my_username]
.text:004014CF push eax ; eax = my_username[i]
.text:004014D0 call isdigit ; mem loc 0x402010
.text:004014D6 pop ecx
.text:004014D7 test eax, eax ; \ username should be
.text:004014D9 jnz short loc_4014FA ; / 4 digits
.text:004014DB push 30h ; uType
.text:004014DD push offset aKeygenmeV6 ; "KeygenMe V6"
.text:004014E2 push offset aPleaseEnterANu ; "Please enter a number as username!"
.text:004014E7 push hDlg ; hWnd
.text:004014ED call ds:MessageBoxA
.text:004014F3 xor eax, eax
.text:004014F5 jmp loc_401AA5
; ...[SNIP]...
.data:00402010 isdigit dd ?
.data:00402010
Build accumulator array
Starting from offset 0x4014FC, there is a loop that is executed 4 times and builds a 4 items array, based on the username.
.text:004014FC loc_4014FC:
.text:004014FC lea eax, [ebp+my_username]
.text:004014FF push eax
.text:00401500 call atoi
.text:00401506 pop ecx
.text:00401507 mov [ebp+my_username_as_int], eax
.text:0040150A and [ebp+counter_j], 0 ; j = 0
.text:0040150E jmp short loc_401517
.text:00401510 ; ---------------------------------------------------------------------------
.text:00401510
.text:00401510 loc_401510:
.text:00401510 mov eax, [ebp+counter_j]
.text:00401513 inc eax
.text:00401514 mov [ebp+counter_j], eax ; j += 1
.text:00401517
.text:00401517 loc_401517:
.text:00401517 cmp [ebp+counter_j], 4 ; while(j<=4)
.text:0040151B jge short loc_40153A
.text:0040151D mov eax, [ebp+counter_j]
.text:00401520 lea ecx, ds:10h[eax*8] ; ecx = i*8+0x10
.text:00401527 mov eax, [ebp+my_username_as_int] ; eax = my_username_as_int
.text:0040152A cdq
.text:0040152B idiv ecx ; eax = eax / (i*8 + 0x10)
.text:0040152D imul eax, [ebp+my_username_as_int] ; eax *= my_username_as_int
.text:00401531 mov ecx, [ebp+counter_j]
.text:00401534 mov [ebp+ecx*4+accumulator], eax ; accumulator[j] = eax
.text:00401538 jmp short loc_401510
Check serial
Main loop
Then, there is a loop that will be executed 20 times, for each character of the serial. Every 5 characters (when the remainder of the division of the loop counter by 5 is zero), the code will jump to 0x401564 to check the separator, excepted when the counter is equal to 0.
.text:0040153A loc_40153A:
.text:0040153A and [ebp+counter_k], 0 ; k = 0
.text:0040153E jmp short loc_401547
.text:00401540 ; ---------------------------------------------------------------------------
.text:00401540
;...[SNIP]...
.text:00401547 loc_401547:
.text:00401547 cmp [ebp+counter_k], 14h ;
.text:00401547 ; while (k <= 20)
.text:0040154B jnb loc_401716
.text:00401551 cmp [ebp+counter_k], 0
.text:00401555 jz short loc_401592 ; if k=0, goto loc_401592
.text:00401557 mov eax, [ebp+counter_k] ; if k != 0
.text:0040155A cdq
.text:0040155B push 5
.text:0040155D pop ecx ; ecx = 5
.text:0040155E idiv ecx ; eax = k / 5
.text:00401560 test edx, edx ; \ if k%5 != 0
.text:00401562 jnz short loc_401592 ; / goto loc_401592
Check separator character
Every 5 runs in the loop, the code will jump to check whether the probed character is '-' (0x2D).
.text:00401564 mov eax, [ebp+counter_k]
.text:00401567 movsx eax, [ebp+eax+my_serial] ; eax = my_serial[k]
.text:0040156C sub eax, '-' ; eax = my_serial[k] - 0x2D
.text:0040156F neg eax ; eax = -eax (CF = 1 if -eax != 0)
.text:00401571 sbb eax, eax ; eax = eax - (eax + CF)
.text:00401573 inc eax ; eax += 1
.text:00401574 mov [ebp+var_51], al
.text:00401577 movzx eax, [ebp+var_51]
.text:0040157B test eax, eax ; \ if serial[k] == '-'
.text:0040157D jnz short loc_401586 ; / goto loc_401586
.text:0040157F jmp loc_401716
.text:00401584 ; ---------------------------------------------------------------------------
.text:00401584 jmp short loc_40158D
.text:00401586 ; ---------------------------------------------------------------------------
.text:00401586
.text:00401586 loc_401586:
.text:00401586 mov eax, [ebp+counter_l]
.text:00401589 inc eax
.text:0040158A mov [ebp+counter_l], eax ; l += 1
Check other characters
You will certainly have noticed the following fork that splits the code into two branches that look very similar:
The code will jump to the left branch when the loop counter is even and to the right one when it is odd:
.text:00401592 loc_401592:
.text:00401592 mov eax, [ebp+counter_k] ; if k = 0 or k%5 != 0
.text:00401595 and eax, 80000001h ; eax = k & 0x80000001
.text:00401595 ; (trick to %2)
.text:0040159A jns short loc_4015A1 ; if k%2 = 0, goto loc_4015A1
.text:0040159C dec eax
.text:0040159D or eax, 0FFFFFFFEh
.text:004015A0 inc eax
.text:004015A1
.text:004015A1 loc_4015A1:
.text:004015A1 test eax, eax ;
.text:004015A1 ; \ if k%2 != 0
.text:004015A3 jnz loc_401661 ; / goto loc_401661
The 2 branches are almost identical, with only 1 difference:
Hence, for the sake of simplicity, we will only analyze one of these 2 branches.
Below is the code of the left branch:
.text:004015A9 mov eax, [ebp+counter_k] ; eax = k
.text:004015AC cdq
.text:004015AD and edx, 3 ; edx = 0
.text:004015B0 add eax, edx ; eax = k
.text:004015B2 sar eax, 2 ; eax = k >> 2
.text:004015B5 mov [ebp+index], eax
.text:004015B8 mov eax, [ebp+index]
.text:004015BB inc eax ; eax = (k >> 2) + 1
.text:004015BC mov [ebp+index], eax ; index = (k >> 2) + 1
.text:004015BF cmp [ebp+index], 3 ; \ if (k >> 2) + 1 < 3
.text:004015C3 jle short loc_4015CE ; / goto loc_4015CE
.text:004015C5 mov eax, [ebp+index]
.text:004015C8 sub eax, 4
.text:004015CB mov [ebp+index], eax ; index -= 4
.text:004015CE
.text:004015CE loc_4015CE:
.text:004015CE mov eax, [ebp+index]
.text:004015D1 mov eax, [ebp+eax*4+accumulator] ; eax = accumulator[index]
.text:004015D5 mov [ebp+accumulator_i], eax
.text:004015D8 mov ecx, [ebp+counter_k]
.text:004015DB inc ecx ; ecx = k + 1
.text:004015DC mov eax, [ebp+accumulator_i] ; eax = accumulator[index]
.text:004015DF cdq
.text:004015E0 idiv ecx ; eax = accumulator[index] / (k+1)
.text:004015E2 mov [ebp+accumulator_i], edx
.text:004015E5 mov eax, [ebp+accumulator_i] ; eax = accumulator[index] % (k+1)
.text:004015E8 shl eax, 1
.text:004015EA mov [ebp+accumulator_i], eax ; accumulator_i = (accumulator[index] % (k+1)) * 2
.text:004015ED inc eax ; \
.text:004015EE inc eax ; |
.text:004015EF inc eax ; |
.text:004015F0 dec eax ; | do nothing
.text:004015F1 dec eax ; |
.text:004015F2 dec eax ; /
.text:004015F3 cmp [ebp+accumulator_i], 9 ; \ if (accumulator[index] % (k+1)) * 2 > 9
.text:004015F7 jg short loc_401602 ; / goto loc_401602
.text:004015F9 mov eax, [ebp+accumulator_i]
.text:004015FC add eax, 4 ; \ accumulator_i += 4 (branch on the left: when k is even)
.text:004015FF mov [ebp+accumulator_i], eax ; / accumulator_i += 3 (branch on the right: when k is odd)
.text:00401602
.text:00401602 loc_401602:
.text:00401602 cmp [ebp+accumulator_i], 24h ; \ if accumulator_i < 0x24
.text:00401606 jl short loc_401611 ; / goto loc_401611
.text:00401608 mov eax, [ebp+accumulator_i]
.text:0040160B sub eax, 0Ah
.text:0040160E mov [ebp+accumulator_i], eax ; accumulator_i -= 0x0A
.text:00401611
.text:00401611 loc_401611:
.text:00401611 cmp [ebp+accumulator_i], 0 ; \ if accumulator_i < 0
.text:00401615 jl short loc_40161D ; / goto loc_40161D
.text:00401617 cmp [ebp+accumulator_i], 23h ; \ if accumulator_i <= 0x23
.text:0040161B jle short loc_401626 ; / goto loc_401626
.text:0040161D
.text:0040161D loc_40161D:
.text:0040161D mov eax, [ebp+counter_k]
.text:00401620 add eax, 7
.text:00401623 mov [ebp+accumulator_i], eax ; accumulator_i = k+7
.text:00401626
.text:00401626 loc_401626:
.text:00401626 inc eax ;
.text:00401626 ; \
.text:00401627 inc eax ; |
.text:00401628 inc eax ; |
.text:00401629 dec eax ; | do nothing
.text:0040162A dec eax ; |
.text:0040162B dec eax ; /
This code computes an index at offset 0x4015B5 that will be used to read a value from the array (accumulator) previously built. Then, it performs some tests and computations that could be sum up as follows:
index = (k >> 2) + 1
if index > 3:
index -= 4
accumulator_i = (accumulator[index] % (k+1)) * 2
if accumulator_i <= 9:
# code of the 2 branches is similar expect here
# 4 is added to accumulator_i when the loop counter is even
# 3 is added to accumulator_i when the loop counter is odd
accumulator_i += [4, 3][k%2]
if accumulator_i >= 0x24:
accumulator_i -= 0xA
if accumulator_i < 0 or accumulator_i > 0x23:
accumulator_i = k + 7
Once accumulator_i has been computed, the same instruction combination (sub, neg, sbb, inc) as detailed previously is used to compare the probed character of the serial with the one expected (accumulator_i)
.text:0040162C mov eax, [ebp+counter_k]
.text:0040162F movsx eax, [ebp+eax+my_serial] ; eax = my_serial[i]
.text:00401634 mov ecx, [ebp+accumulator_i] ; ecx = accumulator_i
.text:00401637 movsx ecx, [ebp+ecx+a0123456789abcd] ; ecx = a0123456789abcd[accumuator_i]
.text:0040163C sub eax, ecx ; \
.text:0040163E neg eax ; |
.text:00401640 sbb eax, eax ; | trick to simply
.text:00401642 inc eax ; | compare chars
.text:00401643 mov [ebp+var_51], al ; |
.text:00401646 movzx eax, [ebp+var_51] ; /
.text:0040164A test eax, eax ; my_serial[i] = a0123456789abcd[accumuator_i]?
.text:0040164C jnz short loc_401655
Success message
Once all characters of the serial have been verified, the code jumps to 0x401716. The function that displayes the sucess message is called at offset 0x401727:
.text:00401716 loc_401716:
.text:00401716 push ss
.text:00401717 pop ss
.text:00401718 mov eax, 5
.text:0040171D xor edx, edx
.text:0040171F movzx eax, [ebp+var_51]
.text:00401723 test eax, eax
.text:00401725 jz short loc_40174A
.text:00401727 mov eax, offset f_success ; success message
.text:0040172C add eax, 15h
.text:0040172F movzx ecx, [ebp+var_51]
.text:00401733 sub eax, ecx
.text:00401735 sub eax, [ebp+counter_l]
.text:00401738 mov [ebp+var_84], eax
.text:0040173E push offset loc_40174A
.text:00401743 push [ebp+var_84]
.text:00401749 retn
Below is the code of the function that displays the success message:
.text:0040134E f_success proc near
.text:0040134E push esi
.text:0040134F push offset String ; "KeygenMe V5 - Full"
.text:00401354 push hDlg ; hWnd
.text:0040135A call ds:SetWindowTextA
.text:00401360 push offset aThankYouForReg ; "Thank you for registering"
.text:00401365 push 68h ; nIDDlgItem
.text:00401367 push hDlg ; hDlg
.text:0040136D call ds:SetDlgItemTextA
.text:00401373 mov esi, ds:EnableWindow
.text:00401379 push 0 ; bEnable
.text:0040137B push hWnd ; hWnd
.text:00401381 call esi ; EnableWindow
.text:00401383 push 0 ; bEnable
.text:00401385 push dword_402020 ; hWnd
.text:0040138B call esi ; EnableWindow
.text:0040138D push 0 ; bEnable
.text:0040138F push dword_40201C ; hWnd
.text:00401395 call esi ; EnableWindow
.text:00401397 push 40h ; uType
.text:00401399 push offset Caption ; "Success"
.text:0040139E push offset Text ; "Thank you for registering!"
.text:004013A3 push hDlg ; hWnd
.text:004013A9 call ds:MessageBoxA
.text:004013AF pop esi
.text:004013B0 retn
.text:004013B0 f_success endp
Solution
Keygen
Below is my keygen:
#!/usr/bin/env python
import sys
def make_serial(user):
accumulator = []
a0123456789abcd = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
my_username = int(user)
serial = []
for i in range(4):
accumulator.append(my_username / (i*8+0x10) * my_username)
for k in range(20):
if k == 0 or k%5 != 0:
index = (k >> 2) + 1
if index > 3:
index -= 4
accumulator_i = (accumulator[index] % (k+1)) * 2
if accumulator_i <= 9:
accumulator_i += [4, 3][k%2]
if accumulator_i >= 0x24:
accumulator_i -= 0xA
if accumulator_i < 0 or accumulator_i > 0x23:
accumulator_i = k + 7
serial.append(a0123456789abcd[accumulator_i])
else:
serial.append('-')
return ''.join([i for i in serial])
if __name__ == '__main__':
if len(sys.argv) < 2:
print "Usage: %s <4_digits_username>" % sys.argv[0]
sys.exit()
print make_serial(sys.argv[1])
Some valid solutions
And here is it in action:
Keygen | Validation |
---|---|
$ ./keygen.py 1234 43478-CBA3-363G-4CCS |
|
$ ./keygen.py 8462 4383A-83G7-7ABC-A7QB |
Comments
Keywords: crackme reverse-engineering maxxor keygen