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.
[*] | Turns out the 28 byte restriction were due to a bug in my code. During testing, I moved the string for the filename from the end to the top of the code. When reading, I reused the buffer for the filename, overwriting my code towards the end. If I read more than 28 bytes, the code after the read was overwritten. Moving the filename back to the end fixes this issue and allows me to read the flag in one read system call.Debugging is hard if you don't have a debugger or any feedback and it's 2am in the morning. (HT @kkotowicz) |