sev1n75's Tech Blog

路漫漫其修远兮,吾将上下而求索

glibc2.26-2.35malloc新机制

| 0 | 前言

根据 glibc.git里glibc-2.31,2.35的malloc commit history 总结了从2.26到2.35新增的对heap exploitation有较大影响的commits

第| 1 | 部分简略总结了加入的机制

第| 2 | 和第 | 3 |部分根据commitdiff具体分析各个commit代码

| 1 | 总结

|1.1| 2.26-2.31

  • 新增了tcache_key机制

    初始化key=tcache地址

    取出时清除key

    free的时候如果有key,则遍历当前tcache检查double free

  • 新增malloc的时候,对unsoredtbin检查

    • victim,next的大小范围在2*SIZE_SZsystem_mem之间
    • prevsize(next) 必须等于 chunksize(victim)
    • bck->fd 必须等于victim ,victim->fd 必须等于unsortedbin
    • prev_inuse(next) 必须等于0
  • 新增检查chunksize(av->top)是否小于system_mem

  • 新增tcache_get检查count是否大于0

|1.2| 2.31-2.35

  • 新增fastbins tcaches Safe-Linking机制

    • PROTECT_PTR(pos,ptr)fd(tcache_entry)指针进行加密
    • pos为当前chunk将要存放指针的地址,ptr为要存放的指向下一个chunk的指针
    • PROTECT_PTR(pos,ptr) = (pos >> 12) ^ ptr
    • 检查tcache_entryfastbin->fd的值有无对其
  • 使用相对随机的值作为tcache_key

  • 移除malloc hooks

| 2 | 2.26-2.31(2017/08/02-2020/2/1)

|2.1| tcache_key

1
2
3
4
5
6
7
@@ -2967,6 +2967,8 @@ mremap_chunk (mchunkptr p, size_t new_size)
typedef struct tcache_entry
{
struct tcache_entry *next;
+ /* This field exists to detect double frees. */
+ struct tcache_perthread_struct *key;
} tcache_entry;
1
2
3
4
5
6
7
8
9
10
11
12
@@ -2990,6 +2992,11 @@ tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
+
+ /* Mark this chunk as "in the tcache" so the test in _int_free will
+ detect a double free. */
+ e->key = tcache; //初始化key = tcache堆地址
+
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
1
2
3
4
5
6
7
@@ -3005,6 +3012,7 @@ tcache_get (size_t tc_idx)
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
+ e->key = NULL; //取出时 key清零防止泄漏
return (void *) e;
}
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
@@ -4218,6 +4226,26 @@ _int_free (mstate av, mchunkptr p, int have_lock)
{
size_t tc_idx = csize2tidx (size);

+ /* Check to see if it's already in the tcache. */
+ tcache_entry *e = (tcache_entry *) chunk2mem (p);
+
+ /* This test succeeds on double free. However, we don't 100%
+ trust it (it also matches random payload data at a 1 in
+ 2^<size_t> chance), so verify it's not an unlikely coincidence
+ before aborting. */
+ if (__glibc_unlikely (e->key == tcache && tcache)) //如果要free的chunk有tcache_key,就遍历
+ {
+ tcache_entry *tmp;
+ LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
+ for (tmp = tcache->entries[tc_idx];
+ tmp;
+ tmp = tmp->next)
+ if (tmp == e) //找到地址一样的chunk
+ malloc_printerr ("free(): double free detected in tcache 2");
+ /* If we get here, it was a coincidence. We've wasted a few
+ cycles, but don't abort. */
+ }
+
if (tcache
&& tc_idx < mp_.tcache_bins
&& tcache->counts[tc_idx] < mp_.tcache_count)

|2.2| unsortedbin 检查

  • unsortedbin完整性检查

    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
    @@ -3716,11 +3716,22 @@ _int_malloc (mstate av, size_t bytes)
    for (;;) { // for大循环
    int iters = 0;
    while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { //当unsortedbin有chunk
    bck = victim->bk;
    - if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
    - || __builtin_expect (chunksize_nomask (victim)
    - > av->system_mem, 0))
    - malloc_printerr ("malloc(): memory corruption");
    size = chunksize (victim);
    + mchunkptr next = chunk_at_offset (victim, size);
    +
    + if (__glibc_unlikely (size <= 2 * SIZE_SZ) //victim太小或大于system_mem
    + || __glibc_unlikely (size > av->system_mem))
    + malloc_printerr ("malloc(): invalid size (unsorted)");
    + if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ) //next太小或大于system_mem
    + || __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
    + malloc_printerr ("malloc(): invalid next size (unsorted)");
    + if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size)) //prevsize != chunksize(victim)
    + malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
    + if (__glibc_unlikely (bck->fd != victim) //bck ->fd != victim 或victim->fd != unsoretedbin
    + || __glibc_unlikely (victim->fd != unsorted_chunks (av)))
    + malloc_printerr ("malloc(): unsorted double linked list corrupted");
    + if (__glibc_unlikely (prev_inuse(next))) //next的prev_inuse != 0
    + malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

    /*
    If a small request, try to use last remainder if it is the
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @@ -3775,6 +3775,8 @@ _int_malloc (mstate av, size_t bytes)
    }

    //前面是第一个for大循环开始,while循环里面遍历unsorted bin 所有chunk的情况下对victim从unsortedbin退链
    /* remove from unsorted list */
    + if (__glibc_unlikely (bck->fd != victim)) //新增了对victim->bk 指向的chunk的检查(但是前面不是检查过了吗)
    + malloc_printerr ("malloc(): corrupted unsorted chunks 3");
    unsorted_chunks (av)->bk = bck;
    bck->fd = unsorted_chunks (av);
  • unsorted chunk 进large bin 检查

    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
    @@ -3876,10 +3876,14 @@ _int_malloc (mstate av, size_t bytes)
    if (fwd != bck) { //当前largebin不为空

    ···

    if ((unsigned long)(size) <
    (unsigned long)(bck->bk->size)) { //bck->bk最右边那个chunk是最小的,如果比他还小,就加入bck->bk

    ···

    } else { //如果victim size比最右边的大,则用fd_nextsize找到小于等于size的chunk
    //因为最右边的fb_nextsize 指向的chunk是最大的,从大的往小的找,依次找到刚好小于等于size的chunk,就插在他左边

    ···

    //插入当前节点
    if ((unsigned long)size == (unsigned long)fwd->size) //大小相等插在右边

    ···

    else { //比fwd更大,fwd就不用改了,更新victim两个_nextsize的值和
    {
    victim->fd_nextsize = fwd;
    victim->bk_nextsize = fwd->bk_nextsize;
    //对fwd->bk_nextsize指向的chunk的fd_nextsize位置的数据进行检查
    + if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
    + malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
    fwd->bk_nextsize = victim;
    victim->bk_nextsize->fd_nextsize = victim;
    }
    bck = fwd->bk;
    //检查bck->fd位置的数据
    + if (bck->fd != fwd)
    + malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
    }
    }
    else
    ···
    }

|2.3| top chunk size 检查

1
2
3
4
5
6
7
8
9
10
11
12
@@ -4076,6 +4076,9 @@ _int_malloc (mstate av, size_t bytes)
//在切分top的部分
victim = av->top;
size = chunksize (victim);

//防止house of force
+ if (__glibc_unlikely (size > av->system_mem)) //如果size被overwrite了一个大于 system_mem的值则会crash
+ malloc_printerr ("malloc(): corrupted top size");
+
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;

|2.4| tcache_get检查

1
2
3
4
5
6
7
8
9
@@ -2946,7 +2946,7 @@ tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
- assert (tcache->entries[tc_idx] > 0);
+ assert (tcache->counts[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;

|2.5| 其他

  • 检查malloc_consolidate中chunk的size的合法性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @@ -4431,6 +4431,12 @@ static void malloc_consolidate(mstate av)
    maxfb = &fastbin(av, NFASTBINS - 1); //maxfb 是指向malloc_chunk*的指针
    fb = &fastbin(av, 0); //从fastbinY[0]开始,用地址是为了方便循环条件设置
    do {// 外层循环遍历fastbinY[]里的每一个fastbin
    p = atomic_exchange_acq(fb, 0); //malloc_chunk p = * fb
    if (p != 0) { //fastbinY[i]不空
    do { //内层循环 遍历当前fastbin 里的每一个chunk
    + {
    + unsigned int idx = fastbin_index (chunksize (p));
    + if ((&fastbin (av, idx)) != fb)
    + malloc_printerr ("malloc_consolidate(): invalid chunk size");
    + }
    +
    check_inuse_chunk(av, p);
    nextp = p->fd;
  • 修复__libc_malloc()在使用tcache时可能出现的整数溢出 (commit)

    1
    2
    3
    4
    5
    6
    7
    8
    @@ -3031,7 +3031,8 @@ __libc_malloc (size_t bytes)
    return (*hook)(bytes, RETURN_ADDRESS (0));
    #if USE_TCACHE
    /* int_free also calls request2size, be careful to not pad twice. */
    - size_t tbytes = request2size (bytes);
    + size_t tbytes;
    + checked_request2size (bytes, tbytes); //用更安全的宏
    size_t tc_idx = csize2tidx (tbytes);
  • tcache小型优化


| 3 | 2.31-2.35(2020/2/1-2022/2/3)

|3.1| Safe-Linking to fastbins and tcaches

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@@ -327,6 +327,18 @@ __malloc_assert (const char *assertion, const char *file, unsigned int line,
# define MAX_TCACHE_COUNT UINT16_MAX
#endif

+/* Safe-Linking:
+ Use randomness from ASLR (mmap_base) to protect single-linked lists
+ of Fast-Bins and TCache. That is, mask the "next" pointers of the
+ lists' chunks, and also perform allocation alignment checks on them.
+ This mechanism reduces the risk of pointer hijacking, as was done with
+ Safe-Unlinking in the double-linked lists of Small-Bins.
+ It assumes a minimum page size of 4096 bytes (12 bits). Systems with
+ larger pages provide less entropy, although the pointer mangling
+ still works. */
+#define PROTECT_PTR(pos, ptr) \
+ ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr))) //把要存到自己里面的 下一个chunk的地址 用自己的地址加密
+#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr) //用自己的地址 把自己存的内容 解密
1
2
3
4
5
6
7
8
@@ -2923,7 +2938,7 @@ tcache_put (mchunkptr chunk, size_t tc_idx)
e->key = tcache;

- e->next = tcache->entries[tc_idx];
+ e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
1
2
3
4
5
6
7
8
9
10
11
12
@@ -2934,9 +2949,11 @@ static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
- tcache->entries[tc_idx] = e->next;
+ tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
+ if (__glibc_unlikely (!aligned_OK (e))) //检查e地址对其
+ malloc_printerr ("malloc(): unaligned tcache chunk detected");
return (void *) e;
}
1
2
3
4
5
6
7
8
9
10
11
12
@@ -2960,7 +2977,10 @@ tcache_thread_shutdown (void)
while (tcache_tmp->entries[i])
{
tcache_entry *e = tcache_tmp->entries[i];
- tcache_tmp->entries[i] = e->next;
+ if (__glibc_unlikely (!aligned_OK (e)))
+ malloc_printerr ("tcache_thread_shutdown(): "
+ "unaligned tcache chunk detected");
+ tcache_tmp->entries[i] = REVEAL_PTR (e->next);
__libc_free (e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -3570,8 +3590,11 @@ _int_malloc (mstate av, size_t bytes)
victim = pp;
if (victim == NULL)
break;
+ pp = REVEAL_PTR (victim->fd);
+ if (__glibc_unlikely (!aligned_OK (pp))) //检查fastbin 对其
+ malloc_printerr ("malloc(): unaligned fastbin chunk detected");
}
- while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
+ while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim))
!= victim);

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -3583,8 +3606,11 @@ _int_malloc (mstate av, size_t bytes)

if (victim != NULL)
{
+ if (__glibc_unlikely (!aligned_OK (victim)))
+ malloc_printerr ("malloc(): unaligned fastbin chunk detected 2");
+
if (SINGLE_THREAD_P)
- *fb = victim->fd;
+ *fb = REVEAL_PTR (victim->fd);
else
REMOVE_FB (fb, pp, victim);
if (__glibc_likely (victim != NULL))
1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -3605,8 +3631,10 @@ _int_malloc (mstate av, size_t bytes)
/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
+ if (__glibc_unlikely (!aligned_OK (tc_victim)))
+ malloc_printerr ("malloc(): unaligned fastbin chunk detected 3");
if (SINGLE_THREAD_P)
- *fb = tc_victim->fd;
+ *fb = REVEAL_PTR (tc_victim->fd);
else
{
REMOVE_FB (fb, pp, tc_victim);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@@ -4196,11 +4224,15 @@ _int_free (mstate av, mchunkptr p, int have_lock)
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
- tmp = tmp->next)
+ tmp = REVEAL_PTR (tmp->next))
+ {
+ if (__glibc_unlikely (!aligned_OK (tmp)))
+ malloc_printerr ("free(): unaligned chunk detected in tcache 2");
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
+ }
}

if (tcache->counts[tc_idx] < mp_.tcache_count)
1
2
3
4
5
6
7
8
9
@@ -4264,7 +4296,7 @@ _int_free (mstate av, mchunkptr p, int have_lock)
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
- p->fd = old;
+ p->fd = PROTECT_PTR (&p->fd, old);
*fb = p;
}
else
1
2
3
4
5
6
7
8
9
10
@@ -4274,7 +4306,8 @@ _int_free (mstate av, mchunkptr p, int have_lock)
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
- p->fd = old2 = old;
+ old2 = old;
+ p->fd = PROTECT_PTR (&p->fd, old);
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2))
!= old2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@@ -4472,13 +4505,17 @@ static void malloc_consolidate(mstate av)
if (p != 0) {
do {
{
+ if (__glibc_unlikely (!aligned_OK (p)))
+ malloc_printerr ("malloc_consolidate(): "
+ "unaligned fastbin chunk detected");
+
unsigned int idx = fastbin_index (chunksize (p));
if ((&fastbin (av, idx)) != fb)
malloc_printerr ("malloc_consolidate(): invalid chunk size");
}

check_inuse_chunk(av, p);
- nextp = p->fd;
+ nextp = REVEAL_PTR (p->fd);

/* Slightly streamlined version of consolidation code in free() */
size = chunksize (p);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@@ -4896,8 +4933,13 @@ int_mallinfo (mstate av, struct mallinfo *m)

for (i = 0; i < NFASTBINS; ++i)
{
- for (p = fastbin (av, i); p != 0; p = p->fd)
+ for (p = fastbin (av, i);
+ p != 0;
+ p = REVEAL_PTR (p->fd))
{
+ if (__glibc_unlikely (!aligned_OK (p)))
+ malloc_printerr ("int_mallinfo(): "
+ "unaligned fastbin chunk detected");
++nfastblocks;
fastavail += chunksize (p);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -5437,8 +5479,11 @@ __malloc_info (int options, FILE *fp)

while (p != NULL)
{
+ if (__glibc_unlikely (!aligned_OK (p)))
+ malloc_printerr ("__malloc_info(): "
+ "unaligned fastbin chunk detected");
++nthissize;
- p = p->fd;
+ p = REVEAL_PTR (p->fd);
}

fastavail += nthissize * thissize;

|3.2| tcache_key 使用随机值替换原来的tcache地址作为key commit

1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -3108,6 +3112,31 @@ typedef struct tcache_perthread_struct
+static void
+tcache_key_initialize (void)
+{
+ if (__getrandom (&tcache_key, sizeof(tcache_key), GRND_NONBLOCK)
+ != sizeof (tcache_key))
+ {
+ tcache_key = random_bits ();
+#if __WORDSIZE == 64
+ tcache_key = (tcache_key << 32) | random_bits ();
+#endif
+ }
+}

|3.3| 移除malloc hooks

经过了几个commit之后移除了malloc hooks