Chunk
chunk 一般结构如下(地址从低到高):
malloc 时:
+---------------+ ← chunk起始地址
| prev_size | 前一个 free chunk 的大小
+---------------+
| size | 分配的内存大小
+---------------+ <-malloc返回的地址
| data |
+---------------+
free 后:
+---------------+ ← chunk起始地址
| prev_size | 前一个 free chunk 的大小
+---------------+
| size | 分配的内存大小
+---------------+
| fd(next) | 指向链表下一个空闲 chunk (previous free chunk)
+---------------+
| bk(prev) | 指向链表上一个空闲 chunk (next free chunk),若在 fast bin 中则不使用。
+---------------+
可见 fd 与 data 的位置是重合的。因此配合 Double Free,可以任意地址写入。
Unsorted Bin
存储 free 了的 chunk 的地址,
是一个双向链表,所以同时使用 chunk 的 fd 和 bk 指针维护链表关系,存在检查:p->fd->bk == p 和 p->bk->fd == p。
采用先进先出原则。
Fast Bin
当 chunk 满足:
$$ size < \text{0x80} $$free 后,它的地址会被放入 fast bin 中,采用先进后出原则。
malloc 的分配方式为地址从低到高,并且优先采用在 fast bin 或 unsorted bin 中已经 free 的堆块。所以我们可以利用 fast bin 或 unsorted bin 的特性来控制下一次 malloc 返回的地址。
Double Free
引自:Link
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
//gcc -g fastbin_dup.c -o fastbin_dup
//pwndbg> set environment GLIBC_TUNABLES=glibc.malloc.tcache_count=0
int main()
{
fprintf(stderr, "这个例子演示了 fastbin 的 double free\n");
fprintf(stderr, "首先申请了 3 个 chunk\n");
char* a = malloc(8);
strcpy(a, "AAAAAAA");
char* b = malloc(8);
strcpy(b, "BBBBBBB");
char* c = malloc(8);
strcpy(c, "CCCCCCC");
fprintf(stderr, "第一个 malloc(8): %p\n", a);
fprintf(stderr, "第二个 malloc(8): %p\n", b);
fprintf(stderr, "第三个 malloc(8): %p\n", c);
fprintf(stderr, "free 掉第一个\n");
free(a);
fprintf(stderr, "当我们再次 free %p 的时候, 程序将会崩溃因为 %p 在 free 链表的第一个位置上\n", a, a);
// free(a);
fprintf(stderr, "我们先 free %p.\n", b);
free(b);
fprintf(stderr, "现在我们就可以再次 free %p 了, 因为他现在不在 free 链表的第一个位置上\n", a);
free(a);
fprintf(stderr, "现在空闲链表是这样的 [ %p, %p, %p ]. 如果我们 malloc 三次, 我们会得到两次 %p \n", a, b, a, a);
char* d = malloc(8);
char* e = malloc(8);
char* f = malloc(8);
strcpy(d, "DDDDDDD");
strcpy(e, "EEEEEEE");
strcpy(f, "FFFFFFF");
fprintf(stderr, "第一次 malloc(8): %p\n", d);
fprintf(stderr, "第二次 malloc(8): %p\n", e);
fprintf(stderr, "第三次 malloc(8): %p\n", f);
}
因为申请的内存较小,free 时 a 和 b 都会被放在 fast bin 中。
现在,a 在 fast bin 的顶端,我们直接再次 free(a) 时程序会崩溃,但只要先 free(b) 再 free(a),就能在 fast bin 中形成 a -> b -> a 的循环链表结构,此后的第一次和第三次 malloc(0x20),都会返回 a 的地址。
当我第一次 malloc 申请到 a ,并通过修改 a 的内容,如改为 0xdeadbeef,那么由于 a 仍在 fastbin 上,并且 fd 与 data 的位置重合,那么 fastbin 链将会改为 b -> a -> (0xdeadbeef)。再 malloc 三次,我们便得到了位于 0xdeadbeef 的“堆”,因为它可供我们修改,就此实现了任意地址写入。
当 a = malloc(0x8), b = malloc(0x8),free 后它们和 0xdeadbeef 都会进入 fastbin 的 [0x20] 处的链表,
并不会对 0xdeadbeef 处的“堆”进行 size == 0x20? 的检查。
值得一提的是,在高版本的 glibc 2.32 中,会出现内存中的 fd 与实际连接的 fd 不一致的情况,这是因为引入了 safe-linking 机制。
加密公式:
stored_fd = actual_fd ^ (chunk_addr >> 12)
解密公式:
actual_fd = stored_fd ^ (chunk_addr >> 12)
当 bin 中只含一个堆时,由于 actual_fd = 0,内存上的 stored_fd
即 chunk_addr >> 12。可以这样处理 safe-linking 机制。
此外,由于 glibc 2.26 引入了 tcache, free 会优先放入 tcache,而 tcache 会遍历整个链表进行检查,使得 tcache 上面的 double free 失效。它的绕过我们以后再说,暂时先在 gdb 时输入该指令,令 tcache 容纳量为 0 吧。(其实所谓绕过就和这个类似^_^)
gdb fastbin_dup
pwndbg> set environment GLIBC_TUNABLES=glibc.malloc.tcache_count=0
pwndbg> b main
pwndbg> r
__free_hook
当程序执行 free 函数时,会检查 __free_hook 的值是否为 0。当不为 0 时,就会直接跳转到 __free_hook 内的地址继续往下执行。
double free 修改 __free_hook 的内容为 system_addr,执行 free("\bin\sh") 时,恰好使 rdi = &"\bin\sh", 就能打通了。
//double free, __free_hook = system_addr
void *a = malloc(0x20);
strcpy(a,"\bin\sh");
free(a);
//now we get shell :)
ps: __free_hook 在 glibc 2.34 移除了……
堆溢出 + Fake Chunk
当程序不进行边界检查时,可以利用堆溢出。
因为堆的分配是相邻的,了解堆的基本组成后,我们可以制作 fake chunk,利用堆溢出修改下一个堆的 fd(next),此后第二次 malloc 的时候就会返回我们覆盖的地址,从而实现任意地址写入。
最基本的攻击思路如下:堆溢出给下一个 Free 掉的堆写入 __free_hook 地址,使它变为 Fake Chunk。 -> malloc 到 Fake Chunk,修改__free_hook -> malloc 新堆,填入”\bin\sh” -> free 这个新堆。
现在只需要获取到 Free hook 的地址就行了。
泄露 Free hook 地址
当 unsorted bin 中只含一个 free 掉的堆时,其 fd 和 bk 指针都会指向 main_arena 的地址,如果我们有程序使用的 libc.so.6,可以由此计算得出 libc 基地址和 __free_hook 的地址。