Solution-borismilner-4N006135-level4

From aldeid
Jump to navigation Jump to search

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