总结一下 glibc 中各版本一些重要保护的差异,以及这些保护对常见堆利用手法造成的影响。
暂时只总结 2.23、2.27、2.29、2.32 版本。源码来自 bminor glibc 镜像 中 release/x.xx/master 分支。
楔子
管理结构
chunk
glibc 中内存分配的单元是 chunk。所有的 chunk 都由一个结构体表示:
1 | struct malloc_chunk { |
每一个域都有相应的含义和作用
prev_size
:用于堆块的合并size
:- chunk 的是否正在使用、是不是 mmap 的 chunk、是不是 main arena 的 chunk。
- chunk 的大小
- 找到下一个 chunk
- 将 chunk 放入某个 bin 中
fd
和bk
:bin 中指向下一个 chunk。fd_nextsize
和bk_nextsize
:largebin 中指向下一个不同 size 的 chunk。
bin
- fastbin:管理较小的 chunk
- tcachebin:libc-2.26 引入,管理中小 chunk,保护较弱(但在之后的版本中逐渐添加了许多检查)
- unsortedbin:管理未被分类的 chunk
- smallbin:管理中小 chunk
- largebin:管理大 chunk
arena
管理堆上重要数据结构,如 bin、topchunk 等。
小总结
通过 size 和 pre_size,chunk 以相邻关系联系起来,用于合并和分割。
通过 fd、bk、fd_nextsize、bk_nextsize,chunk 以相似属性(通常是大小)联系起来,用于分配。
基本操作
堆上的基本操作大致可以分为以下几种类型:
- chunk 放入 bin 中
- chunk 合并
- chunk 切割
理解 glibc 堆的基本操作是搞懂 glibc 上保护的基础。
漏洞
常见的堆漏洞可以分为三种类型:
- 堆溢出
- 随机越界写
- UAF
这些漏洞还可以细分:strcpy
和 read
造成的堆溢出在利用方式上会有差别。堆溢出也有特殊的类型:如 off-by-one 和 off-by-null。UAF 可以是读、写、使用函数指针等。
不过漏洞并不是这篇文章的主题,我之后也许还会总结一下遇到的漏洞。
保护
为了保护与堆相关的数据,glibc 中加入了一系列检查。保护的作用是找出数据不合理的地方,输出相关信息后退出程序。我觉得不合理大致可以分为以下几种:
- 不合理的数据范围(size 大小、指针对齐等)
- 不合理的 bin 链表结构
- 不合理的 chunk 布局
下面就通过不同版本 glibc 中 _int_malloc
和 _int_free
中的检查,来讨论这些不合理。
unlink
next->prev_size != size
2.27、2.29、2.32
1 | if (chunksize (p) != prev_size (next_chunk (p))) |
双向链表完整性检查
2.23、2.27、2.29、2.32
1 | if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) |
双向链表完整性检查(large)
2.23、2.27、2.29、2.32
1 | if (p->fd_nextsize->bk_nextsize != p |
__libc_malloc & _int_malloc
fastbin、tcache fd 保护
2.32
利用 aslr 保护 fastbin 和 tcache 的 fd:
1 | /* Safe-Linking: |
只要泄漏了 fd 的地址就可以绕过。
从 fastbin 中找
size 不匹配
2.23、2.27、2.29、2.32
1 | size_t victim_idx = fastbin_index (chunksize (victim)); |
chunk 没有对齐
2.32
1 | if (__glibc_unlikely (misaligned_chunk (victim))) |
从 fastbin 中 stash chunk 至 tcache
chunk 没有对齐
1 | if (__glibc_unlikely (misaligned_chunk (tc_victim))) |
从 smallbin 中找
双向链表完整性检查 bck->fd != victim
2.23、2.27、2.29、2.32
1 | if (__glibc_unlikely (bck->fd != victim)) |
遍历 unsortedbin
victim 是当前处理的 unsortedbin chunk,next 是与 victim 相邻的高地址处 chunk。
检查 victim
victim size 不合理
2.23、2.27、2.29、2.32
1 | if (__glibc_unlikely (size <= 2 * SIZE_SZ) |
next size 不合理
2.29、2.32
1 | if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ) |
next->prev_size != victim size
2.29、2.32
1 | if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size)) |
双向链表完整性检查 (bck->fd != victim) || (victim->fd != unsorted_chunks (av))
2.29、2.32
1 | if (__glibc_unlikely (bck->fd != victim) |
next prev_inuse 位
2.29、2.32
1 | if (__glibc_unlikely (prev_inuse (next))) |
将 victim 放入相应 bin
bck->fd != victim
1 | /* remove from unsorted list */ |
放入 largebin
初始时,bck 为 largebin 头部,fwd 为 bck->fd。
largebin 中 main arena 检查
2.29、2.32
1 | assert (chunk_main_arena (bck->bk)); |
1 | assert (chunk_main_arena (fwd)); |
双向链表完整性检查 (fwd->bk_nextsize->fd_nextsize != fwd)
2.29、2.32
1 | if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) |
双向链表完整性检查 (bck->fd != victim)
1 | if (bck->fd != fwd) |
从 largebin 中找
分割剩余部分放入 unsortedbin
unsortedbin 双向链表完整性检查
2.23、2.27、2.29、2.32
1 | /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ |
从更大的 bin 中找
访问 binmap
确保一定能找到 bin
2.23、2.27、2.29、2.32
1 | /* Advance to bin with set bit. There must be one. */ |
找到了更大的 bin
victim = last (bin);
victim size >= nb
2.23、2.27、2.29、2.32
1 | /* We know the first chunk in this bin is big enough to use. */ |
分割剩余部分放入 unsortedbin
unsortedbin 双向链表完整性检查
2.23、2.27、2.29、2.32
1 | /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ |
使用 topchunk
topchunk size 不合理
2.29、2.32
1 | if (__glibc_unlikely (size > av->system_mem)) |
返回找到的 chunk
单线程时检查 victim 是否合法
1 | if (SINGLE_THREAD_P) |
__libc_free & _int_free
地址不合理、没对齐
1 | if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0) |
chunk size 不合理、没对齐
1 | if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size))) |
尝试放入 tcache
tcache double free、没对齐
利用位于 bk 位的 key 域。
1 | if (__glibc_unlikely (e->key == tcache)) |
尝试放入 fastbin
next chunk size 不合理
1 | if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size)) |
fastbin double free
1 | if (__builtin_expect (old == p, 0)) |
old size 不匹配
1 | if (have_lock && old != NULL |
检查 nextchunk
nextchunk 是 topchunk
1 | if (__glibc_unlikely (p == av->top)) |
nextchunk 超出了 arena 的边界
1 | if (__builtin_expect (contiguous (av) |
nextchunk 的 prev_inuse 位未设置
1 | if (__glibc_unlikely (!prev_inuse(nextchunk))) |
nextsize 不合理
1 | if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0) |
尝试向前合并
prev_size != chunk size
1 | if (!prev_inuse(p)) { |
放入 unsortedbin
unsortedbin 双向链表完整性检查
1 | bck = unsorted_chunks(av); |
malloc_consolidate
取出 fastbin chunk
chunk 地址不对齐
1 | if (__glibc_unlikely (misaligned_chunk (p))) |
chunk size 不合理
1 | unsigned int idx = fastbin_index (chunksize (p)); |
尝试向前合并
prev_size != chunk size
1 | if (!prev_inuse(p)) { |
__libc_realloc & _int_realloc
sysmalloc
其它
malloc 中的 assert
1 |
|
check_remalloced_chunk
在很多地方都有这个宏,而且检查也挺细致的,不过只有在 MALLOC_DEBUG 宏被定义时才会被展开:
1 |
alloc_perturb
在成功申请到 chunk 后都会有这个函数,不过只有调用 malloc_opt
设置 M_PERTURB 后才会启用。
Man page 中的介绍:
M_PERTURB (since glibc 2.4)
If this parameter is set to a nonzero value, then bytes of
allocated memory (other than allocations via calloc(3))
are initialized to the complement of the value in the
least significant byte of value, and when allocated memory
is released using free(3), the freed bytes are set to the
least significant byte of value. This can be useful for
detecting errors where programs incorrectly rely on
allocated memory being initialized to zero, or reuse
values in memory that has already been freed. The default value for this parameter is 0.
看起来是检测 UAF 用的。