seccomp
- sandbox的典型
- 内核层面过滤system call
- 只要过滤器正确注册,就无法从用户进程绕过
- 这个sandbox非常强,还会结合其他的过滤器作为辅助,以防止意外的解决方案
- 尽量使用复杂的过滤器
- 也存在一部分过滤器存在漏洞的题目
- 只过滤自己的进程,不会影响其他进程
- 使用seccomp(2) 或者 prctl(2)来注册过滤器
- 这是一种通过库函数调用的方式,稍微有点复杂
使用seccomp的流程
- 进程发布seccomp(后面所说的prctl)
- 在内核中创建过滤器,之后每次执行system call都会经过过滤器验证
- 该过滤器适用于这个进程的每个线程,一旦注册,在该进程结束之前就不能进行修改或者删除。
- 如果允许prctl,则可以添加更多过滤器。如果允许fork,则子进程继承过滤器。如果允许execve,则在execve前后过滤器保持不变。
- 如果是允许的system call,则内核执行
- 如果是不允许的system call,则根据过滤器设置进行操作
- 不要直接结束进程,向进程发送SIGSYS信号(无效参数),返回错误代码,然后继续

seccomp的调用方法
有多种方法可以调用,有点复杂
- seccomp.h的库函数
- prctl(2)
- seccomp(2)
seccomp.h的库函数
- 直接使用seccomp.h中定义的库函数
- 最简单的调用方式
- 不需要了解BPF
- seccomp在内部实现了调用BPF(Berkeley Packet Filter)的虚拟机
- 封装之后,使用者不需要去了解BPF命令
- 只需要设置system call编号,参数检查等
- 自动检查平台架构
- prctl(PR_SET_NO_NEW_PRIVS, …)也自动进行
- 无法进行EIP/RIP
- 基本上是这4个
- scmp_filter_ctx seccomp_init(uint32_t def_action)
- int seccomp_rule_add(scmp_filter_ctx ctx, uint32_t action,
- int syscall, unsigned int arg_cnt, …) int seccomp_load(scmp_filter_ctx ctx)
- void seccomp_release(scmp_filter_ctx ctx)
- 函数定义参考这里:
代码示例

SCMP_ACT_KILL: 从内核收到SIGSYS,进程推出
SCMP_ACT_ALLOW: 执行system call
SCMP_ACT_TRAP: 自身发出SIGSYS
SCMP_ACT_ERRNO(x): 返回错误代码
SCMP_ACT_TRACE: 由ptrace控制时发出(SIGTRAP | (PTRACE_EVENT_SECCOMP<<8))
简单使用的话啊就只检查system call编号和参数是否正确
此次生成的内部BPF指令如下:

测试运行,如果禁掉了read,会得到下面的结果:

prctl(2)
- 使用prctl定义的PR_SET_SECCOMP
- 存在两种模式
- 静态模式
- 用户不需要编写过滤器
- 静态模式只有通用的过滤器,放行read, write, exit, sigreturn
- 很少用于CTF
- 过滤模式
- 自己编写并注册过滤器
- 可以在过滤器中检查EIP/RIP
- 如果需要在调用system call的时候检查EIP/RIP,则不能使用seccomp.h的库函数
- 使用seccomp_data.instruction_pointer
- 但是,过滤器结构很复杂
- 所有BPF命令都需要自己写
- 平台架构的检查,system call编号及参数的检查,全都需要使用者自己编写

基本流程如下:
- 创建一个sock_filter结构体数组
- 在这里写BPF命令
- 从sock_filter结构体数组及其大小创建sock_fprog结构体
- prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
- 如果没有这个,在第四步允许execve()的话,如果执行suid二进制文件,则uid可能改变,如果不重置该功能,可能存在各种漏洞。所以还是指定比较好。
- prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog, 0, 0);
参考:
- https://linuxjm.osdn.jp/html/LDP_man-pages/man2/prctl.2.html
- http://www.tiger1997.jp/report/activity/securityreport_20131211.html
- http://www.tiger1997.jp/report/activity/securityreport_20131227.html
FreeBSD的数据包过滤器,同呀昂存在BPF,作为参考:
- http://www.gsp.com/cgi-bin/man.cgi?topic=bpf
- http://www.yosbits.com/opensonar/rest/man/freebsd/man/ja/man4/bpf.4.html
Linux中的编程示例:
代码示例


- seccomp过滤器注册后,调用system call,每次都会向内核传递这样的结构体
- 平台架构检查是必须的。如果不存在此检查,则可能通过将执行上下文从x86切换到x64来跳过system call编号检查
- system call编号由BPF命令检查
- 向隐式累加器A(seccomp虚拟机的32位运算寄存器)使用seccomp_data->nr作为四个字节(BPF_W|BPF_ABS)进行加载(BPF_LD)
- 比较(BPF_JEQ)A与立即值(BPF_K)的__NR_XXX,根据结果产生分支(BPF_JMP)
- 返回(BPF_RET)立即值(BPF_K)
测试运行,如果禁掉了read,会得到下面的结果:

分析seccomp二进制文件的小技巧:
- 假设是prctl(2)方式的seccomp
- 制作BPF过滤器
- 但是,创建BPF过滤器时的命令逐条解析很麻烦
- 检查BPF过滤器的内容
- 使用gdb,到prctl(PR_SET_SECCOMP, …)之前
- 使用gdb-peda的dumpmem,dump BPF过滤器的内存范围
gdb-peda$ dumpmem /tmp/out.dmp 0xffffaaaabb00 0xffffaaaabc00
- 反编译dump出得BPF过滤器
$ disas-seccomp-filter /tmp/out.dmp
- 这样不需要知道构建过滤器过程,只需要知道最终的过滤器
seccomp(2)
prctl(2)的向上兼容
使用方法与prctl(2)基本相同
- 但是,glibc中没有seccomp的包装函数
- glibc的syscall()可以直接指定system call编号进行调用
long syscall(long number, ...);
大概是这样的流程
- 创建一个sock_filter结构体数组
- 从sock_filter结构体数组及其大小创建sock_fprog结构体
- prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
- syscall(__NR_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog)
- 与prctl(2)不同的只有第四步
可以使用SECCOMP_FILTER_FLAG_TSYNC标志来添加过滤器
- 添加过滤器时,请注意将同一过滤器添加到其他线程
- 如果存在无法添加的线程,则返回线程ID
syscall(__NR_seccomp,SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, &prog)
函数定义参考这里:
代码示例
因为库中不存在seccomp(2),这里使用syscall():

这里很容易出错,(虽然它是相同的过滤模式,prctl为2,seccomp为1)

总结
- seccomp.h的库函数
- 简单
- prctl(2)
- 存在静态模式,过滤模式两种
- 可以检查EIP/RIP
- seccomp(2)
- prctl(2)的向上兼容
- glibc中没有库,通过syscall()调用
- 存在同时向所有线程追加的选项