【学习记录】Week14(二):沙箱机制深度剖析与进阶 ORW 绕过体系

📅 2026/7/6 4:22:00 👁️ 阅读次数 📝 编程学习
【学习记录】Week14(二):沙箱机制深度剖析与进阶 ORW 绕过体系

写在前面:在上一篇中,我们建立了信息泄露的系统化方法论,并学习了 ORW(Open-Read-Write)的基础 ROP 构造。然而,出题人的防守并非一成不变。当简单的open也被沙箱拦截时,基础的 ORW 链便会失效。今天,我们将深入剖析 Linux 的沙箱机制,学习如何使用seccomp-tools逆向分析规则,并掌握进阶的 ORW 绕过手法,如openat/proc/self/mem魔法。

📑 目录

  1. 沙箱溯源:Seccomp 与 BPF 机制简介
  2. 破译密码:使用seccomp-tools分析沙箱规则
  3. 进阶 ORW:当open被禁时的openat替换
  4. 深水区绕过:/proc/self/mem魔法读取
  5. 总结与下篇预告

1. 沙箱溯源:Seccomp 与 BPF 机制简介

在 Linux 中,Seccomp(Secure Computing Mode)是一种用于限制进程可用系统调用的内核机制。现代 CTF 中的沙箱通常基于prctl(PR_SET_NO_NEW_PRIVS, 1)seccomp(SECCOMP_MODE_FILTER, ...)实现。

其核心是BPF (Berkeley Packet Filter)。BPF 最初用于网络数据包过滤,后来被引入到系统调用过滤中。出题人会编写一段 BPF 字节码,告诉内核:“如果进程请求的系统调用号是 X,则放行;如果是 Y,则杀死进程”。

由于 BPF 规则是以字节码形式加载到内核的,我们在用户态无法直接 patch 掉它。因此,我们必须先逆向分析这段字节码,找出它的“盲区”。

2. 破译密码:使用seccomp-tools分析沙箱规则

手动逆向 BPF 字节码极其痛苦。感谢安全社区,我们拥有神器seccomp-tools
假设题目附件是./pwn,我们执行:

seccomp-tools dump ./pwn

它会将内核中的 BPF 规则反编译为易读的伪代码。我们经常会看到如下输出:

场景 A:基础沙箱(只禁 execve)

line 1: ALLOW syscalls: open, read, write, mmap, mprotect... line 2: KILL syscalls: execve (59), execveat (322)

*应对策略*:使用上一篇讲的基础 ORW 即可。

场景 B:进阶沙箱(禁用 open)

line 1: ALLOW syscalls: read, write, mmap, mprotect... line 2: KILL syscalls: execve, open (2)

*应对策略*:open被杀,但openat往往幸存。

场景 C:地狱级沙箱(禁用所有 open 家族)

line 1: ALLOW syscalls: read, write, mmap, mprotect... line 2: KILL syscalls: execve, open (2), openat (257)

*应对策略*:无法打开新文件,但可以利用/proc/self/mem配合write实现任意地址读写。

3. 进阶 ORW:当open被禁时的openat替换

open的系统调用号是 2,而openat是 257。它们的功能几乎一样,区别在于openat需要额外指定一个目录文件描述符dirfd

3.1 openat 的函数原型

int openat(int dirfd, const char *pathname, int flags);
  • 如果pathname是绝对路径(如/flag),则dirfd参数会被忽略。
  • 因此,我们只需将dirfd设置为任意值(通常设为AT_FDCWD(-100) 或 0),即可完全等价于open

3.2 ROP 链修改

只需将基础 ORW 中的open部分替换为openat

// openat("/flag", 0) -> sys_openat = 257 pop rdi; ret; -100; // rdi = AT_FDCWD (-100) pop rsi; ret; bss_addr; // rsi = "/flag" 字符串地址 pop rdx; ret; 0; // rdx = O_RDONLY pop rax; ret; 257; // rax = 257 (sys_openat) syscall; ret;

后续的readwrite完全不变。这就是为什么出题人必须把openopenat一起禁掉,否则沙箱形同虚设。

4. 深水区绕过:/proc/self/mem魔法读取

openopenat全部阵亡,我们无法获取新的文件描述符。但幸运的是,程序启动时默认打开了三个流:stdin(0),stdout(1),stderr(2)。更重要的是,Linux 提供了一个特殊的虚拟文件:/proc/self/mem

4.1 核心思想

/proc/self/mem是当前进程内存的镜像。对这个文件进行lseekread/write,等同于直接读写进程自身的内存!
然而,/proc/self/mem并不是一个常规文件,它无法直接被open打开(即使没禁用 open,也可能因为权限问题失败)。但它通常已经被打开了,在文件描述符表中吗?不,它没有。
等等,如果连openat都不能用,怎么打开它?

4.2 终极魔法:利用write绕过限制

如果沙箱允许openat,我们可以打开/proc/self/mem。但如果连openat都禁了呢?
此时如果允许writelseek(系统调用号 8):

  1. 我们通过 ROP 调用openat打开/proc/self/mem… 不行,禁用了。
  2. 真正的魔法(无 open 场景):如果题目允许write,且我们有一个指向 libc 中__free_hook或类似可写区域的指针,我们可以直接写入 shellcode?不,NX 开启。
  3. 修正魔法(结合 mprotect):如果沙箱允许mprotectwrite,且允许open(但不允许openat),这太矛盾了。

真实的/proc/self/mem利用场景
通常发生在:允许openopenat,但不允许直接读flag(例如通过正则过滤了路径名,或者read被限制只能读取特定 fd)。
更极端的场景:允许openat打开/proc/self/mem,然后利用lseek偏移到目标内存,再用write覆盖内存!

具体流程

  1. openat("/proc/self/mem", O_RDWR)-> 返回 fd = 3
  2. lseek(3, target_addr, SEEK_SET)-> 将文件指针移动到我们想写的目标地址(如__free_hook或栈上的返回地址)
  3. write(3, payload, len)-> 将 payload 写入目标地址!
    这种手法可以绕过某些对write系统调用参数有严格检查的沙箱,因为我们写的“文件”是内存本身。

4.3 如果连 openat 都没有,只有 read/write 怎么办?

这是最极端的无 libc 场景或极严沙箱。通常需要利用mprotect将内存改为可执行,然后写入 shellcode 执行(这需要mprotect未被禁)。如果mprotect也被禁,则可能需要利用内核漏洞(超出本周讨论范围)。

5. 总结与下篇预告

5.1 核心知识点总结

  1. Seccomp 与 BPF:理解沙箱的底层原理,沙箱规则是加载到内核的,用户态无法绕过,只能寻找规则的盲区。
  2. seccomp-tools:实战必备工具,打题第一步必先 dump 沙箱规则。
  3. openat替换:最基础的绕过姿势,利用dirfd = AT_FDCWD完美替代open
  4. /proc/self/mem:将内存视为文件,通过lseek+write实现极其隐蔽的任意地址写,绕过对系统调用参数的直接检查。

5.2 下篇预告

在解决了沙箱与 ORW 之后,下一篇我们将转向另一个实战痛点:无 Libc 环境与ret2mprotect进阶应用

  • 当题目不给 libc,且远程环境未知时,如何利用ret2dlresolveret2mprotect破局?
  • 如何在没有pop rdx; ret等 gadget 时,利用__libc_csu_init构造万能 ROP?
  • got2plt劫持在 Partial RELRO 下的妙用。

结语:沙箱不是不可逾越的高墙,而是一道带缝隙的过滤网。出题人受限于系统调用之间的依赖关系,永远无法完全封死读写内存的途径。掌握openat/proc/self/mem,你就掌握了在沙箱中“穿墙”的咒语。