由于最近要打几场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
|
#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)), 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) }; struct sock_fprog prog = { .len = sizeof(filter) / sizeof(filter[0]), .filter = filter }; 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; __u8 jt; __u8 jf; __u32 k; };
|
其他一些常用的宏定义
基本都是位掩码,只需要知道其代表的含义即可。
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
| #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
#define BPF_W 0x00 #define BPF_H 0x08 #define BPF_B 0x10
#define BPF_IMM 0x00 #define BPF_ABS 0x20 #define BPF_IND 0x40 #define BPF_MEM 0x60 #define BPF_LEN 0x80 #define BPF_MSH 0xA0
#define BPF_K 0x00 #define BPF_X 0x08
#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_JA 0x00 #define BPF_JEQ 0x10 #define BPF_JGT 0x20 #define BPF_JGE 0x30 #define BPF_JSET 0x40
#define BPF_RET 0x06 #define SECCOMP_RET_KILL 0x00000000 #define SECCOMP_RET_ALLOW 0x7fff0000
#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