在2001年底,"Vudo Malloc Tricks"和"Once Upon A free()"定义了Linux中的动态内存块溢出的开发。在2004年底,一系列关于GNU libc malloc的补丁实现了十多个强制性完整性断言,有效地地说明了现有的技术已经过时了。
正是由于这个原因,一个几乎不可能的小的建议,就是我打算展示Malloc Maleficarum。
● The House of Prime
● The House of Mind
● The House of Force
● The House of Lore
● The House of Spirit
● The House of Chaos
The House of Prime
首先介绍The House of Prime。一个艺术家有一幅画的第一次描边,一个作家总有过写出一首诗的第一行的经历。而对于我来说,在堆溢出发面,则是House of Prime。这是第一次突破,同时也表明了接下来一切都会水到渠成。可以说,这就是拒绝不可能,同时这也是相当困难的。由于这些原因,我觉得有必要将Prime放在Malloc Maleficarum的首要位置,而且,这也是它应得的。
从纯粹的技术角度出发,House of Prime或许是收集中最没有用的了。当条件允许的时候,使用House of Mind 或者House of Sprit 几乎可以说总是更好的。为了成功地使用House of Prime技术,我们必须释放两个由自己控制的不同size字段的chunk,然后触发对malloc的调用。
该技术大致的思想是在某些不可控的情况下(下面讨论),破坏fastbin的最大值,以便于允许我们劫持由malloc调用的时所使用参数的数据结构arena。这样就允许返回任意内存块,或者直接修改可执行的控制数据。
正如前面所说的,这个技术首先调用free函数释放由我们自己控制的内存。调用free函数会继续调用public_fREe,进而调用内部的函数_int_free。对于House Of Prime来说,public_fREe的细节相对来说不重要。因此我们只需要关注_int_free即可。这是它在glibc-2.3.5中的代码:
void _int_free(mstate av, Void_t* mem)
{
mchunkptr p; /* chunk corresponding to mem */
INTERNAL_SIZE_T size; /* its size */
mfastbinptr* fb; /* associated fastbin */
...
p = mem2chunk(mem);
size = chunksize(p);
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect ((uintptr_t) p & MALLOC_ALIGN_MASK, 0))
{
errstr = "free(): invalid pointer";
errout:
malloc_printerr (check_action, errstr, mem);
return;
}
然后就会出现了一个大肆吹嘘的完整性测试。__builtin_expect被构造用于优化,并且不影响结果的正确性。我们必须确保两个测试都失败以便于继续执行。在这个阶段,达到这样的效果并不是很难。
注意,我们并没有控制p的值。因此可以假设对于非对齐的测试将会失败。另一方面,我们控制了size的值。事实上,这是我们所拥有的最重要的控制,但其范围受到了限制。对于House of Prime,size的确切的上界限制并不重要。下界的限制才是这一技术正确执行的关键。
#define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA)
#define chunksize(p) ((p)->size & ~(SIZE_BITS))
其中PREV_INUSE, IS_MMAPPED 以及 NON_MAIN_ARENA 的定义对应着一个申请的块中size entry里最没有意义的三个比特。Chunksize宏用于清除这三个比特位,这就意味着我们可以控制的chunk最小的值就是8。我们继续看_int_free,然后就清楚为什么这很重要了。
if ((unsigned long)(size) <= (unsigned long)(av->max_fast))
{
if (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0))
{
errstr = "free(): invalid next size (fast)";
goto errout;
}
set_fastchunks(av);
fb = &(av->fastbins[fastbin_index(size)]);
if (__builtin_expect (*fb == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
p->fd = *fb;
*fb = p;
}
这就是fastbin的代码了。至于什么是fastbin以及为什么它们会被使用已经超出了本文的范围了。但是请记住,House of Prime的第一步就是覆盖fastbin的最大size,av->max_fast。为了达到这样的效果,从上面可以看出,我们必须首先提供具有较低下限大小的块。考虑到av->max_fast的默认值是72,我们就很容易知道fastbin代码主要是对于不大于这样大小的块进行操作。但是,也正是因为这个才会导致av-max_fast的损坏不会立即出现效果。
我们应该注意到av是arena的指针,这个东西是一个控制结构,包含了fastbin的最大值,指向fastbins的指针数组。而且,av->max_fast和av->fastbins是连续的。
...
INTERNAL_SIZE_T max_fast;
mfastbinptr fastbins[NFASTBINS];
mchunkptr top;
...
假设下一个大小完整性检验失败了。那么fb就会被设置为&(av->fastbins[fastbin_index(size)]),而其中的size是我们给定的。最后计算出来的下标是由0开始的。然而,下标0对应的chunk大小为最小大小16(一个最小的malloc的块包含prev_size和size)。因此,当我们提供下限值8的时候,什么会发生呢?我们需要进一步分析fastbin_index()。
#define fastbin_index(sz) ((((unsigned int)(sz)) >> 3) - 2)
我们可以看出,如果原来是8的话,经过宏运算就会得到-1。因此,fb就会被设置为av->fastbins[-1]的地址。由于av->max_fast后续跟着av->fastbins相邻,因此很明显,fb被设置为了&av->max_fast。
进一步,由于这时fb不指向p,第二个完整性测试也会失败,最后两行的代码就会被执行。故而,我们的chunk p的forward指针被设置为av->max_fast,av->max_fast被设置为p的值。
上面我们做了一个假设,那就是下一个大小的完整性检验失败。现实中,我们通常需要做一些工作才可以让它们一起失败。如果溢出可以写空字节,那我们就可以很容易达到目的了。然而,如果溢出以一个空字节结尾,那么我们的解法就会依赖于特定的应用。如果测试是因为溢出时自然的内存布局,那我们就不需要依赖于一些固定的条件了,这就没有问题了。否则,我们就需要做一些内存布局的操作来确保nextsize的值是由我们控制的。
然而,House of Prime的挑战部分并不是去覆盖av->max_fast,而是如何利用覆盖进行任意代码执行。House of Prime通过覆盖特定线程变量arena_key来做到这一点。这就是House of Prime所需要的最难的条件。首先,arena_key仅仅在glibc中的malloc函数使用USE_ARENAS标志(默认使用)被编译的时候才存在。进一步,最重要的是,arena_key的地址一定会比真实的arena地址高。
0xb7f00000 <main_arena>: 0x00000000
0xb7f00004 <main_arena+4>: 0x00000049 <-- max_fast
0xb7f00008 <main_arena+8>: 0x00000000 <-- fastbin[0]
0xb7f0000c <main_arena+12>: 0x00000000 <-- fastbin[1]
....
0xb7f00488 <mp_+40>: 0x0804a000 <-- mp_.sbrk_base
0xb7f0048c <arena_key>: 0xb7f00000
由于arena结构和arena_key来自不同的源文件,这种情况发生与否取决于目标libc是如何编译和链接的。我看到过两种不同的形式,因此,这一点很重要。对于现在,我们假设arena_key在高地址上,因此可以被fastbin 代码覆盖。
Arena_key是线程特殊的数据,也就意味着每一个执行的线程都有自己独立的arena_key,不受其他线程的影响。因此,在将House of Prime技术应用到一个线程程序的时候,我们可以考虑一下,否则arena_key可以安全地被作为正常数据去处理。
Arena_key是一个很有意思的目标,因为它被宏arena_get使用来找到正在执行的线程。也就是说,如果arena_key被某个线程控制,并且areba_get被调用了,那么arena就可以被劫持了。这种类型的arena劫持接下来很快就会讲,但是在讲之前我们必须先来考虑一下实际重写arena_key。
为了覆盖arena_key,fastbin代码又会被利用。这对应着我们第二次调用free来释放我们可以控制大小的块。这在House of Prime的先决条件中已经列出来了。正常情况下,fastbin代码不能够写超过av->fastbin,但是既然av->max_fast在先前已经被破坏了,任何一个大小小于我们第一次使用的块的地址的chunk都会被这个fastbin 代码处理。因此,我们可以写到av->fastbins[fastbin_index(av->max_fast)],这样就有很大的范围来得到arena_key了。
在上面的例子中,arena_key离av->fastbins[0]有0x484个字节。因此我们需要设置相应的索引为1156/sizeof(mfastbinptr),以便于把fb设置为arena_key的地址。假设系统的指针是32位的,那我们就需要设置索引为289。大致计算一下fast_index可以得到
(289 + 2) << 3 = 2328
这就意味着2328的大小可以导致fb被设置为arena_key。当然,请注意,这个大小仅仅适用于上面我导出来的镜像,每个系统中的相应偏移一般是不一样的。
现在,如果我们已经破坏了av->max_fast,并且触发了free函数对于大小为2328的chunk进行释放,并且假设接下来的nextsize完整性测试失败,那么fb就会被设置为arena_key。同时,我们的第二个chunk的forward指针将会被设置为已经存在的arena的地址,并且arena_key将会被设置为我们的第二个chunk的地址。
当破坏av->max_fast的时候,对于我们来说,只要nextsize的完整性检查被处理了,那控制溢出的块就没有那么重要了。当覆盖arena_key的时候,然而,对于我们来说,控制溢出chunk的部分数据很重要。这是因为溢出的chunk很快就会变成新的arena了。因此,很自然的,至少chunk的部分数据必须被任意控制,否则我们就不能随意控制malloc的结果了。
调用malloc函数之后会继续调用一个名叫public_malloc的函数。
Void_t* public_mALLOc(size_t bytes)
{
mstate ar_ptr;
Void_t *victim;
...
arena_get(ar_ptr, bytes);
if(!ar_ptr)
return 0;
victim = _int_malloc(ar_ptr, bytes);
...
return victim;
}
Arena_get宏通过检索arena_key的线程的特定数据来找到当前的arena,如果失败的话,就会创建一个新的arena。由于arena_key被覆盖为了一个非零的数,因而,我们可以安全地假设arena_get不会尝试创建一个新的arena。在public_malloc中,它会将ar_ptr设置为arena_key的新值,也就是我们所控制的第二个chunk。反过来,这个值和被请求分配的大小一起被传递到内部的_int_malloc函数。
一旦执行到_int_malloc函数,我们有两种方法继续执行。第一种方法是去执行fastbin分配的代码:
Void_t*
_int_malloc(mstate av, size_t bytes)
{
INTERNAL_SIZE_T nb; /* normalized request size */
unsigned int idx; /* associated bin index */
mfastbinptr* fb; /* associated fastbin */
mchunkptr victim; /* inspected/selected chunk */
checked_request2size(bytes, nb);
if ((unsigned long)(nb) <= (unsigned long)(av->max_fast)) {
long int idx = fastbin_index(nb);
fb = &(av->fastbins[idx]);
if ( (victim = *fb) != 0) {
if (fastbin_index (chunksize (victim)) != idx)
malloc_printerr (check_action, "malloc(): memory"
" corruption (fast)", chunk2mem (victim));
*fb = victim->fd;
check_remalloced_chunk(av, victim, nb);
return chunk2mem(victim);
}
}
Checked_request2size宏会把请求数据块的大小转换一个内存chunk真正的大小。记住av指向我们所控制的内存,并且这个chunk的forward指针已经被fastbin代码破坏了。如果glibc的malloc编译的时候没有添加线程统计(默认选项),那么我们的p->fd对应arena的av->fastbins[0]。为了成功地使用这一攻击技术,av->fastbins[0]必须不能够被使用。这就意味着请求的大小必须大于8.
很有意思的是,如果没有线程统计的话,av->max_fast 对应着p->size。这会强制使得nb小于我们第二个chunk的大小,例子中提供的大小是2328。如果这个成功不了的话,那么我们就需要使用unsorted_chunks或者largebin技术了。
通过在av->fastbins[fastbin_index(nb)]设置一个假的fastbin入口,我们可以使相关代码返回一个实际在栈上的chunk。为了通过victimsize的完整性测试,我们必须让假的fastbin指向一个由我们控制的值。特别地,victim chunk的大小必须和nb有一样的fastbin_index,因此,假的fastbin必须指向我们设置的值的前4个字节处,以便于为chunksize提供正确的位置。
假设栈上有一个我们可以控制的值,应用随后就会处理返回的区域,就好像它在处理一个正常请求返回的内存一样。因此,如果在分配的区域内保存有一个函数返回地址,并且我们可以控制应用向这块区域写什么,那么就可以达到任意代码执行。
我们可以传递一个大于我们第二个chunk大小的参数来触发malloc函数,那使用_int_malloc中的unsorted_chunks代码就可以更好地导致任意内存覆盖。然而,这种技术确需我们我们可以控制第二个chunk更多部分,并且进一步控制目标进程地址空间的两块区域的某处内存。为了触发unsorted_chunk代码,绝对请求的大小必须大于512(最大的smallbin chunk size),而且,必须大于假的arena的av->max_fast。假设达到了,我们就可以执行unsorted_chunks的代码了
for(;;) {
while ( (victim = unsorted_chunks(av)->bk) !=
unsorted_chunks(av)) {
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory"
" corruption", chunk2mem (victim));
size = chunksize(victim);
if (in_smallbin_range(nb) &&
bck == unsorted_chunks(av) &&
victim == av->last_remainder &&
(unsigned long)(size) > (unsigned long)(nb + MINSIZE)) {
...
}
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
if (size == nb) {
...
return chunk2mem(victim);
}
...
这里有很多事情需要考虑,首先unsorted_chunks宏返回av->bins[0].由于我们可以控制av,所以说我们也控制着unsorted_chunks的结果。这也就意味着victim可以通过建立一个假的指向某块内存的av->bins[0](记为A)被设置为一个任意的地址。反过来A->bk就会包含victim将会被设置到的地址(记为B)。由于victim被设置为一个我们可以控制的任意的地址B,临时的变量bck就可以从B->bk被设置为一个任意的地址了。
为了达到这个技术的目的,B->size必须等于nb。这个要求并不是必要的,但它可以很好的通过两个victimsize完整性检验,同时也可以触发上面所说的条件,具有结束malloc的作用。
由于我们可以将bck设置到一个任意的地址,并且unsorted_chunks返回了我们控制的内存区域A,把bck->fd设置为unsorted_chunks的结果使得我们可以随便设置A的地址空间的任意位置的值。重定向执行只需要设置bck为一个GOT表或者..dtors入口减8的地址即可。这会使得重定向执行到A->prev_size,他可以安全地包含一个jmp,以便于跳过在A->bk处的值。和fastbin 分配代码类似,任意执行地址B被返回给了请求的应用。
原文链接:https://dl.packetstormsecurity.net/papers/attack/MallocMaleficarum.txt
本文由 看雪翻译小组 iromise 翻译
校对: daybreak
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开
发者可享99元/年,续费同价!