Seccomp_Before

1.常见Seccomp:

(1)库安装:

1
2
3
#注释头

apt install libseccomp-dev libseccomp2 seccomp

(2)正常的使用seccopm开启:

①先创建初始化scmp_filter_ctx结构体,并且给定初始规则:

scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);

这里将初始规则设置为SCMP_ACT_ALLOW,即允许所有的系统调用。另有规则如下:

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
#注释头

/**
* Kill the process
*/
#define SCMP_ACT_KILL_PROCESS 0x80000000U
/**
* Kill the thread
*/
#define SCMP_ACT_KILL_THREAD 0x00000000U
/**
* Kill the thread, defined for backward compatibility
*/
#define SCMP_ACT_KILL SCMP_ACT_KILL_THREAD
/**
* Throw a SIGSYS signal
*/
#define SCMP_ACT_TRAP 0x00030000U
/**
* Notifies userspace
*/
#define SCMP_ACT_NOTIFY 0x7fc00000U
/**
* Return the specified error code
*/
#define SCMP_ACT_ERRNO(x) (0x00050000U | ((x) & 0x0000ffffU))
/**
* Notify a tracing process with the specified value
*/
#define SCMP_ACT_TRACE(x) (0x7ff00000U | ((x) & 0x0000ffffU))
/**
* Allow the syscall to be executed after the action has been logged
*/
#define SCMP_ACT_LOG 0x7ffc0000U
/**
* Allow the syscall to be executed
*/
#define SCMP_ACT_ALLOW 0x7fff0000U

/* SECCOMP_RET_USER_NOTIF was added in kernel v5.0. */
#ifndef SECCOMP_RET_USER_NOTIF
#define SECCOMP_RET_USER_NOTIF 0x7fc00000U

②添加规则,即添加白名单或者黑名单:

seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);

这里将系统调用execve给禁止了。函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#注释头

/**
* Add a new rule to the filter
* @param ctx the filter context
* @param action the filter action
* @param syscall the syscall number
* @param arg_cnt the number of argument filters in the argument filter chain
* @param ... scmp_arg_cmp structs (use of SCMP_ARG_CMP() recommended)
*
* This function adds a series of new argument/value checks to the seccomp
* filter for the given syscall; multiple argument/value checks can be
* specified and they will be chained together (AND'd together) in the filter.
* If the specified rule needs to be adjusted due to architecture specifics it
* will be adjusted without notification. Returns zero on success, negative
* values on failure.
*
*/
int seccomp_rule_add(scmp_filter_ctx ctx,
uint32_t action, int syscall, unsigned int arg_cnt, ...);

即(结构体,规则,规则生效的系统调用,arg_cnt,scmp_arg_cmp)。

A.其中arg_cnt表示函数seccomp_rule_add后面传入参数的个数,如果为0,则直接禁止execve,后面的scmp_arg_cmp都不用赋值,赋值了也没用。

B.如果不为0,则再看之后seccomp_rule_add函数之后传入的参数,赋值为1,则只允许一条规则。赋值为2,则需同时满足之后的两条规则才会生效。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#注释头

//从0开始计算参数个数
seccomp_rule_add(ctx,SCMP_ACT_KILL,SCMP_SYS(write),1,
SCMP_A2(SCMP_CMP_EQ,0x10));
write(1,"1234567812345678",0x10);//被拦截

seccomp_rule_add(ctx,SCMP_ACT_KILL,SCMP_SYS(write),2,
SCMP_A2(SCMP_CMP_EQ,0x10));
write(1,"1234567812345678",0x10);//不被拦截
//seccomp_rule_add参数个数设置为2,但是后续没有添加规则,则默认不满足,则不会生效

seccomp_rule_add(ctx,SCMP_ACT_KILL,SCMP_SYS(write),2,
SCMP_A2(SCMP_CMP_EQ,0x10),SCMP_A0(SCMP_CMP_EQ,1));
write(1,"1234567812345678",0x10);//被拦截

seccomp_rule_add(ctx,SCMP_ACT_KILL,SCMP_SYS(write),2,
SCMP_A2(SCMP_CMP_EQ,0x10),SCMP_A0(SCMP_CMP_EQ,1));
write(1,"1234567812345678",0x10);//不被拦截

但是使用seccomp,一旦被拦截,则程序直接打印错误信息并中断,无法执行之后的代码,规则如下:

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
61
62
63
64
65
66
67
68
69
70
#注释头

/**
* Specify an argument comparison struct for use in declaring rules
* @param arg the argument number, starting at 0
* @param op the comparison operator, e.g. SCMP_CMP_*
* @param datum_a dependent on comparison
* @param datum_b dependent on comparison, optional
*/
#define SCMP_CMP(...) ((struct scmp_arg_cmp){__VA_ARGS__})

/**
* Specify an argument comparison struct for argument 0
*/
#define SCMP_A0(...) SCMP_CMP(0, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 1
*/
#define SCMP_A1(...) SCMP_CMP(1, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 2
*/
#define SCMP_A2(...) SCMP_CMP(2, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 3
*/
#define SCMP_A3(...) SCMP_CMP(3, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 4
*/
#define SCMP_A4(...) SCMP_CMP(4, __VA_ARGS__)

/**
* Specify an argument comparison struct for argument 5
*/
#define SCMP_A5(...) SCMP_CMP(5, __VA_ARGS__)

/**
* Comparison operators
*/
enum scmp_compare {
_SCMP_CMP_MIN = 0,
SCMP_CMP_NE = 1, /**< not equal */
SCMP_CMP_LT = 2, /**< less than */
SCMP_CMP_LE = 3, /**< less than or equal */
SCMP_CMP_EQ = 4, /**< equal */
SCMP_CMP_GE = 5, /**< greater than or equal */
SCMP_CMP_GT = 6, /**< greater than */
SCMP_CMP_MASKED_EQ = 7, /**< masked equality */
_SCMP_CMP_MAX,
};

/**
* Argument datum
*/
typedef uint64_t scmp_datum_t;

/**
* Argument / Value comparison definition
*/
struct scmp_arg_cmp {
unsigned int arg; /**< argument number, starting at 0 */
enum scmp_compare op; /**< the comparison op, e.g. SCMP_CMP_* */
scmp_datum_t datum_a;
scmp_datum_t datum_b;
};

③启用seccomp保护:

seccomp_load(ctx);

利用seccomp_load函数加载启用保护。函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

/**
* Return the notification fd from a filter that has already been loaded
* @param ctx the filter context
*
* This returns the listener fd that was generated when the seccomp policy was
* loaded. This is only valid after seccomp_load() with a filter that makes
* use of SCMP_ACT_NOTIFY.
*
*/
int seccomp_notify_fd(const scmp_filter_ctx ctx);

这里传入scmp_filter结构体即可,如果不传入

以上的均可在seccomp.h中查看。

▲总的调用就是:

1
2
3
4
5
6
#注释头

scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_load(ctx);

记得引用库:

1
2
3
4
#注释头

#include <seccomp.h>
#include <linux/seccomp.h>

2.Prtctl函数(/usr/includ/linux/prctl.h):

原型:

1
2
3
#注释头

int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

这里的参数可以不需要全部设置上,其中option比较关键,在PWN中大致分以下情况:

(1)若option为PR_SET_NO_NEW_PRIVS(38):

此时将第二个参数arg2设置为1,那么程序的子线程就无法通过execve来提权,就是pwn kernel中即使改掉了cred结构体,使其特权为0,再执行system(“/bin/sh”)依然无法提权。即prctl(38, 1,0,0,0)表示禁用系统调用,也就是system和onegadget都没了,同时子进程也无法这样来获得shell。

(2)若option为PR_SET_SECCOMP(22):

此时可以通过参数来设置规则,道理和seccomp一样的,规则如下:

①如果arg2为SECCOMP_MODE_STRICT(1),则只允许调用read,write,_exit(这个exit不是退出程序的意思),sigreturn这几个syscall。即prctl(22,1,0,0,0)。

②如果arg2为SECCOMP_MODE_FILTER(2),则为过滤模式,其中对syscall的限制通过参数3的结构体,来自定义过滤规则,函数会重定向到另一个同名函数,该函数的原型如下:

1
2
3
#注释头

int prctl(int PR_SET_SECCOMP,const struct* sock_filter SECCOMP_MODE_FILTER,const sock_fprog prog);

但调用之前还是需要禁用execve,即调用形式为:

1
2
3
4
#注释头

prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0); //这里是需要这么写的
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&sfp);

A.参数SECCOMP_MODE_FILTER是一个结构体数组,该sock_filter结构体为:

1
2
3
4
5
6
7
8
#注释头

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

a.code:一个字节,详细定义操作的类型,假设code为0x15

先看前四位:

1
2
3
4
5
6
7
8
9
10
11
#注释头

#define BPF_CLASS(code)
#define BPF_LD 0x00
#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

这里前四位就是0x5,所以对应到之后的四位定义域中去:

1
2
3
4
5
6
7
8
9
#注释头

/*jmp fields */
#define BPF_JA 0x00
#define BPF_JEQ 0x10
#define BPF_JGT 0x20
#define BPF_JGE 0x30
#define BPF_JSET 0x40
#define BPF_SRC(code) ((code) & 0x08)

那么这个0x15的操作就是BPF_JMP+BPF_JEQ。

▲同理,ld、ldx、ALU都有对应的定义域:

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
#注释头

/*ld fields */
#define BPF_W 0x00
#define BPF_H 0x08
#define BPF_B 0x10

/*ldx fields */
#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 fields */
#define BPF_ADD 0x00
#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_K 0x00
#define BPF_X 0x08

而RET一般对应BPF_K,然后在之后参数上写SECCOMP_RET_KILL或者SECCOMP_RET_ALLOW。MISC倒是没见过。

b.JT和JF:是相对于当前语句的偏移。例如(1,0),假设当前语句为0003,则代表之前语句为真,则跳转到0005,为假则跳转到0004。所以如果都是0,相当于是个无跳转语句,如果都是1,相当于是跳过下一条语句。

c.K:可以当作一个参数。如果操作语句是比较,那么就相当于比较的右值A>=K?。以此类推。这个参数的值是从seccomp_data中:

1
2
3
4
5
6
7
8
9
#注释头

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

值即代表偏移,偏移字长为一个字节:

K==0,代表nr;K==4,代表arch;K==8,代表args[0]。而args六个值相当于传参寄存器的值:ebx,ecx,edx,esi,edi,ebp(32位),rdi,rsi,rdx,r10,r8,r9(64位)

▲所以(0x15,0x00,0x01,0x0000003b)就代表if (A!= execve) goto offset_1。这里也可以用一个简单的操作来替代:BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1)。定义规则就可以如下:

1
2
3
4
5
6
7
8
#注释头

struct sock_filter sfi[] = {
{0x20,0x00,0x00,0x00000000},
{0x15,0x00,0x01,0xc000003b},
{0x06,0x00,0x00,0x00000000}, //KILL
{0x06,0x00,0x00,0x7fff0000} //ALLOW
};

或者

1
2
3
4
5
6
7
8
#注释头

struct sock_filter filter[] = {
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),
};

由于在/usr/include/linux/bpf_common.h有宏定义,所以第二种情况也是可以的。

B.sfp也是一个结构体:

1
2
3
4
5
6
#注释头

struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter *filter;
};

a.len即代表语句条数,比如上面的就是4;

b.filter就是指向上面SECCOMP_MODE_FILTER这个结构体的指针。

▲所以完整的就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#注释头

struct sock_filter sfi[] = {
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 sfp = {
(unsigned short)(sizeof(filter)/sizeof(filter[0])),
sfi,
};
prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);

这样设置后,出来的效果如下:

1
2
3
4
5
6
7
8
9
#注释头

root@241adce81c0a:/ctf/seccomp# seccomp-tools dump ./prctl_test2
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x06 0x00 0x00 0x7fff0000 return ALLOW

3.简单的seccomp规则写:

(1)依据简单语法写规则:

1
2
3
4
5
6
7
8
9
10
#注释头

A = sys_number
A == 257? e0:next
A == 1? ok:next
return ALLOW
e0:
return ERRNO(0)
ok:
return ALLOW

保存为seccomp_asm

(2)利用seccomp-tools来生成:

seccomp-tools asm seccomp_asm -f raw|seccomp-tools disasm -

1
2
3
4
5
6
7
8
9
10
#注释头

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x02 0x00 0x00000101 if (A == openat) goto 0004
0002: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0005
0003: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0004: 0x06 0x00 0x00 0x00050000 return ERRNO(0)
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW

这样就可以方便写规则了,同样有个seccomp_bpf.h的东西,比较简单:

secfilter/seccomp-bpf.h at master · ahupowerdns/secfilter · GitHub

4.常见种类及绕过方式:

(1)禁用execve函数时,但是需要getshell才行,此时存在满足一定条件可以使用shellcode:

①未检查arch:

尝试使用shellcode将处理器转换,如果是x64则转为i386,同理类似。这样系统调用号11就不会被解析为64位中的__x64_sys_munmap,而是32位中的sys_execve,绕过检查。

1
2
3
4
5
6
7
8
9
#注释头

to32:
mov DWORD [rsp+4],0x23;
retf;

to64:
mov DWORD [esp+4],0x33;
retf;

由于retf的指令实际效果为:POP CS:EIP,这里CS为0x23即为64位,0x33即为32位,所以如果能找到控制栈的gadget那其实也可以直接用retf,毕竟retf其实也挺常见的。同时还有其他的好用ret指令:

1
2
3
4
5
#注释头

RETQ:POP RIP
RETN: POP EIP
RETF: POP CS:EIP

参考SCTF2020里面的CoolCode

②存在检查漏洞:if (A < 0x40000000),如果对A >= 0x40000000没有限制,那么可以利用64位下的x32漏洞,使用 64位的存器地址和 32位的地址:

具体原理不太懂,应该是传参原因吧,可能syscall的系统调用号寄存器为eax,导致系统调用号0x40000003b绕过if (A < 0x40000000)检查,然后以eax传入系统调用号为0x0000003b,仍然执行了execve。

这样的话在原来的系统调用号加上0x400000000即可绕过检查:

1
2
3
4
#注释头

mov rax,59+0x40000000;
syscall;

(2)禁用evecve等函数,告诉了flag位置,或者将flag位置什么的放在了栈上(可以用peda插件:find flag),又或者需要自己调试爆破flag的位置,不能getshell的,利用open,write,read等没有被禁用的函数进行读取:

①shellcode模式:这里就根据各种规则限制来绕过,自己编写shellcode。(这个需要汇编基础,遇到题目慢慢学吧)

②ROP模式:一般需要劫持栈再来ROP,其实和shellcode差不多的。这种情况一般是禁用了mprotect,所以没办法直接使用shellcode了,那么就利用ROP来搞,借助libc上的syscall。但是这里的open不知道为什么有时候执行不了,所以可以用借助syscall加上open的系统调用号来执行open。

如果没有禁止mprotect,那么通常可以配合SigreturnFrame(),free_hook,setcontext来将堆上内存权限改为可执行,然后再在堆上使用shellcode也可以。

▲有些禁用了open,write,read,那么其实这种情况下可以调用对应的openat,readv,和writev,其实效果一样的,因为前面三个函数其实都对应调用后面的三个函数。如果实在没办法打印,可以利用题目中的错误信息输出,修改IO_FILE来打印,读取。还有更强的用ptrace修改系统调用号,zer0pts CTF2020的sycall kit。实在找不到,后面再补坑吧。

(3)控制了open,write,read的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#注释头

0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x06 0x00 0x00000002 if (A == open) goto 0012
0006: 0x15 0x00 0x06 0x00000000 if (A != read) goto 0013
0007: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0008: 0x25 0x03 0x00 0x00000000 if (A > 0x0) goto 0012
0009: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0013
0010: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0011: 0x35 0x00 0x01 0x00000004 if (A < 0x4) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#注释头

0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x12 0xc000003e if (A != ARCH_X86_64) goto 0020
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0f 0xffffffff if (A != 0xffffffff) goto 0020
0005: 0x15 0x0d 0x00 0x00000002 if (A == open) goto 0019
0006: 0x15 0x0c 0x00 0x00000003 if (A == close) goto 0019
0007: 0x15 0x0b 0x00 0x0000000a if (A == mprotect) goto 0019
0008: 0x15 0x0a 0x00 0x000000e7 if (A == exit_group) goto 0019
0009: 0x15 0x00 0x04 0x00000000 if (A != read) goto 0014
0010: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0011: 0x15 0x00 0x08 0x00000000 if (A != 0x0) goto 0020
0012: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0013: 0x15 0x05 0x06 0x00000000 if (A == 0x0) goto 0019 else goto 0020
0014: 0x15 0x00 0x05 0x00000001 if (A != write) goto 0020
0015: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # write(fd, buf, count)
0016: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0020
0017: 0x20 0x00 0x00 0x00000010 A = fd # write(fd, buf, count)
0018: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x06 0x00 0x00 0x00000000 return KILL

类似这种限制fd的,可以先close再open,改变fd,同样对应参数也可以适当做一些修改来绕过。

参考资料:

seccomp沙盒逃逸基础——沙盒的规则编写 - p0lar1s - 博客园 (cnblogs.com)

(´∇`) 被你发现啦~ seccomp学习笔记 | A1ex’s Blog

PWN题中常见的seccomp绕过方法 - 安全客,安全资讯平台 (anquanke.com)

Seccomp从0到1 - 安全客,安全资讯平台 (anquanke.com)

等等,贴不过来了。