第四章 seccomp规则绕过:白名单绕过、黑名单绕过、系统调用号混淆、参数篡改技术
seccomp,全称是secure computing mode。说白了,它就是Linux内核给你进程装的一个“安检门”。你想调用系统函数?可以,但得先过我这关。CTF里很多Pwn题,你明明拿到了shell,结果一执行命令就被kill,十有八九就是seccomp在搞鬼。
我早期做CTF时,第一次遇到seccomp,心态直接崩了。明明漏洞利用都写好了,结果execve被拦,cat flag被拦,连open都报错。后来我才明白,绕过seccomp,本质上就是跟内核的“安检规则”玩捉迷藏。
这一章,我们就来拆解seccomp绕过的四种核心思路:白名单绕过、黑名单绕过、系统调用号混淆、参数篡改。每一种我都会结合实战案例来讲。
4.1 先搞懂seccomp的两种模式
在动手之前,你得先知道seccomp的两种基本模式。不然你连对手的武器都不清楚,怎么打?
| 模式 | 原理 | CTF常见场景 |
|---|---|---|
| STRICT模式 | 只允许read、write、exit、sigreturn等极少数调用 | 极少见,太严格了,基本没法做题 |
| FILTER模式(BPF) | 通过自定义BPF规则,允许或禁止特定系统调用 | 90%的CTF题都是这种,规则可定制 |
FILTER模式才是我们的主战场。它用一套BPF(Berkeley Packet Filter)规则,对每个系统调用进行裁决。裁决结果有三种:ALLOW(放行)、DENY(拒绝)、KILL(直接杀进程)。
核心认知:seccomp规则是静态的,但系统调用的行为是动态的。绕过,就是利用这个“动静差异”。
4.2 白名单绕过:规则之外的“合法”路径
白名单策略,就是只允许你使用特定的系统调用。比如只允许open、read、write,其他的一律拦截。听起来很安全对吧?但漏洞往往就藏在“允许”里。
思路一:用允许的调用,完成禁止的操作
举个例子。题目只允许open、read、write,但禁止了execve。你想执行/bin/sh拿shell?不可能。但你可以这样:
- 用open打开flag文件
- 用read读取内容到内存
- 用write输出到stdout
这不就绕过了吗?你并没有调用execve,但拿到了flag。这就是白名单绕过的核心——用合法调用拼凑出非法效果。
思路二:利用openat等变体
有些题目只禁止了open,但没禁止openat。openat和open功能几乎一样,只是多了一个目录文件描述符参数。我遇到过一道题,规则里只写了“deny open”,结果我用openat(AT_FDCWD, "flag", O_RDONLY)直接绕过去了。出题人估计自己都忘了还有这个变体。
我的习惯:拿到seccomp规则后,第一件事不是分析它禁止了什么,而是分析它允许了什么。允许的调用里,往往藏着绕过路径。
4.3 黑名单绕过:规则没写全的“漏网之鱼”
黑名单策略,就是只禁止特定的系统调用,其他的都放行。这种策略看似宽松,但出题人往往会漏掉一些关键调用。
常见黑名单漏洞:
- 只禁止了execve,没禁止execveat——execveat是execve的扩展版本,功能几乎一样。
- 只禁止了open,没禁止openat——上面刚说过。
- 只禁止了32位调用,没禁止64位调用——有些规则只检查了x86架构的调用号,但程序运行在x64模式下,调用号完全不同。
- 只禁止了普通socket,没禁止socketpair——某些场景下,socketpair也能用来通信。
实战案例:
我记得有一道题,规则里明确禁止了execve(系统调用号59)。但程序是64位的。我查了一下,64位下execve的调用号是59,但32位兼容模式下,execve的调用号是11。于是我用int 0x80触发32位系统调用,传入调用号11,成功执行了execve。出题人只拦了64位的59号,没拦32位的11号。
避坑指南:我曾经在32位和64位调用号混淆上栽过跟头。后来我养成了一个习惯——先确认程序是32位还是64位,再查对应的系统调用表。两个表差别很大,千万别搞混。
4.4 系统调用号混淆:让seccomp认错人
seccomp规则是基于系统调用号来匹配的。如果你能改变调用号,或者让seccomp匹配到错误的规则,就能实现绕过。
技术一:调用号重映射
有些内核版本或架构下,系统调用号不是固定的。比如通过修改sys_call_table,或者利用LSM(Linux Security Module)钩子,可以改变调用号到实际函数的映射关系。但CTF里很少让你直接改内核,所以这个技术更多是了解。
技术二:利用x32 ABI
x32 ABI是一个特殊的ABI,它使用64位的指令,但指针是32位的。它的系统调用号和x86_64完全不同。如果seccomp规则只写了x86_64的调用号,你用x32 ABI发起调用,seccomp可能就认不出来了。
技术三:间接系统调用
有些库函数或内核接口,内部会调用多个系统调用。比如libc的system()函数,内部会调用clone、execve等多个调用。如果你能控制这些内部调用,就能绕过seccomp的单一规则检查。
核心要点:系统调用号混淆的本质,就是让seccomp的规则匹配逻辑失效。要么让它匹配到错误的规则,要么让它根本匹配不到规则。
4.5 参数篡改:规则检查了,但没完全检查
seccomp规则不仅能检查调用号,还能检查参数。但参数检查往往有漏洞。
常见参数检查漏洞:
- 只检查了参数的值,没检查参数的类型——比如规则禁止open一个名为“flag”的文件,但你可以用符号链接、硬链接、或者/proc/self/fd等方式绕过。
- 只检查了第一个参数,没检查后续参数——有些规则只检查了filename,但没检查flags或mode。
- 只检查了直接参数,没检查间接参数——比如规则检查了execve的argv,但argv本身是一个指针数组,你可以通过修改指针指向的内容来绕过。
实战案例:
我遇到过一道题,seccomp规则禁止了open(“/flag”, O_RDONLY)。但规则只检查了filename字符串是否等于“/flag”。我用了open(“/./flag”, O_RDONLY),路径不同,但实际打开的是同一个文件。规则没匹配到,直接放行了。
另一个经典案例是绕过execve的参数检查。规则禁止execve(“/bin/sh”, …),但你可以execve(“/bin//sh”, …),或者用execve(“/bin/sh”, {“/bin/sh”, “-c”, “cat /flag”, NULL}, NULL)。规则只检查了第一个参数,没检查argv数组里的内容。
我的建议:分析seccomp规则时,重点关注它检查了哪些参数,没检查哪些参数。没检查的参数,就是你的突破口。
4.6 知识体系总览
下面这张图,是我自己总结的seccomp绕过知识体系。每次遇到seccomp题,我都会对照这张图来寻找突破口。
4.7 实战中的避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
我曾经犯过的错:
- 没检查seccomp规则的生效时机——有些规则是在程序启动时加载的,有些是在fork之后。如果你在规则加载前就执行了恶意代码,可能根本不需要绕过。
- 忽略了seccomp的日志输出——很多seccomp实现会记录被拒绝的调用到内核日志。用dmesg查看日志,能帮你快速定位哪些调用被拦了。
- 盲目使用int 0x80——在64位程序里用int 0x80触发32位系统调用,确实能绕过一些规则。但要注意,int 0x80的参数传递方式和syscall不同,搞错了会直接崩溃。
嗯,这一章的内容就到这里。seccomp绕过,说白了就是一场“规则与执行”的博弈。你只要记住:规则是死的,人是活的。多想想“规则没写什么”,而不是“规则写了什么”,思路就打开了。