Mobile wallpaper 1
8493 字
42 分钟
简单堆利用

前言#

最近摆了好久,一直都没有对自己学习完的知识进行总结,现在终于是有时间来对其进行整理了

这里的学习主要依靠 ctf-wikictf-challenges以及ctf show (当然还有各位大佬写的blog,真的受益良多)

感谢这些机构以及大佬们的学习资源 Orz

堆结构及堆内存管理#

这里就不多阐述了,推荐几个讲堆结构的

堆相关数据结构

Linux堆内存管理深入分析上 Linux堆内存管理深入分析下

Heap-Tutorials


Bins#

这里把bin和堆结构分开来,因为各种bin在简单的堆利用pwn题中是很重要的部分,我们需要理解各种bin是如何申请出来,大致的情况是什么样子的

bin大致分为这样几类

  1. Fast bin
  2. Unsorted bin
  3. Small bins
  4. Large bins
  5. tcache (现代Glibc中新增的bin结构)

1.Fast bin#

在64位系统下,存放 size < 0x80 (128字节) 的 chunk 每个 chunk的大小是固定的 (0x20、0x30、0x40 … 0x80)

在fastbin下,有一条单链表,其采用的是 LIFO(Last In First Out 后进先出) 的策略

特性

  • 不合并

    为了放置chunk的碎片化,当 chunk 被 free,放置至 fastbin 时,其 prev_inuse 位将永远被置为 1 ,也就是说在正常情况下,其永远不会与相邻的 free chunk 发生合并。

如何获得一个在 fastbin 中的 chunk ?

  1. 申请一个大小在 fastbin 范围内的内存 ( malloc(0x20) )
  2. free(),其会进入对应大小的 fastbin

当你这是再次申请一个同样大小的内存时, malloc 将会优先从 fastbin 中取出一个相应大小的内存。


2.Unsorted bin#

这是所有除了直接进入 fastbin及tcache 的chunk被释放后到达的第一站。只要不是在fastbin范围内,都将会到达这里。

说一句,这个 Unsorted bin 在pwn题里面属于是老演员了,经常在 泄露libc 时会选择利用 Unsorted bin 泄露 libc

在Unsortedbin下,存在一条双链表(循环双链表)

特性

  • 其是作为 smallbin 及 largebin 的缓冲区存在的

  • 当程序再次申请内存时,malloc首先会遍历 unsortedbin,如果有合适大小的chunk将会直接分配; 如果没有,将会将unsortedbin中的chunk根据大小放入 smallbin 或是 largebin 中

  • Unsortedbin的管理是一个循环双向链表,其遍历顺序是 FIFO,插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取

    当其中有两个bin时,链表结构如下图

    LinkList

如何获得一个在 unsortedbin 中的 chunk ?

    1. 申请一块大于 fastbin 的内存 ( malloc(0x200) )
    2. free()
  1. 一个较大的 chunk被分割后,剩下的部分大于 MINSIZE,就被放至Unsortedbin中

  2. 进行 malloc_consolidate 时,可能会把合并后的 chunk 放到 unsorted bin 中 (如果不是和 top chunk 近邻的话)

在之后,如果你申请一个小于你之前申请的内存(>fastbin),你将会得到一个由之前内存分割出来的一个chunk,并生成一个新的空闲chunk,重新放回UnsortedBin中

glibc 的逻辑(以 ptmalloc2 的 malloc 为例)是这样的:

  • 如果 unsortedbin 里的 chunk ​正好大小合适,就直接返回。
  • 如果 unsortedbin 里的 chunk ​比请求的大很多,glibc 会考虑 ​拆分
  • 但在拆分之前,它会先把这个 chunk 放入对应的 bin(smallbin 或 largebin) ,然后重新走一次分配逻辑。

也就是说,你如果想让你计划进入Smallbin\Largebin的Chunk完整的进入Smallbin\Largebin,你需要在free后申请一个比你之前申请的内存大的内存空间

为什么它能够泄露libc地址 ?

Unsorted bin的结构我就不多说了,特别的,其 bk 指针会指向 glibc 中的 main_arena。

也就是说我们如果像访问正常的 chunk 一样访问 unsorted bin,就能够输出 main_arena 的地址。也就成功泄露出了libc地址了


3. Small bins & Large bins#

刚刚在解释 Unsorted bin 的时候也说了,Small bin 和 Large bin是在进入 Unsorted Bin 再次进行分配进入的

Small bins的大小在64位下为 0x20 <= size < 0x200。每个 bin 中的 chunk 大小相同,总共存在 62个

Large bins的大小在64位下为 size >= 0x200。

在Tcache未填满时,进入Largebin的最小size为 0x410

large_bins中含有63个bin,而large_bins总体又被分成6个组,每个组对应一个区间,且容纳个数呈指数性减少,大致如下

组序容纳个数公差
1320x40 (64)
2160x200 (512)
380x1000 (4096)
440x8000 (32768)
520x40000 (262144)
61无上限

特性

  • Small Bins 为 遵循 FIFO(First In First Out 先进先出) 规则的双向循环链表结构。其相邻的 free chunk 是可以发生合并的。
  • Large Bins 为 一个链表结构,其 chunk 按大小降序排列1,结构最为复杂。

如何获得一个在 Small bins \ Large bins 中的 chunk ?

  1. 申请一块在 Small bins \ Large bins 范围内的内存
  2. free(),会先进入 unsorted bin
  3. 再申请一个大小大于之前申请内存的内存,触发 glibc 的机制
  4. 之前释放的 chunk 会被整理到 Small bins \ Large bins 中

4.Tcache#

为了提升多线程程序的性能,Tcache应运而生。

每个线程都会拥有一个私有的缓存(Tcache),其分配和释放不会加锁。其优先级为最高级。

其在64位下默认存放 size <= 0x408 的 chunk,且每个 bin 中最多存储 7 个 相同大小的 chunk。

其结构为 LIFO 规则的单链表

新增了两个结构体,分别为 tcache_entry 及 tcache_perthread_struct

/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
/* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
static __thread tcache_perthread_struct *tcache = NULL;

有两个重要的函数, tcache_put() 和 tcache_get()

static void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
static void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}

这两个函数会在 free 及 malloc 的开始被调用,其中 tcache_put 会在malloc的size<0x408,并给定大小的 tcachebin 未满时调用。 (一个 tcache bin 中的最大块数为 7 )

/* This is another arbitrary limit, which tunables can change. Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7
#endif

tcache_get 中,检查同样极少,其仅仅检查了 tc_idx

我们可以直接将tcache是为一个类似于 fastbin 的独特链表,仅仅是其check较于fastbin更为简单,仅会检查 tcache->entries[tc_idx] = e->next

特性

  • 其存储的 chunk 不会发生合并 (类似于 fastbin)

  • 其存在改变了分配器的行为。

    在free时,会优先放置至 tcache 内;同样的,malloc时也会优先从 tcache 中获取内存空间。

  • 每个大小的 tcache bin 只会存放 7 个 chunk。在当前大小的bin满后,free的 chunk 会进入传统的 bin 中 (fast bin、unsorted bin、small bins、large bins)

如何申请?

  • 对于 tcache,无需特殊申请。在glbic2.26后出现。

堆溢出#

和简单的栈溢出大差不差,只不过多了对堆的Size进行修改,使得程序运行流发生变动的新玩法


Off-By-One#

Off-By-One漏洞,一般是指溢出点为一个字节的漏洞,又被称为 单字节溢出漏洞。

一般出现在字符串操作不合适(一般产生off-by-null) 或是 循环写入数据时次数设置错误 时

即我们需要重点关注 edit函数 或是 create函数

一般我们不会单独的利用到 OffByOne 漏洞,而是会结合其他的一些操作来利用 OffByOne,来达到自己的目的

Off-By-One 的一般用法#

OffByOne的一般用法其实理解起来很简单

  1. 修改堆块大小,造成堆块之间的重叠(Overlapping),从而泄露亦或覆盖其他堆块的数据

    (这种用法在可控制为任一字节,与NULL字节溢出时皆可行)

  2. 溢出NULL字节,使得prev_in_use位被覆盖,从而使得前堆块被认为为free

    1. 配合unlink方法
    2. 伪造prev_size,从而造成堆块之间重叠 (2.28之后的版本基本失效)

Chunk Extend#

这是一种堆漏洞的利用手法,通过 extend 实现 chunk 的 overlapping

其技术需要满足以下条件

  • 程序中存在堆漏洞
  • 漏洞可以控制 chunk header 的数据

Chunk Extend 原理#

ptmalloc 在 对堆进行操作的各种宏存在可利用部分,该技术即基于这些可利用部分构造而成

ptmalloc中,有两种获取 chunk 大小的操作,一种为直接获取 chunk 大小,一种为忽略掩码部分,获取 chunk 大小

/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS))
/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)

ptmalloc 中,获取下一chunk地址是 直接将当前块的指针地址加上当前块大小

/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p)))

ptmalloc 中,获取前一chunk 地址是 通过 malloc_chunk->prev_size 获取前一chunk的大小,后使用本 chunk 地址减去其所得大小

/* Size of the chunk below P. Only valid if prev_inuse (P). */
#define prev_size(p) ((p)->mchunk_prev_size)
/* Ptr to previous physical malloc_chunk. Only valid if prev_inuse (P). */
#define prev_chunk(p) ((mchunkptr)(((char *) (p)) - prev_size(p)))

ptmalloc 中,获取当前chunk inUse状态是 查看下一chunk的 prev_inuse位,而下一chunk的地址是通过当前 chunk 的size计算得到的

#define inuse(p)
((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)

由上面几个宏中可以看到, ptmalloc是通过 chunk header 的数据来判断 chunk 的 inUse 即前后chunk的地址。

简单的说, 我们只需要控制 chunk header 的 size 及 prev_size 就能实现跨 chunk 操作,从而导致 overlapping。

这也就是 chunk extend 这个利用手法所做的事。

Lab#

  1. Lab1 对 inUse 的 Fast bin Extend

    Lab glibc2.23

    该Lab为 64位程序下的,如若想测试 32位下的程序,请将 0x8改为0x4

    int main(void)
    {
    void *ptr,*ptr1;
    ptr=malloc(0x10);//分配第一个0x10的chunk
    malloc(0x10);//分配第二个0x10的chunk
    *(long long *)((long long)ptr-0x8)=0x41;// 修改第一个块的size域
    free(ptr);
    ptr1=malloc(0x30);// 实现 extend,控制了第二个块的内容
    return 0;
    }

    第六条后

    image-20250906172801-DIoCnJCn

    修改后

    image-20250906172823-0i4HtOzr

    执行 free 后

    image-20250906172912-NNP6w7jJ

    这里可以发现,这一次free将两个chunk的位置都归入了 bin 中

    随后, malloc(0x30) 将会得到 chunk1+chunk2的块,即可以直接控制 chunk2 的内容。

    image-20250906172948-L9P8wpor

    我们也叫这个状态 为 overlapping chunk

  2. Lab2 对 inUse 的 Small bin Extend

    Lab glibc2.23

    我们在学习Bin的时候,能够知道:

    处于 fastbin 范围的 chunk 释放后会被置入 fastbin链表中,而不处于该范围的 chunk 被释放后会先放在 unsorted bin 中。

    在这个 Lab 中,我们将会使用 0x80 这个大小来分配堆 (Fastbin 默认的最大chunk为 0x70)

    int main()
    {
    void *ptr,*ptr1,*chunk2;
    ptr = malloc(0x80);//分配第一个 0x80 的chunk1
    chunk2 = malloc(0x10); //分配第二个 0x10 的chunk2
    malloc(0x10); //防止与top chunk合并
    *(long long *)((long long)ptr - 0x8) = 0xb1;
    free(ptr);
    ptr1 = malloc(0xa0);
    *(long long *)chunk2 = 1;
    *(long long *)(ptr1 + 0x88) = 0x41;
    }

    在第7行,也就是所有malloc执行之后

    image-20250906163113-MjemM1O0

    发生修改后

    image-20250906163140-Q6x3mI9D

    free后

    image-20250906163203-3p4dM5q1

    再次malloc返回的(但实际上仍然能够对之前发生了overlapping 的 chunk进行操作)

    image-20250906163250-INAmuajr

    测试,chunk2是否仍然可用

    image-20250906163348-PEieKzrQ

    在后面的测试中,overlapping chunk使得我们可以直接控制chunk中的所有内容,包括 chunk header

    image-20250906164549-SDbSYZgd

  3. Lab3 对 Free 的 Small bin Extend

    Lab glibc2.23

    这个Lab 是基于 Lab2进行的,但这次我们将会先 free chunk1,之后再修改处于 unsorted bin 中的 chunk1 的size域

    实际效果基本与Lab2没什么差别

    int main()
    {
    void *ptr, *ptr1;
    ptr = malloc(0x80); // 分配第一个0x80的chunk1
    malloc(0x10); // 分配第二个0x10的chunk2
    free(ptr); // 首先进行释放,使得chunk1进入unsorted bin
    *(int *)((int)ptr - 0x8) = 0xb1; // free(ptr)后,伪造chunk1的size域为0xb1
    ptr1 = malloc(0xa0); // 重新申请0xa0大小(实际大小0xb0)的chunk
    }

    两次malloc完成后

    image-20250907150129-3V1o9HwV

    free后

    image-20250907151135-0Ykbadq4

    修改后

    image-20250907151442-MbkA73Ae

    重新申请后

    image-20250907151518-TLEVgTSl

    此处chunk3与chunk2就发生了 overlapping

  4. Lab4 通过 Extend 后向 Overlapping

    Lab glibc2.23

    该Lab将会通过 Extend技术实现后弦 Overlapping。这是我们在CTF中最为常见的使用方法

    int main()
    {
    void *ptr,*ptr1;
    ptr=malloc(0x10);//分配第1个 0x80 的chunk1
    malloc(0x10); //分配第2个 0x10 的chunk2
    malloc(0x10); //分配第3个 0x10 的chunk3
    malloc(0x10); //分配第4个 0x10 的chunk4
    *(int *)((int)ptr-0x8)=0x61;
    free(ptr);
    ptr1=malloc(0x50);
    }

    这里就不运行了。这里使用 malloc(0x50)后,ptr这个size被改为0x61的chunk会被申请出来。而之前分配的第二、三个chunk仍然可以正常操作,也就是此时已经发生了 chunk overlapping。此时,我们如果再对 overlapping chunk 进行操作就可以实现 fastbin attack

  5. Lab5 通过 Extend 前向 Overlapping

    Lab glibc2.23

    该 Lab 将展示通过修改 prev_inuse 位 及 prev_size 实现与前面块的合并

    int main(void)
    {
    void *ptr1,*ptr2,*ptr3,*ptr4;
    ptr1=malloc(0x80);//smallbin1
    ptr2=malloc(0x10);//fastbin1
    ptr3=malloc(0x10);//fastbin2
    ptr4=malloc(0x80);//smallbin2
    malloc(0x10);//防止与top合并
    free(ptr1);
    *(int *)((long long)ptr4-0x8)=0x90;//修改pre_inuse域
    *(int *)((long long)ptr4-0x10)=0xd0;//修改pre_size域
    free(ptr4);//unlink进行前向extend
    malloc(0x150);//占位块
    }

    前向 Extend 利用了 Smallbin 的 unlink 机制,其通过修改 prev_size 能够跨越多个 chunk 进行合并,从而实现 Overlapping


Unlink#

Unlink操作在低版本和高版本是有部分差别的。但你能够遇到的基本上就是这样一个利用思路

利用思路#

条件#

  1. UAF ,可修改 free 状态下 smallbin 或是 unsorted bin 的 fd 和 bk 指针
  2. 已知位置存在一个指针指向可进行 UAF 的 chunk

效果#

使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x18

思路#

设指向可 UAF chunk 的指针的地址为 ptr

  1. 修改 fd 为 ptr - 0x18
  2. 修改 bk 为 ptr - 0x10
  3. 触发 unlink

ptr 处的指针会变为 ptr - 0x18。

在低版本glibc下,如果存在两个物理空间连续的 chunk,即 Q和NextChunk。

// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
// largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV);

Q正在使用,NextChunk已经Free。

如果此时我们通过其他技术手段使得 NextChunk的fd及bk变为指定的值,那么在free(Q)时

  1. glibc判断该块为 small chunk
  2. 判断前向合并,前一个chunk未free,不进行
  3. 判断后向合并,后一个chunk已经free,需要合并
  4. 对NextChunk进行Unlink

Unlink

  1. FD = P -> fd (目标地址 - 12)
  2. BK = P -> bk (期望值)
  3. FD -> bk = BK 即 *(目标地址-12+12) = BK = 期望值
  4. BK -> fd = FD 即 *(期望值+8) = FD = 目标地址-12

也就是说,我们只要保证 期望值+8 相应的地址具有写权限,就可以通过 Unlink 直接实现任意地址写。

在高版本下,Unlink增加了对 fd及bk 的检查

// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

我们此时一般会通过伪造来绕过该机制

  1. 首先,我们会通过其他技术,使得 NextChunk 的 FD 指向 fakeFD, BK 指向 fakeBK,为了通过验证,我们需要让

    fakeFD->bk == P fakcBK->fd == P ,也就是 *(fakeFD + 12) == P *(fakeBK + 8) == P

  2. 当上面的验证通过后,我们就能够进入Unlink环节,也就是进行

    fakeFD -> bk = fakeBK fakeBK -> fd = fakeFD ,也就是 *(fakeFD + 12) = fakeBK *(fakeBK + 8) = fakeFD

  3. 如果我们能够让 fakeFD+12 与 fakeBK+8 指向 同一个 指向P的 指针,那么我们就能够让

    *P = P-8 *P = P-12

这样,我们就让 P的指针指向了比原先 P低12的地址。


UAF(Use-After-Free)#

UAF,即Use After Free,指一个堆块在Free后仍能够使用。

一般出现在一个堆块Free后未给指向其内存的指针置空,我们会将其指针称为 dangling pointer

这里就不着重讲了,可以自己去体会一下。


Fastbin Attack#

从这里开始,你才正式踏过堆利用的门槛

Fastbin Attack实际是对于一类漏洞利用方法的总称。实际是指向所有基于 Fastbin 机制漏洞的利用方法

其利用前提为

  • 存在堆溢出、UAF等能够控制 chunk 内容的漏洞
  • 漏洞发生于 fastbin 类型的 chunk 中

我们将 Fastbin Attack 细分,可以得到几个方向

  1. Fastbin Double Free
  2. House of Spirit
  3. Alloc to Stack
  4. Arbitrary Alloc

其中,前两个侧重于利用free函数释放 真的亦或是伪造的 chunk,再次申请chunk 进行攻击

后两个侧重于修改fd指针,直接利用malloc申请到指定位置的 chunk 进行攻击

Fastbin 是使用一个单链表来维护管理释放堆块的。

由 Fastbin 管理的 chunk 即使被释放,其对应的 next_chunk 的 prev_inuse 位也不会变动。

这就提供了 Fastbin Attack 的可行性

Fastbin Double Free#

其漏洞名指的是 存在在 fastbin 的 chunk 是可以发生多次释放的。

能被多次释放,从而在 fastbin 的单链表中可以存在多个 同个chunk。

这就导致了我们可以在分配时获取到同一个堆块,也就是说能够得到多个指针同时指向同一个堆块。

这时,我们结合堆块的数据内容就能够实现类似于 类型混淆(Type Confused ) 的效果

其利用了

  • fastbin 的堆块释放后 next_chunk 的 prev_inuse 位不会变为 0
  • fastbin 在执行free时仅验证了 main_arena 直接指向的块是否与当前 free 的块一致
/* Another simple check: make sure the top of the bin is not the
record we are going to add (i.e., double free). */
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}

还有一个校验需要绕过

if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim));
return NULL;
}

实际使用#

  • 申请两个 chunk ,记为 1、2
  • free 1, free 2, free 1
  • 伪造一个chunk,使其size与之前申请的chunk一致 (提前设置好size)
  • 申请回一个chunk,修改其 fd位 为自己伪造的chunk (也就是直接写入)
  • 再申请回两个chunk,这样之前放在 fd位的伪造chunk 就可以被申请出来
  • 再次申请chunk,成功获得伪造chunk

House of Spirit#

其利用手法 是 《the Malloc Maleficarum》 提出的一种技术

核心操作在于,在目标位置伪造一个 fastbin chunk,并将其释放。从而达到我们通过malloc获得​**指定地址**​ chunk 的目的

我们为了构造一个 fastbin fake chunk 保证其释放时,可以进入相应的 fastbin 链表中,需要绕过不少检查

  1. fake chunk 的 ISMMAP 位不可为 1

    free时,若该chunk为 MMAP chunk,会发生单独处理

  2. fake chunk 地址对齐, MALLOC_ALIGN_MASK(对齐的最小内存块)

  3. fake chunk 的 next_chunk 大小有要求 2*SIZE_SZ <= size <= av->system_mem

  4. 其对应的 fastbin链表头不能是 fake chunk

具体操作#

  • 确保malloc结构已经准备好(确保程序已经调用一次malloc)
  • 在指定位置准备一个对齐的 fake chunk
  • 将其size位,也就是 fakechunk+0x8的位置设为 fastbin大小要求的大小
  • 将next_chunk的size也设置好(即设置fakechunk+size+0x8为指定大小)
  • 伪造指针,使一个指针指向 fakechunk+0x10
  • 调用free函数,对指针进行 free(ptr)操作
  • 调用相应大小的 malloc ( 即设置的size为0x40则 malloc(0x30) ),成功获得指向指定位置的指针

Alloc to Stack#

如果你已经基本理解了前面的 House of Spirit、 Fastbin Double Free 技术,也就是 chunk的伪造以及 Fastbin的基本玩法,那么理解这个技术也不会有任何问题

该技术的核心操作在于 劫持 Fastbin 链表中的 fd 指针, 把其 fd 指针指向我们想要分配的栈上,从而能够实现控制栈中的关键数据,如返回地址等数据

Lab

typedef struct _chunk
{
long long prev_size;
long long size;
long long fd;
long long bk;
} CHUNK,*PCHUNK;
int main(void)
{
CHUNK stack_chunk;
void *chunk1;
void *chunk_a;
stack_chunk.size=0x21;
chunk1=malloc(0x10);
free(chunk1);
*(long long *)chunk1=&stack_chunk;
malloc(0x10);
chunk_a=malloc(0x10);
return 0;
}

Tip

这里之所以不用去像 House of Spirit 里面一样伪造 next_chuink,是因为其对 next_chunk 的检查机制是在 free 中,而非在 malloc 中。

也就是在这个Lab里无需像 House of Spirit 一样进行伪造

我们现在开始跑一遍 Lab

Arbitrary Alloc#

其玩法与 Alloc to Stack 几乎是一致的,唯一的区别即是分配的目标不再在栈上。

事实上,只要满足目标地址存在合法 size域 (size域是人工构造,还是自然存在都一样),我们就能够将 chunk 分配到任意的可写内存之中。(如 bss heap data stack)

int main(void)
{
void *chunk1;
void *chunk_a;
chunk1=malloc(0x60);
free(chunk1);
*(long long *)chunk1=0x7ffff7dd1af5-0x8;
malloc(0x60);
chunk_a=malloc(0x60);
return 0;
}

这是一个极其简陋的 Lab (这里直接借了ctf-wiki的 Lab)

在该例子里,我们使用了字节错位来直接分配 fastbin 到 _malloc_hook 的为止,相当于覆盖掉 _malloc_hook 来控制程序流程

这里直接修改的地址是由本机情况得出的值,实际上你在实战中需要通过泄露相应位置的数据来观察写入地址附近是否存在可以发生字节错位的情况

0x7ffff7dd1a88 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1a90 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1a98 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1aa0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1aa8 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1ab0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1ab8 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1ac0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1ac8 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1ad0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1ad8 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1ae0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1ae8 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1af0 0x60 0x2 0xdd 0xf7 0xff 0x7f 0x0 0x0
0x7ffff7dd1af8 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x7ffff7dd1b00 0x20 0x2e 0xa9 0xf7 0xff 0x7f 0x0 0x0
0x7ffff7dd1b08 0x0 0x2a 0xa9 0xf7 0xff 0x7f 0x0 0x0
0x7ffff7dd1b10 <__malloc_hook>: 0x30 0x28 0xa9 0xf7 0xff 0x7f 0x0 0x0

在这里,我们可以观察到,在 0x7ffff7dd1af8的位置断开,能够构造一个0x7f的size,也就是在 0x7ffff7dd1af8-0x8处申请一个 被视为 0x70大小的 chunk,就能够成功得到一个合法的 chunk。

即我们 malloc 0x60即可得到该之前放在fd,被归入fastbin中,可以取出的 chunk

Arbitrary Alloc比起Alloc to Stack,在CTF中使用更加频繁,因其可以通过字节错位等方法绕过size域的校验,实现在任意的地址分配 chunk。最后实现相当于 任意地址写 的效果

小结#

这里的 Fastbin Double Free 是利用 double free 来将相应地址放入fd,再执行malloc,使其地址被放入fastbin中,让其可以被申请出来

Alloc to Stack、Arbitrary Alloc都是在fd中写入相应地址,让其malloc,触发地址放入fastbin

House of Spirit 实际上就是多一个伪造chunk的过程

Unsortedbin Leak#

这是一个小trick,在实际游玩时,这个trick还是很好用的

当unsortedbin中只有一至两个chunk时,其fd与bk中必有一个指向main_arena

而此时如果有 UAF 漏洞,那么此时就可以通过对free chunk 的访问获得到 heap_address 和 libc_address

而 main_arena 的地址与 __malloc_hook 的地址的插值为 0x10, 即我们可以通过__malloc_hook +0x10 的偏移及 main_arena的地址确定 libc基址

一般情况下,将获得的堆地址 & ~0xFFF就得到了 heap基址

Unsortedbin Attack#

Unsortedbin Attack 实际上是一种年代较为久远的攻击手法,常常会作为其他攻击方式的辅助手段。

如通过修改 global_max_fast 为一个较大的值,使得几乎所有的 chunk 都用 fastbin 的管理方式进行分配、释放。

或者修改 _IO_list_all 伪造 _IO_FILE 进行攻击。

我们需要控制 Unsortedbin 的 bk ,将其设置为 Target - 0x10 ,即可将 Target 的值修改为一个极大的(同时也随机的)值

Largebin Attack#

libc2.31之前#

使用条件

  1. 存在Largebin chunk,且大小为bin中最小,能够修改其 bk及bk_nextsize
  2. 释放一个大一些的 unsortedbin 进入 Largebin中,使其在 整理阶段被整理进Largebin中

攻击思想

  • 向 Largebin 中放入 chunk1,同时向 unsortedbin 中预备一个大于 Largebin 中的 chunk1 的 chunk2
  • 修改 chunk1 的 bk_nextsizetarget_chunk-0x20, bktarget_chunk-0x10
  • 申请一个堆块,触发 unsortedbin 整理机制,使 chunk2 链入 Largebin,完成攻击

完成攻击的结果,即是在 target_chunk 写入一个 chunk 的地址

其攻击的用法与unsortedbin attack基本一致,在没有特殊情况下,一般是修改 global_max_fast 为一个较大的值,使得几乎所有的 chunk 都用 fastbin 的管理方式进行分配、释放,或者修改 _IO_list_all 伪造 _IO_FILE 进行攻击。

libc2.31及之后#

libc2.31新增了检查机制,其检查机制使得我们在libc2.31前的攻击方式需要进行改变,需要另寻他路

其增加了两个双链表的完整性检查

类似地,我们首先准备一个 Largebin Chunk和一个 Unsortedbin Chunk

我们此时只对Largebin Chunk的bk_nextsize进行修改,将其修改为 Target_addr-0x20 ,再申请,触发整理机制,即可完成任意写

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#define PTR size_t *
int main()
{
size_t Target = 0;
PTR p1 = malloc(0x428);
PTR g1 = malloc(0x18);
// 保护不进行 consolidate (合并)
PTR p2 = malloc(0x418);
// malloc一个小于p1的,属于同一个bin中的chunk
PTR g2 = malloc(0x18);
// 保护不进行 consolidate (合并)
free(p1);
// 将相对更大的那个chunk free 掉,使得p1进入 unsorted bin
PTR p3 = malloc(0x438);
// malloc 一个比 p1大的chunk,使得p1进入 Largebin
free(p2);
// 将p2 free 掉
/*
这时,
我们就有一个chunk在 Large Bin中,
一个chunk在 unsorted bin 中,
我们只需要修改 Large Bin 中 chunk 的 bk_nextsize 指针为 Target -0x20,
再malloc一个比p2大的chunk ,
即可对 Target 进行写入了 (这里写入的是p2的头地址)
*/
p1[3] = (size_t)((&Target) - 4);
// 修改 Large Bin 中 chunk 的 bk_nextsize 指针为 Target -0x20
PTR g4 = malloc(0x438);
// malloc一个比p2大的chunk,触发将 p2 放入Largebin,
printf("p2 addr:%p\ntarget:%p\n", p2 - 2, (void *)Target);
return 0;
}

此时我们就可以整理出当前的利用条件

  1. 能够对Largebin Chunk的bk_nextsize进行修改
  2. 被整理进Largebin的Unsortedbin Chunk的size为Largebin中的最小

Tcache Attack#

Tcache poisoning#

我们通过覆盖 tcache的 next,即可在不伪造任何 chunk结构的情况下实现 malloc到任意地址

实现很简单,

我们只需要malloc一个能进入tcache的chunk,

之后修改tcache的next位为Target_address,再进行一次malloc

即可让Target_address进入相应的tcache

从而在对相应大小的chunk进行的第二次malloc,就会返回一个大小为自己申请的大小、地址在Target_address的chunk

Tcache dup#

由于Tcache_put()函数没有任何检查,这使得我们可以直接对同一个 chunk 进行多次 free,造成 cyclic-ed list

Tcache Perthread Corruptio#

tcache_perthread_struct 是整个 tcache的管理结构,我们如果能够控制这个结构体,那么我们无论 malloc 的size为多少,其malloc的地址都会是可控的

我们实际利用该技术时,一般会配合 Tcache poisoning来获得相应 tcache_perthread_struct 的地址,从而对其内容进行任意形式的修改。

这里引用一下 ctf-wiki的图片,也就是利用Tcache poisoning使得我们可以申请到 tcache_perthread_struct 的地址

tcache_ +------------+
\perthread |...... |
\_struct +------------+
|counts[i] |
+------------+
|...... | +----------+
+------------+ |header |
|entries[i] |--------->+----------+
+------------+ |NULL |
|...... | +----------+
| | | |
+------------+ +----------+
tcache_ +------------+<---------------------------+
\perthread |...... | |
\_struct +------------+ |
|counts[i] | |
+------------+ |
|...... | +----------+ |
+------------+ |header | |
|entries[i] |--------->+----------+ |
+------------+ |target |------+
|...... | +----------+
| | | |
+------------+ +----------+

这样我们就能够通过修改entries的值来完全控制每次malloc返回的地址了

Tcache — House of Spirit#

几乎和之前演示的 House of Spirit 一致,我们在栈/数据区伪造一个 chunk,后让一个指针指向 fake chunk,再将其free掉。

由于 tcache的检查不严格,free后会将 fake chunk 放入 tcache freelist,从而可以在后续的malloc时获取到该 chunk。

malloc(1)
unsigned long long *a; // 模拟相应的覆盖的指针
unsigned long long fake_chunk[10];
fake_chunk[1] = 0x40; //size = 0x40
a = &fake_chunk[2]; // 指向 fakechunk 的 "用户区"
free(a);
malloc(0x30); //这里就能够得到指向fake_chunk的指针

当 Smallbin 中存在 空闲块时,会同时将同大小的空闲块放入tcache中。

此时会发生 Unlink 操作, 但相较于 Unlink宏,其少了链的完整性校验,也就是说,原本的Unlink操作在此时也能够使用

其相应源码如下

if (in_smallbin_range (nb))
{
idx = smallbin_index (nb); // 获取size对应的small bin索引
bin = bin_at (av, idx); // 获取对应small bin链表头部
if ((victim = last (bin)) != bin)
// victim就是要脱链的堆块,也就是small bin里的最后一个
// 这个if在判断我们所需要的size的那条small bin链上是否存在堆块,存在的话就把victim给脱链
{
bck = victim->bk; // 获取victim的bk指针
if (__glibc_unlikely (bck->fd != victim)) // 对small bin的双向链表的完整性做了检查,确保victim->bk->fd指向的还是victim
// 如果我们在这里劫持了victim的bk指针,就会导致bck的fd指向的并不是victim,从而触发异常
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb); // 设置下一个(高地址)chunk的prev_inuse位
bin->bk = bck; // 将victim脱链
bck->fd = bin;
if (av != &main_arena)
set_non_main_arena (victim); // 如果不是主arena,设置非主arena标志
check_malloced_chunk (av, victim, nb); // 检查分配的chunk
#if USE_TCACHE
/* 在这里,如果看到其他相同大小的chunk,把它们放入tcache。 */
size_t tc_idx = csize2tidx (nb); // 获取size对应的tcache索引
if (tcache && tc_idx < mp_.tcache_bins) // 如果这个索引在tcache bin的范围里,也就是这个size属于tcache bin的范围
{
mchunkptr tc_victim;
/* 当bin不为空且tcache未满时,将chunks复制过去。 */
while (tcache->counts[tc_idx] < mp_.tcache_count // 如果tcache bin没有满
&& (tc_victim = last (bin)) != bin) // 如果small bin不为空,tc_victim为small bin中的最后一个堆块
{
if (tc_victim != 0)
{
bck = tc_victim->bk; // 这里取tc_victim的bk指针,并没有针对bck做双向链表完整性检查,因此我们可以去攻击tc_victim的bk指针
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck; // 将tc_victim从small bin中脱链
bck->fd = bin; // 如果我们伪造bck,这里就可以将bck->fd的位置写入一个bin的地址(main_arena+96)
tcache_put (tc_victim, tc_idx); // 将tc_victim链入tc_idx这条链
}
}
}
#endif
void *p = chunk2mem (victim); // 将chunk转换为用户可用内存
alloc_perturb (p, bytes); // 对分配的内存进行扰动
return p; // 返回分配的内存
}
}

该攻击手法主要利用了 calloc函数 在取出堆块时,会越过 tcachebin,从smallbin中取出chunk 的特性。

(在 smallbin 分配后,同样大小的空闲块会被挂载进入 tcache)

其攻击手法能够实现两个效果:

  1. 和Unsortedbin Attack类似,能够在任意地址上写一个较大的数
  2. 在任意地址分配 chunk
unsigned long fake_chunk[0x10] = {0};
unsigned long *chunk_list[0x10] = {0};
unsigned long *target;
//首先,我们要将一个可写的地址写入 fake_chunk->bk 来绕过 bck->fd = victim 的检查
//这里我们让 fake_chunk的fd位作为 fake_bk
//我们待会可以看到 *(fake_chunk->bk +0x10),也就是fake_chunk[4]成为libc地址
fake_chunk[3] = (unsigned long)(&fake_chunk[2]);
//之后我们分配 9 个 chunk
for(int i=0;i<9;i++) chunk_list[i] = (unsigned long*)malloc(0x90);
//之后我们释放 7 个,使其进入 tcache
for(int i=3;i<9;i++) free(chunk_list[i]);
free(chunk_list[1]);
//它们两个将被放入unsorted bin
free(chunk_lis[0]);
free(chunk_lis[2]);
//我们此时 malloc 一个chunk来使两个 unsortedbin中的chunk进入 smallbin
malloc(0xa0); // size>0x90
//此时,我们 malloc 两次,会优先清理掉 tcache中的两个 chunk,使得tcache中存在5个chunk,smallbin中存在2个chunk
malloc(0x90);
malloc(0x90);
//这里我们模拟一个 UAF漏洞,使其修改 bin2->bk 为 fake_chunk
chunk_list[2][1] = (unsigned long)fake_chunk;
//此时,我们使用 calloc 来分配一个 0x90 大小的chunk来触发攻击。
//这会使得之前释放的一个 smallbin 返回给用户,另一个 chunk 和 fake_chunk 被链接至 tcache 中
calloc(1, 0x90);
//这样,我们的 fake_chunk将会被放入 0xa0 大小的 tcache列表中。 其fd指针将指向下一个 freed chunk。而其 bck->fd 将会被更改为libc地址
printf("fake_chunk[2]:%p\n\nfake_chunk[4]:%p\n\n",(void*)fake_chunk[2], (void*)fake_chunk[4]);
//此时,我们再进行一次malloc,就能够返回我们的 fake_chunk (此时在栈上)
target = malloc(0x90);

Libc leak#

在以前的 libc 版本中,我们只需这样:

#include <stdlib.h>
#include <stdio.h>
int main()
{
long *a = malloc(0x1000);
malloc(0x10);
free(a);
printf("%p\n",a[0]);
}

但是在 2.26 之后的 libc 版本后,我们首先得先把 tcache 填满:

#include <stdlib.h>
#include <stdio.h>
int main(int argc , char* argv[])
{
long* t[7];
long *a=malloc(0x100);
long *b=malloc(0x10);
// make tcache bin full
for(int i=0;i<7;i++)
t[i]=malloc(0x100);
for(int i=0;i<7;i++)
free(t[i]);
free(a);
// a is put in an unsorted bin because the tcache bin of this size is full
printf("%p\n",a[0]);
}

总结#

到此,简单的堆利用我们就讲完了。如果你还想要了解堆利用的其他玩法,比如House of 系列。大多数就需要去学习一些其他的东西了 (比如IOFILE)

这里也不对House of系列的其他利用方法进行赘述。具体就请你们自己去看其他师傅的文献了。

  • 在large_bin中的排列顺序是从大到小的顺序

    所以越大的chunk越靠前,越小的chunk越靠后,最小的chunk指向main\_arena+一定偏移。
    也就是说,**非尾部的fd_nextsize指向的是更小的chunk,非头部的bk_nextsize指向的是更大的chunk**
  • 在相同大小的情况下,按照free的时间进行排序

  • 只有首堆块的fd_nextsize,bk_nextsize会指向其它大小的堆块,而其后的堆块中fd_nextsize,bk_nextsize无效,通常为0

Footnotes#

  1. Largebin的排列规则#

    借用别人的图片

    image-20250917220231-maMbp1nY

简单堆利用
https://blog.mindedness.top/posts/简单堆利用/
作者
Mindedness
发布于
2025-09-20
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00