通过一道pwn题探究_IO_FILE结构攻击利用
(原文首发自安全客)
前言
前一段时间学了IO-file的知识,发现在CTF中IO_file也是一个常考的知识点,这里我就来总结一下IO_file的知识点,顺便可以做一波笔记。首先讲一下IO_file的结构体,然后其利用的方法,最后通过一道HITB-XCTF 2018 GSEC once的题目来加深对IO_file的理解。
libc2.23 版本的IO_file利用
这是一种控制流劫持技术,攻击者可以利用程序中的漏洞覆盖file指针指向能够控制的区域,从而改写结构体中重要的数据,或者覆盖vtable来控制程序执行流。
IO_file结构体
在ctf中调用setvbuf(),stdin、stdout、stderr结构体一般位于libc数据段,其他大多数的FILE 结构体保存在堆上,其定义如下代码:
1 | struct _IO_FILE { |
FILE结构体会通过struct _IO_FILE *_chain链接成一个链表,64位程序下其偏移为0x60,链表头部用_IO_list_all指针表示。如下图所示
IO_file结构体外面还被一个IO_FILE_plus结构体包裹着,其定义如下
1 | struct _IO_FILE_plus |
其中包含了一个重要的虚表*vtable,它是IO_jump_t 类型的指针,偏移是0xd8,保存了一些重要的函数指针,我们一般就是改这里的指针来控制程序执行流。其定义如下
1 | struct _IO_jump_t |
利用方法(FSOP)
这是利用程序中的漏洞(如unsorted bin attack)来覆盖_IO_list_all(全局变量)来使链表指向一个我们能够控制的区域,从而改写虚表*vtable。通过调用 _IO_flush_all_lockp()函数来触发,,该函数会在下面三种情况下被调用:
1:当 libc 执行 abort 流程时。
2:当执行 exit 函数时。当执行流从 main 函数返回时
3:当执行流从 main 函数返回时
当 glibc 检测到内存错误时,会依次调用这样的函数路径:malloc_printerr ->
libc_message->__GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW
要让正常控制执行流,还需要伪造一些数据,我们看下代码
1 | if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) |
这时我们伪造 fp->_mode = 0, fp->_IO_write_ptr > fp->_IO_write_base就可以通过验证
新版本下的利用
新版本(libc2.24以上)的防御机制会检查vtable的合法性,不能再像之前那样改vatable为堆地址,但是_IO_str_jumps是一个符合条件的 vtable,改 vtable为 _IO_str_jumps即可绕过检查。其定义如下
1 | const struct _IO_jump_t _IO_str_jumps libio_vtable = |
其中 _IO_str_overflow 函数会调用 FILE+0xe0处的地址。这时只要我们将虚表覆盖为 _ IO_str_jumps将偏移0xe0处设置为one_gadget即可。
还有一种就是利用io_finish函数,同上面的类似, io_finish会以 _IO_buf_base处的值为参数跳转至 FILE+0xe8处的地址。执行 fclose( fp)时会调用此函数,但是大多数情况下可能不会有 fclose(fp),这时我们还是可以利用异常来调用 io_finish,异常时调用 _ IO_OVERFLOW
是根据IO_str_overflow在虚表中的偏移找到的, 我们可以设置vtable为_IO_str_jumps-0x8异常时会调用io_finish函数。
具体题目(HITB-XCTF 2018 GSEC once)
1、先简单运行一下程序,查看保护
主要开启了CANARY和NX保护,不能改写GOT表
2、ida打开,反编译
这里当输入一个不合法的选项时,就会输出puts的地址,用于泄露libc的基地址。
第一个函数是创建一个chunk保存数据
第二个函数和第三个函数只能执行一次,有个任意地址写漏洞,这时我们可以利用第二个函数改写off_202038+3d为_IO_list_all-0x10,然后分别执行第三和第一个函数,最后_IO_list_all就会指向0x555555757040的位置
第四个函数主要是对堆块的操作,我们可以利用利用这个函数伪造一个_IO_FILE结构
3、具体过程
1、泄露libc,输入一个“6”即可得到puts函数的地址,然后酸算出libc基地址
1 | p.recvuntil('>') |
2、利用任意地址写改写_IO_list_all为堆的地址
1 | p.sendline('1') |
3、这时只要我们再利用第四个函数伪造__IO_FILE结构体,改写vtable为_IO_str_jumps,file+0xe0设置
为one_gadget
1 | p.sendline('4') |
4、输入“5”,执行exit()函数触发one_gadget
1 | p.recvuntil('>') |
完整EXP
1 | from pwn import* |