Here’s my solution to a nice crackme, not so hard but enjoyable. The crackme rules are simple:
1. No patching!
2. A valid solution has a keygen and a tutorial!
Extra info: just a very simple algo with some anti-analysis tricks
The crackme is not packed/protected, I decided to start looking at the disasm output.
GetDlgItemTextA function is used to read both name and serial. Once the checks over name and serial are passed (strlen(name) > 4, strlen(serial) == 8) you’ll face the next snippet:
403ED0 nop
403ED1 mov ecx, dword_40ADB0
403ED7 call 4033A0
403EDC test eax, eax
403EDE jz short loc_403ED0
403EE0 mov ecx, dword_40ADB0
403EE6 cmp dword ptr [ecx+4], 0FFh
403EED mov esi, ecx
403EEF jnz short loc_403F19
...
Show congratulation box
...
403F16 retn 10h
403F19
...
Show error notification box
...
403F3E retn 10h
It’s obvious, first of all I have to understand what’s the content of the procedure at 0x4033A0 address is. The procedure’s code is really long but a trained eye understands immediately what’s behind everything. Why? Mainly because there’s a big switch, and there are a lot of snippets with common things like “esi+0C”, “esi+438”, “add ecx, value”. When I face this kind of algorithm I always start trying to verify if it’s an implementation of a Virtual Machine or not. I could be wrong but most of the time VM protections are easily identificable. To verify the correctness of my assumption is quite easy, a glance to the snippets used to implement VM instructions is enough.
(0x00: nop):
403A45 nop
403A46 add [esi+438h], ebx <-- update eip by 1
403A4C pop edi
403A4D pop esi
403A4E xor eax, eax
403A50 pop ebx
403A51 retn
This is the first snippet I checked, it only adds one to the dword pointed by esi+0x438. That’s a nop, almost every VM has a nop instruction.
Let’s take a look to another snippet:
(0x61: mov reg_i, dword_val):
4036F4 mov ecx, esi
4036F6 call loc_403360
4036FB test al, al
4036FD jz loc_40342F
403703 mov eax, [esi+438h] <-- eip
403709 movzx edx, byte ptr [eax+1] <-- register index
40370D mov eax, [eax+2] <-- dword value
403710 mov [esi+edx*4+0Ch], eax <-- save the value inside register
403714 add dword ptr [esi+438h], 6 <--- update eip
40371B pop edi
40371C pop esi
40371D xor eax, eax
40371F pop ebx
403720 retn
The snippet is used to move a dword value inside a register. The instruction has two parameters, the register index and the value to move.
As you can see it’s really easy to understand every single snippet with a possible VM implementation in mind.
The VM implemented inside the crackme is simple:
eip is stored at [esi+0x438]
flag is stored at [esi+0x42]
there are 8 dword registers stored from [esi+0xc] to [esi+0x28]
starting from [esi+0x2c] there’s a private memory buffer
It’s all quite easy except the memory buffer that needs some more details. The memory buffer used by the VM contains data that are not stored in a sequential way. What does it mean? Think of a memory buffer like a dword sequence where each bytes inside a dword has a specific meaning:
1° byte: is used to store the username’s chars
2° byte: is used to store the right_serial’s chars
3° byte: is used to store the fake serial’s chars
4° byte: you can live without knowing its meaning
So, an hypotetical piece of memory could be:
[esi+0x2c]: 5A 59 31 00 61 69 32 00 69 59 33 00 ...
where you can clearly see part of the username (“Zai”), part of the right serial (“YiY”) and part of the fake serial (“123”).
How does the crackme read a specific byte from the username, or the serial? The memory is addressed by a dword at [esi+0x42c], each byte of the dword is used as an index position of a specific byte of the username/right serial/fake serial.
i.e.: [esi+0x42c] = 02 00 01 00 means that the next operation over the:
– username will be done with the 1° byte of the 3° dword of the memory buffer (char ‘i’ of “Zai”)
– right serial will be done with the 2° byte of the 1° dword of the memory buffer (char ‘Y’)
– serial will be done with the 3° byte of the 2° dword of the memory buffer (char ‘2’)
Having said that, here is a description of the 0x62 VM opcode used to move a byte from memory buffer to a register.
mov reg_i, memory_buffer_j:
00403750 mov ecx, esi ; case 0x62
00403752 call loc_403360
00403757 test al, al
00403759 jz loc_40342F
0040375F mov eax, [esi+438h] <-- eip
00403765 movzx ecx, byte ptr [eax+2] <-- 2° operand
00403769 movzx edx, byte ptr [ecx+esi+42Ch] <-- memory index position (refer to name, fake serial or right serial position)
00403771 movzx eax, byte ptr [eax+1] <-- 1° operand
00403775 lea ecx, [ecx+edx*4+2Ch] <-- position inside memory
00403779 movzx edx, byte ptr [ecx+esi] <-- get byte from memory buffer
0040377D mov [esi+eax*4+0Ch], edx <-- store byte into register
00403781 mov ecx, [esi+438h]
00403787 movzx edx, byte ptr [ecx+2]
0040378B add [edx+esi+42Ch], bl <-- add 1 to the memory index position
00403792 add dword ptr [esi+438h], 3 <-- eip update
00403799 lea eax, [edx+esi+42Ch]
004037A0 pop edi
004037A1 pop esi
004037A2 xor eax, eax
004037A4 pop ebx
004037A5 retn
Every VM memory operation has the ability to increment the index memory pointer value, memory is read/write in sequential order.
Just another thing and then I’ll show you the complete algorithm with a simple keygen. Do you remember when I said “flag is stored at [esi+0x42]”? The VM has some conditional jump instructions and the only way to decide to jump or not depends on the value of a specific flag. This VM has one single flag, it takes 3 possible values: 1, 2 or 3 according to the result of a VM instruction like this one:
(0x81: cmp reg_i_val, dword_val):
40397C mov ecx, esi
40397E call loc_403360
403983 test al, al
403985 jz loc_40342F
40398B mov edx, [esi+438h] <-- eip
403991 movzx ecx, byte ptr [edx+1] <-- 1° parameter: reg index
403995 mov eax, [esi+ecx*4+0Ch] <-- get dword value from reg
403999 mov ecx, [edx+2] <-- 2° parameter: dword value
40399C cmp eax, ecx <-- compare the two dword values
40399E jnz short loc_4039B2
4039A0 add edx, 6
4039A3 pop edi
4039A4 mov [esi+8], bl <-- values are equal: flag = 1
4039A7 mov [esi+438h], edx
..
4039B8 mov byte ptr [esi+8], 3 <-- flag value 3
...
4039CB mov byte ptr [esi+8], 2 <-- flag value 2
There’s a compare between two values, one from a register and the other from a dword value. The flag at [esi+8] is updated depending on the compare result.
You can now imagine how a conditional jump instruction looks like:
4038C3 cmp byte ptr [esi+8], 2 <-- check flag value
4038C7 jnz loc_4035F9
4038CD sub ecx, [ecx+1] <-- get the jump offset value
4038D0 pop edi
4038D1 mov [esi+438h], ecx <-- jump!
...
4035F9 add ecx, 5 <-- add length conditional jump instruction
4035FC pop edi
4035FD mov [esi+438h], ecx <-- update eip
The VM instructions are taken starting from 0x40A000, here is the initial byte sequence:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 61 01 00 00 00 00 61 02 00 00 00 00...
I showed you few instructions but the VM instruction set is simple, there are basically almost all the instructions of an x86 instruction set: mov, cmp, conditional jump and direct jump. Parameters are stored using little-endian system. The VM doesn’t have call instructions, and so it doesn’t need a real stack implementation.
The interesting part of the VM algorithm is:
0: 00 nop
00 nop
00 nop
00 nop
00 nop
00 nop
00 nop
00 nop
00 nop
00 nop
00 nop
00 nop
00 nop
00 nop
61 01 00 00 00 00 mov reg_1, 0x00000000
61 02 00 00 00 00 mov reg_2, 0x00000000
61 03 00 00 00 00 mov reg_3, 0x00000000
61 04 00 00 00 00 mov reg_4, 0x00000000
61 06 00 00 00 00 mov reg_6, 0x00000000
61 07 00 00 00 00 mov reg_7, 0x00000000
61 00 00 00 00 00 mov reg_0, 0x00000000
20 00 00 mov m_0, 0x00
00 nop
20 00 01 mov m_0, 1
3F: 40 02 jmp $+2
41: 00 nop
42: 05 04 inc reg_4
00 nop
62 01 00 mov reg_1, m_0 <-- get current char from name
00 nop
81 01 00 00 00 00 cmp reg_1, 0x00000000
00 nop
01 01 add reg_5, reg_0, reg_1
00 nop
60 05 00 mov reg_0, reg_5
00 nop
57: 54 15 00 00 00 ja $-0x00000015
00 nop
06 04 dec reg_4
00 nop
60 00 03 mov reg_3, reg_0
13 AC DC 00 00 mul reg_5, reg_0, 0x0000DCAC
61 07 4A 00 00 00 mov reg_7, 0x0000004A
6E: 40 02 jmp $+2
00 nop
63 07 03 mov m_3, reg_7
74: 46 06 loop $-0x06
00 nop
00 nop
00 nop
60 05 00 mov reg_0, reg_5
00 nop
17 88 77 66 55 xor reg_5, reg_0, 0x55667788
00 nop
60 05 00 mov reg_0, reg_5
00 nop
14 15 00 00 00 div reg_5, reg_0, 0x00000015
00 nop
60 05 00 mov reg_0, reg_5
00 nop
03 06 mul reg_5, reg_0, reg_6
60 05 00 mov reg_0, reg_5
00 nop
03 04 mul reg_5, reg_0, reg_4
60 05 00 mov reg_0, reg_5
60 00 01 mov reg_0, reg_1
61 07 08 00 00 00 mov reg_7, 0x00000008
A5: 14 3C 00 00 00 div reg_5, reg_0, 0x0000003C
60 06 00 mov reg_0, reg_6
11 41 00 00 00 add reg_5, reg_0, 0x00000041
60 05 00 mov reg_0, reg_5
63 00 01 mov m_1, reg_0 <-- save right serial char
03 01 mul reg_5, reg_0, reg_1
60 05 00 mov reg_0, reg_5
07 01 xor reg_5, reg_0, reg_1
60 05 00 mov reg_0, reg_5
03 04 mul reg_5, reg_0, reg_4
60 05 00 mov reg_0, reg_5
07 06 xor reg_5, reg_0, reg_6
60 05 00 mov reg_0, reg_5
17 00 BB 00 DD xor reg_5, reg_0, 0xDD00BB00
60 05 01 mov reg_1, reg_5
D4: 45 2F jnz_7 $-0x2F
61 07 07 00 00 00 mov reg_7, 0x00000007
20 00 02 mov m_pos_2, 0x00 <-- my fake serial
20 00 01 mov m_pos_1, 0x00 <-- right serial
E2: 62 01 02 mov reg_1, memory_buffer_2
62 02 01 mov reg_2, memory_buffer_1
85 01 02 cmp reg_1, reg_2
43 10 00 00 00 jb $+0x00000010
44 0B 00 00 00 ja $+0x0000000B
F5: 45 13 jnz_7 $-0x13
00 nop
00 nop
00 nop
FF VM_SUCCESS
The implemented algorithm is quite easy, once you have the instructions sequence on a paper you can easily write a keygen. Here is a quick and dirty implementation of the keygen:
sum = 0;
for(int i=0;i< strlen(name);i++)
sum += (__int8)name[i];
sum = (sum * 0xDCAC) ^ 0x55667788;
q = sum / 0x15;
r = sum % 0x15;
len = strlen(name);
tmp1 = (q * r) * len;
tmp2 = tmp1;
for(int j=0;j<8;j++) {
r = tmp2 % 0x3c;
serial[j] = (char)(r + 0x41);
tmp2 = ((((tmp1 * (__int32)serial[j]) ^ tmp1) * len) ^ r);
tmp1 = tmp2 ^ 0xDD00BB00;
}
Valid combinations are:
ZaiRoN / YiYaiQMm
crackmes.de / nmUQuME]
reversing / emAMy]Qy
That’s all folks!