写在前面:在上一篇中,我们建立了信息泄露的系统化方法论,并学习了 ORW(Open-Read-Write)的基础 ROP 构造。然而,出题人的防守并非一成不变。当简单的
open也被沙箱拦截时,基础的 ORW 链便会失效。今天,我们将深入剖析 Linux 的沙箱机制,学习如何使用seccomp-tools逆向分析规则,并掌握进阶的 ORW 绕过手法,如openat与/proc/self/mem魔法。
📑 目录
- 沙箱溯源:Seccomp 与 BPF 机制简介
- 破译密码:使用
seccomp-tools分析沙箱规则 - 进阶 ORW:当
open被禁时的openat替换 - 深水区绕过:
/proc/self/mem魔法读取 - 总结与下篇预告
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;后续的read和write完全不变。这就是为什么出题人必须把open和openat一起禁掉,否则沙箱形同虚设。
4. 深水区绕过:/proc/self/mem魔法读取
当open和openat全部阵亡,我们无法获取新的文件描述符。但幸运的是,程序启动时默认打开了三个流:stdin(0),stdout(1),stderr(2)。更重要的是,Linux 提供了一个特殊的虚拟文件:/proc/self/mem。
4.1 核心思想
/proc/self/mem是当前进程内存的镜像。对这个文件进行lseek和read/write,等同于直接读写进程自身的内存!
然而,/proc/self/mem并不是一个常规文件,它无法直接被open打开(即使没禁用 open,也可能因为权限问题失败)。但它通常已经被打开了,在文件描述符表中吗?不,它没有。
等等,如果连openat都不能用,怎么打开它?
4.2 终极魔法:利用write绕过限制
如果沙箱允许openat,我们可以打开/proc/self/mem。但如果连openat都禁了呢?
此时如果允许write和lseek(系统调用号 8):
- 我们通过 ROP 调用
openat打开/proc/self/mem… 不行,禁用了。 - 真正的魔法(无 open 场景):如果题目允许
write,且我们有一个指向 libc 中__free_hook或类似可写区域的指针,我们可以直接写入 shellcode?不,NX 开启。 - 修正魔法(结合 mprotect):如果沙箱允许
mprotect和write,且允许open(但不允许openat),这太矛盾了。
真实的/proc/self/mem利用场景:
通常发生在:允许open或openat,但不允许直接读flag(例如通过正则过滤了路径名,或者read被限制只能读取特定 fd)。
更极端的场景:允许openat打开/proc/self/mem,然后利用lseek偏移到目标内存,再用write覆盖内存!
具体流程:
openat("/proc/self/mem", O_RDWR)-> 返回 fd = 3lseek(3, target_addr, SEEK_SET)-> 将文件指针移动到我们想写的目标地址(如__free_hook或栈上的返回地址)write(3, payload, len)-> 将 payload 写入目标地址!
这种手法可以绕过某些对write系统调用参数有严格检查的沙箱,因为我们写的“文件”是内存本身。
4.3 如果连 openat 都没有,只有 read/write 怎么办?
这是最极端的无 libc 场景或极严沙箱。通常需要利用mprotect将内存改为可执行,然后写入 shellcode 执行(这需要mprotect未被禁)。如果mprotect也被禁,则可能需要利用内核漏洞(超出本周讨论范围)。
5. 总结与下篇预告
5.1 核心知识点总结
- Seccomp 与 BPF:理解沙箱的底层原理,沙箱规则是加载到内核的,用户态无法绕过,只能寻找规则的盲区。
seccomp-tools:实战必备工具,打题第一步必先 dump 沙箱规则。openat替换:最基础的绕过姿势,利用dirfd = AT_FDCWD完美替代open。/proc/self/mem:将内存视为文件,通过lseek+write实现极其隐蔽的任意地址写,绕过对系统调用参数的直接检查。
5.2 下篇预告
在解决了沙箱与 ORW 之后,下一篇我们将转向另一个实战痛点:无 Libc 环境与ret2mprotect进阶应用。
- 当题目不给 libc,且远程环境未知时,如何利用
ret2dlresolve或ret2mprotect破局? - 如何在没有
pop rdx; ret等 gadget 时,利用__libc_csu_init构造万能 ROP? got2plt劫持在 Partial RELRO 下的妙用。
结语:沙箱不是不可逾越的高墙,而是一道带缝隙的过滤网。出题人受限于系统调用之间的依赖关系,永远无法完全封死读写内存的途径。掌握
openat和/proc/self/mem,你就掌握了在沙箱中“穿墙”的咒语。