LOADING

加载过慢请开启缓存 浏览器默认开启

pwn-堆学习笔记

2025/6/27 heap pwn

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 的地址,
是一个双向链表,所以同时使用 chunkfdbk 指针维护链表关系,存在检查:p->fd->bk == pp->bk->fd == p

采用先进先出原则。

Fast Bin

chunk 满足:
$$ size < \text{0x80} $$
free 后,它的地址会被放入 fast bin 中,采用先进后出原则。

malloc 的分配方式为地址从低到高,并且优先采用在 fast binunsorted bin 中已经 free 的堆块。所以我们可以利用 fast binunsorted 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);
}

因为申请的内存较小,freeab 都会被放在 fast bin 中。

现在,afast 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 引入了 tcachefree 会优先放入 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 的地址。