self-made sandbox

collectcrop Lv3

由于最近要打几场awd,一个一个漏洞改过来感觉有点麻烦,所以想到了上沙箱,但是一般好像比赛会禁止上通防,这里想试试看自己通过系统调用写一个沙箱出来,看看能不能瞒天过海。

一、c代码实现沙箱

不是很清楚沙箱具体用到的系统调用,所以先让gpt生成了一段正常用c语言实现的沙箱代码。

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
//test.c
//gcc -g test.c -o test
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <sys/prctl.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>

void install_seccomp() {
struct sock_filter filter[] = {
// 读取系统调用号
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),

// 检查是否为 execve 系统调用(编号 59)
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 59, 0, 1),

// 如果是 execve,则杀死进程
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),

// 允许其他系统调用
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
};

struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter
};

// 启用 seccomp 过滤模式
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) {
perror("prctl");
_exit(1);
}

}

int main() {
install_seccomp();
// 你可以在这里运行原来的程序逻辑
system("/bin/sh");
return 0;
}

这个 install_seccomp() 函数的作用是通过 seccomp(安全计算模式)设置一个简单的沙箱来限制程序能够调用的系统调用,具体来说,是禁止调用 execve 系统调用。如果程序尝试调用 execve,则会被杀死。其余的系统调用则被允许执行。

以下是其中每个部分的功能和用法的详细解释:

struct sock_filter filter[]

1
2
3
4
5
6
struct sock_filter filter[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 59, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
};

这是一个 BPF(Berkeley Packet Filter)过滤器指令数组,用来定义程序的过滤规则。每一条 sock_filter 定义了一条 BPF 指令,用来判断并处理系统调用。

第一条指令

1
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr))
  • 这里使用 BPF_LD 加载 seccomp_data 结构体中的 nr 成员(即系统调用号)。
  • BPF_W 表示加载的是一个 32 位的值。
  • BPF_ABS 表示从固定的偏移量读取数据。
  • offsetof(struct seccomp_data, nr)seccomp_data 结构体中 nr 成员的偏移量,nr 是存储系统调用号的字段。

这条指令的作用是读取当前正在执行的系统调用号,以便后续判断是否是 execve 系统调用。

1
2
3
4
5
6
struct seccomp_data {
int nr; // 系统调用号
__u32 arch; // 架构信息
__u64 instruction_pointer; // 指令指针
__u64 args[6]; // 系统调用参数
};

其中由于linux内核中结构体定义如上,所以实际上offsetof(struct seccomp_data, nr)也可以写为0。

第二条指令

1
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 59, 0, 1)
  • BPF_JMP 指令执行跳转。
  • BPF_JEQ 是“跳转如果相等”的操作符,表示如果条件为真则跳转。
  • BPF_K 指定了一个立即数(这里为 59)。

这条指令会判断加载的系统调用号是否等于 execve 的系统调用号(59)。如果是,则跳过后面的第一条指令,继续执行杀死进程的语句;否则跳过后面的两条指令,直接允许执行系统调用。

第三条指令

1
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL)
  • BPF_RET 表示返回结果。
  • BPF_K 表示一个立即数操作,这里为 SECCOMP_RET_KILL,这是 seccomp 的一个特定返回值,表示当遇到 execve 系统调用时杀死进程。

这条指令表示如果系统调用号是 execve,则终止进程。

第四条指令

1
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
  • 这条指令会返回 SECCOMP_RET_ALLOW,表示允许其他系统调用正常执行。

struct sock_fprog prog

1
2
3
4
struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter
};
  • struct sock_fprog 是一个包含 seccomp 过滤器程序的结构体:
    • len 表示 filter 数组的长度(指令数量)。通过 sizeof(filter) / sizeof(filter[0]) 来计算过滤器中有多少条指令。
    • filter 是一个指向 sock_filter 数组的指针,用于存储 seccomp 过滤器的指令集。

该结构体用于将 BPF 过滤器指令加载到内核中。

prctl 系统调用

1
2
3
4
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) {
perror("prctl");
_exit(1);
}
  • prctl 是用来对进程的某些属性进行设置的系统调用。
  • PR_SET_SECCOMP 是用于启用 seccomp 的选项。
  • SECCOMP_MODE_FILTER 表示以过滤模式运行 seccomp,意味着我们会使用 BPF 过滤器来决定哪些系统调用可以执行,哪些会被拒绝。
  • &prog 是指向我们定义的过滤器程序的指针,它告诉内核使用这个过滤器来限制系统调用。

如果 prctl 调用失败,意味着 seccomp 设置失败,会输出错误信息并终止进程。

这里其实也可以把SECCOMP_MODE_FILTER改成 SECCOMP_MODE_STRICT ,然后就不用构建并传prog了,这样一来会仅仅允许exit,sigreturn,read以及write的系统调用。

其中BPF_STMT与BPF_JUMP实际上是一个宏定义,是条件编译后赋值的sock_filter结构体,这就是为什么struct sock_filter filter[]这个声明是声明结构体数组。

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

而sock_filter的结构体定义如下:

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

其他一些常用的宏定义

基本都是位掩码,只需要知道其代表的含义即可。

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
// 主指令类别(2 位)
#define BPF_LD 0x00 // Load
#define BPF_LDX 0x01 // Load with index
#define BPF_ST 0x02 // Store
#define BPF_STX 0x03 // Store with index
#define BPF_ALU 0x04 // ALU (arithmetic logic unit) operation
#define BPF_JMP 0x05 // Jump
#define BPF_RET 0x06 // Return
#define BPF_MISC 0x07 // Miscellaneous

// 操作码的大小(3 位)
#define BPF_W 0x00 // Word (32-bit)
#define BPF_H 0x08 // Half-word (16-bit)
#define BPF_B 0x10 // Byte (8-bit)

// 加载/存储操作的模式(3 位)
#define BPF_IMM 0x00 // Immediate value
#define BPF_ABS 0x20 // Absolute
#define BPF_IND 0x40 // Indirect
#define BPF_MEM 0x60 // Memory
#define BPF_LEN 0x80 // Packet length
#define BPF_MSH 0xA0 // Modulo shift

// 计算和比较操作的源(1 位)
#define BPF_K 0x00 // Constant
#define BPF_X 0x08 // Register

// ALU 运算符(4 位)
#define BPF_ADD 0x00 // Addition
#define BPF_SUB 0x10 // Subtraction
#define BPF_MUL 0x20 // Multiplication
#define BPF_DIV 0x30 // Division
#define BPF_OR 0x40 // Bitwise OR
#define BPF_AND 0x50 // Bitwise AND
#define BPF_LSH 0x60 // Left shift
#define BPF_RSH 0x70 // Right shift
#define BPF_NEG 0x80 // Negation
#define BPF_MOD 0x90 // Modulo
#define BPF_XOR 0xA0 // Bitwise XOR

// 跳转条件(4 位)
#define BPF_JA 0x00 // Jump unconditionally
#define BPF_JEQ 0x10 // Jump if equal
#define BPF_JGT 0x20 // Jump if greater than
#define BPF_JGE 0x30 // Jump if greater or equal
#define BPF_JSET 0x40 // Jump if bits are set

// Return 操作码
#define BPF_RET 0x06 // Return
#define SECCOMP_RET_KILL 0x00000000 // Kill process
#define SECCOMP_RET_ALLOW 0x7fff0000 // Allow syscall

//对seccomp mode 进行操作
#define PR_GET_SECCOMP 21
#define PR_SET_SECCOMP 22

二、调试获取汇编写法

查看pwndbg,install_seccomp()函数的主逻辑如下,我们可以发现除了prctl系统调用外,前面的一系列结构体的初始化都没有调用函数,而且这一大通操作实际上很多是一个一个字节进行的修改,那么我们在eh_frame空间有限的情况下,实际可以通过8字节直接赋值参数来节省掉许多指令字节。

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
0x5555555551e4 <install_seccomp+27>    mov    word ptr [rbp - 0x30], 0x20     [0x7fffffffe450] => 0x20
0x5555555551ea <install_seccomp+33> mov byte ptr [rbp - 0x2e], 0 [0x7fffffffe452] => 0
0x5555555551ee <install_seccomp+37> mov byte ptr [rbp - 0x2d], 0 [0x7fffffffe453] => 0
0x5555555551f2 <install_seccomp+41> mov dword ptr [rbp - 0x2c], 0 [0x7fffffffe454] => 0
0x5555555551f9 <install_seccomp+48> mov word ptr [rbp - 0x28], 0x15 [0x7fffffffe458] => 0x15
0x5555555551ff <install_seccomp+54> mov byte ptr [rbp - 0x26], 0 [0x7fffffffe45a] => 0
0x555555555203 <install_seccomp+58> mov byte ptr [rbp - 0x25], 1 [0x7fffffffe45b] => 1
0x555555555207 <install_seccomp+62> mov dword ptr [rbp - 0x24], 0x3b [0x7fffffffe45c] => 0x3b
0x55555555520e <install_seccomp+69> mov word ptr [rbp - 0x20], 6 [0x7fffffffe460] => 6
0x555555555214 <install_seccomp+75> mov byte ptr [rbp - 0x1e], 0 [0x7fffffffe462] => 0
0x555555555218 <install_seccomp+79> mov byte ptr [rbp - 0x1d], 0 [0x7fffffffe463] => 0
0x55555555521c <install_seccomp+83> mov dword ptr [rbp - 0x1c], 0 [0x7fffffffe464] => 0
0x555555555223 <install_seccomp+90> mov word ptr [rbp - 0x18], 6 [0x7fffffffe468] => 6
0x555555555229 <install_seccomp+96> mov byte ptr [rbp - 0x16], 0 [0x7fffffffe46a] => 0
0x55555555522d <install_seccomp+100> mov byte ptr [rbp - 0x15], 0 [0x7fffffffe46b] => 0
0x555555555231 <install_seccomp+104> mov dword ptr [rbp - 0x14], 0x7fff0000 [0x7fffffffe46c] => 0x7fff0000
0x555555555238 <install_seccomp+111> mov word ptr [rbp - 0x40], 4 [0x7fffffffe440] => 4
0x55555555523e <install_seccomp+117> lea rax, [rbp - 0x30] RAX => 0x7fffffffe450 ◂— 0x20 /* ' ' */
0x555555555242 <install_seccomp+121> mov qword ptr [rbp - 0x38], rax [0x7fffffffe448] => 0x7fffffffe450 ◂— 0x20 /* ' ' */
0x555555555246 <install_seccomp+125> lea rax, [rbp - 0x40] RAX => 0x7fffffffe440 ◂— 4
0x55555555524a <install_seccomp+129> mov rdx, rax RDX => 0x7fffffffe440 ◂— 4
0x55555555524d <install_seccomp+132> mov esi, 2 ESI => 2
0x555555555252 <install_seccomp+137> mov edi, 0x16 EDI => 0x16
0x555555555257 <install_seccomp+142> mov eax, 0 EAX => 0
0x55555555525c <install_seccomp+147> call prctl@plt <prctl@plt>

最后整个prog结构体的地址是存在rdx中,也就是rbp - 0x40这个地址处。但之后遇到了问题,在prctl函数的实现中,在prctl的系统调用时始终不成功,返回的rax代表的错误码。之后尝试直接执行test,发现是权限问题。

这下尴尬了,本来想上沙箱是为了方便防御,这么一来二去搞awd前还得提权,更加麻烦了,这下就只能算是学习沙箱实现的机制了。那么我们得切到root用户进行调试(本来想setuid改文件,但好像wsl不支持),进到prctl具体实现里看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x7ffff7eae338 <prctl+8>        mov    r10, rcx                        R10 => 0x555555557da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555180 (__do_global_dtors_aux) ◂— endbr64
0x7ffff7eae33b <prctl+11> mov qword ptr [rsp + 0x28], rsi [0x7fffffffe408] => 2
0x7ffff7eae340 <prctl+16> mov qword ptr [rsp + 0x30], rdx [0x7fffffffe410] => 0x7fffffffe440 ◂— 4
0x7ffff7eae345 <prctl+21> mov qword ptr [rsp + 0x38], rcx [0x7fffffffe418] => 0x555555557da0 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555180 (__do_global_dtors_aux) ◂— endbr64
0x7ffff7eae34a <prctl+26> mov qword ptr [rsp + 0x40], r8 [0x7fffffffe420] => 0x7ffff7fa3f10 (initial+16) ◂— 4
0x7ffff7eae34f <prctl+31> mov rax, qword ptr fs:[0x28] RAX, [0x7ffff7d85768] => 0xc68810ecdaf50d00
0x7ffff7eae358 <prctl+40> mov qword ptr [rsp + 0x18], rax [0x7fffffffe3f8] => 0xc68810ecdaf50d00
0x7ffff7eae35d <prctl+45> xor eax, eax EAX => 0
0x7ffff7eae35f <prctl+47> lea rax, [rsp + 0x60] RAX => 0x7fffffffe440 ◂— 4
0x7ffff7eae364 <prctl+52> mov dword ptr [rsp], 8 [0x7fffffffe3e0] => 8
0x7ffff7eae36b <prctl+59> mov qword ptr [rsp + 8], rax [0x7fffffffe3e8] => 0x7fffffffe440 ◂— 4
0x7ffff7eae370 <prctl+64> lea rax, [rsp + 0x20] RAX => 0x7fffffffe400 ◂— 0
0x7ffff7eae375 <prctl+69> mov qword ptr [rsp + 0x10], rax [0x7fffffffe3f0] => 0x7fffffffe400 ◂— 0
0x7ffff7eae37a <prctl+74> mov eax, 0x9d EAX => 0x9d
0x7ffff7eae37f <prctl+79> syscall <SYS_prctl>

最后syscall时rdi为0x16,rsi为2,rdx为0x7fffffffe440(prog结构体地址),前面的操作用处不大,就是把寄存器参数加载到了栈上以及存了下canary。r10用rcx赋值,r8不变。那么我们只要把那个结构体伪造出来,就可以直接syscall来禁用一些系统调用。理论可行,开始手搓。

我们可以拿一道简单的栈溢出来试一下,更改程序我用的是 https://github.com/aftern00n/AwdPwnPatcher 这个库,我们可以把call init patch成一个跳转到ehframe段执行,再在ehframe段最后手动加上call init。AwdPwnPatcher中有个add_constant_in_ehframe方法,其中会将传入的字符串转换为字节字符串。我感觉还是直接在传参前用p64伪造好结构体的各字段比较方便,所以就把库里面字符串的encode逻辑给注释掉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from AwdPwnPatcher import *
from pwn import *

binary = "./pwn"
awd_pwn_patcher = AwdPwnPatcher(binary)
sock_filter = p64(0x20) + p64(0x3b01000015) + p64(6) + p64(0x7fff000000000006)
sock_filter_addr = awd_pwn_patcher.add_constant_in_ehframe(sock_filter)

prog = p64(4) + p64(sock_filter_addr)
prog_addr = awd_pwn_patcher.add_constant_in_ehframe(prog)

assembly = f"""
mov rax,157
mov rdi,22
mov rsi,2
lea rdx,[{prog_addr}]
syscall
call 0x40117A
"""

awd_pwn_patcher.patch_by_jmp(jmp_from=0x4011F0,jmp_to=0x4011F5,assembly=assembly)
awd_pwn_patcher.save()

这时候本来充满期待开始调试二进制程序,但是首先遇到的问题是ehframe段不可执行,一般来说awd_pwn_patcher.save()时会自动把这个段设为可执行,但这里不知道出了什么问题,得自己手动来。根据https://blog.csdn.net/qq_46106285/article/details/124972056 这篇博客能够成功地设置段权限。第二个问题是我在root用户下安装pwntools后,发现会让正常用户gdb调试时出现问题,这个问题的解决方案是起一个python的虚拟环境给root用,或者是sudo pip install --user pwntools来指定只给root用户安装python库。之后运行就不会出现问题了。

运行没有patch过的程序:

经过patch之后:

经过计算,构造禁用execve沙箱所用的仅有0x53字节,一般而言ehframe段还是够用的,每个多加的规则也只会多出8字节的开销,改一下结构体的参数就行。但由于前提是要有root权限执行程序,也是挺鸡肋的。

三、自动化工具

感觉这个挺有意思的,感觉逻辑也挺简单,上头了小写一手自动化工具(虽然没有什么卵用)

效果图:

https://github.com/collectcrop/SandboxAttacher

  • 标题: self-made sandbox
  • 作者: collectcrop
  • 创建于 : 2024-09-21 15:08:12
  • 更新于 : 2024-09-21 17:44:52
  • 链接: https://collectcrop.github.io/2024/09/21/self-made-sandbox/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。