seccomp

  • sandbox的典型
  • 内核层面过滤system call
    • 只要过滤器正确注册,就无法从用户进程绕过
    • 这个sandbox非常强,还会结合其他的过滤器作为辅助,以防止意外的解决方案
  • 尽量使用复杂的过滤器
    • 也存在一部分过滤器存在漏洞的题目
  • 只过滤自己的进程,不会影响其他进程
  • 使用seccomp(2) 或者 prctl(2)来注册过滤器
    • 这是一种通过库函数调用的方式,稍微有点复杂

使用seccomp的流程

  1. 进程发布seccomp(后面所说的prctl)
  2. 在内核中创建过滤器,之后每次执行system call都会经过过滤器验证
    • 该过滤器适用于这个进程的每个线程,一旦注册,在该进程结束之前就不能进行修改或者删除。
    • 如果允许prctl,则可以添加更多过滤器。如果允许fork,则子进程继承过滤器。如果允许execve,则在execve前后过滤器保持不变。
  3. 如果是允许的system call,则内核执行
  4. 如果是不允许的system call,则根据过滤器设置进行操作
    • 不要直接结束进程,向进程发送SIGSYS信号(无效参数),返回错误代码,然后继续

seccomp的调用方法

有多种方法可以调用,有点复杂

  1. seccomp.h的库函数
  2. prctl(2)
  3. seccomp(2)

seccomp.h的库函数

代码示例

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

基本流程如下:

  1. 创建一个sock_filter结构体数组
    • 在这里写BPF命令
  2. 从sock_filter结构体数组及其大小创建sock_fprog结构体
  3. prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    • 如果没有这个,在第四步允许execve()的话,如果执行suid二进制文件,则uid可能改变,如果不重置该功能,可能存在各种漏洞。所以还是指定比较好。
  4. prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog, 0, 0);

参考:

FreeBSD的数据包过滤器,同呀昂存在BPF,作为参考:

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, ...);
  • 大概是这样的流程

    1. 创建一个sock_filter结构体数组
    2. 从sock_filter结构体数组及其大小创建sock_fprog结构体
    3. prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    4. 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)

总结

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