简介

chunk extend (堆块扩展)是堆漏洞一种常见利用手法, 通过 extend 可以实现 chunk overlapping (堆块重叠)

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

原理

chunk extend 技术能够产生的原因在于 ptmalloc 在堆 chunk 进行操作时使用的各种宏

获取 chunk 块大小

在 ptmalloc 中, 获取 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)

一种是直接获取 chunk 的大小, 不忽略掩码部分, 另一种是忽略掩码部分

获取下一 chunk 块地址

在 ptmalloc 中, 获取下一 chunk 块的地址的操作如下:

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

利用了隐式链表的技术, 就是当前块指针加上当前块的大小

获取前一个 chunk 信息

在 ptmalloc 中, 获取前一个 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)))

即通过 malloc_chunk->pre_size 获取前一个块大小, 然后利用本 chunk 地址减去所得大小

判断当前 chunk 是否 inuse

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

即查看下一 chunk 的 prev_inuse 域, 而下一块的地址又如我们前面所述是根据当前 chunk 的 size 计算得出的

chunk 的判断 就是依赖于malloc_chunk结构体里面的内容, 简单来说就是chunk_header
, 那么利用思路就自然而然的出来了, 我们通过堆漏洞(heap overflow之类的)可以改变chunk_header进而实现漏洞利用

小结

通过上面几个宏可以看出, ptmalloc 通过 chunk header 的数据判断 chunk 的使用情况和对 chunk 的前后块进行定位。那么我们可以通过堆漏洞堆 chunk header 进行修改, 从而改变 chunk header 进而实现漏洞利用

示例

示例都是在64位下进行, 如果想在32位下进行, 应把8字节偏移改为4字节

对 inuse 的 fastbin 进行 extend

更改第一个块的大小来控制第二个块的内容
libc version: 2.23

#include <stdlib.h>
#include <stdio.h>

int main() {
void *ptr1, *ptr2;

ptr1 = malloc(0x10); // 分配大小为0x10的 chunk1

ptr2 = malloc(0x10); // 分配大小为0x10的 chunk2

*(long long *)((long long)ptr1 - 0x8) = 0x41; // 修改 chunk1 的 size 域

free(ptr1); // 释放 chunk1

ptr1 = malloc(0x30); // 实现 extend
return 0;
}

malloc 两块 chunk 后堆分布如下

pwndbg> x/10gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x602010: 0x0000316b6e756863 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021 <=== chunk2
0x602030: 0x0000326b6e756863 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000020fc1 <=== top chunk

之后, 我们把 chunk1 的 size 域更改为 0x41, 0x41 是因为 chunk 的 size 域包含了用户控制的大小和 header 的大小。如上所示正好大小为0x40。在题目中这一步可以由堆溢出得到。

pwndbg> x/10gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000041 <=== 篡改大小
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000020fc1

执行 free 之后, 我们可以看到 chunk2 与 chunk1 合成一个 0x40 大小的 chunk, 一起释放了。

pwndbg> heap
Free chunk (fastbins) | PREV_INUSE
Addr: 0x602000
Size: 0x41
fd: 0x00

Top chunk | PREV_INUSE
Addr: 0x602040
Size: 0x20fc1

之后我们通过 malloc(0x30) 得到 chunk1+chunk2 的块, 此时就可以直接控制 chunk2 中的内容, 我们也把这种状态称为 overlapping chunk。

pwndbg> chunkinfo 0x602000
==================================
Chunk info
==================================
Status : Used
Freeable : True
prev_size : 0x0
size : 0x40
prev_inused : 1
is_mmap : 0
non_mainarea : 0

对 inuse 的 smallbin 进行 extend

通过之前深入理解堆的实现部分的内容, 我们得知处于 fastbin 范围的 chunk 释放后会被置入 fastbin 链表中, 而不处于这个范围的 chunk 被释放后会被置于unsorted bin链表中。 以下这个示例中, 我们使用 0x80 这个大小来分配堆(作为对比, fastbin 默认的最大的 chunk 可使用范围是0x70)

#include<stdlib.h>

int main()
{
void *ptr, *ptr1;

ptr=malloc(0x80);// 分配大小为 0x80 的chunk1
malloc(0x10); //分配第二个 0x10 的 chunk2
malloc(0x10); //防止与top chunk合并

*(int *)((int)ptr-0x8)=0xb1;
free(ptr);
ptr1=malloc(0xa0);
}

在这个例子中, 因为分配的 size 不处于 fastbin 的范围, 因此在释放时如果与 top chunk 相连会导致和top chunk合并。所以我们需要额外分配一个chunk, 把释放的块与top chunk隔开。

malloc 三块 chunk 以后

pwndbg> heap
Allocated chunk | PREV_INUSE <=== chunk1
Addr: 0x405000
Size: 0x91

Allocated chunk | PREV_INUSE <=== chunk2
Addr: 0x405090
Size: 0x21

Allocated chunk | PREV_INUSE <=== chunk3
Addr: 0x4050b0
Size: 0x21

Top chunk | PREV_INUSE <=== top chunk
Addr: 0x4050d0
Size: 0x20f31

修改 chunk1 的 size 域为0xb1

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0xb1

Allocated chunk | PREV_INUSE
Addr: 0x4050b0
Size: 0x21

Top chunk | PREV_INUSE
Addr: 0x4050d0
Size: 0x20f31

pwndbg> x/32gx 0x405000
0x405000: 0x0000000000000000 0x00000000000000b1 <=== chunk1
0x405010: 0x0000000000000000 0x0000000000000000
0x405020: 0x0000000000000000 0x0000000000000000
0x405030: 0x0000000000000000 0x0000000000000000
0x405040: 0x0000000000000000 0x0000000000000000
0x405050: 0x0000000000000000 0x0000000000000000
0x405060: 0x0000000000000000 0x0000000000000000
0x405070: 0x0000000000000000 0x0000000000000000
0x405080: 0x0000000000000000 0x0000000000000000
0x405090: 0x0000000000000000 0x0000000000000021 <=== chunk2
0x4050a0: 0x0000000000000000 0x0000000000000000
0x4050b0: 0x0000000000000000 0x0000000000000021 <=== chunk3
0x4050c0: 0x0000000000000000 0x0000000000000000
0x4050d0: 0x0000000000000000 0x0000000000020f31 <=== top chunk
0x4050e0: 0x0000000000000000 0x0000000000000000
0x4050f0: 0x0000000000000000 0x0000000000000000

释放后, chunk1 把 chunk2 的内容吞并掉并一起置入 unsorted bin

pwndbg> heap
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x405000
Size: 0xb1
fd: 0x7ffff7dd4b78
bk: 0x7ffff7dd4b78

Allocated chunk
Addr: 0x4050b0
Size: 0x20

Top chunk | PREV_INUSE
Addr: 0x4050d0
Size: 0x20f31

此时 unsorted bin

pwndbg> unsorted
unsortedbin
all: 0x405000 —▸ 0x7ffff7dd4b78 (main_arena+88) ◂— 0x405000

再次进行分配的时候就会取回 chunk1 和 chunk2 的空间, 此时我们就可以控制 chunk2 中的内容

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0xb1

Allocated chunk | PREV_INUSE
Addr: 0x4050b0
Size: 0x21

Top chunk | PREV_INUSE
Addr: 0x4050d0
Size: 0x20f31

对 free 的 smallbin 进行 extend

在上一代码基础上进行的, 这次先释放 chunk1, 然后再修改处于 unsorted bin 中的 chunk1 的 size 域

#include <stdlib.h>

int main() {
void *ptr, *ptr1;

ptr = malloc(0x80); // 分配大小为0x80的 chunk1
malloc(0x10); // 分配大小为0x10的 chunk2

free(ptr); // 首先进行释放, 使得chunk1进入unsorted bin

*(long long*)((long long)ptr - 0x8) = 0xb1;
ptr1 = malloc(0xa0);
}

两次 malloc 之后的结果如下

pwndbg> heap
Allocated chunk | PREV_INUSE <=== chunk1
Addr: 0x405000
Size: 0x91

Allocated chunk | PREV_INUSE <=== chunk2
Addr: 0x405090
Size: 0x21

Top chunk | PREV_INUSE <=== top chunk
Addr: 0x4050b0
Size: 0x20f51

我们首先释放 chunk1 使它进入 unsorted bin

pwndbg> unsortedbin
unsortedbin
all: 0x405000 —▸ 0x7ffff7dd4b78 (main_arena+88) ◂— 0x405000

然后篡改 chunk1 的 size 域

pwndbg> heap
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x405000
Size: 0xb1
fd: 0x7ffff7dd4b78
bk: 0x7ffff7dd4b78

Top chunk | PREV_INUSE
Addr: 0x4050b0
Size: 0x20f51

pwndbg> x/8gx 0x405000
0x405000: 0x0000000000000000 0x00000000000000b1 <=== size域被篡改
0x405010: 0x00007ffff7dd4b78 0x00007ffff7dd4b78
0x405020: 0x0000000000000000 0x0000000000000000
0x405030: 0x0000000000000000 0x0000000000000000

此时再进行 malloc 分配就可以得到 chunk1+chunk2 的堆块, 从而控制了chunk2 的内容

Chunk Extend/Shrink 可以做什么

一般来说, 这种技术并不能直接控制程序的执行流程, 但是可以控制 chunk 中的内容。如果 chunk 存在字符串指针、函数指针等, 就可以利用这些指针来进行信息泄漏和控制执行流程。

此外通过 extend 可以实现 chunk overlapping, 通过 overlapping 可以控制 chunk 的 fd/bk 指针从而可以实现 fastbin attack 等利用

通过 extend 后向 overlapping

#include <stdlib.h>

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
*(long long *)((long long)ptr-0x8)=0x61;
free(ptr);
ptr1 = malloc(0x50);
}

在 malloc(0x50) 对 extend 区域重新占位后, 其中 0x10 的 fastbin 块依然可以正常的分配和释放, 此时已经构成 overlapping, 通过对overlapping 的进行操作可以实现 fastbin attack。

通过 extend 前向 overlapping

这里展示通过修改 pre_inuse 域和 pre_size 域实现合并前面的块

#include <stdlib.h>

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

}

前向 extend 利用了 smallbin 的 unlink 机制, 通过修改 pre_size 域可以跨越多个 chunk 进行合并实现 overlapping

例题

HITCON Trainging lab13、2015 hacklu bookstore、2016 Nuit du Hack CTF Quals : night deamonic heap

参考

CTF Wiki Chunk Extend and Overlapping