hfs-mbr (re213)

We made a military-grade secure OS for HFS members. Feel free to beta test it for us!

Download: hfs-os.tar.gz

First thing to do is to extract the files in the archive.

➤ tar -xf ../hfs-os.tar.gz
➤ ls -l
total 20496
-rw-r--r--@ 1 daniellimws  staff       405 Apr  4 21:39 README
drwxr-xr-x@ 7 daniellimws  staff       224 Mar 28 01:46 bin
-rw-r--r--@ 1 daniellimws  staff  10485760 Apr  4 09:27 dos.img
-rwxr-xr-x@ 1 daniellimws  staff       288 Apr  4 06:09 run
➤ cat README
./run debug (gdb stub) or ./run

How to debug with IDA
In IDA > Debugger > Attach > Remote debugger (host:1234) > (Debug options > Set specific options, UNCHECK 'software breakpoints at eip+1', CHECK 'use CS:IP in real mode')  > OK
When attached, Debugger > Manual memory regions > Insert > CHECK 16bit segment > OK
In the IDA-View, press G, 0x7c00 is where the bootloader starts. Set a BP > F9

We are given a wrapper script for loading the challenge in QEMU.

➤ cat run
#! /bin/bash

if [ "$1" = "debug" ] ; then
    cd bin && ./`QEMU`-system-i386 -s -S -m 16 -k en-us -rtc base=localtime -nographic -drive file=../dos.img -boot order=c
    cd bin && ./`QEMU`-system-i386 -m 16 -k en-us -rtc base=localtime -nographic -drive file=../dos.img -boot order=c

Nothing much of our interest in bin/, just stuff for QEMU.

➤ ls -al bin
total 120824
drwxr-xr-x@ 7 daniellimws  staff       224 Mar 28 01:46 .
drwxr-xr-x  6 daniellimws  staff       192 Apr  8 11:23 ..
-rw-r--r--@ 1 daniellimws  staff    262144 Mar 28 01:45 bios-256k.bin
-rw-r--r--@ 1 daniellimws  staff    240128 Mar 28 01:45 efi-e1000.rom
-rw-r--r--@ 1 daniellimws  staff      9216 Mar 28 01:45 kvmvapic.bin
-rwxr-xr-x@ 1 daniellimws  staff  61304648 Mar 28 01:45 `QEMU`-system-i386
-rw-r--r--@ 1 daniellimws  staff     38912 Mar 28 01:45 vgabios-stdvga.bin

dos.img here is a MBR boot sector.

➤ file dos.img
dos.img: DOS/MBR boot sector; partition 1 : ID=0x1, active, start-CHS (0x0,1,1), end-CHS (0x13,15,63), startsector 63, 20097 sectors

Executing the run script, we will see the following load screen


[HFS SECURE BOOT] Loading  ...
.-. .-.----.----.   .-.   .-.----..----.
| {_} | {_{ {__     |  `.'  | {}  | {}  }
| { } | | .-._} }   | |\ /| | {}  | .-. \
`-' `-`-' `----'    `-' ` `-`----'`-' `-'
Enter the correct password to unlock the Operating System

Set up environment

The task here is to find the password to unlock the OS. Although there are instructions in README on how to setup IDA for reversing this program, I used GHIDRA for this challenge.

When importing dos.img into GHIDRA, it will not be able to recognize the file format. We can import it as a raw binary and set the processor to be x86 in 16-bit real mode. Before importing the file, also go to options and set the base address to be 0000:7c00 because 0x7c00 is where the bootloader starts.

To debug this in GDB, we can do ./run debug to tell QEMU to run a gdbserver. In GDB, we can run the following commands to prepare the environment.

(gdb) target remote localhost:1234
Remote debugging using localhost:1234
(gdb) set architecture i8086
The target architecture is assumed to be i8086
(gdb) break *0x7c00
Breakpoint 1 at 0x7c00

Notice the -s and -S flags passed to QEMU when using ./run debug. -s is for starting a gdbserver at port 1234, while -S is for QEMU to freeze CPU at startup, only starting execution after entering c (continue) in GDB.

Although I normally like to use GEF with GDB, but because gef-remote does not properly support x86 architectures at this point, I used this gdbinit script I found that is made to support real mode with QEMU.

This blog explains some things about debugging programs running in real mode and under QEMU.

Analyse the program

Similar to syscalls in our typical programs, the program uses interrupts int 0x10, int 0x13, int 0x16 for interacting with the user.

  • int 0x10 - For reading/writing from/to the screen (https://en.wikipedia.org/wiki/INT_10H)
  • int 0x13 - For disk read/write (https://en.wikipedia.org/wiki/INT_13H)
  • int 0x16 - For control of the keyboard (https://en.wikipedia.org/wiki/INT_16H)

At the start, the program does some initial setup like setting the video mode. They aren’t very important so I’ll omit those. The first important part is

0000:7c21 b8 02 02        MOV        AX,0x202
0000:7c24 b9 04 00        MOV        CX,0x4
0000:7c27 30 f6           XOR        DH,DH
0000:7c29 b2 80           MOV        DL,0x80
0000:7c2b bb 00 7e        MOV        BX,0x7e00
0000:7c2e cd 13           INT        0x13
0000:7c30 ff e3           JMP        BX

The program sets up the registers for interrupt call 0x13. Referring to the documentation, based on the CHS (cylinder-head-sector) addressing scheme, this will be reading 2 sectors at CHS (0, 0, 4) from drive into address 0x7e00. Then, the program jumps to 0x7e00.

This formula describes how to convert between CHS to LBA (a linear addressing scheme), which will tell us that the program is reading from the 4th sector (each sector is 512 bytes).

We can copy this part into a separate file using dd.

dd if=dos.img of=part2 skip=3 obs=512 count=2`

(obs=512 is to say each block to copy is 512 bytes, skip=3 because we want to start copying from the 4th block, and count=2 to copy 2 blocks)’

Another way to obtain the loaded code is by setting a breakpoint in GDB at 0x7c30 (just after the interrupt call), then dump the memory in 0x7e00.

dump memory part2 0x7e00 0x8200

Reverse the password checking code

Since the code is from the same file, we can just scroll down to address 0x8000 to analyse it. However, it would be better to load the file dumped earlier (file1 or file2), and set the base address to 0x7e00 instead, to avoid missing any references and debugging would be easier knowing the exact addresses.

Now, we can see the code where the program tells us its loading and prompts for a password. Following which, it reads the password from our keyboard and checks if it is correct.

0000:7e37 b7 01           MOV        BH,0x1
0000:7e39 b4 00           MOV        AH,0x0
0000:7e3b cd 16           INT        0x16

0000:7e3d 3c 61           CMP        AL,'a'
0000:7e3f 0f 8c c1 01     JL         die
0000:7e43 3c 7a           CMP        AL,'z'
0000:7e45 0f 8f bb 01     JG         die
0000:7e49 b4 0e           MOV        AH,0xe
0000:7e4b cd 10           INT        0x10

The first interrupt call int 0x16 will read one character from the keyboard, and save it in ah. Then, the program checks if it is within the range of lowercase letters. If outside the range, the program exits as the password is wrong, otherwise writes the character to the screen.

0000:7e4d 30 e4           XOR        AH,AH
0000:7e4f 88 c2           MOV        DL,AL

0000:7e51 2c 61           SUB        AL,0x61
0000:7e53 d0 e0           SHL        AL,1

0000:7e55 31 db           XOR        BX,BX
0000:7e57 88 c3           MOV        BL,AL
0000:7e59 b8 26 80        MOV        AX,0x8026
0000:7e5c 01 c3           ADD        BX,AX
0000:7e5e 8b 07           MOV        AX,word ptr [BX]
0000:7e60 ff e0           JMP        AX

After that, the provided character will be used as an index for a jump table located at 0x8026. So each input character is handled differently.

This part is fairly trivial at this point, the check is performed by xoring the current position of the input with the input character. The following are the requirements of the password.

index ^ 'e' == 0x62 => 7
index ^ 'j' == 0x68 => 2
index ^ 'n' == 0x68 => 6
index ^ 'o' == 0x6e => 1
index ^ 'p' == 0x74 => 4
index ^ 'r' == 0x7a => 8
index ^ 's' == 0x73 => 0
index ^ 'u' == 0x76 => 3
index ^ 'w' == 0x72 => 5

From this, we can recover the password, sojupwner.

Send this to the challenge server, and we get

[HFS SECURE SHELL] Here is your flag for HFS-MBR: midnight{w0ah_Sh!t_jU5t_g0t_REALmode}
[HFS SECURE SHELL] loaded at 100f:0100 (0x101f0) and ready for some binary carnage!

Onward to part 2, pwning the OS to read FLAG2.

Feel free to ask anything in the comments.