house of mind fastbin
很有趣的一个攻击,用到了新的知识点。虽然名字是 fastbin,但其实是用的多线程中寻找 arena 的方式。
最终效果:很一般的效果,向堆中某个地址写入堆的某个地址。
原理
对于单线程而言,heap 的管理很简单,此时用的是全局变量 main_arena,即通过这个变量去找 fastbin 在哪里等等操作。但是对于多线程而言,heap 的管理就复杂了,下面进行详细介绍。
多线程下共用单 heap 问题
在以前,glibc 多线程也是使用一个堆空间,但这个效率会很低。
首先:多线程并不意味着轮流执行,同一时刻只有一个线程运行。在现代多核 CPU 中,真的有可能是在同时运行。
CPU Core 0 → Thread A
CPU Core 1 → Thread B
CPU Core 2 → Thread C
CPU Core 3 → Thread D
其次:多线程意味着要上锁,而 malloc 和 free 不是原子操作,所以如果在中途退出,那么会大大降低效率。
Thread A -> malloc
Thread A -> lock heap
Thread A -> malloc 还未结束时退出
Thread B -> malloc
Thread B -> lock heap -> 被占用,等待 heap 被释放
解决思路:各线程都有各自 heap
那么如何改善,显然让各个线程都有各自的 heap,这个就非常好,各个线程各自进行 malloc 和 free 的时候,不用担心上锁的问题。
当时我的一个疑问:教科书上不是说多线程中共享栈、共享堆的吗?如果各个线程自己有各自堆,那还是共享吗?
回答:还是共享。因为这里的各自的堆不是说看不见的,举例而言:
1. Thread A 申请了一个数组data,那么它的内容地址在 Heap A 中,那么 Thread B 也是知道这个data的地址的(为什么知道,这种全局变量可能是保存在一段共享内存)。
2. Thread B 依然可以访问或者修改data,只不过这个时候就触发了访问 Heap A,此时需要加把锁才行,那看来还是需要有锁机制。
3. 总结而言,Thread 访问 Heap(即便是自己的)依然需要锁机制,只不过这种的资源竞争概率比共用一个 Heap 大幅度降低。
实际情况:线程和 heap 不是一对一
但实际情况不是这样的,一个程序可能会开好多个线程,这样要好多 heap 空间,我们知道 heap 大小一般是固定的,比如 132KB,可想而知这样有多可怕。
所以其实是线程共用一个 heap 池,即 heap 有最大数量。对于 32-bit 系统,是 1+2*CoreNum;对于 64-bit 系统,是 1+8*CoreNum。
举例而言,假设一个多线程应用,是主线程 + 3 个用户线程,而系统上限是只能创建 3 个 Heap,共享过程如下:
- 主线程第一次调用 malloc:使用已存在的 main arena,无竞争。
- 线程 1 和线程 2 第一次调用 malloc:各自创建新的 arena 并使用,仍然无竞争。到这里为止,线程与 arena 仍是 一对一 的关系。
-
线程 3 第一次调用 malloc:发现已超过限制,于是不再新建 arena,而是遍历已有的 arena,尝试对某个 arena 加锁:
- 若成功(例如成功锁住 main arena),则将该 arena 返回给线程 3 使用;
- 若所有 arena 都被占用,则阻塞,等待下一个可用的 arena(比如之后线程 2 的
malloc完成结束,它释放了锁)
-
假如线程 3 成功锁住某个 arena,那么线程 3 以后就默认用这个 arena 了。(这是我的个人理解)
其实上面的过程,就是每个 Heap 加上锁机制。还是要强调一个观点:多个线程共用 heap 没有关系,只要上锁解锁能弄好就行。
对于上面第 4 小点,为什么我会个人认为会默认尽量让线程和 arena 绑定关系不动呢?因为我认为线程大部分都是操控自己申请的变量,比如线程 A 申请了 pa=malloc(0x100)、线程 B 申请了 pb=malloc(0x100),那么线程 A 大部分时候是对 pa 进行操作,而不是操作别人申请的变量。所以默认不动 arena,这样大概率不会要等待锁。
实际情况:arena 和 heap
上面的讲解中,都认为是对 Heap 进行和线程的绑定,其实是 arena,它是多个 Heap 组成的区域。一开始的时候 arena 里面只有一个 Heap(如默认的 132KB),当 Heap 用完之后,系统就会再申请一个 Heap,如此反复。
这个会给上面的多线程带来一个问题:arena 里面管理了 fastbin 等等链表,这个肯定是放在头部的,当有多个 Heap 的时候,释放某个块时,如何知道他是哪一个 arena?
如下图所示,蓝色和粉色是不同的 arena,他们各自有三个和两个 Heap。现在释放某个块的时候,肯定要更新 fastbin 等链表,那么怎么去找到蓝色 arena 头部的管理结构?即如何找到 arena 的基地址?

当 arena 只有一个 heap 的时候,其实很好知道基地址,即 (ptr) & ~(HEAP_MAX_SIZE - 1) 就行了。但是多个 heap 就不可以了,这里脑洞一下,看到这里我感觉这个很像磁盘管理,所以可以使用 FAT 等文件系统的做法:
- 如果 heap 是连续的,我们可以在每个 heap 的开头标上自己属于 arena 第几个 heap 了,这样可以迅速找到第一个 heap 的地址。
- 如果 heap 不是连续的,我们可以在每个 heap 的开头标上
prev和next指针。
这里说一下 glibc 的做法,其实它有点像第二小点,在开头标上指针。这里的管理内容叫 heap_info,只不过 heap_info 里面其中一项存的是 ar_ptr,即管理 fastbin 链表等数据结构的地方。如下图所示:

当时的疑问:为什么需要这么麻烦?每个线程中保存一个 arena 的变量不就可以了?
回答:当前操作的块不一定就是自己 arena 中的。比如在 Thread A 是生产者,它不断申请了块p=malloc(0x20);而 Thread B 是消费者,它不断的free(p)。对于 Thread B 来说,此时p不在 arena b 中,所以只能通过这种方式进行。
其他说明
在 chunk 的 size 中,末尾三位用于一些特殊含义,比如最后一位是 prev_in_use,而倒数第三位就是 NON_MAIN_ARENA,这个就是用于表示 chunk 是否是 main arena 中的。如果是的,直接用全局变量 main_arena 即可;如果不是,则需要按照上面的规则,先找 heap_info,再找 ar_ptr。
攻击说明
攻击就是伪造一个 arena,下面进行说明。
1. 申请一块空间用于 fake arena
如下所示,申请一个空间,这个空间用于 fake_arena,就是上图中的黑色块,用于管理 arena 的数据结构(比如 fastbin 链表、tcache 链表等)。
uint8_t* fake_arena = malloc(0x1000);
printf("Set 'system_mem' (offset 0x888) for fake arena\n");
fake_arena[0x888] = 0xFF;
fake_arena[0x889] = 0xFF;
fake_arena[0x88a] = 0xFF;
这个结构叫做 malloc_state,其中 0x888-0x88a 写入 0xFF 是写入这个结构中的 system_mem 字段,这个是为了过掉检查:
if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim)
> av->system_mem, 0))
malloc_printerr ("malloc(): memory corruption");
struct malloc_state
{
// ......
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
// .....
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
2. 准备下一个 Heap
如下代码,首先计算下一个 Heap 的基地址,这个是很平常的计算方式,没啥说的;然后就是不断申请内存,直到最终系统会新申请一片内存。
uint64_t new_arena_value = (((uint64_t) fake_arena) + HEAP_MAX_SIZE) & ~(HEAP_MAX_SIZE - 1);
uint64_t* user_mem = malloc(MAX_SIZE);
while((long long)user_mem < new_arena_value){
user_mem = malloc(MAX_SIZE);
}

3. 准备 heap info
如下代码,这个没啥好说的,就是伪造第二个 Heap 块最开始的部分,这个最开始的部分就是 heap_info,它里面的 ar_ptr 就是在第一个,所以偏移量是 0:
uint64_t* fake_heap_info = (uint64_t*) new_arena_value;
fake_heap_info[0] = (uint64_t) fake_arena;

4. 准备一个 fastbin chunk
如下代码,最后一句是把 size 里面倒数第三个 bit 置为 1,即 NO_MAIN_ARENA。
uint64_t* fastbin_chunk = malloc(0x50); // Size of 0x60
for(int i = 0; i < 7; i++){ tcache_chunks[i] = malloc(0x50); }
for(int i = 0; i < 7; i++){ free(tcache_chunks[i]); }
uint64_t* chunk_ptr = fastbin_chunk - 2; // Point to chunk instead of mem
chunk_ptr[1] = 0x60 | 0x4; // Setting the non-main arena bit

5. 释放块进行攻击
此时释放这个块,会找到 heap_info,继而找到 ar_ptr,正常来说应该就是把 fastbin 中 0x50 对应的链表填入 fastbin_chunk 的地址。

评价
其实攻击中前三步还是相当有趣的,但是第四步和第五步就有点比较没有想象力了,最终的效果就是写入了堆上的地址,感觉效果很一般。但是前三步搭建了很不错的舞台,这个应该是有其他的利用价值的。
参考
- https://maxwelldulin.com/BlogPost?post=2257705984 :how2heap 中对应攻击方法的作者的博客文章
- https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc :相当推荐的一篇关于 malloc 的介绍文章
- https://hornos3.github.io/2023/02/28/how2heap-%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0-7 :一篇中文博客,作为补充点很不错