Solution-andrewl-us-Crackme-1
Introduction
Description
This crackme available here is relatively easy and is an excellent crackme for beginners.
When run, the crackme shows a dialog box with 2 fields: Name and Serial. If the serial is incorrect, a popup with the message "NOPE" is displayed.
Code overview
As shown below, the cross references from the start function show a call to the DialogFunc function, which then checks for the serial.
Let's analyze DialogFunc.
Code analysis
DialogFunc
Branches
The interesting part of this function is shown below:
.text:004011B2 push 20h ; cchMax
.text:004011B4 lea eax, [ebp+my_serial]
.text:004011B7 push eax ; lpString
.text:004011B8 push 69h ; nIDDlgItem
.text:004011BA mov ecx, [ebp+hWnd]
.text:004011BD push ecx ; hDlg
.text:004011BE call ds:GetDlgItemTextA
.text:004011C4 lea edx, [ebp+my_serial]
.text:004011C7 push edx
.text:004011C8 call f_check
.text:004011CD add esp, 4
.text:004011D0 test eax, eax
.text:004011D2 jz short loc_4011DD
.text:004011D4 mov [ebp+lpText], offset unk_403000
.text:004011DB jmp short loc_4011E4
.text:004011DD ; ---------------------------------------------------------------------------
.text:004011DD
.text:004011DD loc_4011DD:
.text:004011DD mov [ebp+lpText], offset unk_403008
.text:004011E4
.text:004011E4 loc_4011E4:
.text:004011E4 push 7 ; string length
.text:004011E6 mov eax, [ebp+lpText]
.text:004011E9 push eax ; encrypted string
.text:004011EA call sub_401000 ; decryption rountine
.text:004011EF add esp, 8
.text:004011F2 push 0 ; uType
.text:004011F4 mov ecx, [ebp+lpText]
.text:004011F7 push ecx ; lpCaption
.text:004011F8 mov edx, [ebp+lpText]
.text:004011FB push edx ; lpText
.text:004011FC push 0 ; hWnd
.text:004011FE call ds:MessageBoxA
.text:00401204 push 7
.text:00401206 mov eax, [ebp+lpText]
.text:00401209 push eax
.text:0040120A call sub_401000
.text:0040120F add esp, 8
.text:00401212 jmp short loc_40121B
Starting from offset 0x4011BE, the program gets the serial entered in the Serial field and saves it to edx. It is used as argument to the f_check function called at offset 0x4011C8. The return value is tested at offset 0x4011D0. If the function returns 0, the program jumps to 0x4011DD. If on the contrary, the value 1 is returned, the program continues to 0x4011D4. Depending on the branch, a different value is saved to lpText, the message that will be displayed at offset 0x4011FE via a call to MessageBoxA.
Decoding messages
Now, we see that both messages are crypted:
.data:00403000 unk_403000 db 28h ; (
.data:00403001 db 36h ; 6
.data:00403002 db 31h ; 1
.data:00403003 db 31h ; 1
.data:00403004 db 3Ah ; :
.data:00403005 db 2Dh ; -
.data:00403006 db 7Fh ;
.data:00403007 db 0
.data:00403008 unk_403008 db 31h ; 1
.data:00403009 db 30h ; 0
.data:0040300A db 2Fh ; /
.data:0040300B db 3Ah ; :
.data:0040300C db 7Fh ;
.data:0040300D db 7Fh ;
.data:0040300E db 7Fh ;
.data:0040300F db 0
As lpText is pushed to the stack just before a call to sub_401000 at offset 0x4011EA, we can guess that sub_401000 is the decryption routine and we rename the function accordingly.
The decryption routine is easy to understand. It simply XORs bytes of the encrypted string sent as parameter to the function, with 0x7F:
.text:00401000 decrypt_string proc near
.text:00401000
.text:00401000 encrypted_string= dword ptr 8
.text:00401000 len_string = byte ptr 0Ch
.text:00401000
.text:00401000 55 push ebp
.text:00401001 8B EC mov ebp, esp
.text:00401003
.text:00401003 loc_401003:
.text:00401003 0F BE 45 0C movsx eax, [ebp+len_string]
.text:00401007 8A 4D 0C mov cl, [ebp+len_string]
.text:0040100A 80 E9 01 sub cl, 1
.text:0040100D 88 4D 0C mov [ebp+len_string], cl
.text:00401010 85 C0 test eax, eax
.text:00401012 74 19 jz short loc_40102D
.text:00401014 8B 55 08 mov edx, [ebp+encrypted_string]
.text:00401017 0F BE 02 movsx eax, byte ptr [edx]
.text:0040101A 83 F0 7F xor eax, 7Fh ; encrypted_string[i] ^ 0x7F
.text:0040101D 8B 4D 08 mov ecx, [ebp+encrypted_string]
.text:00401020 88 01 mov [ecx], al
.text:00401022 8B 55 08 mov edx, [ebp+encrypted_string]
.text:00401025 83 C2 01 add edx, 1
.text:00401028 89 55 08 mov [ebp+encrypted_string], edx
.text:0040102B EB D6 jmp short loc_401003
.text:0040102D ; ---------------------------------------------------------------------------
.text:0040102D
.text:0040102D loc_40102D:
.text:0040102D 5D pop ebp
.text:0040102E C3 retn
.text:0040102E decrypt_string endp
We can write a python script and execute it in IDA-Pro to decrypt the strings:
def decrypt_string(loc):
i = 0
while True:
b = Byte(loc+i)
if b == 0x0:
break
PatchByte(loc+i, b ^ 0x7F)
i += 1
# Decrypt WINNER
decrypt(0x403000)
# Decrypt NOPE
decrypt(0x403008)
Once run (Alt+F7), the script decrypts the strings:
Now, we know that the f_check function at offset 0x4011C8 should return 1 to have a valid serial.
f_check()
Once again, we have to deal with encryption. As shown below, the program is building an hard-coded array starting from offset 0x4010A6.
.text:004010A0 f_check proc near
.text:004010A0
.text:004010A0 hardcoded_8272386= byte ptr -8
.text:004010A0 var_7 = byte ptr -7
.text:004010A0 var_6 = byte ptr -6
.text:004010A0 var_5 = byte ptr -5
.text:004010A0 var_4 = byte ptr -4
.text:004010A0 var_3 = byte ptr -3
.text:004010A0 var_2 = byte ptr -2
.text:004010A0 var_1 = byte ptr -1
.text:004010A0 my_serial = dword ptr 8
.text:004010A0
.text:004010A0 55 push ebp
.text:004010A1 8B EC mov ebp, esp
.text:004010A3 83 EC 08 sub esp, 8
.text:004010A6 C6 45 F8 47 mov [ebp+hardcoded_8272386], 47h
.text:004010AA C6 45 F9 4D mov [ebp+var_7], 4Dh
.text:004010AE C6 45 FA 48 mov [ebp+var_6], 48h
.text:004010B2 C6 45 FB 4D mov [ebp+var_5], 4Dh
.text:004010B6 C6 45 FC 4C mov [ebp+var_4], 4Ch
.text:004010BA C6 45 FD 47 mov [ebp+var_3], 47h
.text:004010BE C6 45 FE 49 mov [ebp+var_2], 49h
.text:004010C2 C6 45 FF 00 mov [ebp+var_1], 0
.text:004010C6 6A 07 push 7 ; len(encrypted_string)
.text:004010C8 8D 45 F8 lea eax, [ebp+hardcoded_8272386]
.text:004010CB 50 push eax
.text:004010CC E8 2F FF FF FF call decrypt_string ; decrypted content: '8272386'
.text:004010D1 83 C4 08 add esp, 8
.text:004010D4 8B 4D 08 mov ecx, [ebp+my_serial] ; ecx = my_serial
.text:004010D7 51 push ecx
.text:004010D8 8D 55 F8 lea edx, [ebp+hardcoded_8272386]
.text:004010DB 52 push edx
.text:004010DC E8 4F FF FF FF call f_make_serial
.text:004010E1 83 C4 08 add esp, 8
.text:004010E4 85 C0 test eax, eax ; \ f_make_serial() should return 1
.text:004010E6 74 07 jz short FAIL ; / to win
.text:004010E8 B8 01 00 00 00 mov eax, 1
.text:004010ED EB 10 jmp short loc_4010FF
.text:004010EF ; ---------------------------------------------------------------------------
.text:004010EF
.text:004010EF FAIL:
.text:004010EF 6A 07 push 7
.text:004010F1 8D 45 F8 lea eax, [ebp+hardcoded_8272386]
.text:004010F4 50 push eax
.text:004010F5 E8 06 FF FF FF call decrypt_string
.text:004010FA 83 C4 08 add esp, 8
.text:004010FD 33 C0 xor eax, eax
.text:004010FF
.text:004010FF loc_4010FF:
.text:004010FF 8B E5 mov esp, ebp
.text:00401101 5D pop ebp
.text:00401102 C3 retn
.text:00401102 f_check endp
Here again, we can see that the decryption routine is the same as previously (bytes XOR'ed with 0xF7). Let's use the following python script to decode the hard-coded string and display it as a comment at location 0x4010CC:
# Decrypt var_8
loc = 0x4010A6
loc_comm = 0x4010CC
MakeComm(loc_comm, ''.join([chr(Byte(loc+(i+1)*4-1) ^ 0x7F) for i in range(7)]))
We can see that the hard-coded string ('8272386') and the serial entered by the user are provided as arguments to the f_make_serial function at offset 0x4010DC which should return 1 to win. Let's analyze this function.
f_make_serial()
This function is a bit longer but very easy to understand. We immediately notice a loop that will end when the trailing null character of the hard-coded string will be met (offset 0x40103A). Inside the loop, here is what happens:
- bytes of the serial are read with an index and the current character is saved to edx at offset 0x40104B
- bytes of the hard-coded values are read with an index and the current character is saved to ecx at offset 0x401051
- ecx and edx are compared at offset 0x401066. If they are the same, the loop continues with the next bytes but otherwise, the loop exits prematurely (fail)
- Both the serial and the hard-coded value are shifted 1 byte so that the first byte correspond to the current position of the index. At the end of the loop, they will be completely overwritten with the trailing null character
.text:00401030 f_make_serial proc near
.text:00401030
.text:00401030 var_4 = dword ptr -4
.text:00401030 hardcoded_8272386= dword ptr 8
.text:00401030 my_serial = dword ptr 0Ch
.text:00401030
.text:00401030 55 push ebp
.text:00401031 8B EC mov ebp, esp
.text:00401033 51 push ecx
.text:00401034
.text:00401034 loop:
.text:00401034 8B 45 08 mov eax, [ebp+hardcoded_8272386]
.text:00401037 0F BE 08 movsx ecx, byte ptr [eax] ; ecx = hardcoded_8272386[i]
.text:0040103A 85 C9 test ecx, ecx ; loop 7 times until all chars are read
.text:0040103C 74 2E jz short exit_loop
.text:0040103E 8B 55 0C mov edx, [ebp+my_serial]
.text:00401041 0F BE 02 movsx eax, byte ptr [edx] ; eax = my_serial[i]
.text:00401044 85 C0 test eax, eax
.text:00401046 74 24 jz short exit_loop
.text:00401048 8B 4D 0C mov ecx, [ebp+my_serial]
.text:0040104B 0F BE 11 movsx edx, byte ptr [ecx] ; edx = my_serial[i]
.text:0040104E 8B 45 08 mov eax, [ebp+hardcoded_8272386]
.text:00401051 0F BE 08 movsx ecx, byte ptr [eax] ; ecx = hardcoded_8272386[i]
.text:00401054 8B 45 0C mov eax, [ebp+my_serial] ; eax = my_serial
.text:00401057 83 C0 01 add eax, 1
.text:0040105A 89 45 0C mov [ebp+my_serial], eax ; my_serial = my_serial[i+1]
.text:0040105D 8B 45 08 mov eax, [ebp+hardcoded_8272386] ; eax = hardcoded_8272386
.text:00401060 83 C0 01 add eax, 1
.text:00401063 89 45 08 mov [ebp+hardcoded_8272386], eax ; hardcoded_8272386 = hardcoded_8272386[i+1]
.text:00401066 3B CA cmp ecx, edx ; \ if hardcoded_8272386[i] != my_serial[i]
.text:00401068 75 02 jnz short exit_loop ; / exit_loop (FAIL)
.text:0040106A EB C8 jmp short loop
.text:0040106C ; ---------------------------------------------------------------------------
.text:0040106C
.text:0040106C exit_loop:
.text:0040106C 8B 4D 08 mov ecx, [ebp+hardcoded_8272386]
.text:0040106F 0F BE 11 movsx edx, byte ptr [ecx]
.text:00401072 85 D2 test edx, edx ; hardcoded_8272386 = 0?
.text:00401074 75 13 jnz short FAIL
.text:00401076 8B 45 0C mov eax, [ebp+my_serial]
.text:00401079 0F BE 08 movsx ecx, byte ptr [eax]
.text:0040107C 85 C9 test ecx, ecx
.text:0040107E 75 09 jnz short FAIL
.text:00401080 C7 45 FC 01 00 00 00 mov [ebp+var_4], 1
.text:00401087 EB 07 jmp short loc_401090
.text:00401089 ; ---------------------------------------------------------------------------
.text:00401089
.text:00401089 FAIL:
.text:00401089 C7 45 FC 00 00 00 00 mov [ebp+var_4], 0
.text:00401090
.text:00401090 loc_401090:
.text:00401090 8B 45 FC mov eax, [ebp+var_4]
.text:00401093 8B E5 mov esp, ebp
.text:00401095 5D pop ebp
.text:00401096 C3 retn
.text:00401096 f_make_serial endp
Starting from offset 0x40106C, the program checks that the initial hardcoded value is overwritten with the null character (which should be the case if the loop hasn't exited prematurely) and the same test is performed against the serial, starting from offset 0x401076.
Conclusion and solution
As a conclusion, the program is only checking that the serial is 8272386 and doesn't care of the username. Let's check:
Comments
Keywords: assembly x86 reverse-engineering crackme andrewl.us crackmes.de