pwn入门-sandbox(Seccomp)

Seccomp从0到1-安全KER - 安全资讯平台

栈沙箱学习之orw - 先知社区

pwn知识——ORW - Falling_Dusk - 博客园

Seccomp(secure computing mode)在2.6.12版本(2005年3月8日)中引入linux内核,将进程可用的系统调用限制为四种:read,write,_exit,sigreturn。最初的这种模式是白名单方式,在这种安全模式下,除了已打开的文件描述符和允许的四种系统调用,如果尝试其他系统调用,内核就会使用SIGKILL或SIGSYS终止该进程。Seccomp来源于Cpushare项目,Cpushare提出了一种出租空闲linux系统空闲CPU算力的想法,为了确保主机系统安全出租,引入seccomp补丁,但是由于限制太过于严格,当时被人们难以接受。

image-20250115110823392

SECCOMP_SET_MODE_FILTER

  • Seccomp-Berkley Packet Filter
  • 允许用户使用可配置的策略过滤系统调用
  • 使用BPF规则自定义测量
  • 可对任意系统调用及其参数进行过滤
1
apt install libseccomp-dev libseccomp2 seccomp
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>

int main() {
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
char *buf = "hello world!n";
write(0,buf,0xc);
printf("%s",buf);
}

未使用seccomp:hello world!hello world!

使用seccomp:hello world!Killed

初代的seccomp就是这样暴力,禁用白名单之外的所有函数!

image-20250115111605692

Seccomp-BPF

尽管seccomp保证了主机的安全,但由于限制太强实际作用并不大。在实际应用中需要更加精细的限制,为了解决此问题,引入了Seccomp – Berkley Packet Filter(Seccomp-BPF)。Seccomp-BPF是Seccomp和BPF规则的结合,它允许用户使用可配置的策略过滤系统调用,该策略使用Berkeley Packet Filter规则实现,它可以对任意系统调用及其参数(仅常数,无指针取消引用)进行过滤。

Seccomp-BPF 的作用

  1. 减少攻击面
    • 限制进程只能使用必要的系统调用,降低攻击者利用不需要的系统调用发起攻击的可能性。
    • 即使程序存在漏洞,也因为受限的系统调用而难以进一步利用。
  2. 隔离进程
    • 将高风险的代码(如处理不可信输入的模块)运行在受限的沙箱中。
    • 防止这些模块对操作系统或其他进程造成威胁。
  3. 提升程序安全性
    • 通过定义允许的系统调用清单,降低因为内核或用户态程序漏洞导致的威胁。
    • 常用于容器(如 Docker)、浏览器(如 Chrome)、网络服务和安全敏感的应用程序。

BPF 定义了一个伪机器这个伪机器可以执行代码有一个累加器,寄存器,赋值,算术,跳转

img

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <malloc.h>
#include <sys/prctl.h>
#include <seccomp.h>
#include <linux/seccomp.h>
#include <linux/filter.h>

int main() {
struct sock_filter filter_1[] = {
//前面两步用于检查arch
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
//将帧的偏移0处,取4个字节数据,也就是系统调用号的值载入累加器
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
//当 A == 59时,顺序执行下一条规则,否则跳过下一条规则,这里的59就是x64的execve系统调用
BPF_JUMP(BPF_JMP+BPF_JEQ,1,2,3),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
//当 A == 59时,顺序执行下一条规则,否则跳过下一条规则,这里的59就是x64的execve系统调用
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
//KILL
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
//ALLOW
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog_1 = {
.len = (unsigned short)(sizeof(filter_1)/sizeof(filter_1[0])),
.filter = filter_1,
};

struct sock_filter filter_4[] = {
//前面两步用于检查arch
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
//将帧的偏移0处,取4个字节数据,也就是系统调用号的值载入累加器
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,8),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
// read 系统调用
BPF_JUMP(BPF_JMP+BPF_JEQ,0,0,3),
// 读取第一个参数 0x10 0x20 0x30
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0x20),
BPF_JUMP(BPF_JMP+BPF_JEQ,0,0,1),
//KILL
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
//ALLOW
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog_4 = {
.len = (unsigned short)(sizeof(filter_4)/sizeof(filter_4[0])),
.filter = filter_4,
};

struct sock_filter filter_2[] = {
//前面两步用于检查arch
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
//将帧的偏移0处,取4个字节数据,也就是系统调用号的值载入累加器
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
//当 A == 59时,顺序执行下一条规则,否则跳过下一条规则,这里的59就是x64的execve系统调用
BPF_JUMP(BPF_JMP+BPF_JEQ,0x38,13,0),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,0x9,11,0),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xe,9,0),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xd,7,0),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,0,5,0),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,1,3,0),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
BPF_JUMP(BPF_JMP+BPF_JEQ,59,1,0),
//KILL
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
//ALLOW
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
};
struct sock_fprog prog_2 = {
.len = (unsigned short)(sizeof(filter_2)/sizeof(filter_2[0])),
.filter = filter_2,
};


struct sock_filter filter_3[] = {
//前面两步用于检查arch
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
//将帧的偏移0处,取4个字节数据,也就是系统调用号的值载入累加器
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
//当 A == 59时,顺序执行下一条规则,否则跳过下一条规则,这里的59就是x64的execve系统调用
BPF_JUMP(BPF_JMP+BPF_JEQ,255,1,0),
//ALLOW
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
//KILL
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
};
struct sock_fprog prog_3 = {
.len = (unsigned short)(sizeof(filter_3)/sizeof(filter_3[0])),
.filter = filter_3,
};

char *argv[]={0, NULL};
char *envp[]={0,NULL};

prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog_4);
char buf[0x30];
int fd = open("flag");
read(fd,buf,0x10);
write(1,buf,0x10);
read(0,buf,0x10);
write(1,buf,0x10);
/* prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog_2); */
/* prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog_3); */
//execve("/bin/sh",argv,envp);
/* write(1,"start!\n",7); */
/* system("/bin/sh"); */
/* execve("/bin/sh",argv,envp); */
return 0;
}

常见的沙箱绕过思路:ORW

Open /open Read /read Write /writev

seccomp在ctf中大多用于禁用execve函数,解决办法就是构造shellcode,用open->read->write的方式读flag

绕过不检查系统号

image-20250115114837008

image-20250115114900903

特殊的沙箱绕过思路-未检查范围

在x64下还可以直接使用x32-abi绕过X32为x86-64下的一种特殊的模式,使用64位的寄存器和32位的地址,只需直接加 __X32_SYSCALL_BIT(0X40000000)即原本的syscall number + 0x40000000

根据具体规则,结合syscall调用表找没被过滤的替代行数如execveat openv readv writev

  1. 泄露内存地址
  2. 找到libc中的open,read,write的地址
  3. 在内存中写入“flag”3.利用ORW读出flag
  4. ibc中的函数syscall
  5. syscall_ret pop_rax

read(系统调用号 0)

write(系统调用号 1)

open(系统调用号 2)

exit(系统调用号 60)

在禁用execve的情况下,我们需要经过以下操作来得到flag值

1
2
3
open开flag文件
read出flag的内容
write显示flag的值

在知晓大概的流程之后,就得设置寄存器的参数了,我们得知道各个函数对应的参数分别代表什么意思
open(file,oflag),read(fd,buf,n_bytes)write(fd,buf,n_bytes)

open

  • file就是我们要读取的文件名,CTF中一般为flag,或者flag.txt。
  • 而oflag则是我们以何种方式打开文件,如只读,只写,可读可写。一般来说我们都设置oflag=0,以默认方式打开文件,一般来说都是只读,我们并不需要对flag进行其它操作,所以只读的权限就够了

read和write

这两个是大同小异的。fd是文件描述符,通过设置它来决定函数的操作。在大多数时候,我们常常设置read的fd为0,代表标准输入,但在ORW中,我们需要设置read的fd为3,表示从文件中读取,buf就是我们读取出的flag值存放的地址,n_bytes就是能输入多少字节的数据。write的fd还是如常,依旧为1.