3. seccomp 规则分析:从 dump 到自定义编写
seccomp 规则分析,说白了就是搞清楚目标程序到底禁用了哪些系统调用。我刚开始接触 Pwn 题时,总觉得这步可有可无,直到有一次在 ARM64 的题目里栽了跟头——明明本地能打通,远程就是不行。后来一查 seccomp 规则,好家伙,open 被 ban 了,但 openat 还活着。嗯,从那以后我再也不敢跳过规则分析了。
3.1 用 seccomp-tools dump 规则
seccomp-tools 是我个人最常用的工具,没有之一。它能把内核里的 BPF 字节码反编译成人类可读的规则列表。安装很简单:
# 安装 seccomp-tools
$ gem install seccomp-tools
# 或者从源码编译
$ git clone https://github.com/david942j/seccomp-tools
$ cd seccomp-tools
$ make install
拿到一个二进制文件后,我习惯先看看它有没有 seccomp 保护:
$ seccomp-tools dump ./pwn_binary
# 输出示例
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A == AUDIT_ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x03 0x0000003b if (A == execve) goto 0007
0004: 0x15 0x00 0x02 0x00000039 if (A == fork) goto 0007
0005: 0x15 0x00 0x01 0x0000003a if (A == vfork) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
这段输出什么意思?说白了就是:
- 先检查架构,如果不是 x86_64 就直接放行(防止架构逃逸)
- 如果是 x86_64,检查系统调用号
- 遇到
execve、fork、vfork就杀掉进程 - 其他调用全部放行
seccomp-tools dump ./pwn_binary 2>/dev/null 过滤掉 stderr 的干扰信息,只保留规则输出。
3.2 用 seccomp-tools 解析规则
光看 dump 还不够,有时候我们需要更结构化的信息。seccomp-tools 提供了 seccomp-tools disasm 子命令,能把 BPF 字节码反汇编成更清晰的格式:
$ seccomp-tools disasm ./pwn_binary
# 输出示例
A = arch
if (A != AUDIT_ARCH_X86_64) goto ALLOW
A = sys_number
if (A == execve) goto KILL
if (A == fork) goto KILL
if (A == vfork) goto KILL
ALLOW: return ALLOW
KILL: return KILL
我个人更喜欢这种格式,因为它去掉了行号和偏移量,直接展示逻辑分支。你想想看,如果规则有几十条,用 dump 看会眼花,但用 disasm 就能一眼看出哪些调用被 ban 了。
还有一个更高级的用法——seccomp-tools visualize,它能生成规则的流程图。不过这个功能依赖 Graphviz,需要先安装:
$ sudo apt-get install graphviz
$ seccomp-tools visualize ./pwn_binary -o rule.png
嗯,这里要注意:visualize 生成的图片是静态的,对于复杂规则(比如有嵌套条件判断的),图片会变得很乱。我一般只在规则少于 20 条时用这个功能。
3.3 自定义 seccomp 规则编写
分析别人的规则只是第一步,真正的高手要学会自己写规则。我常用的工具有两个:seccomp-tools 自带的 DSL 和 libseccomp 的 C API。
3.3.1 用 seccomp-tools DSL 编写
seccomp-tools 支持一种类似汇编的 DSL,写起来很直观。举个例子,假设我们要写一个规则:只允许 read、write、openat、mmap、exit_group,其他全部杀掉:
# 文件名: my_rule.asm
A = arch
if (A != AUDIT_ARCH_X86_64) goto KILL
A = sys_number
if (A == 0) goto ALLOW # read
if (A == 1) goto ALLOW # write
if (A == 257) goto ALLOW # openat
if (A == 9) goto ALLOW # mmap
if (A == 231) goto ALLOW # exit_group
goto KILL
ALLOW: return ALLOW
KILL: return KILL
然后编译成 BPF 字节码:
$ seccomp-tools asm my_rule.asm -o my_rule.bpf
这个 .bpf 文件可以直接被 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...) 加载。我在项目中遇到过好几次,有些 CTF 题目会要求选手自己构造 seccomp 规则来限制 shellcode 的行为,这时候 DSL 就派上用场了。
3.3.2 用 libseccomp C API 编写
如果你在写 C 代码,用 libseccomp 更合适。它提供了类型安全的接口,不容易写错系统调用号:
#include <seccomp.h>
#include <unistd.h>
int setup_seccomp() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL); // 默认杀掉所有调用
// 白名单模式:只允许 read, write, openat, mmap, exit_group
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
// 加载规则
seccomp_load(ctx);
seccomp_release(ctx);
return 0;
}
open 是 2,但 ARM64 的 open 是 56。写规则前一定要确认目标架构。
3.4 规则编写的高级技巧
写规则不只是列白名单那么简单。我总结了几种常见模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 白名单模式 | 默认 KILL,只放行特定调用 | 安全性要求高,功能明确 |
| 黑名单模式 | 默认 ALLOW,只 ban 特定调用 | 兼容性要求高,只需禁用少数危险调用 |
| 参数过滤模式 | 根据系统调用的参数决定放行或杀死 | 需要精细控制,比如只允许打开特定文件 |
| 架构感知模式 | 先检查架构,再根据架构执行不同规则 | 多架构兼容,防止架构逃逸 |
参数过滤模式比较高级,我举个例子。假设我们只允许打开 /tmp 目录下的文件:
A = arch
if (A != AUDIT_ARCH_X86_64) goto KILL
A = sys_number
if (A != 257) goto CHECK_OTHER # 不是 openat 就检查其他调用
# 检查 openat 的第二个参数(文件名指针)
# 这里需要读取用户空间内存,seccomp 本身不支持
# 需要用 seccomp_data 结构体的 args 字段
# 但 seccomp-tools DSL 不支持这种高级操作
# 建议用 libseccomp 的 seccomp_rule_add_exact 配合 SCMP_FLTATR_CTL_OPTIMIZE
CHECK_OTHER:
# ... 其他规则
3.5 规则分析实战:一个典型 CTF 题目
我记得有一次打 CTF,题目给了个 ARM64 的二进制,seccomp 规则 dump 出来是这样的:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == AUDIT_ARCH_AARCH64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x03 0x00000038 if (A == 56) goto 0008 # open
0005: 0x15 0x00 0x02 0x00000039 if (A == 57) goto 0008 # openat
0006: 0x15 0x00 0x01 0x0000003c if (A == 60) goto 0008 # exit
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL
分析一下:
- 只允许 AARCH64 架构,其他架构直接杀掉
- ban 掉了
open(56)、openat(57)、exit(60) - 其他调用全部放行
嗯,这里有个坑:exit 被 ban 了,但 exit_group(94)还活着。所以 shellcode 里不能用 exit,得用 exit_group。另外,open 和 openat 都被 ban 了,但 read、write、mmap 都还在。这意味着我们可以用 mmap 映射文件到内存,然后直接读内存——前提是文件描述符已经存在。
我当时就是利用了这个漏洞:程序启动时已经打开了 flag 文件(fd=3),虽然 open 被 ban 了,但 read 还能用,直接 read(3, buf, 0x100) 就把 flag 读出来了。
3.6 本章知识结构
下面这张图总结了 seccomp 规则分析的核心流程:
这张图展示了从拿到二进制到制定绕过策略的完整链路。你想想看,每一步都有对应的工具和技巧,缺一不可。