Escape from seccomp-sandbox and container
前言
学习一下seccomp沙箱逃逸和容器逃逸的方法。提到的方法有:
- ptrace
- open_by_handle_at
- 切换模式
- X32 ABI
主要的参考资料:
ptrace逃逸的poc https://gist.github.com/thejh/8346f47e359adecd1d53
通过ptrace绕过chroot https://www.xctf.org.cn/library/details/2d52db4f1a60bedbb2b388fd4da897d00e842233/
open_by_handle_at https://blog.ssrf.in/post/escape-seccomped-container-with-open-by-handle-at/
一个需要绕过seccomp的题目WP https://xz.aliyun.com/t/3083#toc-2
通过open_by_handle_at逃逸容器 https://juejin.im/entry/578deeec79bc44005ff7d66a
ptrace修改寄存器绕过沙箱
基础知识
syscall
在写shellcode时经常使用syscall来完成函数调用,例如常用的orw等。以下为一个简单的例子:
1 |
|
1 | ➜ seccomp-escape ./syscall |
seccomp
常用的沙箱,原理是禁用特定的系统调用,或只允许特定的系统调用。
假如prctl来禁用:
1 |
|
1 | ➜ seccomp-escape ./syscall |
通过seccomp来禁用:
1 |
|
默认状态为全允许,然后禁用了write函数。
1 | ➜ seccomp-escape ./syscall |
另外,seccomp的沙箱同样适用于子进程,即通过fork也不能逃出sandbox。
1 |
|
1 | ➜ seccomp-escape ./syscall |
需要注意的是,rule add这个函数的功能要强大很多,可以设定在哪种条件下杀掉系统调用。例如指定只有在write的第三个参数为x时杀掉系统调用。
ptrace
ptrace是一个系统调用,和名字一样,用来提供对进程的追踪功能。可以在一个进程中观察&控制另一个进程的状态。gdb就是基于ptrace实现的。ptrace也作为沙箱保护的一个工具。
参考:
- https://blog.betamao.me/2019/02/02/Linux%E6%B2%99%E7%AE%B1%E4%B9%8Bptrace/
- https://www.cnblogs.com/mysky007/p/11047943.html
信号
ptrace通过信号来完成进程间的通信。通过kill可以发送信号:
1 | ➜ seccomp-escape kill -l |
kill -9
就是杀进程:KILL
。
作用
- 实现gdb,strace等调试用的工具
- 反追踪。这是因为一个进程只能被一个进程追踪,但是一个进程可以追踪多个进程。如果进程A已经被另一个进程追踪了,那么其他进程就不能通过基于ptrace的方式追踪这个进程。该方法常用于加密解密等。
- 向其他进程注入代码,和调试类似
- 实现沙箱,监控系统调用
ptrace基础用法
例子:
1 |
|
运行结果:
1 | Syscall 1 |
父继承读取到了子进程调用了write,并读取了寄存器的值,然后让子进程继续运行。并拿到了子进程的返回值。
ptrace进阶用法
主要是用到了两个功能:
读取addr上的值
1 | ptrace(PTRACE_PEEKDATA, pid, addr, NULL); |
指定值写入指定地址
1 | ptrace(PTRACE_POKEDARA, pid, addr, NULL); |
有点gdb的意思了。
修改系统调用参数
核心代码:
1 | if(orig_rax == 1) |
在seccomp沙箱中禁止了write调用的长度为27的情况:
1 | scmp_filter_ctx ctx; |
绕过的方法就是通过ptrace将第三个参数修改了。
修改系统调用号
上面的方法看起来似乎只能绕过对参数的限制。如果限制不能调用write,也就不适用了。那如何绕过呢?
方法:先调用一个允许的系统调用,然后通过ptrace修改系统调用号。
例如,在沙箱中禁用了mkdir
,其他可以调用,那我们可以调用getpid
。
1 | syscall(SYS_getpid, SYS_mkdir, "dir", 0777); |
巧妙的是,mkdir
的系统调用号和参数我们作为getpid
的参数传进去,这样子进程的ptrace可以很容易拿到mkdir
的参数并赋值:
1 | struct user_regs_struct regs; |
这个顺序应该更清楚些:
1 | clone (parent, child) |
可参考的实现
如果涉及到这个步骤应该题目可以拿到libc的地址了,可以通过mprotect来创造rwx的段,写shellcode进去。
以下是一个可供参考的实现方式:
1 | clone = asm(''' |
涉及的题目为:SECCON 2018 - PWN SimpleMemo,比赛的过程中只有2解。
参考链接:
题目的源码和参考exp:
这个题目感觉很不错,有空完整复现一下。
open_by_handle_at打开文件
通过open_by_handle_at
逃逸容器是一个很经典的手法。这个方法可以逃逸docker容器等。不过似乎需要root权限才可以使用对应的syscall,所以绕过像seccomp这种沙箱应该不太行。
基础知识
Linux Capability
翻译应该是权能?普通用户可能需要root权限才能工作,于是将root的特权操作分成几个Capability,分配给可执行文件或进程。
Unix文件系统
proc结构体
保存进程状态,优先级等信息,给内核访问。常驻内存。
1 | struct proc { |
user结构体
保存进程打开的文件等信息。并不会长驻内存。proc.p_addr(数据段地址)最开始就是user结构体。user结构体中的u_ofile
比较有用,和文件描述符有关。
文件描述符
文件描述符:内核为了管理已经打开的文件做的索引,例如stdin=0,stdout=1,stderr=2。
如果两个进程打开了同一个文件(如图),在file[]
中是两个结构体,所以flip
偏移不同。但是都指向inode中的同一个结构体。
以下情况会使得两个进程的文件描述符指向同一个file
:(并不全,可能有其他情况)
- 父进程fork子进程,两者共享一个file 用于容器逃逸
- 使用dup或者dup2,复制现有文件描述符
fork的这个场景就适用于容器逃逸或沙箱逃逸:
- Docker Daemon 进程就是容器进程的父进程
open_by_handle_at函数
原型:
1 | int open_by_handle_at(int mount_fd, struct file_handle *handle, int flags); |
用于打开struct file_handle *handle
结构体指针描述的文件。mount_fd
参数为 file_handle
结构体指针所描述文件所在的文件系统中,任何一个文件或者是目录的文件描述符。使用该函数需要CAP_DAC_READ_SEARCH
权能。这个权能可以忽略所有对读、搜索操作的限制。在旧版本的docker中没有将此权能加入黑名单,导致可以通过open_by_handle_at这个函数暴力搜索宿主机的文件并打开。
运行docker时需要加上该权能:
1 | $ docker run -rm -ti --cap-add=DAC_READ_SEARCH centos:7 /bin/bash |
读取容器内文件
首先通过stat
指令查看/etc/passwd的inode:
1 | File: '/etc/passwd' |
将1060907
转换为16进制保存到file_hadle.f_handle中:
1 |
|
gcc编译后可以运行:
1 | root@--name:/ctf/work/openat# ./a.out |
但是不是root用户是无法调用的:
1 | pwn@--name:/ctf/work/openat$ id |
读取宿主机文件
上面的例子是如何读取容器内的文件,怎么读取宿主机的文件?一般来说/
的inode是2:
1 | $ stat / |
这是一个相对不变的值,通过这个可以扫描到所有文件。完整的Poc:
简单来说就是暴力的试incode,判断文件名是不是想要的问价。是的话就读取。
使用该方法可以获取宿主机的shell:
1 |
|
测试的场景为:在ubuntu16.04的宿主机上,运行ubuntu16.04的docker。
将编译好的bin通过base64传送给docker内,执行:
1 | root@d427205646aa:/tmp# ./a.out |
成功拿到了宿主机的shell。
结合ptrace攻击
即使seccomp sandbox禁用了open_by_handle_at,同样可以通过ptrace来绕过该限制。
容器的攻击面很多,沙箱可以提高安全性,但并不是绝对安全。
切换至32位模式绕过沙箱
主要的方法为:
- 利用
retf
更改cs寄存器的值,使其变为32位模式
直接用ROPgadget应该是找不到retf的gadgets的,先用汇编得到字符串,然后再gdb search。
X32 ABI 额外的系统调用号
参考Panda师傅的文章:http://p4nda.top/2018/07/27/CISCN-Final/
一个系统两个调用号。例如open的系统调用号为2,而用0x40000000+2=0x40000002也可以调用open。
1 | /* call open() */ |
这个方法没有测试,具体可以查看链接中提到的参考链接。
来个4层pwn?
1 | 宿主机 | docker | qemu | kernel | user |