最近的题目,其实都偏向高版本 glibc 的应用和 都开启了 seccomp保护机制。刚开始学习这个机制,先补充一点基础知识,然后再补充相应题目(可能得要一段时间才能来填坑了,欠了太多的题目了)。
Seccomp基础知识
seccomp 是一种内核中的安全机制,未使用seccomp时,程序可以使用使用的 syscall,这是不安全的,因为 我们常见的 getshell 方式都是通过程序中一些 不安全的 函数中的 sysycall 来导致的,比如劫持 execve。但是通过seccomp,可以在程序中禁用掉某些 syscall,这样就算劫持了程序流也只能调用部分 syscall了。
编程使用seccomp
调用seccomp 的程序是能够直接运行,但是在自己编写调用 seccomp 程序的代码时,需要我们先安装相应的 头文件:
1 | sudo apt install libseccomp-dev libseccomp2 seccomp |
在如下测试程序中:
1 | //gcc -g simple_syscall_seccomp.c -o simple_syscall_seccomp -lseccomp |
运行结果如下所示,执行 execve 时程序报错退出。原因就是 seccomp 阻止了程序通过 execve来执行 syscall。
解释一下上述代码:
ctx
是 Filter context/handle
,其中 typedef void *scmp_filter_ctx
;
seccomp_init
是初始化的过滤状态,这里用的是 SCMP_ACT_ALLOW
,表示默认允许所有的 syscall,如果初始化状态为 SCMP_ACT_KILL
则表示不允许所有的 syscall。
1 | /* |
seccomp_rule_add
是添加一条规则,函数原型如下:
1 | /** |
seccomp_load
是应用过滤,如果不调用 seccomp_load
则上面的所有的过滤都不会生效。
1 | /** |
上面代码中 seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
arg_cnt=0,是表示不管 execve 的参数是什么,都会直接限制 execve 执行 syscall。
如果 arg_cnt
不为0,那 arg_cnt
表示后面限制的参数的个数,也就是只有调用 execve,且参数满足要求时,才会拦截 syscall。
1 | /** |
举例,拦截 write 函数 参数大于 0x10 时的系统调用
1 |
|
Prtctl简介
除了 seccomp,还有一个叫 prctl
的函数 也能做到类似的效果,函数原型如下:
1 |
|
早期 seccomp 是使用 prctl 系统调用实现的,后来封装成了一个 libcseccomp 库,该函数时进行进程控制的,这里也有 seccomp 的功能。
首先,使用 Prctl 需要有 CAP_SYS_ADMIN
权能,否则就要设置 PR_SET_NO_NEW_PRIVS
位,若不这样做 非 root 用户使用该程序时 seccomp
保护将会失效,设置了 PR_SET_NO_NEW_PRIVS
位后能保证 seccomp
对所有用户都能起作用,并且会使子进程即 execve 后的进程依然失控,意思就是 即使执行了 execve
这个系统调用替换了整个 binary 权限不会变化,设置后也不能再更改。
1 | prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0); //设为1 |
当 option 为 PR_SET_NO_NEW_PRIVS
(38),且 arg2 为 1时,将无法获得特权。
1 | PR_SET_NO_NEW_PRIVS (since Linux 3.5) |
举例:
1 |
|
1 | prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);//第一个参数要进行什么设置,第二个是设置为过滤模式,第三个参数就是过滤规则 |
当 option 为 PR_SET_SECCOMP
(22)时,效果就是我们上面的 seccomp 了,只不过这里的格式略有不同:
1 | PR_SET_SECCOMP (since Linux 2.6.23) |
如果 arg2 为 SECCOMP_MODE_STRICT
(1),则只允许调用 read,write,exit(not exit_group),sigreturn 这几个 syscall,如果 arg2 为 SECCOMP_MODE_FILTER
(2),则为过滤模式,其中对 syscall 的限制通过 arg3 用 BPF(Berkley Packet Filter) 的形式传递进来,是指向 struct sock_fprog 数组的指针。
第三个参数 prog
是如下结构体的指针 sock_fprog,这个结构体记录了过滤规则个数与规则数组起始位置。而 filter 域指向了具体的规则,每一条规则有如下 sock_filter 形式:
1 | /* |
为了操作方便定义了一组宏来完成filter的填写(定义在/usr/include/linux/bpf_common.h
。更详细的解释,参考:https://eigenstate.org/notes/seccomp
1 |
其中 code,是由 多个 变量组成,变量之间使用 + 连接:
1 |
|
另在与SECCOMP有关的定义在/usr/include/linux/seccomp.h
,现在来看看怎么写规则,首先是BPF_LD
,它需要用到的结构为:
1 | struct seccomp_data { |
其中args中是6个寄存器,在32位下是:ebx,ecx,edx,esi,edi,ebp
,在64位下是:rdi,rsi,rdx,r10,r8,r9
,现在要将syscall时eax的值载入RegA,可以使用:
1 | BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0) //这会把偏移0处的值放进寄存器A,读取的是seccomp_data的数据 |
跳转语句写法如下:
1 | BPF_JUMP(BPF_JMP+BPF_JEQ,59,1,0) //这回把寄存器A与值k(此处为59)作比较,为真跳过下一条规则,为假不跳转 |
后两个参数代表成功跳转到第几条规则,失败跳转到第几条规则,这是相对偏移。
最后当验证完成需要返回结果时,即是否允许:
1 | BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL) |
过滤的规则列表里可以有多条规则,seccomp 会从第 0 条开始逐条执行,直到遇到 BPF_RET
返回,决定是否允许该操作以及做某些修改。
示例
将 第一个代码中的 seccomp 改为 prctl 如下:
1 | //gcc -g simple_syscall_seccomp.c -o simple_syscall_seccomp -lseccomp |
通过 seccomp_export_bpf
的函数能够将设置的 seccomp 以 bpf 的形式导出,上面代码输出为 bpf.out
改用 prctl:
1 |
|
也能够成功拦截。
另一个示例可以参考 https://blog.betamao.me/2019/01/23/Linux%E6%B2%99%E7%AE%B1%E4%B9%8Bseccomp/
Seccomp-tools
现有的工具 seccomp-tools可以帮助我们分析 seccomp机制。
https://github.com/david942j/seccomp-tools
使用如下:
可知 限制了 execve函数,和 write 函数的 输出长度等于 0x10时禁止执行 syscall 函数。
seccomp绕过
未检查 arch
当未检查 arch 参数时,可以尝试转换当前的处理器模式,即在32位程序中转到64位或者相反,因为i386
和 x86-64
拥有不同的系统调用号,例如程序为 x86-64
的并且禁止 execve
:
1 | 11 64 munmap __x64_sys_munmap |
若改变模式让其认为当前正在处理 i386
的程序,那么系统调用号 11
将不会被解析为 __x64_sys_munmap
而是 sys_execve
,这样就绕过了保护。利用条件为:
1 | 1. 未检查arch |
第三点是因为要转换 CPU 的处理模式,所以在大部分情况下很难找到现成的 gadget 利用,需要手动注入shellcode 并能够执行。所以就需要一块可写可执行的内存,这个shellcode 的主要部分如下:
1 | to32: ;;将CPU模式转换为32位 |
原理是 RETF
指令,能够改变 CS
寄存器,当 CS
为 0x23 时表示当前为 64位,当为 0x33 时表示为32位:
1 | RETQ:POP RIP |
X64下使用 X32
如果程序开启了 arch 检查,就不能转换 处理器模式了。但是 x86-64 下有一种特殊模式 X32,它使用 64位的存器地址和 32位的地址,此时 nr 会在原来基础上加上 _X32_SYSCALL_BIT (0X400000000),即原本的 syscall number + 0x40000000,这会达到一样的效果,此时的 shellcode 效果如下:
1 | section .text |
还有思路,就是考虑系统有没有没有限制到的 syscall。
2015 baby playpen fence
程序分析
程序功能比较简单,就是接受用户的输入,并将用户的输出在 seccomp环境里执行。
程序使用了 prctl 函数,过滤了部分函数的 syscall,如下:
1 | struct sock_filter filter[] = { |
只禁用了open,mmap,openat, open_by_handle_at和ptrace,没有禁用 execve 函数。
原WP以及大佬博客上可见:Since 3.4 the Linux kernel has had a feature called the X32 ABI; 64bit syscalls with 32bit pointers.
1 | /usr/include/x86_64-linux-gnu/asm/unistd_x32.h:#define __NR_open (__X32_SYSCALL_BIT + 2) |
Using that syscall, you can bypass the seccomp filter. Blacklisting is bad.
也就是使用 X32_SYSCALL_BIT 可以绕过 seccomp 的黑名单限制,在上文中 X64下使用X32
提到的,系统调用号如下所示:
1 |
|
最终可以利用 X32_SYSCALL 来构造 shell.
1 | pop rdi |
EXP
1 | #coding=utf8 |
2015 big prison fence
程序分析
和第一个程序的总体功能类似,只不过在 seccomp沙箱这里,启动了更严格的 SECCOMP_MODE_STRICT`(1),则只允许调用 read,write,exit(not exit_group),sigreturn 这几个 syscall。
还有点不懂,后续补坑。
EXP
HITCON 2017 qual
程序分析
程序开启了 prctl 函数。
程序漏洞十分明显,最开始数组初始化话时溢出。后面还分别存在一个 任意位置输出 和 任意位置任意写漏洞。
利用分析
分析seccomp 的保护机制,可以看到存在两条绕过路线:
1 | 1. 任意函数(不为 mprotect),只要他的 第二个参数 等于他自己的 系统调用号 即可绕过沙箱检测。 |
利用思路如下:
1 | 1. 调用 mprotect,修改 .bss 的内存保护属性为 可写可执行。(一个内存页只要有了可写可执行权限,那么就算没读权限他也是可读的),需要满足的条件是: |
EXP
1 | #!/usr/bin/env python |
参考文献
https://veritas501.space/2018/05/05/seccomp%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
https://github.com/yvrctf/2015/tree/master/bigprisonfence
https://github.com/yvrctf/2015/tree/master/babyplaypenfence
https://blog.betamao.me/2019/01/23/Linux%E6%B2%99%E7%AE%B1%E4%B9%8Bseccomp/
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2020/09/27/seccomp学习笔记/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!