NavajaNegra2024 - InnotecOS
InnotecOS challenge writeup, part of Navaja Negra 2024 Con CTF.
Reversing a modified DOS/MBR boot sector.
Table of Contents
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:
0x000 - 0x0DB | MBR code |
0x0DC - 0x18A | Strings |
0x18B - 0x1B3 | Null byte padding |
0x1B4 - 0x1F9 | Disk signature & Partition table (replaced by the encoded flag) |
0x1FE - 0x1FF | MBR 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”
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!”
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…
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)