Solution-borismilner-4N006135-level4
Introduction
Description
This crackme is the 5th level of Borismilner's 4N006135 crackmes, available here.
Code overview
Code Analysis
Strings
Encryption?
If you're looking for strings, you will notice that there is a string "WOW - You are good!" without any cross reference. It's simply because this string is not used by the program.
On the other hand, you won't find any reference to "NOT A GOOD JOB" or "GOOD JOB". It's because the strings are encrypted.
Decrypt the NOT A GOOD JOB string
You can easily decrypt the "NOT A GOOD JOB" string because it's simplied XOR'ed with 0x7:
.text:0040148B mov eax, offset aIhsF@hhcMhe ; 0x40902A
.text:00401490 mov ecx, 10h ; ecx = 16 (counter)
.text:00401495
.text:00401495 loc_401495:
.text:00401495 xor byte ptr [eax+ecx], 7 ; byte[i] ^ 0x7
.text:00401499 loop loc_401495 ; ecx -= 0
.text:0040149B xor byte ptr [eax+ecx], 7 ; revert XOR of byte[0]
.text:0040149F push eax ; Format
.text:004014A0 call printf
We can write a python script that will add the decrypted string as a comment:
# Decrypt badboy string in IDA-Pro (Alt+F7)
loc_encr = 0x40902A
loc_comm = 0x40148B
comm = []
for ecx in range(1, 0x10):
b = Byte(loc_encr + ecx)
comm.append(b ^ 0x7)
MakeComm(loc_comm, ''.join([chr(i) for i in comm]))
Decrypt the GOOD JOB string
The GOOD JOB string uses the same XOR loop as for the bad boy but with ecx set to 16 as depicted below:
.text:00401460 mov byte ptr loc_409000, 10h ; byte_409000 = 16
; ... [SNIP] ...
.text:00401476 sub byte ptr loc_409000, 7 ; byte_409000 -= 7
.text:0040147D movsx ecx, byte ptr loc_409000 ; ecx = 16 - 7 = 9
.text:00401484 mov eax, offset aIhsF@hhcMhe ; 0x40902A
.text:00401489 jmp short loc_4014A9
; ... [SNIP] ...
.text:004014A9 loc_4014A9:
.text:004014A9 add eax, 7 ; eax = 0x40902A + 0x7 = 0x409031
.text:004014AC jmp short loc_401495 ; XOR loop (same as for bad boy)
Once again, let's write a python script to decrypt the string at offset 0x401484:
# Decrypt goodboy string in IDA-Pro (Alt+F7)
loc_encr = 0x409031
loc_comm = 0x401484
comm = []
for ecx in range(9):
b = Byte(loc_encr + ecx)
comm.append(b ^ 0x7)
MakeComm(loc_comm, ''.join([chr(i) for i in comm]))
You can apply both scripts in IDA-Pro by selecting File > Script file....
Initialization and Mario test
The below block of code displays the banner and prompts for a password at offset 0x401404 which size should be less or equal than 9 characters.
.text:004013E0 sub_4013E0 proc near
.text:004013E0
.text:004013E0 arg_0 = dword ptr 4
.text:004013E0
.text:004013E0 ; FUNCTION CHUNK AT .data:00409000 SIZE 00000014 BYTES
.text:004013E0
.text:004013E0 push offset Format ; "\nCrackme - Level 4 - by 60Ô15\n-------"...
.text:004013E5 call printf
.text:004013EA add esp, 4
.text:004013ED push offset aPassword ; "Password : "
.text:004013F2 call printf
.text:004013F7 add esp, 4
.text:004013FA push offset my_password
.text:004013FF push offset a9s ; '%9s' => 0 < len(password) <= 9
.text:00401404 call scanf
.text:00401409 add esp, 8
.text:0040140C cmp ds:my_password, 'M'
.text:00401413 jz short BAD
.text:00401415 cmp ds:byte_40D021, 'a'
.text:0040141C jz short BAD
.text:0040141E cmp ds:byte_40D022, 'r'
.text:00401425 jz short BAD
.text:00401427 cmp ds:byte_40D023, 'i'
.text:0040142E jz short BAD
.text:00401430 cmp ds:byte_40D024, 'o'
.text:00401437 jz short BAD
A curious test is starting from offset 0x40140C to check that the password is not Mario :)
Self-modifying code
Starting from offset 0x401439, some computations occur where EAX, ECX and EBX are updated, along with a self-modifying code. The comments left in the below extract should be self-explanatory.
The most interesting part is at offset 0x409004 where the number of arguments (this includes arg[0], the program name) is moved to ECX. The program then jumps to 0x40145B.
.text:00401439 mov ecx, offset my_password ; ecx = 0x40D020
.text:0040143E and ecx, 7 ; ecx = 0x40D020 & 0x7 = 0
.text:00401441 mov eax, offset my_password
.text:00401446 repne ror eax, 3
.text:0040144A jp short $+2 ; jump loc_40144C
.text:0040144C
.text:0040144C loc_40144C:
.text:0040144C mov eax, offset loc_409000 ; eax = 0x409000
.text:00401451 mov ebx, offset loc_40145B ; ebx = 0x40145B
.text:00401456 jmp loc_409000
; ... [SNIP] ...
.data:00409000 loc_409000:
.data:00409000 xor byte ptr [eax+4], 0F7h ; xor byte 0x409004 with 0xF7
.data:00409004 mov ecx, [esp+arg_0] ; aldready patched in IDA
.data:00409004 ; ecx = nb arg
.data:00409008 xor byte ptr [eax+4], 7 ; patch not applied in IDA
.data:00409008 ; Erase previous instruction
.data:0040900C or ecx, 0F0F0h ; ecx = nb_arg | 0xF0F0
.data:00409012 jmp ebx ; jump loc_40145B
Parity and Overflow tests
The code at offset 0x40145B is as follows:
.text:0040145B loc_40145B:
.text:0040145B shl eax, 3 ; eax = 0x409000 << 3 = 0x2048000
.text:0040145E xor eax, ecx ; eax = 0x2048000 ^ (nb_arg | 0xF0F0)
.text:00401460 mov byte ptr loc_409000, 10h ; patch byte 0x409000
.text:00401460 ; patch not applied in IDA
.text:00401467 ror eax, 4 ; eax = ROR(0x2048000 ^ (nb_arg | 0xF0F0), 4, 32)
.text:0040146A xor eax, 0 ; \ parity flag should
.text:0040146D jnp short BAD ; / be set for eax
.text:0040146F add eax, 10000000h ; eax = ROR(0x2048000 ^ (nb_arg | 0xF0F0), 4, 32) + 0x10000000
.text:00401474 jno short BAD ; jump to BAD if not overflow
At offset 0x401467, EAX is computed as follows (where nb_arg is the number of arguments, as explained previously):
eax = ROR(0x2048000 ^ (nb_arg | 0xF0F0), 4, 32)
If the Parity Flag (PF) is not set, the program will jump (offset 0x40146D) to the badboy. Then, the program adds 0x10000000 at offset 0040146F and checks that it results in an overflow. If the Overflow Flag (OF) is not set, the code will also jump to the badboy.
At this stage, I have developed a python script that displays the values of EAX, PF and OF depending on the number of arguments (see next section).
Script and solution
Script
#!/usr/bin/env python
def parity_flag(n):
if bin(n)[-8:].count('1') % 2 == 0:
pf = 1
else:
pf = 0
return pf
def bit0(n):
if len(bin(n)[2:]) < 32:
return 0
else:
return bin(n)[2:3]
def overflow_flag(n1, n2):
if bit0(n1) != bit0(n2):
of = 0
else:
if bit0(n1 + n2) != bit0(n1):
of = 1
else:
of = 0
return of
ror = lambda val, r_bits, max_bits=32: \
((val & (2**max_bits-1)) >> r_bits%max_bits) | \
(val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))
print "Nb arg\teax\t\tPF\tOF"
print "------\t----------\t--\t--"
for nb_arg in range(1,15):
eax = ror(0x2048000 ^ (nb_arg | 0xF0F0), 4)
print "%s\t%s\t%s\t%s" % (nb_arg, \
hex(eax), \
parity_flag(eax),
overflow_flag(eax, 0x10000000))
Solution
Below is the output of the above script:
$ ./solution.py Nb arg eax PF OF ------ ---------- -- -- 1 0x1020470f 1 0 2 0x2020470f 1 0 3 0x3020470f 1 0 4 0x4020470f 1 0 5 0x5020470f 1 0 6 0x6020470f 1 0 7 0x7020470f 1 1 8 0x8020470f 1 0 9 0x9020470f 1 0 10 0xa020470f 1 0 11 0xb020470f 1 0 12 0xc020470f 1 0 13 0xd020470f 1 0 14 0xe020470f 1 0
We know that both the Parity Flag (PF) and the Overflow Flag (OF) should be set. It is true when the number of arguments is 7 (including the program name which is arg[0]). Let's check:
C:\crackmes>level-4.exe anything1 anything2 anything3 anything4 anything5 anything6 Crackme - Level 4 - by 60Ô15 ---------------------------- Password : anything GOOD JOB !
As a conclusion, the program will return "GOOD JOB" if all of the below conditions are satisfied:
- 6 arguments are provided
- 0 < password length <= 9
Comments
Keywords: assembly x86 reverse-engineering crackme borismilner 4N006135 crackmes.de