Solution-DaXXoR-101-KeygenMe-3
Introduction
Description
In this crackme available here, it is requested to patch the nag screen and write a keygen.
When run, the crackme shows a nag screen and then a form containing 2 fields (probably respectively for the username and the serial) as well as a Check button as follows:
Code overview
The code overview from the start function is as follows:
Code analysis
sub_401031
We can see from the start function that it calls sub_401031. Let's analyze this function.
.text:00401031 ; int __stdcall sub_401031(HINSTANCE hInstance, int, int, int)
.text:00401031 sub_401031 proc near
.text:00401031
.text:00401031 hWnd = dword ptr -50h
.text:00401031 Msg = tagMSG ptr -4Ch
.text:00401031 var_30 = WNDCLASSEXA ptr -30h
.text:00401031 hInstance = dword ptr 8
.text:00401031
.text:00401031 55 push ebp
.text:00401032 8B EC mov ebp, esp
.text:00401034 83 C4 B0 add esp, 0FFFFFFB0h
.text:00401037 E8 3B 04 00 00 call f_patch_memory
.text:0040103C
.text:0040103C loc_40103C:
.text:0040103C 6A 10 push 10h ; uType
.text:0040103C ; (patch: NOP)
.text:0040103E
.text:0040103E loc_40103E:
.text:0040103E 68 35 30 40 00 push offset Caption ;
.text:0040103E ; 'Nag'
.text:0040103E ; (patch: NOP)
.text:00401043
.text:00401043 loc_401043:
.text:00401043 68 21 30 40 00 push offset Text ;
.text:00401043 ; 'Patch Me if you can'
.text:00401043 ; (patch: NOP)
.text:00401048
.text:00401048 loc_401048:
.text:00401048 6A 00 push 0 ;
.text:00401048 ; hWnd
.text:00401048 ; (patch: NOP)
.text:0040104A
.text:0040104A loc_40104A:
.text:0040104A E8 33 06 00 00 call MessageBoxA ;
.text:0040104A ; (patch: NOP)
.text:0040104F C7 45 D0 30 00 00 00 mov [ebp+var_30.cbSize], 30h
.text:00401056 C7 45 D4 03 00 00 00 mov [ebp+var_30.style], 3
.text:0040105D C7 45 D8 3E 11 40 00 mov [ebp+var_30.lpfnWndProc], offset sub_40113E
.text:00401064 C7 45 DC 00 00 00 00 mov [ebp+var_30.cbClsExtra], 0
.text:0040106B C7 45 E0 00 00 00 00 mov [ebp+var_30.cbWndExtra], 0
.text:00401072 FF 35 40 30 40 00 push dword_403040
.text:00401078 8F 45 E4 pop [ebp+var_30.hInstance]
.text:0040107B 33 C0 xor eax, eax
.text:0040107D B4 F4 mov ah, 0F4h
.text:0040107F C1 E0 08 shl eax, 8
.text:00401082 B4 D9 mov ah, 0D9h
.text:00401084 B0 BC mov al, 0BCh
.text:00401086 50 push eax ; color
.text:00401087 E8 56 06 00 00 call CreateSolidBrush
.text:0040108C 89 45 F0 mov [ebp+var_30.hbrBackground], eax
.text:0040108F C7 45 F4 00 00 00 00 mov [ebp+var_30.lpszMenuName], 0
.text:00401096 C7 45 F8 00 30 40 00 mov [ebp+var_30.lpszClassName], offset ClassName ; "Window"
.text:0040109D 68 F4 01 00 00 push 1F4h ; lpIconName
.text:004010A2 FF 75 08 push [ebp+hInstance] ; hInstance
.text:004010A5 E8 D2 05 00 00 call LoadIconA
.text:004010AA 89 45 E8 mov [ebp+var_30.hIcon], eax
.text:004010AD 89 45 FC mov [ebp+var_30.hIconSm], eax
.text:004010B0 68 03 7F 00 00 push 7F03h ; lpCursorName
.text:004010B5 6A 00 push 0 ; hInstance
.text:004010B7 E8 BA 05 00 00 call LoadCursorA
.text:004010BC 89 45 EC mov [ebp+var_30.hCursor], eax
.text:004010BF 8D 45 D0 lea eax, [ebp+var_30]
.text:004010C2 50 push eax ; WNDCLASSEXA *
.text:004010C3 E8 C6 05 00 00 call RegisterClassExA
.text:004010C8 6A 00 push 0 ; lpParam
.text:004010CA FF 75 08 push [ebp+hInstance] ; hInstance
.text:004010CD 6A 00 push 0 ; hMenu
.text:004010CF 6A 00 push 0 ; hWndParent
.text:004010D1 68 96 00 00 00 push 96h ; nHeight
.text:004010D6 68 2C 01 00 00 push 12Ch ; nWidth
.text:004010DB 68 00 00 00 80 push 80000000h ; Y
.text:004010E0 68 00 00 00 80 push 80000000h ; X
.text:004010E5 68 00 00 CF 00 push 0CF0000h ; dwStyle
.text:004010EA 68 07 30 40 00 push offset WindowName ; "Crackme"
.text:004010EF 68 00 30 40 00 push offset ClassName ; "Window"
.text:004010F4 6A 00 push 0 ; dwExStyle
.text:004010F6 E8 57 05 00 00 call CreateWindowExA
.text:004010FB 89 45 B0 mov [ebp+hWnd], eax
.text:004010FE 6A 01 push 1 ; nCmdShow
.text:00401100 FF 75 B0 push [ebp+hWnd] ; hWnd
.text:00401103 E8 8C 05 00 00 call ShowWindow
.text:00401108 FF 75 B0 push [ebp+hWnd] ; hWnd
.text:0040110B E8 90 05 00 00 call UpdateWindow
.text:00401110
.text:00401110 loc_401110:
.text:00401110 6A 00 push 0 ; wMsgFilterMax
.text:00401112 6A 00 push 0 ; wMsgFilterMin
.text:00401114 6A 00 push 0 ; hWnd
.text:00401116 8D 45 B4 lea eax, [ebp+Msg]
.text:00401119 50 push eax ; lpMsg
.text:0040111A E8 45 05 00 00 call GetMessageA
.text:0040111F 0B C0 or eax, eax
.text:00401121 74 14 jz short loc_401137
.text:00401123 8D 45 B4 lea eax, [ebp+Msg]
.text:00401126 50 push eax ; lpMsg
.text:00401127 E8 6E 05 00 00 call TranslateMessage
.text:0040112C 8D 45 B4 lea eax, [ebp+Msg]
.text:0040112F 50 push eax ; lpMsg
.text:00401130 E8 29 05 00 00 call DispatchMessageA
.text:00401135 EB D9 jmp short loc_401110
.text:00401137 ; ---------------------------------------------------------------------------
.text:00401137
.text:00401137 loc_401137:
.text:00401137 8B 45 BC mov eax, [ebp+Msg.wParam]
.text:0040113A C9 leave
.text:0040113B C2 10 00 retn 10h
.text:0040113B sub_401031 endp
As we can see, at offset 0x401037, the function starts by calling sub_401477 (renamed f_patch_memory), which we will analyze later. Then, starting from offset 0x40103C, it pushes the 4 arguments for the MessageBoxA function that is called at offset 0x40104A. This is actually the nag screen. At least, we can see that the sub_40113E function is used as the lpfnWndProc parameter (offset 0x40105D) for the CreateSolidBrush function called at offset 0x401087.
Now, let's analyze the mysterious function sub_401477.
sub_401477
Here is the code of the function:
.text:00401477 f_patch_memory proc near
.text:00401477 mov GetCurrentProcessId_0, offset GetCurrentProcessId
.text:00401481 sub GetCurrentProcessId_0, 4
.text:00401488 add OpenProcess_0, 4
.text:0040148F add OpenProcess_0, offset OpenProcess
.text:00401499 add GetCurrentProcessId_0, 4
.text:004014A0 mov WriteProcessMemory_0, offset loc_4016D0
.text:004014AA inc WriteProcessMemory_0
.text:004014B0 call GetCurrentProcessId_0
.text:004014B6 sub OpenProcess_0, 4
.text:004014BD push eax ; dwProcessId: handle to process
.text:004014BE push 0 ; bInheritHandle
.text:004014C0 push 1F0FFFh ; dwDesiredAccess: PROCESS_ALL_ACCESS
.text:004014C5 call OpenProcess_0
.text:004014CB mov hObject, eax ; hObject = handle to process
.text:004014D0 dec WriteProcessMemory_0
.text:004014D6 push dword_403CA0 ; *lpNumberOfBytesWritten
.text:004014DC push 2 ; nSize
.text:004014DE push offset push_10h ; lpBuffer
.text:004014E3 push offset loc_40103C ; lpBaseAddress
.text:004014E8 push hObject ; hProcess
.text:004014EE call WriteProcessMemory_0
.text:004014F4 mov dword_403CA0, 0
.text:004014FE push dword_403CA0 ; *lpNumberOfBytesWritten
.text:00401504 push 6 ; nSize
.text:00401506 push offset nag ; lpBuffer
.text:0040150B push offset loc_40103E ; lpBaseAddress
.text:00401510 push hObject ; hProcess
.text:00401516 call WriteProcessMemory_0
.text:0040151C mov dword_403CA0, 0
.text:00401526 push dword_403CA0 ; *lpNumberOfBytesWritten
.text:0040152C push 6 ; nSize
.text:0040152E push offset patch_me_if_you_can ; lpBuffer
.text:00401533 push offset loc_401043 ; lpBaseAddress
.text:00401538 push hObject ; hProcess
.text:0040153E call WriteProcessMemory_0
.text:00401544 mov dword_403CA0, 0
.text:0040154E push dword_403CA0 ; *lpNumberOfBytesWritten
.text:00401554 push 2 ; nSize
.text:00401556 push offset push_0 ; lpBuffer
.text:0040155B push offset loc_401048 ; lpBaseAddress
.text:00401560 push hObject ; hProcess
.text:00401566 call WriteProcessMemory_0
.text:0040156C mov dword_403CA0, 0
.text:00401576 push dword_403CA0 ; *lpNumberOfBytesWritten
.text:0040157C push 5 ; nSize
.text:0040157E push offset call_sub_4026CF ; lpBuffer
.text:00401583 push offset loc_40104A ; lpBaseAddress
.text:00401588 mov my_serial_field, offset ReadProcessMemory
.text:00401592 push hObject ; hProcess
.text:00401598 call WriteProcessMemory_0
.text:0040159E sub my_serial_field, 2
.text:004015A5 push hObject ; hObject
.text:004015AB call CloseHandle
.text:004015B0 xor eax, eax
.text:004015B2 retn
.text:004015B2 f_patch_memory endp
We immediately notice calls to GetCurrentProcessId, OpenProcess and WriteProcessMemory, which make us think of memory injection, which is likely to trigger alerts from the antivirus. Now, an inspection of the lpBuffer and lpBaseAddress shows that this function is simply overwriting the content of the nag screen (arguments pushed to the stack and call to the MessageBoxA function). This trick ensures that even if the nag screen has been removed by NOP'ing the instructions, it will overwrite it in memory. Clever, but not enough to be enough for us :) Let's keep that in mind when we will patch the program.
sub_40113E
Code overview
The function layout is as follows:
Check username length
Starting from offset 0x401264, the code checks that the username is 5 to 7 characters long:
.text:00401264 mov lstrlenA_0, offset lstrlenA
.text:0040126E dec lstrlenA_0
.text:00401274 mov GetWindowTextLengthA_0, offset GetWindowTextLengthA
.text:0040127E inc GetWindowTextLengthA_0
.text:00401284 push my_user_field
.text:0040128A dec GetWindowTextLengthA_0
.text:00401290 call GetWindowTextLengthA_0
.text:00401296 cmp eax, 5 ; len(my_username) >= 5
.text:00401299 jl exit_func
.text:0040129F cmp eax, 8 ; len(my_username) < 8
.text:004012A2 jge exit_func
Serial 2nd part
Starting from offset 0x4012A8, the code is building the 2nd part of the serial, based on the username:
.text:004012A8 mov ecx, eax ; ecx = len(my_username)
.text:004012AA mov len_my_username, ecx
.text:004012B0 xor ebx, ebx ; ebx = 0
.text:004012B2 lea esi, my_name
.text:004012B8 lea edi, expected_serial_2
.text:004012BE
.text:004012BE loc_4012BE:
.text:004012BE mov al, [ecx+esi-1] ; my_username[i]
.text:004012BE ; with i starting from the end of string
.text:004012C2 cmp cl, 4
.text:004012C5 jz short loc_4012DA
.text:004012C7 cmp al, 4Dh ; 'M'
.text:004012C9 jl short loc_4012CF
.text:004012CB sub al, 11h
.text:004012CD jmp short loc_4012D1
.text:004012CF ; ---------------------------------------------------------------------------
.text:004012CF
.text:004012CF loc_4012CF:
.text:004012CF add al, 15h
.text:004012D1
.text:004012D1 loc_4012D1:
.text:004012D1 xor al, cl ; AL ^= cl
.text:004012D3 xor al, 2 ; AL ^= 2
.text:004012D5 mov [ebx+edi], al
.text:004012D8 jmp short loc_4012DE
.text:004012DA ; ---------------------------------------------------------------------------
.text:004012DA
.text:004012DA loc_4012DA:
.text:004012DA mov byte ptr [ebx+edi], 2Dh
.text:004012DE
.text:004012DE loc_4012DE:
.text:004012DE inc ebx
.text:004012DF loopne loc_4012BE ; ecx -= 1
The algorithm is simple. It saves the username length in ecx at offset 0x4012A8 and decrements it each time the loop ends (instruction loopne at offset 0x4012DF). It reads the username in reverse order and performs the following operations for each character:
- if ecx is equal to 4, the current character for the expected 2nd part of the serial will be '-' (0x2D in hex).
- Else (ecx != 4):
- If the current character of the username is lower than M (0x4D in hex), it adds 0x15 to the character
- Else, it substracts 0x11 from the character
- It then XORs the result with CL (remember how ECX evolves in the loop)
- It XORs the result with 2
This results in an array saved at memory location 0x403254 which is the second part of the expected serial number.
Serial 1st part
Starting from offset 0x4012E1, the first part of the expected serial is computed, using exactly the same technique as for the 2nd part, but this time using the result of the previous computation (second part of the serial) instead of the username.
.text:004012E1 lea esi, expected_serial_2 ; 0x403254 - this time, the algo will process
; the expected serial part 2 instead of username
.text:004012E7 lea edi, expected_serial_1
.text:004012ED mov ecx, len_my_username
.text:004012F3 xor ebx, ebx
.text:004012F5 mov dword_403C5C, offset lstrcatA
.text:004012FF sub dword_403C5C, 2
.text:00401306 mov smiley, offset asc_40207F ; ":-)"
.text:00401310 mov Correct_Serial, offset aCorrectSerial ; "Correct Serial"
.text:0040131A sub smiley, 5
.text:00401321 sub Correct_Serial, 5
.text:00401328
.text:00401328 loc_401328:
.text:00401328 cmp cl, 4
.text:0040132B jz short loc_401344
.text:0040132D mov al, [ecx+esi-1]
.text:00401331 cmp al, 4Dh ; 'M'
.text:00401333 jl short loc_401339
.text:00401335 sub al, 11h
.text:00401337 jmp short loc_40133B
.text:00401339 ; ---------------------------------------------------------------------------
.text:00401339
.text:00401339 loc_401339:
.text:00401339 add al, 15h
.text:0040133B
.text:0040133B loc_40133B:
.text:0040133B xor al, cl
.text:0040133D xor al, 2
.text:0040133F mov [ebx+edi], al
.text:00401342 jmp short loc_401348
.text:00401344 ; ---------------------------------------------------------------------------
.text:00401344
.text:00401344 loc_401344:
.text:00401344 mov byte ptr [ebx+edi], 2Dh
.text:00401348
.text:00401348 loc_401348:
.text:00401348 inc ebx
.text:00401349 loopne loc_401328 ; ecx -= 1
Final checks
There are 2 final checks:
- the code calls lstrlen at offset 0x40138E to get the size of my_serial, pushed to the stack at offset 0x401389. It then compares the value against the expected serial length at offset 0x401394 and exits if the values don't match.
- it checks that the provided serial matches the expected one (performed by sub_401450, renamed f_check_serial in the below extract.
.text:0040134B mov MessageBoxA_0, offset MessageBoxA
.text:00401355 sub MessageBoxA_0, 7
.text:0040135C push offset expected_serial_2 ; _DWORD
.text:00401361 push offset expected_serial_1 ; _DWORD
.text:00401366 add dword_403C5C, 2
.text:0040136D call dword_403C5C
.text:00401373 push offset expected_serial_1 ; _DWORD
.text:00401378 inc lstrlenA_0
.text:0040137E call lstrlenA_0
.text:00401384 mov expected_serial_length, eax
.text:00401389 push offset my_serial ; _DWORD
.text:0040138E call lstrlenA_0 ; eax = len(my_serial)
.text:00401394 cmp eax, expected_serial_length ; \ if len(my_serial) != len(expected_serial)
.text:0040139A jnz short exit_func ; / exit
.text:0040139C push len_my_username
.text:004013A2 push offset expected_serial_1 ; \
.text:004013A7 push offset my_serial ; | check that my_serial = expected_serial
.text:004013AC call f_check_serial ; / sub_401450
.text:004013B1 cmp eax, 1 ; \
.text:004013B4 jz short exit_func ; / function should return 0 to win
If both tests pass, the success popup is displayed:
.text:00401306 mov smiley, offset asc_40207F ; ":-)"
.text:00401310 mov Correct_Serial, offset aCorrectSerial ; "Correct Serial"
; ... [SNIP] ...
.text:004013B6 add Correct_Serial, 5
.text:004013BD add smiley, 5
.text:004013C4 push 40h ; _DWORD
.text:004013C6 push smiley ; _DWORD
.text:004013CC push Correct_Serial ; _DWORD
.text:004013D2 push [ebp+hWndParent] ; _DWORD
.text:004013D5 add MessageBoxA_0, 7
.text:004013DC call MessageBoxA_0
Integrity check
At the end of the function, there is a call to sub_401408 (renamed f_integrity_check).
.text:004013E2 exit_func:
.text:004013E2
.text:004013E2 call f_integrity_ckeck ; should be NOP'ed
.text:004013E7 leave
.text:004013E8 retn 10h
Without going in the details, sub_401408 calls another function, sub_4015B3, at offset 0401449, that performs an integrity check. At the end of the function, we can see that the code is exited if the byte at memory location 0x403C94 is not equal to 0xC0:
Let's also keep that in mind when we will patch the code.
Solution
Patching
Now that we have completely analyzed the code and have identified the parts that need to be patched, we can NOP the following instructions:
.text:00401037 E8 3B 04 00 00 call f_patch_memory ; patch with NOP (0x90)
.text:0040103C 6A 10 push 10h ; patch with NOP (0x90)
.text:0040103E 68 35 30 40 00 push offset Caption ; patch with NOP (0x90)
.text:00401043 68 21 30 40 00 push offset Text ; patch with NOP (0x90)
.text:00401048 6A 00 push 0 ; patch with NOP (0x90)
.text:0040104A E8 33 06 00 00 call MessageBoxA ; patch with NOP (0x90)
; ... [SNIP] ...
.text:00401449 E8 65 01 00 00 call sub_4015B3 ; patch with NOP (0x90)
You can use hiew to patch the code.
Keygen and solution
Below is the keygen I wrote:
#!/usr/bin/env python
import sys
def make_serial_part(s):
serial2 = []
len_user = len(s)
for c, l in enumerate(s[::-1]):
if int(len_user - c) == 4:
serial2.append(0x2D)
else:
if ord(l) < 0x4D:
l_ = ord(l) + 0x15
else:
l_ = ord(l) - 0x11
serial2.append(l_ ^ int(len_user-c) ^ 2)
return ''.join([chr(i) for i in serial2])
def make_serial(my_username):
serial_part2 = make_serial_part(my_username)
return "%s%s" % (make_serial_part(serial_part2), serial_part2)
if __name__ == '__main__':
if len(sys.argv) < 2:
print "Usage: %s <username>" % sys.argv[0]
sys.exit(1)
my_username = sys.argv[1]
if len(my_username) < 5 or len(my_username) >= 8:
print "Username should be 5 to 7 characters long"
sys.exit(2)
print make_serial(my_username)
Let's check:
$ ./my_keygen.py aldeid FM-CNEW_-R[S
Comments
Keywords: assembly x86 reverse-engineering crackme DaXXoR_101 crackmes.de