第四章 绕过思路总览:架构差异利用、系统调用号映射、32位兼容模式、时间竞争窗口
好,我们正式进入绕过思路的全局梳理。这一章我不打算讲具体漏洞利用,而是把沙箱绕过这整件事的「地图」先画出来。你想想看,沙箱本质上是个什么东西?它就是一个规则集,告诉你「哪些系统调用能用,哪些不能用」。绕过沙箱,说白了就是找到规则没覆盖到的地方,或者让规则失效。
我个人习惯把绕过思路分成四大类。这四类不是互斥的,很多时候一个题目会同时用到两三种。我们先看总览,再逐个拆解。
核心观点:沙箱绕过不是魔法,而是对「规则边界」的精确打击。你越了解底层机制,就越容易找到盲区。
4.1 四大绕过思路概览
我整理了一张总览图,把四种思路和它们对应的技术点放在一起。你可以先看这张图,再往下读文字。
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)问题。
沙箱的工作流程通常是:
- 捕获系统调用
- 检查参数(比如文件路径、地址范围)
- 允许或拒绝
如果在步骤 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 位兼容模式基本是必杀技。