Crackmes.de – Matteo KeygenMe by Matteo
- May 19, 2015
- reverse engineering
- crackmes
- no comments
- Part 1: Anti-Debugging Measures
- Trick 1: TLS Callbacks
- Trick 2: Dynamically Added TLS Callbacks
- Trick 3: Anti-Disassembly with Fake Jumps and Garbage Bytes
- Trick 4: Trap Flag
- Trick 5: Push/Ret-Jumps
- Trick 6: Self-Modifying Code
- Trick 7: PEB->BeingDebugged
- Trick 8: NtGlobalFlag
- Trick 9: Software Breakpoint Detection
- Trick 10: SEH - Triggered with Single Step Exception
- Trick 11: Exception's Context Structure - Safe Place
- Trick 11: Exception's Context Structure - Hardware Registers
- Trick 12: Checksum
- Trick 13: Correct Exception Record -> C3
- Trick 14: Check Software Breakpoint at Popular API Calls
- Trick 15: Thread Hiding
- Trick 16: Patch Vectored Exception Handlers
- Trick 17: Timing Check
- Part 2: Key Validation
- The Keyfile
- Valid Second Line
- The Third Line
- Determining Valid Seeds
- The Keygen
- Example Keyfile
The crackme Matteo KeygenMe by Matteo has been published February 24, 2015. It is rated at 4 - Needs special knowledge. The crackme is written in Assembler and runs on Windows.
The crackme has two major parts. The first part is all about trying to stop you from getting to the relevant code by throwing a handful of anti-debugging and anti-disassembly techniques at you. The second part then validates the key file that you provide.
I used IDA Pro and WinDBG to solve the crackme. No anti-debugging scripts have been used — all tricks have been manually defused.
Part 1: Anti-Debugging Measures
Trick 1: TLS Callbacks
The crackme uses TLS callbacks. Those are invoked before the main entry point:
.data:0040400C TlsCallbacks_ptr dd offset TlsCallbacks
.data:00404010 TlsSizeOfZeroFill dd 0
.data:00404014 TlsCharacteristics dd 0
.data:00404018 TlsIndex db 0
.data:00404019 db 0
.data:0040401A db 0
.data:0040401B db 0
.data:0040401C TlsCallbacks dd offset TlsCallback_0
.data:0040401C
.data:00404020 dword_404020 dd 0
.data:00404024 dword_404024 dd 0
Make sure you have set a breakpoint at the TlsCallback_0
offset, or that the debugger is set to stop at TLS callbacks.
Trick 2: Dynamically Added TLS Callbacks
All the first TLS callback does is create another TLS callback:
00401EC7 TlsCallback_0 proc near
00401EC7 mov dword_404020, offset loc_401E76
00401ED1 retn
00401ED1 TlsCallback_0 endp
Add a breakpoint at loc_401E76
and run there.
Trick 3: Anti-Disassembly with Fake Jumps and Garbage Bytes
The second TLS callback routine at first probably looks like that:
00401E76 push (offset loc_401E7F+1)
00401E7B stc
00401E7C jnb short loc_401E7F
00401E7E retn
00401E7F ; -------------------------------------------------------------------------
00401E7F
00401E7F loc_401E7F:
00401E7F
00401E7F jmp dword ptr [esi-74h]
00401E82 ; -------------------------------------------------------------------------
00401E82 shl byte ptr [esi-72h], 1
00401E85 shl byte ptr [esi-64h], 1
The above code uses a fake conditional jump (the jump is always taken), a PUSH/POP-jump (the RETN instruction actually jumps to loc_401E7F+1), and garbage bytes. This causes the wrong disassembly above. Once you step into the code with debugger, IDA should show the correct disassembly. You can also manually fix the disassembly:
00401E76 push offset loc_401E80
00401E7B stc
00401E7C jnb short near ptr byte_401E7F
00401E7E retn
00401E7E ; -------------------------------------------------------------------------
00401E7F byte_401E7F db 0FFh
00401E80 ; -------------------------------------------------------------------------
00401E80
00401E80 loc_401E80:
00401E80 mov ax, ss
00401E83 mov ss, ax
Trick 4: Trap Flag
Next come theses instructions:
00401E80 mov ax, ss
00401E83 mov ss, ax
00401E86 pushfw
00401E88 test byte ptr [esp+1], 1
00401E8D jnz short loc_401E9C
00401E8F popfw
The second MOV-instruction writes SS, this causes the processor to lock all interrupts until after the following instruction. The PUSHFW instruction then pushes the flags on the stack, and [esp+1]
accesses the trap flag. Since interrupts, including INT 1, are locked until after PUSHFW, the debugger has no chance to unset the flag.
Either don’t single step over the pushfw instruction, or manually set the zero flag at the jump. The routine sets a third callback routine.
Trick 5: Push/Ret-Jumps
Because of the anti-disassembly techniques before, the third callback routine is not yet marked as code:
00401DBB db 68h ; h
00401DBC db 0C5h ; +
00401DBD db 1Dh
00401DBE db 40h ; @
00401DBF db 0
00401DC0 db 0F8h ; °
00401DC1 db 72h ; r
00401DC2 db 1
Hitting C
in IDA produces the following disassembly:
00401DBB ; -----------------------------
00401DBB push offset off_401DC5
00401DC0 clc
00401DC1 jb short near ptr unk_401DC4
00401DC3 retn
00401DC3 ; -----------------------------
This snippet uses the PUSH/RET-jump. Pushing the offset and then returning is almost equivalent to a regular jump, but they might prevent the disassembler from properly detecting the function boundaries.
Trick 6: Self-Modifying Code
The following instructions follow:
00401DCF loc_401DCF:
00401DCF mov decrypted, 1
00401DD6 mov eax, 771881844 ; key
00401DDB mov ecx, offset start_of_ciphertext
00401DE0 mov edx, offset end_of_ciphertext
00401DE5
00401DE5 decryption_loop:
00401DE5 xor [ecx], al
00401DE7 ror eax, 1
00401DE9 inc ecx
00401DEA cmp ecx, edx
00401DEC jnz short decryption_loop
00401DEC ; ------------------------------------------------------------------
00401DEE start_of_ciphertext db 23h
00401DEF db 0DEh ; ¦
00401DF0 db 56h ; V
00401DF1 db 7Bh ; {
00401DF2 db 87h ; ç
00401DF3 db 0DBh ; ¦
After the decryption the cipertext turns into meaningful code:
00401DEE start_of_ciphertext
00401DEE push edi
00401DEF mov edx, large fs:30h
...
The crackme uses similar snippets in many places. Make sure to not set memory breakpoint inside the modified section. Memory breakpoints affect the memory, and those changes will get encrypted or decrypted too, leading to corrupt code.
Trick 7: PEB->BeingDebugged
The code then checks the BeingDebugged
-flag of the PEB (equivalent to calling IsDebuggerPresent
, but more sneaky):
00401DEE push edi
00401DEF mov edx, large fs:30h
00401DF6 mov al, [edx+2]
00401DF9 test al, al
00401DFB jnz short being_debugged
Manually patch the flag, or set the zero flag at the jump.
Trick 8: NtGlobalFlag
With edx
still pointing to the PEB, the crackme also checks the NtGlobalFlag
field. The field has three flags that indicate the presence of a debugger:
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)
The proper way to check all three flags would therefore be with bitmask 0x70. The crackme is cruder and just checks if the entire NtGlobalFlag
field is zero:
00401DFD cmp dword ptr [edx+68h], 0
00401E01 jnz short being_debugged
Again patch the field or manually set the zero flag.
Trick 9: Software Breakpoint Detection
The entry point of any executable is an obvious place to put software breakpoints (opcode 0xCC). The next lines search 0x26 bytes of the memory starting at the entry point for 0xCC:
00401E04 mov edi, offset start
00401E09 mov ecx, 26h
00401E0E mov al, 0CCh
00401E10 repne scasb
00401E12 jz short being_debugged
Remove software breakpoints before the check, or manually unset the zero flag.
Trick 10: SEH - Triggered with Single Step Exception
After three more self modifying loops, we end here:
00401D01 push 8E4C9A90h
00401D06 push 838042730
...
00401D13 mov edx, large fs:18h
00401D1A mov ecx, [edx]
00401D1C xchg ebx, ecx
00401D1E pop eax
00401D1F add [esp], eax
00401D22 push ebx
00401D23 mov ebx, esp
00401D25 xchg ebx, ecx
00401D27 mov [edx], ecx
00401D29 pushfw
00401D2B or byte ptr [esp+1], 1
00401D30 popfw
00401D32 mov eax, offset off_404041
00401D37 mov byte ptr [eax], 0D4h
The offset fs:18h points to the Structured Exception Handler (SEH) on the stack. The code adds another handler with address 8E4C9A90h + 31F3846Ah = 0x401efa (mod 2**32). The exception is triggered by setting the trap flag (pushfw, or, popfw). Set a breakpoint at the new SEH handler. Then make sure you pass the single step exception 0x80000004 to the application. Debuggers often consume this exception (it is intended for debuggers after all).
Trick 11: Exception’s Context Structure - Safe Place
The exception handler at 0x401EFA first decrypts the following block:
00401F12 start_of_ciphertext_0:
00401F12 mov eax, [esp+0Ch]
00401F16 mov dword ptr [eax+0B8h], offset loc_401D3A
The third argument to the exception handler, passed in [eax+0Ch]
, is the context structure of the exception:
struct _CONTEXT
....
+B8 EIP; # register
...
The field 0xB8 is set to 0x401D37, or the EIP we would return to after the exception handler. By overwriting the register to 0x401D3A, the crackme changes the flow to a different address. Take note of the address to later set a breakpoint there when returning from the SEH.
Trick 11: Exception’s Context Structure - Hardware Registers
The crackme then also checks four other offsets into the context structure:
00401F23 test [eax+4], edx
00401F26 jnz being_debugged_0
00401F2C test [eax+8], edx
00401F2F jnz being_debugged_0
00401F35 test [eax+0Ch], edx
00401F38 jnz being_debugged_0
00401F3E test [eax+10h], edx
00401F41 jnz being_debugged_0
00401F47 push esi
00401F48 xor edx, edx
Those four values are the debug registers:
struct _CONTEXT
...
+04 Dr0;
+08 Dr1;
+0C Dr2;
+10 Dr3;
...
They indicate if the hardware breakpoints are set. Disable all hardware breakpoints, or manually unset the zero flag at each jump.
Trick 12: Checksum
The following disassembly calculates a checksum of 0x332 bytes from the entry point of the crackme:
00401F4A mov esi, offset start
00401F4F mov ecx, 332h
00401F54 cld
00401F55
00401F55 loc_401F55:
00401F55 lodsd
00401F56 add edx, eax
00401F58 rol edx, 1
00401F5A dec ecx
00401F5B jnz short loc_401F55
00401F5D pop esi
00401F5E cmp edx, checksum
00401F64 jz short checksum_is_good
00401F66 mov eax, [esp+0Ch]
00401F6A mov dword ptr [eax+0B8h], offset loc_401D32
00401F74 jmp being_debugged_0
00401F79 ; ------------------------------------------------------------------
If the checksum does not match a hardcoded value, we are busted. The checksum method detects software breakpoints as well any kind of patches. Remove breakpoints and patches, or manually set the zero flag at 401f64.
Trick 13: Correct Exception Record -> C3
The first parameter of the exception handler points to an EXCEPTION_RECORD
:
struct _EXCEPTION_RECORD
+00: ExceptionCode DWORD
+04: ExceptionFlags DWORD
...
The next instructions of the crackme read the exception code:
00401F79 checksum_is_good:
00401F79 mov edx, [esp+4]
00401F7D mov edx, [edx]
We know it was a single step exception that triggered the exception, see Trick 10. This exception has the code 0x80000004:
#define STATUS_SINGLE_STEP ((DWORD) 0x80000004)
By shifting that right 5 bytes and adding 0x33h, we get the value 0xC3
00401F7F rol edx, 5 ; ---> 80
00401F82 add edx, 33h ; ---> C3
The value 0xC3 is stored at 2 bytes into 0x404041
00401F85 mov eax, offset byte_404041
00401F8A mov [eax+2], dl
So the data becomes:
.data:00404041 dword_404041 dd 16C381B9h
The effect of the change will be relevant at the start of the entry point. Make sure that C3 is written, and manually correct the byte if your debugger changed the exception code.
Trick 14: Check Software Breakpoint at Popular API Calls
The crackme then checks if there is a software breakpoint at the jump table entry for GetProcessAddress:
00401F8D mov edx, offset get_process_address
00401F92 cmp byte ptr [edx], 0CCh
00401F95 jz being_debugged_1
The get_process_address address disassembles to:
00402162 get_process_address:
00402162 ; 004020C5p ...
00402162 jmp ds:GetProcAddress
The crackme also checks the actual offset of GetProcAddress by extracting the offset from the jmp
instruction:
00401F9B mov edx, [edx+2]
00401F9E mov edx, [edx]
00401FA0 cmp byte ptr [edx], 0CCh
00401FA3 jz being_debugged_1
The crackme repeats the same two checks for GetModuleHandleA and GetModuleHandleW. Disable plugins that add breakpoints at these API, or just unset the zero flag.
Trick 15: Thread Hiding
The crackme dynamically loads the NtSetInformationThread function:
0040207A apis_clean:
0040207A push offset aNtdll_dll ; "NtDll.dll"
0040207F call get_module_handle_w
00402084 test eax, eax
00402086 jz loc_40211A
0040208C push offset aNtsetinformationth ; "NtSetInformationThread"
00402091 push eax
00402092 call get_process_address
00402097 test eax, eax
00402099 jz short loc_40211A
The crackme also checks if there is a breakpoint at the beginning of the routine – hinting a potential anti-anti measure:
0040209B mov edx, eax
0040209D cmp byte ptr [edx], 0CCh
004020A0 jz short being_debugged_1
Next, NtSetInformationThread is called with 0x11 as the second parameter:
004020A2 push 0
004020A4 push 0
004020A6 push 11h
004020A8 push 0FFFFFFFEh
004020AA call eax
0x11 stands for HideThreadFromDebugger, which will causes the debugger to no longer receive any events. To skip the call in WinDbg you can use the following commands:
>r @eip = @eip + 2
>r @esp = @esp + 0x10
Trick 16: Patch Vectored Exception Handlers
The following disassembly follows. It patches the beginning of the API function AddVectoredExceptionHandler with the 5 bytes at return_null:
004020AC push offset aKernel32_dll_0 ; "Kernel32.dll"
004020B1 call get_module_handle_w
004020B6 push ebx
004020B7 push esi
004020B8 push edi
004020B9 mov ebx, eax
004020BB test eax, eax
004020BD jz short loc_402117
004020BF push offset aWriteprocessmemory ; "WriteProcessMemory"
004020C4 push ebx
004020C5 call get_process_address
004020CA test eax, eax
004020CC jz short loc_402117
004020CE mov edi, eax
004020D0 cmp byte ptr [edi], 0CCh
004020D3 jz short being_debugged_1
004020D5 push offset aAddvectoredexcepti ; "AddVectoredExceptionHandler"
004020DA push ebx
004020DB call get_process_address
004020E0 test eax, eax
004020E2 jz short loc_402117
004020E4 push 0
004020E6 push 5
004020E8 push offset return_null
004020ED push eax
004020EE push 0FFFFFFFFh
004020F0 call edi
The stub return_null is:
00402075 return_null:
00402075
00402075 33 C0 xor eax, eax
00402077 C2 08 00 retn 8
So crackme replaces AddVectoredExceptionHandler with return 0. It does the same with AddVectoredContinueHandler. I had to ask the crackme author what the purpose of patching the VEH is. According to Matteo, they prevent VEH-based debugger from working, for example Cheat Engine.
After Trick 16, we leave the exception handler and return to the address that was set in Trick 11. First, the handler is removed:
00401D3A loc_401D3A:
00401D3A 64 8B 15 18 00 00 00 mov edx, large fs:18h
00401D41 8F 02 pop dword ptr [edx]
00401D43 44 inc esp
00401D44 83 C4 08 add esp, 8
00401D47 83 EC 05 sub esp, 5
Next, we again calculate a checksum for the first bytes of the entry point:
00401D4A 33 D2 xor edx, edx
00401D4C 33 C0 xor eax, eax
00401D4E 56 push esi
00401D4F 57 push edi
00401D50 BE 00 10 40 00 mov esi, offset start
00401D55 B9 26 00 00 00 mov ecx, 26h
00401D5A FC cld
00401D5B
00401D5B loc_401D5B:
00401D5B AC lodsb
00401D5C 03 D0 add edx, eax
00401D5E D1 C2 rol edx, 1
00401D60 49 dec ecx
00401D61 75 F8 jnz short loc_401D5B
00401D63 81 FA 2B C6 16 5D cmp edx, offset checksum2
00401D69 74 17 jz short checksum_is_good2
This does the same as Trick 12. After that, we finally are done with the TlsCallbacks and enter the entry point of the crackme.
Trick 17: Timing Check
The first step of the entry point is to decrypt the content at 401026. The decryption key is stored at 0x404041, this is where Trick 13 made the modification based on the exception record:
00401000 public start
00401000 start:
00401000 mov esi, offset loc_404041
00401005 mov ecx, 0CA5h
0040100A mov edx, offset loc_401026
0040100F cld
00401010
00401010 loc_401010:
00401010 lodsb
00401011 cmp esi, offset loc_404047
00401017 jl short loc_40101E
00401019 mov esi, offset loc_404041
0040101E
0040101E loc_40101E:
0040101E dec ecx
0040101F xor [ecx+edx], al
00401022 test ecx, ecx
00401024 jnz short loc_401010
After some irrelevant code we get to these lines:
004010B4 add esp, 4
004010B7 mov time_in_secs, eax
004010BC call ds:GetTickCount
004010C2 xor edx, edx
004010C4 mov ecx, 3E8h
004010C9 div ecx
004010CB mov tickcount_in_secs, eax
Here we store the current time in seconds (retrieved by an earlier call to time
, not shown), and the tick count in seconds. These values become relevant further down in the crackme, here:
0040153D cmp tickcount_in_secs, 0
00401544 jl short loc_40154F
00401546 cmp tickcount_in_secs, 4
0040154D jle short good ; At most 4 seconds passed
and here:
00401567 cmp time_in_secs, 0
0040156E jl short bad
00401570 cmp time_in_secs, 4
00401577 jg short bad
These two blocks check if at most 4 seconds passed according to time
or the tick count. If either one is true, we are fine. If on the other hand a debugger causes a greater delay than four seconds, then later the crackme jumps over setting a flag at offset 004015BC:
004015B9 loc_4015B9:
004015B9 jmp short loc_4015BD
004015B9 ; ----------------------------------------------------------------
004015BB unk_4015BB db 0EBh ; d
004015BC ; ----------------------------------------------------------------
004015BC
004015BC timing_ok:
004015BC ; 004015A9j
004015BC inc ecx
004015BD
004015BD loc_4015BD:
004015BD ; loc_4015B9j
004015BD pop edx
004015BE and ecx, edx
These are all the anti-debugging checks that I found. Part 2 shows how the key validation works.
Part 2: Key Validation
The Keyfile
The registration information is stored in a file called TheKey.k
in the same directory as the crackme:
004010E0 push 0
004010E2 push 80h
004010E7 push 3
004010E9 push 0
004010EB push 0
004010ED push 80000000h
004010F2 push offset aThekey_k ; "TheKey.k"
004010F7 call ds:CreateFile
004010FD xor dword ptr ds:aThekey_k, offset unk_218F6F18
00401107 xor dword ptr ds:aThekey_k+4, offset unk_218F6F18
00401111 mov fileHandle, eax
00401116 test eax, eax
00401118 jz fail
0040111E inc eax
0040111F test eax, eax
00401121 jz fail
00401127 push 0
00401129 push offset ContentLength
0040112E push 40h
00401130 push offset keyContent
00401135 push fileHandle
0040113B call ds:ReadFile
00401141 push fileHandle
00401147 call ds:CloseHandle
The keyfile needs to have 3 lines. The first two lines need to be terminated with 0xD
(carriage return). The last line must not have a line terminator. The following disassembly determines the length of the three lines, and store pointer to each line:
0040115A mov pKeyContent, offset keyContent
00401164 push 0Dh
00401166 push 15h
00401168 push offset keyContent
0040116D call line_length
00401172 cmp eax, 0FFFFFFFFh
00401175 jz fail
0040117B mov line1_len, eax
00401180 mov edx, offset keyContent
00401185 add edx, eax
00401187 mov byte ptr [edx], 0
0040118A inc edx
0040118B inc edx
0040118C mov dword ptr pLine2, edx
00401192 sub ecx, eax
00401194 push 0Dh
00401196 push 15h
00401198 push edx
00401199 call line_length
0040119E cmp eax, 0FFFFFFFFh
004011A1 jz fail
004011A7 mov line2_len, eax
004011AC mov edx, offset keyContent
004011B1 add edx, eax
004011B3 add edx, line1_len
004011B9 add edx, 2
004011BC mov byte ptr [edx], 0
004011BF add edx, 2
004011C2 mov pLine3, edx
004011C8 push 15h
004011CA push edx
004011CB call line_length_f
004011D0 cmp eax, 0FFFFFFFFh
004011D3 jz fail
004011D9 mov line3_len, eax
The content of the keyfile needs to be alphanumeric, i.e., only contain letters and digits:
004011EC mov esi, pKeyContent
004011F2
004011F2 loc_4011F2:
004011F2 cmp ebx, 0
004011F5 jz short loc_401205
004011F7 mov al, [esi]
004011F9 push eax
004011FA call is_alpha_numeric
004011FF and edi, eax
00401201 inc esi
00401202 dec ebx
00401203 jmp short loc_4011F2
00401205 ; ---------------------------------------------------------------
00401205
00401205 loc_401205:
00401205 mov ebx, line2_len
0040120B mov esi, dword ptr pLine2
00401211
00401211 loc_401211:
00401211 cmp ebx, 0
00401214 jz short loc_401224
00401216 mov al, [esi]
00401218 push eax
00401219 call is_alpha_numeric
0040121E and edi, eax
00401220 inc esi
00401221 dec ebx
00401222 jmp short loc_401211
00401224 ; ---------------------------------------------------------------
00401224
00401224 loc_401224:
00401224 mov ebx, line3_len
0040122A mov esi, pLine3
00401230
00401230 loc_401230:
00401230 cmp ebx, 0
00401233 jz short loc_401243
00401235 mov al, [esi]
00401237 push eax
00401238 call is_alpha_numeric
0040123D and edi, eax
0040123F inc esi
00401240 dec ebx
00401241 jmp short loc_401230
00401243 ; ---------------------------------------------------------------
00401243
00401243 loc_401243:
00401243 mov eax, edi
Finally, there is an obfuscated check to see if the first and second line of the key have the same length:
004012EF mov ecx, line2_len
004012F5 mov unpredictable, eax
004012FA add eax, 93E8h
004012FF sub eax, unpredictable
00401305 add ecx, eax
00401307 mov edx, line1_len
0040130D sub ecx, edx
0040130F xor ecx, 75382
00401315 cmp ecx, 112030
0040131B jnz fail
The above check boils down to:
(line2len - line1len + 0x93e8) XOR 75382 = 112030
(line2len - line1len + 0x93e8) = 0x93e8
line1len = line2len
Valid Second Line
The crackme applies a series of transformations to the first line:
00401267 mov ecx, line1_len
0040126D cmp ecx, 3
00401270 jb fail
00401276 xor ecx, 5Ch
In pseudo-code:
line1_len = len(line1)
line1[0] ^= line1_len ^ 0x5c
Then we XOR characters from the end with characters at the start:
00401283 mov ecx, line1_len
00401289 add edx, ecx
0040128B dec edx
0040128C
0040128C loc_40128C:
0040128C mov cl, [edx]
0040128E xor [eax], cl
00401290 inc eax
00401291 dec edx
00401292 cmp eax, edx
00401294 jl short loc_40128C
In pseudo-code:
i = 1
j = line1_len-1
while i < j:
line1[i] ^= line1[j]
i += 1
j -= 1
A similar routine follows:
00401353 mov ecx, line1_len
00401359 dec ecx
0040135A shr ecx, 1
0040135C mov eax, pLine1
00401361 mov edx, eax
00401363 add edx, ecx
00401365 inc edx
00401366 mov al, [eax]
00401368
00401368 loc_401368:
00401368 xor [edx], al
0040136A inc al
0040136C inc edx
0040136D cmp byte ptr [edx], 0
00401370 jnz short loc_401368
It does:
i = (line1_len-1)//2 + 1
c = line1[0]
for i in range(i, line1_len):
line1[i] ^= c
c += 1
Finally, the bytes in the line are made alphanumeric by calling:
00401378 push pLine1
0040137E call make_alphanumeric
The routine make_alphanumeric is:
0040166A ; =============== S U B R O U T I N E =============================
0040166A
0040166A ; Attributes: bp-based frame
0040166A
0040166A make_alphanumeric proc near
0040166A
0040166A data = dword ptr 8
0040166A length = dword ptr 0Ch
0040166A
0040166A push ebp
0040166B mov ebp, esp
0040166D push edi
0040166E mov edi, [ebp+data]
00401671
00401671 loc_401671:
00401671 mov cl, 25h
00401673 cmp [ebp+length], 0
00401677 jz short loc_4016A9
00401679
00401679 loc_401679:
00401679 cmp byte ptr [edi], '9'
0040167C jg short loc_401685
0040167E cmp byte ptr [edi], '0'
00401681 jl short loc_401685
00401683 jmp short loc_4016A3
00401685 ; -----------------------------------------------------------------
00401685
00401685 loc_401685:
00401685 ; make_alphanumeric+17j
00401685 cmp byte ptr [edi], 'Z'
00401688 jg short loc_401691
0040168A cmp byte ptr [edi], 'A'
0040168D jl short loc_401691
0040168F jmp short loc_4016A3
00401691 ; -----------------------------------------------------------------
00401691
00401691 loc_401691:
00401691 ; make_alphanumeric+23j
00401691 cmp byte ptr [edi], 'z'
00401694 jg short loc_40169D
00401696 cmp byte ptr [edi], 'a'
00401699 jl short loc_40169D
0040169B jmp short loc_4016A3
0040169D ; -----------------------------------------------------------------
0040169D
0040169D loc_40169D:
0040169D ; make_alphanumeric+2Fj
0040169D add [edi], cl
0040169F inc cl
004016A1 jmp short loc_401679
004016A3 ; -----------------------------------------------------------------
004016A3
004016A3 loc_4016A3:
004016A3 ; make_alphanumeric+25j ...
004016A3 inc edi
004016A4 dec [ebp+length]
004016A7 jmp short loc_401671
004016A9 ; -----------------------------------------------------------------
004016A9
004016A9 loc_4016A9:
004016A9 pop edi
004016AA leave
004016AB retn 8
004016AB make_alphanumeric endp
004016AB
This routine decompiles to:
def make_alphanumeric(chars):
for i in range(len(chars)):
c = 37
while not chr(chars[i]).isalnum():
chars[i] += c
c += 1
chars[i] &= 0xFF
c &= 0xFF
return chars
The crackme then XORs the result with unpredictable data:
00401384 mov edi, pLine1
0040138A mov edx, unpredictable
00401390 mov ecx, line1_len
00401396 add ecx, edi
00401398
00401398 loc_401398:
00401398 xor [edi], dl
0040139A inc edi
0040139B rol edx, 8
0040139E cmp edi, ecx
004013A0 jnz short loc_401398
The crackme also XORs the second line with the same key. It then compares the transformed first and second line:
00401398 loc_401398:
00401398 xor [edi], dl
0040139A inc edi
0040139B rol edx, 8
0040139E cmp edi, ecx
004013A0 jnz short loc_401398
004013A2 push esi
004013A3 mov esi, pLine1
004013A9 mov edi, dword ptr pLine2
004013AF mov ecx, line1_len
004013B5 cld
004013B6 repe cmpsb
004013B8 pop esi
004013B9 pop edi
004013BA jnz fail
The XOR encryption of the first and second line with the same key can be omitted, such that we have the following relationship between first and second line:
line1 = "phildunphy"
line1_len = len(line1)
line1_codes = [ord(c) for c in line1]
line1_codes[0] ^= line1_len ^ 0x5c
i = 1
j = line1_len-1
while i < j:
line1_codes[i] ^= line1_codes[j]
i += 1
j -= 1
i = (line1_len-1)//2 + 1
c = line1_codes[0]
for i in range(i, line1_len):
line1_codes[i] ^= c
c += 1
x = make_alphanumeric(line1_codes)
line2 = ''.join([chr(xx) for xx in x])
# >>> second line is: K6LAUSIXAS
The Third Line
The third line is the trickiest. The crackme first generates two seeds based on the third and first line. The first seed is determined as follows:
004013C4 mov edx, pLine3
004013CA mov eax, [edx]
004013CC add eax, [edx+4]
004013CF mov ecx, [edx+8]
004013D2 rol eax, cl
004013D4 mov ecx, [edx+0Ch]
004013D7 ror eax, cl
004013D9 xor eax, [edx+10h]
004013DC mov edx, dword ptr line1_copy
004013E2 add edx, dword ptr line1_copy+4
004013E8 mov ecx, dword ptr line1_copy+8
004013EE rol edx, cl
004013F0 mov ecx, dword ptr line1_copy+0Ch
004013F6 ror edx, cl
004013F8 xor edx, dword ptr line1_copy+10h
004013FE xor edx, eax
00401400 mov seed1, edx
This boils down to the following pseudo-code:
def hash1(line):
eax = get_int_from_string(line[:4])
eax += get_int_from_string(line[4:8])
ecx = get_int_from_string(line[8:12])
eax = rol(eax, ecx & 0xFF)
ecx = get_int_from_string(line[12:16])
eax = ror(eax, ecx & 0xFF)
ecx = get_int_from_string(line[16:20])
eax ^= ecx
return eax
eax = hash1(line3)
edx = hash1(line1)
eax ^= edx
seed1 = eax
With get_int_from_string converting a string into the little endian integer:
def get_str_from_int(val):
s = ""
for i in range(4):
s += chr(val & 0xFF)
val >>= 8
return s
The second seed is calculated as follows:
00401406 mov eax, pLine3
0040140B add eax, 9
0040140E mov eax, [eax]
00401410 rol eax, 8
00401413 xor al, al
00401415 ror eax, 8
00401418 mov seed2, eax
Which is:
s = get_int_from_str(line3[9:13])
The seeds are then used to build a 16 bytes hash:
00401422 mov eax, seed1
00401427 push eax
00401428 call ds:srand
0040142E add esp, 4
00401431 jmp short loc_401442
00401433 ; ----------------------------------------------------------------
00401433
00401433 loc_401433:
00401433 mov eax, seed2
00401438 push eax
00401439 call ds:srand
0040143F add esp, 4
00401442
00401442 loc_401442:
00401442 mov esi, 4
00401447
00401447 loc_401447:
00401447 ; 00401473j
00401447 call ds:rand
0040144D push 4
0040144F push eax
00401450 call modulus
00401455 mov edi, eax
00401457 cmp word ptr hash[ebx+edi*4], 0
00401460 jnz short loc_401447
00401462 call ds:rand
00401468 add word ptr hash[ebx+edi*4], ax
00401470 dec esi
00401471 test esi, esi
00401473 jnz short loc_401447
00401475 call ds:rand
0040147B xor ecx, ecx
0040147D
0040147D loc_40147D:
0040147D xor word ptr hash[ecx*4], ax
00401485 inc ecx
00401486 cmp ecx, 4
00401489 jnz short loc_40147D
0040148B test ebx, ebx
0040148D jnz short loc_40149C
0040148F mov ebx, 2
00401494 xor seed2, eax
0040149A jmp short loc_401433
0040149C ; ----------------------------------------------------------------
0040149C
0040149C loc_40149C:
0040149C call ds:rand
004014A2 mov ecx, eax
004014A4 rol eax, 10h
004014A7 mov ax, cx
004014AA xor ecx, ecx
004014AC
004014AC loc_4014AC:
004014AC xor dword ptr hash[ecx*4], eax
004014B3 inc ecx
004014B4 cmp ecx, 4
004014B7 jnz short loc_4014AC
The following C-code shows how the hash is calculated for given two seeds:
#include <stdio.h>
inline int rand(int *seed) {
*seed = *seed*0x343fd + 0x269EC3;
return ((*seed) >> 0x10) & 0x7FFF;
}
long int main (long int argc, char *argv[]) {
unsigned char hash[16];
unsigned int i;
unsigned int base = 0;
unsigned int seed1 = 0x0110469A;
unsigned int seed2 = 0x006C7972;
unsigned int seed = seed1;
unsigned int offset;
unsigned int tmp;
for(i = 0; i < 16; i++)
hash[i] = 0;
for(base = 0; base <= 2; base += 2) {
for(i = 0; i < 4; i++)
{
do
{
offset = rand(&seed) % 4;
} while ( hash[base + offset*4] != 0 || hash[base + offset*4 + 1] != 0 );
tmp = rand(&seed);
hash[base + offset*4 + 1] += (tmp >> 8);
hash[base + offset*4] += (tmp & 0xFF);
}
tmp = rand(&seed);
for(i = 0; i < 4; i++) {
hash[i*4 + 1] ^= (tmp >> 8);
hash[i*4] ^= tmp & 0xFF;
}
if(base == 0) {
seed2 ^= tmp;
seed = seed2;
}
}
tmp = rand(&seed);
tmp = (tmp<<16) + tmp;
for(base = 0; base < 4; base++) {
for(i = 0; i < 4; i++) {
hash[base*4 + i] ^= (tmp & 0xFF);
tmp = (tmp >> 8) | ((tmp & 0xFF) << 24);
}
}
printf("hash is:\n");
for(i = 0; i < 16; i++)
printf("%x ", hash[i]);
printf("\n");
}
The calculated hash is finally compared to a hardcoded value:
004014BB cmp dword ptr hash, 3C0E7DEBh
004014C5 jnz short loc_4014EC
004014C7 cmp dword ptr hash+4, 3AD11611h
004014D1 jnz short loc_4014EC
004014D3 cmp dword ptr hash+8, 0B070195h
004014DD jnz short loc_4014EC
004014DF cmp dword ptr hash+0Ch, 36263E26h
004014E9 jnz short loc_4014EC
004014EB inc ecx ; set correct flag
Determining Valid Seeds
The first step to crack the algorithm is two find seeds that lead to the correct hash. I did this by first brute forcing the seeds for the second round, see program brute_force2.c
. It should produce the following four seeds four the second round:
- 006f445a
- 406f445a
- 806f445a
- c06f445a
The starting seed of the second round is actually calulated like this:
seed2 ^= tmp;
seed = seed2;
where tmp is the last random number. All random numbers have at most 2 bytes. Also the seed2 is at most 0xFFFFFF. Therefore, only the first of the four seeds can actually be produced by the code. The value of the last random number call is also output by the program, the number is 0x210e and we need to XOR the seed with this value to get the actual calculated value in seed2.
The seed for the first round seed1 can be found similarly, I used the program brute_force1.c:
- 01a01234
- 41a01234
- 81a01234
- c1a01234
The seed 81a01234 did not work with the crackme, I don’t know why but all we need is one working seed anyways.
For the keygen we need a way to find lines that produce the desired hash. I used the following properties of the hashing to quickly find good lines:
The seed1 is calculated with a hash routine, that uses bytes 16 to 19 of the second line in an XOR operation. We get the correct seed by simply adjusting these four bytes. Some of the adjustments will lead to strings that are not alpha numeric. I therefore simply loop over random strings until one corrects to an alphanumeric string.
The seed2 is equivalent to the three bytes 9, 10, 11 of the second line. The seed2 is known to be 0x6f445a XOR 0x210e = 6F6554, or “Teo” as a little endian ASCII string.
The keygen first generates a random string of 20 characters. It then sets bytes 9, 10, and 11 to “Tor”. After that, it patches the last four bytes to match the first seed. If the result is not alphanumeric, it simply tries again.
The Keygen
The following Python code expects a name as the first and only argument. The name must be alphanumeric (so no spaces allowed). It then calculates the second line and third line. The result is writen to TheKey.k, which you need to copy to the crackme directory. The Crackme should show a message box with PERFECT as the text.
import random
import sys
def keygen(name):
def get_int_from_string(s):
val = 0
for x in s[::-1]:
val <<= 8
val += ord(x)
return val
def get_str_from_int(val):
s = ""
for i in range(4):
s += chr(val & 0xFF)
val >>= 8
return s
def rol(val, places):
shift = places % 32;
val = (val << shift) + (val >> (32-shift))
val &= 0xFFFFFFFF
return val
def ror(val, places):
shift = places % 32;
val = (val >> shift) + (val << (32-shift))
val &= 0xFFFFFFFF
return val
def make_alphanumeric(txt_codes):
for i in range(len(txt_codes)):
c = 37
while not chr(txt_codes[i]).isalnum():
txt_codes[i] += c
c += 1
txt_codes[i] &= 0xFF
c &= 0xFF
return txt_codes
def random_alphanumeric(l):
return ''.join(random.sample(map(chr, range(48, 57) + range(65, 90) +\
range(97, 122)), l))
def hash1(line):
eax = get_int_from_string(line[:4])
eax += get_int_from_string(line[4:8])
ecx = get_int_from_string(line[8:12])
eax = rol(eax, ecx & 0xFF)
ecx = get_int_from_string(line[12:16])
eax = ror(eax, ecx & 0xFF)
ecx = get_int_from_string(line[16:20])
eax ^= ecx
return eax
def find_line3_seed1(line1, line3):
#valid_seed1 = [0x1a01234, 0x41a01234, 0x81a01234, 0xc1a01234]
valid_seed1 = [0x1a01234, 0x41a01234, 0xc1a01234]
eax = hash1(line3)
edx = hash1(line1)
eax ^= edx
for s in valid_seed1:
diff = eax ^ s
ecx = get_int_from_string(line3[16:20])
ecx ^= diff
new_str = get_str_from_int(ecx)
if new_str.isalnum():
return line3[0:16] + new_str
return None
def find_line3_seed2():
line3 = random_alphanumeric(20)
valid_seed2 = 0x6f445a ^ 0x210e
s = get_str_from_int(valid_seed2)[:3]
line3 = line3[0:9] + s + line3[12:]
return line3
"""
check if name is alphanumeric, crackme won't accept other names
"""
if not name.isalnum():
print("The name must be alphanumeric")
return
if not 3 <= len(name) <= 20:
print("Name too long or too short")
return
"""
generate second line from name
"""
line1 = name
line1_len = len(line1)
line1_codes = [ord(c) for c in line1]
line1_codes[0] ^= line1_len ^ 0x5c
i = 1
j = line1_len-1
while i < j:
line1_codes[i] ^= line1_codes[j]
i += 1
j -= 1
i = (line1_len-1)//2 + 1
c = line1_codes[0]
for i in range(i, line1_len):
line1_codes[i] ^= c
c += 1
x = make_alphanumeric(line1_codes)
line2 = ''.join([chr(xx) for xx in x])
"""
semi brute force valid third line
"""
while True:
line3 = find_line3_seed2()
line3 = find_line3_seed1(line1, line3)
if line3:
break
with open("TheKey.k", "wb") as w:
w.write("{}\r\n".format(line1))
w.write("{}\r\n".format(line2))
w.write("{}".format(line3))
keygen(sys.argv[1])
Example Keyfile
The following example for name “phildunphy” should give the PERFECT message:
phildunphy
K6LAUSIXAS
vS2LeI0ylTeocCnkbqDS