0%

glibc各版本的堆保护

总结一下 glibc 中各版本一些重要保护的差异,以及这些保护对常见堆利用手法造成的影响。

暂时只总结 2.23、2.27、2.29、2.32 版本。源码来自 bminor glibc 镜像 中 release/x.xx/master 分支。

楔子

管理结构

chunk

glibc 中内存分配的单元是 chunk。所有的 chunk 都由一个结构体表示:

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

每一个域都有相应的含义和作用

  1. prev_size:用于堆块的合并

  2. size

    1. chunk 的是否正在使用、是不是 mmap 的 chunk、是不是 main arena 的 chunk。
    2. chunk 的大小
    3. 找到下一个 chunk
    4. 将 chunk 放入某个 bin 中
  3. fdbk:bin 中指向下一个 chunk。

  4. fd_nextsizebk_nextsize:largebin 中指向下一个不同 size 的 chunk。

bin

  1. fastbin:管理较小的 chunk
  2. tcachebin:libc-2.26 引入,管理中小 chunk,保护较弱(但在之后的版本中逐渐添加了许多检查)
  3. unsortedbin:管理未被分类的 chunk
  4. smallbin:管理中小 chunk
  5. largebin:管理大 chunk

arena

管理堆上重要数据结构,如 bin、topchunk 等。

小总结

通过 size 和 pre_size,chunk 以相邻关系联系起来,用于合并和分割。

通过 fd、bk、fd_nextsize、bk_nextsize,chunk 以相似属性(通常是大小)联系起来,用于分配。

基本操作

堆上的基本操作大致可以分为以下几种类型:

  1. chunk 放入 bin 中
  2. chunk 合并
  3. chunk 切割

理解 glibc 堆的基本操作是搞懂 glibc 上保护的基础。

漏洞

常见的堆漏洞可以分为三种类型:

  1. 堆溢出
  2. 随机越界写
  3. UAF

这些漏洞还可以细分:strcpyread 造成的堆溢出在利用方式上会有差别。堆溢出也有特殊的类型:如 off-by-one 和 off-by-null。UAF 可以是读、写、使用函数指针等。

不过漏洞并不是这篇文章的主题,我之后也许还会总结一下遇到的漏洞。

保护

为了保护与堆相关的数据,glibc 中加入了一系列检查。保护的作用是找出数据不合理的地方,输出相关信息后退出程序。我觉得不合理大致可以分为以下几种:

  1. 不合理的数据范围(size 大小、指针对齐等)
  2. 不合理的 bin 链表结构
  3. 不合理的 chunk 布局

下面就通过不同版本 glibc 中 _int_malloc_int_free 中的检查,来讨论这些不合理。

next->prev_size != size

2.27、2.29、2.32

1
2
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");

双向链表完整性检查

2.23、2.27、2.29、2.32

1
2
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");

双向链表完整性检查(large)

2.23、2.27、2.29、2.32

1
2
3
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");

__libc_malloc & _int_malloc

fastbin、tcache fd 保护

2.32

利用 aslr 保护 fastbin 和 tcache 的 fd:

1
2
3
4
5
6
7
8
9
10
11
12
/* 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)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

只要泄漏了 fd 的地址就可以绕过。

从 fastbin 中找

size 不匹配

2.23、2.27、2.29、2.32

1
2
3
size_t victim_idx = fastbin_index (chunksize (victim));
if (__builtin_expect (victim_idx != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");

chunk 没有对齐

2.32

1
2
if (__glibc_unlikely (misaligned_chunk (victim)))
malloc_printerr ("malloc(): unaligned fastbin chunk detected 2");

从 fastbin 中 stash chunk 至 tcache

chunk 没有对齐

1
2
if (__glibc_unlikely (misaligned_chunk (tc_victim)))
malloc_printerr ("malloc(): unaligned fastbin chunk detected 3");

从 smallbin 中找

双向链表完整性检查 bck->fd != victim

2.23、2.27、2.29、2.32

1
2
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");

遍历 unsortedbin

victim 是当前处理的 unsortedbin chunk,next 是与 victim 相邻的高地址处 chunk。

检查 victim

victim size 不合理

2.23、2.27、2.29、2.32

1
2
3
if (__glibc_unlikely (size <= 2 * SIZE_SZ)
|| __glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): invalid size (unsorted)");
next size 不合理

2.29、2.32

1
2
3
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
next->prev_size != victim size

2.29、2.32

1
2
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
双向链表完整性检查 (bck->fd != victim) || (victim->fd != unsorted_chunks (av))

2.29、2.32

1
2
3
if (__glibc_unlikely (bck->fd != victim)
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
next prev_inuse 位

2.29、2.32

1
2
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

将 victim 放入相应 bin

bck->fd != victim
1
2
3
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
放入 largebin

初始时,bck 为 largebin 头部,fwd 为 bck->fd。

largebin 中 main arena 检查

2.29、2.32

1
assert (chunk_main_arena (bck->bk));
1
2
3
4
5
6
7
assert (chunk_main_arena (fwd));
/* 找到size大于等于victim的chunk */
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
双向链表完整性检查 (fwd->bk_nextsize->fd_nextsize != fwd)

2.29、2.32

1
2
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
双向链表完整性检查 (bck->fd != victim)
1
2
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

从 largebin 中找

分割剩余部分放入 unsortedbin

unsortedbin 双向链表完整性检查

2.23、2.27、2.29、2.32

1
2
3
4
5
/* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here.  */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
malloc_printerr ("malloc(): corrupted unsorted chunks");

从更大的 bin 中找

访问 binmap

确保一定能找到 bin

2.23、2.27、2.29、2.32

1
2
3
4
5
6
7
/* Advance to bin with set bit. There must be one. */
while ((bit & map) == 0)
{
bin = next_bin (bin);
bit <<= 1;
assert (bit != 0); // 避免无限循环
}

找到了更大的 bin

victim = last (bin);

victim size >= nb

2.23、2.27、2.29、2.32

1
2
/*  We know the first chunk in this bin is big enough to use. */
assert ((unsigned long) (size) >= (unsigned long) (nb));

分割剩余部分放入 unsortedbin

unsortedbin 双向链表完整性检查

2.23、2.27、2.29、2.32

1
2
3
4
5
/* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here.  */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
malloc_printerr ("malloc(): corrupted unsorted chunks 2");

使用 topchunk

topchunk size 不合理

2.29、2.32

1
2
if (__glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): corrupted top size");

返回找到的 chunk

单线程时检查 victim 是否合法

1
2
3
4
5
6
7
if (SINGLE_THREAD_P)
{
victim = _int_malloc (&main_arena, bytes);
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
&main_arena == arena_for_chunk (mem2chunk (victim)));
return victim;
}

__libc_free & _int_free

地址不合理、没对齐

1
2
3
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
malloc_printerr ("free(): invalid pointer");

chunk size 不合理、没对齐

1
2
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
malloc_printerr ("free(): invalid size");

尝试放入 tcache

tcache double free、没对齐

利用位于 bk 位的 key 域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
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. */
}
}

尝试放入 fastbin

next chunk size 不合理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
<= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0))
{
bool fail = true;
/* We might not have a lock at this point and concurrent modifications
of system_mem might result in a false positive. Redo the test after
getting the lock. */
if (!have_lock)
{
__libc_lock_lock (av->mutex);
fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
|| chunksize (chunk_at_offset (p, size)) >= av->system_mem);
__libc_lock_unlock (av->mutex);
}

if (fail)
malloc_printerr ("free(): invalid next size (fast)");
}

fastbin double free

1
2
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");

old size 不匹配

1
2
3
if (have_lock && old != NULL
&& __builtin_expect (fastbin_index (chunksize (old)) != idx, 0))
malloc_printerr ("invalid fastbin entry (free)");

检查 nextchunk

nextchunk 是 topchunk

1
2
if (__glibc_unlikely (p == av->top))
malloc_printerr ("double free or corruption (top)");

nextchunk 超出了 arena 的边界

1
2
3
4
if (__builtin_expect (contiguous (av)
&& (char *) nextchunk
>= ((char *) av->top + chunksize(av->top)), 0))
malloc_printerr ("double free or corruption (out)");

nextchunk 的 prev_inuse 位未设置

1
2
if (__glibc_unlikely (!prev_inuse(nextchunk)))
malloc_printerr ("double free or corruption (!prev)");

nextsize 不合理

1
2
3
if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0))
malloc_printerr ("free(): invalid next size (normal)");

尝试向前合并

prev_size != chunk size

1
2
3
4
5
6
7
8
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

放入 unsortedbin

unsortedbin 双向链表完整性检查

1
2
3
4
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
malloc_printerr ("free(): corrupted unsorted chunks");

malloc_consolidate

取出 fastbin chunk

chunk 地址不对齐

1
2
3
if (__glibc_unlikely (misaligned_chunk (p)))
malloc_printerr ("malloc_consolidate(): "
"unaligned fastbin chunk detected");

chunk size 不合理

1
2
3
unsigned int idx = fastbin_index (chunksize (p));
if ((&fastbin (av, idx)) != fb)
malloc_printerr ("malloc_consolidate(): invalid chunk size");

尝试向前合并

prev_size != chunk size

1
2
3
4
5
6
7
8
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size in fastbins");
unlink_chunk (av, p);
}

__libc_realloc & _int_realloc

sysmalloc

其它

malloc 中的 assert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifdef NDEBUG
# define assert(expr) ((void) 0)
#else
# define assert(expr) \
((expr) \
? ((void) 0) \
: __malloc_assert (#expr, __FILE__, __LINE__, __func__))

extern const char *__progname;

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

check_remalloced_chunk

在很多地方都有这个宏,而且检查也挺细致的,不过只有在 MALLOC_DEBUG 宏被定义时才会被展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef MALLOC_DEBUG
#define MALLOC_DEBUG 0
#endif

#if !MALLOC_DEBUG

# define check_chunk(A, P)
# define check_free_chunk(A, P)
# define check_inuse_chunk(A, P)
# define check_remalloced_chunk(A, P, N)
# define check_malloced_chunk(A, P, N)
# define check_malloc_state(A)

#else

# define check_chunk(A, P) do_check_chunk (A, P)
# define check_free_chunk(A, P) do_check_free_chunk (A, P)
# define check_inuse_chunk(A, P) do_check_inuse_chunk (A, P)
# define check_remalloced_chunk(A, P, N) do_check_remalloced_chunk (A, P, N)
# define check_malloced_chunk(A, P, N) do_check_malloced_chunk (A, P, N)
# define check_malloc_state(A) do_check_malloc_state (A)

alloc_perturb

在成功申请到 chunk 后都会有这个函数,不过只有调用 malloc_opt 设置 M_PERTURB 后才会启用。

Man page 中的介绍:

​ M_PERTURB (since glibc 2.4)
​ If this parameter is set to a nonzero value, then bytes of
​ allocated memory (other than allocations via calloc(3))
​ are initialized to the complement of the value in the
​ least significant byte of value, and when allocated memory
​ is released using free(3), the freed bytes are set to the
​ least significant byte of value. This can be useful for
​ detecting errors where programs incorrectly rely on
​ allocated memory being initialized to zero, or reuse
​ values in memory that has already been freed.

​ The default value for this parameter is 0.

看起来是检测 UAF 用的。