Post

NavajaNegra2024 - InnotecOS


InnotecOS challenge writeup, part of Navaja Negra 2024 Con CTF.

Reversing a modified DOS/MBR boot sector.


Table of Contents

  1. Intro
  2. Easy way
  3. Hard way

Intro

This is a tipical revering engineering crackme challenge. But instead of analyzing a Windows PE or a Linux ELF, you have to analyze of a DOS/MBR boot sector.

As in any CTF, I was in a hurry to solve the challenge so I looked for an alternative approach that would save me to understand the algorithm. I eventually realized that this approach was much more difficult. But I have deceided to share it in the “Hard way” section because in other scenarios, this might have been the easy way.

Once the ctf was finished, I solved it in the intended way. That solution is found in the “Easy way” section.

Easy way

Having a fixed size of 512 bytes makes MBR structure very simple:

MBR hexdump
0x000 - 0x0DBMBR code
0x0DC - 0x18AStrings
0x18B - 0x1B3Null byte padding
0x1B4 - 0x1F9Disk signature & Partition table (replaced by the encoded flag)
0x1FE - 0x1FFMBR signature (55 AA)

To analyze the MBR with IDA, it has to be opened in 16 bit mode. Again, since it has only 512 bytes, there is no much to look at. The first thing in the user input to be checked is the length. When it is different from 35, the string at address 0x166 (MBRs are loaded at 0x7C00 memory address) is printed: “Sorry, password length is incorrect”

Length check

If the user input has the expected length, MBR code will loop over it byte by byte. Some operations are applied to each byte(1) and the result is compared with a encoded flag WORD(2). When a comparison fails, the string at address 0x14E is printed(3): “Password is incorrect”. But if all comparisons are successful, then the string at address 0x12D is printed(4): “Yay! the password is the flag!”

MBR algorithm

The bitwise AND operation in the algorithm causes some information to be lost. Then the flag cannot be decrypted without applying bruteforce. This solver extracts the encoded flag from the MBR and decrypts it:

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
#!/usr/bin/python3
from string import ascii_letters

ABC = ascii_letters + "0123456789_!{}"

def assembly_simulation(eax, edx):
    ebx = (eax*0x1312) ^ edx
    ecx = eax*0x1337
    return ((eax >> 2)+ebx-ecx) & 0xffff


# Get encrypted flag
with open("InnotecOS.bin", "rb") as f:
    enc_flag = f.read()[0x1b4:0x1fa]

# Decrypt flag
flag = ""
edx = 1
for i in range(0, len(enc_flag), 2):
    enc_w = enc_flag[i:i+2]
    enc_w = int(enc_w[::-1].hex(), 16) # swap endianness
    for c in ABC:
        if assembly_simulation(ord(c), edx) == enc_w:
            flag += c
            edx = enc_w
            break

print(flag)

Hard way

Once I realized that the user input is checked byte by byte, I decieded to patch the MBR. I modified the flag length to 1. Then run the patched MBR and forcebrute the first flag character (sending keystrokes to QEMU) until the win message is printed. Then patch the flag length to 2 and so on…

Patched MBR

This is the script I used to patch and forcebrute the flag:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/python3
# qemu-system-i386 -drive format=raw,file=InnotecOS.bin -monitor tcp:localhost:1234,server,nowait
import socket
import string
from time import sleep
ABC = string.ascii_letters + string.digits

""" PATCH
with open("./InnotecOS.bin.bac", "rb") as f:
    b = f.read()

b = bytearray(b)
b[0x43] = 33
with open("./InnotecOS.bin", "wb") as f:
    f.write(b)
exit()
"""

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 1234))

for c in ABC:
    s.send(b"sendkey f\n")
    s.send(b"sendkey l\n")
    s.send(b"sendkey a\n")
    s.send(b"sendkey g\n")
    s.send(b"sendkey shift-bracket_left\n")
    s.send(f"sendkey shift-w\n".encode())
    s.send(f"sendkey 0\n".encode())
    s.send(f"sendkey w\n".encode())
    s.send(f"sendkey shift-minus\n".encode())
    s.send(f"sendkey y\n".encode())
    s.send(f"sendkey 0\n".encode())
    s.send(f"sendkey shift-u\n".encode())
    s.send(f"sendkey shift-minus\n".encode())
    s.send(f"sendkey m\n".encode())
    s.send(f"sendkey shift-u\n".encode())
    s.send(f"sendkey s\n".encode())
    s.send(f"sendkey shift-t\n".encode())
    s.send(f"sendkey shift-minus\n".encode())
    s.send(f"sendkey b\n".encode())
    s.send(f"sendkey 3\n".encode())
    s.send(f"sendkey shift-minus\n".encode())
    s.send(f"sendkey 4\n".encode())
    s.send(f"sendkey shift-minus\n".encode())
    s.send(f"sendkey shift-r\n".encode())
    s.send(f"sendkey 3\n".encode())
    s.send(f"sendkey 4\n".encode())
    s.send(f"sendkey l\n".encode())
    s.send(f"sendkey shift-minus\n".encode())
    s.send(f"sendkey shift-h\n".encode())
    s.send(f"sendkey 4\n".encode())
    s.send(f"sendkey x\n".encode())
    s.send(f"sendkey 0\n".encode())
    s.send(f"sendkey shift-r\n".encode())
    s.send(f"sendkey shift-1\n".encode())
    s.send(f"sendkey shift-1\n".encode())
    """
    if c.isupper():
        s.send(f"sendkey shift-{c.lower()}\n".encode())
    else:
        s.send(b"sendkey "+c.encode()+b'\n')
    #"""
    s.send(f"sendkey shift-bracket_right\n".encode())
    s.send(b"sendkey ret\n")
    sleep(1)

This post is licensed under CC BY 4.0 by the author.