ptmalloc

1
2
3
4
5
6
7
8
172.16.9.52   9W8K#J7v  22/8888  5032/6032
172.16.9.53 9Cr3ReEc 22/8888 5033/6033
student37
ATE5GtBW

ssh ctf@172.16.9.52 -p 9W8K#J7v
ssh ctf@172.16.9.53 -p 9Cr3ReEc
https://172.20.1.1

Heap

  • • dlmalloc – General purpose allocator
  • • ptmalloc2 – glibc
  • • jemalloc - FreeBSD and firefox
  • • tcmalloc – Google
  • • libumem - Solaris

ptmalloc

Ubuntu16:

  • libc6_2.23-0ubuntu11.2_amd64
  • libc6_2.23-0ubuntu11.2_i386

Ubuntu18:

  • libc6_2.27-3ubuntu1.3_amd64
  • libc6_2.27-3ubuntu1.3_i386

32

1

64

chunksize = 0x180 则malloc( ( 0x180 - 24, 0x180 - 8 ] )

堆块块(Chunk), 其头部保存着自身的大小等信息, 在这之后即 是用户数据。一共有 3 种类型, 它们都使用同一种数 据结构(malloc_chunk)定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct malloc_chunk {
/* Size of previous chunk (if free). */
INTERNAL_SIZE_T prev_size;
/* Size in bytes, including overhead. */
INTERNAL_SIZE_T size;
/* double links -- used only if free. */
struct malloc_chunk* fd;
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger
size. */
/* double links -- used only if free. */
struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
};

具体来说, 用于管理的堆块主要分为 3 种类型。

Allocated Chunk: 已分配块

image-20201216143511628

Free Chunk: 被释放块

image-20201216143528038

Top Chunk: 顶块

位于所有块之后, 保存着 未分配的所有内存, 与已分配块使用相同的域。

glibc 使用 size 域的最低 3 位来 存储一些其它信息。相关的掩码信息定义如下:

1
2
3
#define PREV_INUSE 0x1  //如果此位为 0 则表示上一块为被释放的块, 这个时候此块的 PREV_SIZE 域保存的是上一块的地 址以便在 free 此块时能够找到上一块的地址并进行 合并操作。
#define IS_MMAPPED 0x2 //第 2 位表示此块是否由 mmap 分配, 如果 此位为 0 则此块是由 top chunk 分裂得来, 否则是由 mmap 单独分配而来。
#define NON_MAIN_ARENA 0x4 //第 3 位表示此块是否不属于 main_arena, 在之后会提到main_arena是主线程用于保存堆状态的结构, 如果此位为 0 则表示此块是在 主线程中分配的。
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
pwndbg> arena   <- main_arena
{
mutex = 0,
flags = 1,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x6020a0,
last_remainder = 0x0,
bins = {0x7ffff7dd1b78 <main_arena+88> = arena.top, 0x7ffff7dd1b78 <main_arena+88>, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1be8 <main_arena+200>, 0x7ffff7dd1be8 <main_arena+200>, 0x7ffff7dd1bf8 <main_arena+216>, },
binmap = {0, 0, 0, 0},
next = 0x7ffff7dd1b20 <main_arena>,
next_free = 0x0,
attached_threads = 1,
system_mem = 135168,
max_system_mem = 135168
}

fastbinsY = {0x602020, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
pwndbg> bins
fastbins
0x20: 0x602020 —▸ 0x602000 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

Bins:

Fast Bins:

  • 单向链表

  • 每个元素是一 条单向链表的头部

  • 且同一条链表中块的大小相同。 主要保存大小 32 至 128 字节的块 (0x20 - 0x80). malloc(0-0x78)

  • 特点是当 free 时 不取消下一块的 PREV_INUSE 位, 也不检查是否能 够进行合并操作

  • Fast bins 的取用机 制是 LIFO (Last In First Out) , 即后释放的块将先被利用。(从fastbin头开始存和取)修改 fd

  • Free 1 2 3 M 3 2 1

  • 链无数量限制

Unsorted Bins

从头开始插入, 从尾部解链分配

之后的 Small Bins 与 Large Bins 也是同样的机制。

1
2
3
4
5
6
7
8
 bins = {0x602120, 0x602000, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>,...}

pwndbg> x/4gx 0x602120 first
0x602120: 0x0000000000000000 0x0000000000000091
0x602130: 0x0000000000602000 next 0x00007ffff7dd1b78 prev
pwndbg> x/4gx 0x602000 last
0x602000: 0x0000000000000000 0x0000000000000091
0x602010: 0x00007ffff7dd1b78 next 0x0000000000602120 prev

Small Bins:

  • 每个元素是一 条双向循环链表的头部
  • 且同一条链表中块的大小相同
  • 主要保存大小 32 至 1024 字节的块。
  • 由于是 双向链表, Small Bins 的取用机制是 FIFO (First In First Out) , 即先释放的块会先被利用,
  • Free 1 2 3 M 1 2 3

Large Bins

  • 每个元素是一条 双向循环链表的头部
  • 同一条链表中块的大小不一 定相同
  • 按照从大到小的顺序排列
  • 每个 bin 保存一定 大小范围的块, 主要保存大小 1024 字节以上的块。

malloc流程

#假设malloc一个0x80的块)

malloc hook指针

在第一次执行malloc函数时, 系统会使用brk系 统调用向操作系统扩展程序的数据区, 此时 glibc 将 初始化top chunk与main_arena, 取得132KB的空间。 如果之后所有的 malloc 操作都可以满足, 即最后总 是能在此 132KB 中找到合适的内存块返回, 则不再 使用系统调用与内核交互。这段时间, 程序的堆内存 由 glibc 管理。glibc 首先将其加上 8 字节的额外开销,再对齐32字节。

  • 检查 Fast bins 如果请求大小满足 Fast bins, 则在对应的 bin 中 寻找是否有相同大小的块, 如果有则直接将其取出 返回给程序, 同时更新 fast bins 中对应 bin 存储的链 表头指针

  • 检查 Small bins 如果块大小符合 Small bins, 则在对应大小的 Small bin 中寻找是否有合适的块, 如果有则直接返 回, 同时更新 Small bins 中对应 bin 的链表中该块的 上一块和下一块的指针。

  • 处理 Unsorted bin 如果之前没能返回恰好符合的块, 则开始处理 Unsorted bin 中的块(这里有一个例外, 如果 Unsorted bin 中只有一个块且这个块是 last_remainder, 而且大 小足够, 则优先使用此块, 分裂后将前一块返回给 用户, 剩下的一块作为新的 last_remainder 再次放入 Unsorted bin.[7])

  1. a) 逐个迭代Unsorted bin中的块, 如果发现块的 大小正好是需要的大小, 则迭代过程中止, 直接返 回此块;否则将此块放入到对应的 Small bin 或者 large bin 中, 这也是整个 glibc 堆管理中唯一会将块 放入 Small bins 与 large bins 中的代码。
  2. b) 迭代过程直到 Unsorted bin 中没有块或超过 最大迭代次数(10000)为止。
  3. c) 随后开始在 Small bins 与 large bins 中寻找最 合适的块(指大于请求大小的最小块), 如果能够找到, 则分裂后将前一块返回给用户, 剩下的块放入 Unsorted bin 中。
  4. d) 如果没能找到, 则回到开头, 继续迭代过程, 直到 Unsorted bin 空为止 2.2.4 使用顶块 如果之前的操作都没能找到合适的块, 将分裂 Top chunk 返回给用户, 若 Top chunk 的大小仍然不 足, 则再次执行 malloc_consolidate 函数清除 Fast bins, 若Fast bins已空, 只能使用sysmalloc函数借助 系统调用拓展空间。

free流程

  1. Free 函数是 glibc 的释放接口, 将之前分配得到 的用户内存指针作为参数, glibc 会释放这一块空间。 主要的功能由_int_free 函数实现。
  2. 首先, 进行一系列的检查, 包括内存地址对齐, PREV_INUSE 位是否正确等等, 能够发现一些破坏 与 double free 的问题。
  3. 如果块大小满足 Fast bins, 则不取消下一块的 PREV_INUSE 位, 也不设置下一块的 prev_size 域, 直接将该块放入对应的 Fast bin 中, 且不进行相邻块 的合并操作。
  4. 检查被 free 内存块的前一块(这里的前一块指连 续内存中的上一块, 通过 prev_size 域来寻找), 如果 未使用, 则合并这 2 块, 将前一块从其 bin 中移除之 后再检查其后一块, 如果发现是 Top chunk, 则最后将 合并到 Top chunk 中, 不放入任何 bin; 如果不是 Top chunk 且未使用, 则再合并这 2 块, 将后一块从其 bin 中移除, 并且将合并过的大块放入 Unsorted bin 中。

heap伪代码

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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
void* find_from_fastbin(int idx){
bin = &arena->fastbin[idx] - 0x10;
chunk* res = bin->fd;
bin->fd = res->fd;
// 高版本有fastbin检查
vassert(res->size == idx*16+0x20);
// 但这段代码没 有检查内存地址的对齐
return res;
}
// LIFO
void mv_to_fastbin(void* chunk, int idx){
void* fd = arena->fastbin[idx]->fd;
arena->fastbin[idx]->fd = chunk;
chunk->fd = fd;
}

// 处理 Small Bins 的 代码如下所示:
void* find_from_smallbin(int idx){
// 大小相等
small_bin = &arena->bins[idx] - 16;
chunk* victim = small_bin->bk;
if (victim == 0){/* initialization check */
malloc_consolidate(av);
return
}
bck = victim->bk;
set_inuse_bit_at_offset(victim, nb);
bin->bk = bck;
bck->fd = small_bin;
return victim;
}

void* find_from_small_large(size_t size, int idx){
// 满足最小大小
if (chunk = find_it()){
vassert(chunk->size > size);
relloc(chunk, size);
return chunk
}
return null;
}

// FIFO double link insert
void mv_to_bins(void* chunk){
//这也是整个 glibc 堆管理中唯一会将块 放入 Small bins 与 large bins 中的代码。
fd = bins[chunk->size/0x10]->fd;
bk = bins[chunk->size/0x10]->bk;
bins[chunk->size/0x10]->fd = chunk;
chunk->fd = fd;
chunk->bk = bk;
fd->bk = chunk;
bk->fd = chunk;
}
// double link delete
void unlink(void* chunk){
vassert(fd->bk == chunk && bk->fd == chunk, "unlink error");//早期无此检查
bk, fd = chunk->bk, chunk->fd;
bk->fd = fd;
fd->bk = bk;
}

void unlink_auto(void* chunk){
if(is_fastbin(chunk)){
/*遍历fastbin一条链将其删除*/
。。。。。
}else{
unlink(chunk)
}
}

void* __libc_malloc(size_t need_size){
chunksize = ((need_size-1)&~0b1111) + 0x20;
int idx = chunksize/0x10 - 2;

if(fastbin[idx/*0-6*/]有空闲块 && chunksize>=0x20 && chunksize<=0x80) //chunksize 0x20-0x80
return find_from_fastbin(idx)


if(bins[idx + 2 /*2-2+61*/]有空闲块 && chunksize>=0x20 && chunksize<=0x3f0) //chunksize 0x20 - 0x3f0
return find_from_smallbin(idx + 2)


// 剩下的一块作为新的 last_remainder 再次放入 Unsorted bin
if(bins[1]->fd != &bins[1]-0x10 &&
bins[1]->fd-> == &bins[1]-0x10 && // 如果 Unsorted bin 中只有一个块且这个块是 last_remainder,
&& last_remainder.size >= chunksize) // 而且大小足够, 则优先使用此块, 分裂后将前一块返回给用户,
{
if(last_remainder.size == chunksize) { // 大小足够且相等
last_remainder = nullptr;
unlink_auto(last_remainder);
return last_remainder;
}
else{ // last_remainder需要分裂
if(last_remainder->size - chunksize >= 32){ //满足最小分裂要求
// 分裂last_remainder
unlink_auto(last_remainder);
void new_last_remainder = chunk + chunksize;
new_last_remainder->size = last_remainder->size - chunksize;
// 返回last_remainder
void* res = last_remainder;
last_remainder = new_last_remainder; // 保存分裂的块
//再次放入Unsorted bin
bins[1]->fd = new_last_remainder;
return res;
}
// 不满足
}
}

// 从unsorted bins
int count = 0;
for(curr = bins[1], curr != &bins[1] - 0x10 && count<=10000; curr = curr->fd, count++){
if(curr->size == chunksize){
//没有采用unlink,注意的是这里无unkink检查,这就很好
bck = curr->bk;
...
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
return curr;
}else{//没找到
// 将此块放入到对应的 Small bin 或者 large bin 中,
// 这也是整个 glibc 堆管理中唯一会将块 放入 Small bins 与 large bins 中的代码。
unlink(curr);
// 放入Small bins or large bins
mv_to_bins(curr);
// 从Small bins 与 large bins查找
void* ret = find_from_small_large(chunksize, idx);
if(ret) return ret;
}
}

// 从 topchunk分裂一个
if(chunksize<=topchunk->size)
return topchunk;
else{ // topchunk大小不够
// 清除 Fast bins, 若 Fast bins 已空, 只能使用 sysmalloc 函数借助 系统调用拓展空间。
void* ret = malloc_consolidate(fastbins);
if (ret){
return ret
}else{// 若 Fast bins 已空 , 或者释放了还是不够
return sys_brk()
}
}
}

// -------------------------------- free ---------------------------------
void* global_prev_free_fastbin = nullptr;
void __libc_free(void* ptr){
void* chunk = ptr - 0x10;
void* fd_chunk = chunk + chunk->size;
vassert(chunk&0b1111==0, "");//对齐检查
vassert(fd_chunk->prev_inuse == true, "");// 下一块的 prev_inuse 检查
vassert(bins[1]->fd->bk == &bins[1]-0x10, "unsorted bin 指向自身检查");
if(chunk->size > 0x20 && chunk->size<=0x80){ // fastbins
//这种释放相当于直接把该chunk引用放到了fastbins,啥也不做,只是做下检查和链表链接
vassert(global_prev_free_fastbin!=chunk, "double free");
global_prev_free_fastbin = chunk;
mv_to_fastbin(chunk, chunk->size/0x10 - 2); // 直接放入fastbin
return;
}else{
fd_chunk->prev_size = chunk->size; // // 设置下一块 prev_size = size;
fd_chunk->prev_inuse = false; // 设置下一块 prev_inuse 为false;
if (chunk->prev_inuse == false){ // 前一块是释放了的
void* bk_chunk = chunk - chunk->prev_size;
vassert(bk_chunk->size == chunk->prev_size, "");
// 合并相邻的前一块
// 将上一块从其所在bins移除掉
unlink(bk_chunk);
// 上一块的 bk_chunk->size += chunk->size
// 更新下一chunk 的 prev_size = bk_chunk->size
merge(bk_chunk, chunk, fd_chunk);
}

void* fd_fd_chunk = fd_chunk + fd_chunk->size;
if( fd_fd_chunk->prev_inuse == false){ // chunk的下一块为释放的块
if(fd_chunk == top_chunk)//发现是 Top chunk
merge(fd_chunk, top_chunk)
else{
// 合并相邻的下一块
// 将下一块从其所在bins移除掉
unlink(fd_chunk);
// 当前chunk->size += fd_chunk->size
// 更新下下一chunk 的 prev_size = bk_chunk->size
merge(chunk, fd_chunk, fd_fd_chunk);
}
}
}
}

// -------------------------------- relloc ---------------------------------
void* __libc_relloc(void* ptr, size_t new_size){
void* chunk = ptr - 0x10;
chunksize = ((need_size-1)&~0b1111) + 0x20;
void *fd_chunk = chunk + chunk->size;
if(chunksize < chunk->size){ // 缩容
if(chunk->size - chunksize >=32 ){// 分裂出的部分如果达到 块的最小大小(32 字节)
chunk->size = chunksize;
_int_free(chunk + chunk->size + 0x10);
return chunk;
}else{
goto new_copy;
}

}else{ // 扩容
if(fd_chunk == top_chunk){ // 已分配的块后一块是 Top chunk
top_chunk_size = top_chunk->size - (chunksize - chunk->size);
top_chunk = chunk + new_size;
top_chunk->size = top_chunk_size;
top_chunk->prev_inuse = true;
return chunk;
}else{
void* fd_fd_chunk = fd_chunk + fd_chunk->size;
if(fd_fd_chunk->prev_inuse == false && // chunk的下一块为释放的块
(fd_chunk->size + chunk->size) - chunksize >=32 ){ // 且大小满足
unlink(fd_chunk);
_int_free(fd_chunk);
}
else if(fd_fd_chunk->prev_inuse == false && // chunk的下一块为释放的块
(fd_chunk->size + chunk->size) == chunksize){ // 且大小相等
unlink(fd_chunk);
}//否则
goto new_copy;
}
}
//impossible
return;
new_copy:
void* res = malloc(new_size);
memcpy(res, ptr, chunk->size - 0x10);
free(ptr);
return res;
}





image-20201218114127335
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

// ----------------------- unlink ---------------------------

/*
fakefd->bk == fake_chunk
fakebk->fd == fake_chunk
{
*ptr == chunk
fakefd = ptr - 0x18
fakebk = ptr - 0x10
}

fakefd->bk = fakebk
fakebk->fd = fakefd;
{
导致 *(ptr - 0x18 + 0x18) = ptr - 0x10
导致 *(ptr - 0x10 + 0x10) = ptr - 0x18
也就是 *ptr = ptr - 0x18
}

*/

Fastbin_attack

1
2
3



The House of Prime glibc [2.3.5 - 2.19)

此方法是利用 glibc 在判断 Fast Bins 的下标时的 一个疏忽来破坏main_arena中的数据, 在早期的glibc 2.3.5 版本中, main_arena 结构与现在有所不同:

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_state {
/* Serialize access. */
mutex_t mutex;
// Should we have padding to move the mutex to its own cache line?
#if THREAD_STATS
/* Statistics for locking. Only used if THREAD_STATS is defined. */
long stat_lock_direct, stat_lock_loop, stat_lock_wait;
#endif
/* The maximum chunk size to be eligible for fastbin */
INTERNAL_SIZE_T max_fast; /* low 2 bits used as flags */
/* Fastbins */
mfastbinptr fastbins[NFASTBINS]; ...

而 glibc 在根据块大小取得 Fast bins 数组下标时 使用的宏如下面所示:

#define fastbin_index(sz) ((((unsigned int)(sz)) >> 3) - 2)

若此时的 sz 为 8, 则最后得到的下标为 (8 >> 3) – 2 = –1, 这就意味着若溢出时将下一块的 sz 修改为 8,则会造成 fastbins 数组的上一个域即 max_fast 变量遭到破坏。

但在glibc 2.19版本中, main_arena的结构已经 发生了变化, 虽然 fastbin_index 宏仍然存在漏洞, 但 在 fastbins 数组的前面不再是 max_fast 变量, 此变量 被移出结构体作为全局变量 global_max_fast 使用, 这样一来, 通过破坏前一个域来进行利用的方法已 不可行。

The House of Mind. [当前流行的 glibc 中已不再可行]

此方法在当前流行的 glibc 中已不再可行, glibc 在 free 时都需要对 Unsorted bin 进行检查, 确认其中 的第一个块的后向指针是否指向自身, 如此一来若 攻击者将 Unsorted bin 指向希望控制的内存区域, 则 会导致检查失败而无法利用。

The House of Force 【需要进行heap泄露】

此方法通过溢出Top chunk的size域来欺骗glibc, 使得攻击者 malloc 一个很大的数目(负有符号整数) 时能够使用 Top chunk 来进行分配, 此时 Top chunk 的位置会加上这个大数, 造成整数溢出, 结果是 Top chunk 能够被转移到堆之前的内存地址(如程序的.bss 段, .data 段, GOT 表等), 下次再执行 malloc 时, 就会 返回转移之后的地址, 攻击者即能够控制其他的内 存数据。

32为例子

  • 攻击者溢出 Top chunk 的上一块, 将 Topchunk 的大小修改为 0xffffffff。

  • 攻击者需要能够控制下一次 malloc 时传入的

  • 大小, 此时传入 0xffffffec, 经过处理得到的大小是 0xfffffff0, glibc 的大小使用的是 size_t 类型存储, 这 是一种与机器字长相等的无符号整形。故此时 glibc 认为 0xffffffff > 0xfffffff0, 即 Top chunk 的空间足够, 于是使用 Top chunk 进行分配。假设 Top chunk 之前 的位置是 0x804a010 分配后, Top chunk 的地址将处 于 0x804a010 + 0xfffffff0 = 0x804a000。

下次 malloc 时, 攻击者就能够取得 0x804a000 这块内存, 进一步修改就能够干扰程序的运行。

此方法的缺点在于, 会受到之后提到的 ASLR 技术的影响, 如果攻击者需要修改指定位置的内存, 他需要知道当前 Top chunk 的位置以构造合适的 malloc 大小来取得对应的内存。而 ASLR 开启时, glibc 的堆内存地址将会是随机的, 如果想要利用此 项技术, 则需要进行信息泄露。

The House of Lore 【可行】

glibc 2.19也可使用,需绕过

此方法希望通过破坏已经放入 Small bins 中块 的 bk 指针来达到取得任意地址的目的

在早期的 glibc 2.3.5 版本的_int_malloc 中, 处理 Small Bins 的 代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//参考上面的 find_from_smallbin
bin = small_bin[idx] - 0x10

// 能够 chunk->bk = fake_chunk
malloc(nb){
// fake_bck = fake_chunk->bk
// 导致 bck->fd = bin;
// 导致 bin->bk = fake_bck
}
malloc(nb){



}

即如果请求的大小符合 Small Bins, 则在对应的 bin 中寻找是否有空闲的块, 如果有就直接从 bin 的 链表中解链并返回给用户。

基于上面的机制, 当一个块存在于 Small Bin 的 第一个块(这里的第一块指的是在取 Small Bin 块时 选择的第一块, 即 Small Bin 的 bk 指针指向的那一块) 时, 通过溢出修改其 bk 指针指向某个地址 ptr, 当下 一次 malloc 对应大小的块时, 就有 bck = victim->bk= ptr, 且 bin->bk = bck = ptr, 这样以来就成功地将这 个 bin 的第一块指向了 ptr, 下次再 malloc 对应大小 就能够返回 ptr+16 的位置了, 这样攻击者再对取回 的块进行写入就能控制 ptr+16 的内存内容。

尽管在 glibc 2.19 版本的代码中, 已经加入了对 应的防御机制, 通过检查 bin 中第二块的 bk (我感觉是fd) 指针是 否指向第一块, 可以发现对 Small bins 的破坏。但攻 击者如果同时伪造了 bin 中的前 2 块(smallbin->bk->bk->fd == smallbin->bk), 就可以绕过这 个检查。故此种利用方式仍然可以进行。

The House of Spirit [fastbins attack]

此方法通过堆的Fast bin机制来辅助栈溢出的利 用, 一般的栈溢出漏洞的利用都希望能够覆盖函数 的返回地址以控制 EIP 来劫持控制流, 但若栈溢出 的长度无法覆盖返回地址, 但是能够覆盖到栈上的 一个即将被 free 的堆指针, 此时可以将这个指针改 写为栈上的地址并在相应位置构造一个 Fast bin 块的 元数据。接着在 free 操作时, 这个栈上的“堆块”即 被投入 Fast Bin 中, 下一次 malloc 对应的大小时, 由 于 Fast Bin 的机制为先进后出, 故上次 free 的栈上的 “堆块”能够被优先返回给用户, 再次写入时就可能 造成返回地址的改写, 亦可进行信息泄漏。

此方法的缺点与The House of Force方法类似, 需要对栈地址进行泄漏, 否则无法正确覆盖需要释 放的堆指针, 且在构造数据时, 需要满足对齐的要 求, 存在很多的限制。

现代保护手段NX ALSR PIE RELRO

  • 不可执行位 NX Bit
  • 地址空间布局随机化 ASLR
  • 位置无关可执行文件 PIE
  • 重定位只读 RELRO
    1. 部分 RELRO: 在程序装入后, 将其中一些段 (如.dynamic)标记为只读, 防止程序的一些重定位信 息被修改。
    2. 完全 RELRO: 在部分 RELRO 的基础上, 在 程序装入时, 直接解析完所有符号并填入对应的值, 此时所有的 GOT 表项都已初始化, 且不装入 link_map 与_dl_runtime_resolve 的地址(二者都是程 序动态装载的重要结构和函数)。

堆的攻击手段

  1. 现代利用手段
  • 现代解链操作 UNLINK 攻击当前的glibc对unlink宏进行了 加固, 传统的 unlink 利用方法已不再可行。为了绕过 当前的 glibc 对 unlink 做的检查, 需要在内存中找到 指向构造的伪块的指针
  • 攻击 Fast bins
  • 释放后使用漏洞的利用
  • 两次释放漏洞的利用 double-free
  • 改写 Realloc Hook
  • 利用单字节溢出漏洞
  • 扩展被释放块
  • 扩展已分配块
  • 收缩被释放块
  • 攻击 Unsorted Bin
  • 重分配函数的利用

double free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
free(a)
// fast -> a -> null
free(b)
// fast -> b -> a -> null
free(a)
// fast -> a -> b -> a -> b
d = malloc() // = a
// fast -> b -> a -> b
e = malloc() // = b
// fast -> a -> b
change(e) {//就等于change b
e->fd = free_got;
f = malloc() // = a
// fast -> b -> free_got
malloc()
change(malloc())

}

__realloc_hook [fastbin attack]

__realloc_hook 在程序启动时有一个非 0 的初 始化值, 在 64 位系统中, 这个地址将以 7f 开头

攻击者能够通过刻意的内存地址错位来进行 Fast Bin Attack, 从而达到改写__realloc_hook 的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0x7ffff7dd3798 <_IO_stdfile_0_lock+8>:  0x0000000000000000      0x0000000000000000
0x7ffff7dd37a8 <__free_hook>: 0x0000000000000000 0x0000000000000000



0x7ffff7dd1ae8 <_IO_wide_data_0+296>: 0x0000000000000000 0x00007ffff7dd0260
0x7ffff7dd1af8: 0x0000000000000000 0x00007ffff7a92ea0
0x7ffff7dd1b08 <__realloc_hook>: 0x00007ffff7a92a70 0x0000000000000000
0x7ffff7dd1b18: 0x0000000000000000 0x0000000100000000
0x7ffff7dd1b28 <main_arena+8>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b38 <main_arena+24>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b48 <main_arena+40>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b58 <main_arena+56>: 0x0000000000000000 0x0000000000000000

__realloc_hook = main_arena - 0x20
__free_hook = main_arena - 0x1c88


// 0x7fxxxxxxxx
// chunk = 7fpointer - 8 + 5. size = 0x7f
freed_fastchunk->fd = &__realloc_hook - 8 + 5

Off-by-one 单字节溢出漏洞

通过溢出下一个块的size域,攻击者能够在堆中创造出重叠的内存块

扩展被释放块

若溢出块的下一块为被释放的块且处于Unsorted Bin 中, 则可以通过溢出一个字节来将其大 小扩大, 下次取得此块时就意味着其后的块将被覆 盖而造成进一步的溢出。

image-20201218181759112

扩展已分配块

若溢出块的下一块为使用中的块, 则需要合理 控制溢出的字节, 使其被释放时的合并操作能够顺 利进行, 例如直接加上下一块的大小使其完全被覆 盖。下一次分配对应大小时, 即可取得已经被扩大的 块, 并造成进一步溢出。

image-20201218181813210

收缩被释放块

此情况针对溢出的字节只能为 0 的情况, 此时

可以将下一个被释放块的大小缩小, 如此一来在之 后分裂此块时将无法正确更新后一块的 prev_size 域,导致释放时出现重叠的堆块。

image-20201218183231401

B2为fasten

The House of Einherjar

此方法也是针对溢出字节只能为 0 的情况, 利 用的思路是不将溢出块下一块的大小缩小, 而是仅 仅更新后一块的 prev_size 域, 使其在被释放时能够 找到之前一个合法的被释放块并与其合并, 造成堆 块的重叠

image-20201218183211613
1
2
3
4
5
C->prev_size = 0x200
LOWb(C->size) = 0 // prev_inuse = false
free(C);
malloc(0x300-8) // = A+B+C

成功后的利用方式

malloc包含已释放块

如果是fastbin那就简单

malloc包含已分配块

攻击 Unsorted Bin [可le a k]

当 Unsorted Bin 中的块恰好为分配的块大小时

则会直接从 Unsorted Bin 中移除 返回给用户。若不是, 则将其投入对应的 Bin 之中。

无论结果如何, 其最终都将从 Unsorted Bin 中移除, 而这里移除使用的方法并不是 2.3 节中释放操作使 用的 unlink 宏, 而是如下代码:

1
2
3
4
bck = victim->bk; 
...
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

不同于 unlink 宏, 这里对 Unsorted Bin 的链表完 整性并没有做检查, 导致如果能够可以修改某一个 处于 Unsorted Bin 中的块的 bk 指针, 则可以造成一 个任意内存写入, 可以将 bk + 16 的位置改写为 Unsorted Bin 的地址, 但 Unsorted Bin 的 bk 指针将被 破坏, 下一次再使用 Unsorted Bin 时, 将可能由于 bck 的位置不可写而造成程序崩溃。

Leak: 如果last->bk = ptr, 则malloc后, ptr->fd = unsorted_bin, 可以直接泄漏ptr->fd, 且无检查

2.29 tcache 【无le a k】

tcache全称thread local caching,是glibc2.26后新加入的一种缓存机制(在Ubuntu 18及之后的版本中应用),提升了不少性能,但是与此同时也大大牺牲了安全性,在ctf-wiki中介绍tcache的标题便是tcache makes heap exploitation easy again,与fastbin非常相似但优先级高于fastbin,且相对于fastbin来说少了很多检查,所以更加便于进行漏洞利用。

  • 可直接double free

tcache 和 fastbin在free中并没有被清除inuse标志

  1. tcache机制的主体是tcache_perthread_struct结构体,其中包含单链表tcache_entry

  2. 单链表tcache_entry,也即tcache Bin的默认最大数量是64,在64位程序中申请的最小chunk size为32,之后以16字节依次递增,所以size大小范围是0x20-0x410,也就是说我们必须要malloc size≤0x408的chunk

  3. 每一个单链表tcache Bin中默认允许存放的chunk块最大数量是7

  4. 在申请chunk块时,如果tcache Bin中有符合要求的chunk,则直接返回;

    如果在fastbin中有符合要求的chunk,则先将对应fastbin中其他chunk加入相应的tcache Bin中,直到达到tcache Bin的数量上限,然后返回符合符合要求的chunk;

    如果在smallbin中有符合要求的chunk,则与fastbin相似,先将双链表中的其他剩余chunk加入到tcache中,再进行返回

  5. 在释放chunk块时,如果chunk size符合tcache Bin要求且相应的tcache Bin没有装满,则直接加入相应的tcache Bin

  6. 与fastbin相似,在tcache Bin中的chunk不会进行合并,因为它们的pre_inuse位会置成1

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
	idx 0   bytes 0..24 (64-bit) or 0..12 (32-bit) 16
idx 1 bytes 25..40 or 13..20 17
idx 2 bytes 41..56 or 21..28


typedef struct tcache_entry
{
struct tcache_entry *next; //默认允许存放的chunk块最大数量是 7
} tcache_entry;

typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS]; //数组counts用于存放每个bins中的chunk数量
struct tcache_entry *entries[TCACHE_MAX_BINS/*64*/]; //数组entries用于放置64个bins
} tcache_perthread_struct;


typedef struct tcache_entry
{
//指向chunk当中的用户内存区,第一个变量是一个指针,指向tcache的下一个chunk,
//就是单链表访问
struct tcache_entry *next;
/* 这个字段是用来检测双重free释放的 */
struct tcache_perthread_struct *key;
} tcache_entry;

tcache机制允许将空闲的chunk以链表的形式缓存在线程各自tcache的bin中。

下一次malloc时可以优先在tcache中寻找符合的chunk并提取出来。

他缺少充分的安全检查,如果有机会构造内部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
42
43
44
static void
+tcache_put (mchunkptr chunk, size_t tc_idx)
+{
+ tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
+ assert (tc_idx < TCACHE_MAX_BINS);
+ e->next = tcache->entries[tc_idx];
+ tcache->entries[tc_idx] = e;
+ ++(tcache->counts[tc_idx]);
+}

+static void *
+tcache_get (size_t tc_idx)
+{
+ tcache_entry *e = tcache->entries[tc_idx];
+ assert (tc_idx < TCACHE_MAX_BINS);
+ assert (tcache->entries[tc_idx] > 0);
+ tcache->entries[tc_idx] = e->next;
+ --(tcache->counts[tc_idx]);
+ return (void *) e;
+}

#if USE_TCACHE
//检查bytes合法性,并获取请求的标准chunk大小
checked_request2size (bytes, tbytes);
//获得tcache对应请求大小bin的索引
size_t tc_idx = csize2tidx (tbytes);
//如果没有初始化tcache,那么我们初始化tcache
MAYBE_INIT_TCACHE ();

DIAG_PUSH_NEEDS_COMMENT;
//检查tcache给定索引是否有chunk满足条件,有就直接取出来
//索引大小不能超过index,并且tcache存在,并且tcache对应索引有块,初始化时不为0
if (tc_idx < mp_.tcache_bins
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);//获取tcache
}
DIAG_POP_NEEDS_COMMENT;
#endif


file:///Users/notify/ctf-wiki/pwn/linux/glibc-heap/tcache_attack-zh/

file:///Users/notify/ctf-wiki/site/pwn/linux/glibc-heap/tcache_attack-zh/index.html

References

http://phrack.org/issues/58/4.html

http://acez.re/ctf-writeup-hitcon-ctf-2014-stkof-or-modern-heap-overflow/

Glibc Adventures: The Forgotten Chunks,

https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/