第四章 绕过思路总览:架构差异利用、系统调用号映射、32位兼容模式、时间竞争窗口

好,我们正式进入绕过思路的全局梳理。这一章我不打算讲具体漏洞利用,而是把沙箱绕过这整件事的「地图」先画出来。你想想看,沙箱本质上是个什么东西?它就是一个规则集,告诉你「哪些系统调用能用,哪些不能用」。绕过沙箱,说白了就是找到规则没覆盖到的地方,或者让规则失效。

我个人习惯把绕过思路分成四大类。这四类不是互斥的,很多时候一个题目会同时用到两三种。我们先看总览,再逐个拆解。

核心观点:沙箱绕过不是魔法,而是对「规则边界」的精确打击。你越了解底层机制,就越容易找到盲区。

4.1 四大绕过思路概览

我整理了一张总览图,把四种思路和它们对应的技术点放在一起。你可以先看这张图,再往下读文字。

沙箱绕过四大思路总览 沙箱绕过 架构差异利用 ARM vs x86 vs MIPS 系统调用号映射 不同架构的syscall表 32位兼容模式 AArch32 / x86-32 时间竞争窗口 TOCTOU / 竞态条件 四种思路可单独使用,也可组合攻击 实际CTF题目中,架构差异 + 系统调用号映射是最常见组合

4.2 架构差异利用

这个思路的核心很简单:沙箱的过滤规则通常是针对某个特定架构写的。但题目运行的环境可能支持多种架构。你想想看,如果沙箱只检查了 ARM64 的系统调用,但你用 ARM32 模式去执行,会发生什么?

我在项目中遇到过真实案例:一个 IoT 设备的沙箱只过滤了 ARM Thumb 指令集的系统调用,但攻击者切换到 ARM 标准模式后,系统调用号完全变了,沙箱直接失效。嗯,这就是架构差异利用的典型场景。

架构对 差异点 绕过潜力
ARM64 vs ARM32 系统调用号不同,调用约定不同 高
x86-64 vs x86-32 syscall vs int 0x80,参数传递方式不同 高
MIPS64 vs MIPS32 系统调用号映射完全不同 中
大端 vs 小端 数据布局差异,可能绕过字符串检查 低

实战技巧:当你看到题目是 ARM64 架构,但沙箱代码里只检查了 svc #0 的调用号,试试用 svc #0x80000 或者切换到 AArch32 模式。我曾经用这招在 15 分钟内解出一道原本需要 2 小时的题目。

4.3 系统调用号映射

这个思路和架构差异利用是孪生兄弟。不同架构的系统调用号映射表完全不同。举个例子,在 x86-64 上 open 是 2 号,但在 ARM64 上 open 是 56 号。沙箱如果写死了某个数字,却没考虑架构切换,那就留下了绕过空间。

我整理了一份常见架构的系统调用号对比表,只列了最常用的几个:

系统调用 x86-64 x86-32 ARM64 ARM32
read 0 3 63 4
write 1 4 64 5
open 2 5 56 5
execve 59 11 221 11
mmap 9 192 222 192

你看,execve 在 x86-64 上是 59,在 ARM64 上是 221。如果沙箱只过滤了 59,那切换到 ARM64 模式后,221 号调用就畅通无阻。说白了,沙箱作者只考虑了一种架构,这就是你的机会。

注意:系统调用号映射不是简单的「查表替换」。不同架构的参数传递方式也可能不同。x86-64 用 rdi, rsi, rdx 传参,ARM64 用 x0, x1, x2。切换架构时,你的 shellcode 必须同时调整调用号和参数传递方式。

4.4 32位兼容模式

这个思路值得单独拿出来讲,因为它太常用了。很多现代 CPU 都支持 32 位兼容模式:ARM64 芯片可以运行 AArch32 代码,x86-64 芯片可以运行 32 位 x86 代码。沙箱呢?往往只检查了 64 位的系统调用路径。

为什么会这样?因为沙箱开发者默认「题目是 64 位的,攻击者也只能用 64 位代码」。但如果你能切换到 32 位模式,系统调用号、调用约定、甚至系统调用的实现路径都可能不同。

我记得有一次打比赛,题目是 ARM64 架构,seccomp 规则写得非常严格,几乎只允许 read/write。我花了两个小时没找到突破口。后来突然想到——试试 AArch32 模式?结果发现 seccomp 在 AArch32 模式下根本没生效。嗯,因为内核的 seccomp 实现只注册了 64 位的钩子。那次之后,我养成了一个习惯:看到 ARM64 题目,第一件事就是检查 AArch32 模式是否可用。

关键点:32位兼容模式绕过通常需要两个步骤:1) 切换到 32 位模式(通过修改 CPSR 或 CS 寄存器);2) 使用 32 位的系统调用号。两步缺一不可。

4.5 时间竞争窗口

这个思路和前三个不太一样。它不是利用架构差异,而是利用沙箱检查和执行之间的时间差。说白了,就是 TOCTOU(Time-of-Check Time-of-Use)问题。

沙箱的工作流程通常是:

  1. 捕获系统调用
  2. 检查参数(比如文件路径、地址范围)
  3. 允许或拒绝

如果在步骤 2 和步骤 3 之间,攻击者能改变参数的内容,那沙箱检查的就是旧值,实际执行的是新值。这就是时间竞争窗口。

我举个例子:沙箱检查 open("/safe/file", O_RDONLY),允许了。但在检查通过后、内核真正打开文件前,你用另一个线程把 /safe/file 替换成符号链接指向 /etc/shadow。嗯,这就是经典的符号链接竞态攻击。

攻击类型 原理 适用场景
符号链接竞态 检查路径后替换为恶意链接 沙箱检查文件路径
内存竞态 检查内存内容后修改 沙箱检查缓冲区内容
文件描述符竞态 检查 fd 后 dup2 到其他 fd 沙箱检查 fd 类型

避坑指南:我曾经在时间竞争窗口上栽过跟头。当时我写了一个竞态攻击,但忽略了 CPU 缓存一致性。在多核系统上,一个线程修改了内存,另一个线程可能看不到最新值。嗯,记得加上内存屏障(memory barrier)指令。

4.6 四种思路的优先级判断

面对一道具体的题目,你应该先试哪个思路?我个人的判断顺序是这样的:

  • 第一优先级:架构差异利用 + 系统调用号映射。这两个通常一起用,而且最容易实现。只需要改 shellcode 里的系统调用号和调用方式,不需要复杂的竞态条件。
  • 第二优先级:32位兼容模式。如果题目是 ARM64 或 x86-64,先试试能不能切到 32 位模式。很多时候沙箱根本没覆盖 32 位路径。
  • 第三优先级:时间竞争窗口。这个最难利用,需要多线程、精确的时序控制,而且成功率受系统负载影响。但一旦成功,效果往往最好。

当然,这不是绝对的。有些题目会故意把架构差异堵死,逼你走竞态路线。嗯,具体情况具体分析。

我的习惯:拿到题目后,先看沙箱的过滤规则。如果规则里明确写了架构(比如 seccomp_arch_add(SCMP_ARCH_AARCH64)),那架构差异利用的可能性就很大。如果规则里没写架构,那 32 位兼容模式基本是必杀技。