# 概述

沙盒机制也就是我们常说的沙箱,英文名 sandbox ,是计算机领域的虚拟技术,常见于安全方向。一般说来,我们会将不受信任的软件放在沙箱中运行,一旦该软件有恶意行为,则禁止该程序的进一步运行,不会对真实系统造成任何危害。

在 ctf 比赛中,pwn 题中的沙盒一般都会限制 execve 的系统调用,这样一来 one_gadgetsystem 调用都不好使,只能采取 open/read/write 的组合方式来读取 flag。当然有些题目可能还会将上面三个系统调用砍掉一个,进一步限制我们获取到 flag.

# 1.prctl 函数调用

prctl 是进程管理函数,沙箱规则通过 prctl 函数实现(也可以通过 seccomp 库函数实现),由它决定了哪些 系统调用函数 能被调用哪些不能被调用

c
// 函数原型
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

option 选项有很多,剩下的参数也由 option 确定,这里介绍两个主要的 option
PR_SET_NO_NEW_PRIVS(38)PR_SET_SECCOMP(22)

c
/* Get/set process seccomp mode */
    #define PR_GET_SECCOMP      21
    #define PR_GET_SECCOMP     22
/*
 * 用 22 时一般只允许 read、write、exit、sigereturn 函数执行
 * 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
// 为 38 时一般禁用 execve

1.option 为 22 的情况 ( #define PR_GET_SECCOMP 22 )

  • 第二个参数为1 ,只允许调用 read/write/_exit(not exit_group)/sigreturn 这几个 syscall(系统调用) : prctl(22,1LL,&V1)
  • 第二个参数为 2,则为过滤模式,其中对 syscall 的限制通过参数 3 的结构体来自定义过滤规则: prctl(22, 2LL, &v1); (第二个参数为 2 时,则会利用 参数3 的指向的 sock_fprog结构体 的成员指向的 sock_filter 定义的规则来进行过滤 任意系统调用和系统调用参数 ,通过这种方式我们可以自己定义想要过滤的系统调用)

2.option 为 38 的情况 ( #define PR_SET_NO_NEW_PRIVS 38 )

  • 第二个参数设置为1 ,则禁用 execve 系统调用且可以通过 fork () 函数和 clone () 函数 继承给子进程 : prctl(38, 1LL, 0LL, 0LL, 0LL);
  • 第二个参数设置为2 ,则为过滤模式,其中对 syscall 的限制通过参数 3 的结构体来自定义过滤规则

# BPF 过滤规则(伯克利封装包过滤)

上面提到的 PT_SET_SECCOMP 这个参数,后面接到的第一个参数,就是它设置的模式,第三个参数,指向 sock_fprog结构体 ,sock_fprog 结构体中,又有指向 sock_filter结构体 的指针 ( struct sock_filter *filter; /*指向包含struct sock_filter的结构体数组指针*/ ),sock_filter 结构体这里,就是我们要设置 规则 的地方

这里利用别人定义的 sock_filter :

c
#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;
}

设置了 sock_filter 结构体数组。这里为什么是一个结构体数组呢?

结构体数组:
数组中的每个元素都是一个结构体

因为我们看到里面有 BPF_STMT 和 BPF_JMP 的宏定义,其实 BPF_STMT 和 BPF_JMP 都是条件编译后 赋值 的 sock_filter 结构体

sock_filer 结构体:

c
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
}

https://www.cnblogs.com/L0g4n-blog/p/12839171.html

# 2.seccomp 库函数

SCMP_ACT_ALLOW(0x7fff0000U) 黑名单外的进程可以被执行(黑名单内的不能执行)
SCMP_ACT_KILL( 0x00000000U) 白名单外的进程被杀死
c
__int64 sandbox()
{
  
  // 两个重要的宏,SCMP_ACT_ALLOW (0x7fff0000U) SCMP_ACT_KILL ( 0x00000000U)
  //seccomp_init 初始化,参数为 0 表示白名单模式,即没有匹配到规则的系统调用都会杀死进程,默认不允许所有的 syscall
  // 参数为 0x7fff0000U 则为黑名单模式,在名单里的会被杀死
  v1 = seccomp_init(0LL);
  if ( !v1 )    // 初始化失败,v1<=0
  {
    puts("seccomp error");
    exit(0);
  }
  
  //seccomp_rule_add 添加规则
  //v1 对应上面初始化的返回值
  // 0x7fff0000 即对应宏 SCMP_ACT_ALLOW(黑名单)
  // 第三个参数代表对应的系统调用号,0-->read/1-->write/2-->open/60-->exit
  // 第四个参数表示是否需要对对应系统调用的参数做出限制以及指示做出限制的个数,传 0 不做任何限制
  seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 0LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 1LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 60LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 231LL, 0LL);
  // seccomp_load - Load the current seccomp filter into the kernel
  if ( seccomp_load(v1) < 0 )  //load 成功则返回 0,这里 load 失败会执行 if 内部函数
  {
  	// seccomp_release - Release the seccomp filter state
  	// 但对已经 load 的过滤规则不影响
    seccomp_release(v1);
    puts("seccomp error");
    exit(0);
  }
  return seccomp_release(v1);  释放
}

https://blog.csdn.net/A951860555/article/details/116738676

这里给出关于 seccomp_load 的定义:

c
#include <seccomp.h>
typedef void * scmp_filter_ctx;
int seccomp_load(scmp_filter_ctx ctx);
Link with -lseccomp.

DESCRIPTION:

Loads the seccomp filter provided by ctx into the kernel; if the function succeeds the new seccomp filter will be active when the function returns . 函数返回时过滤开始运行【seccomp_load 是应用过滤,seccomp_reset 是解除过滤。】

返回值:Returns zero on success or one of the following error codes on failure;成功时返回 0 ,失败返回对应错误代码

https://man.archlinux.org/man/seccomp_load.3.en#DESCRIPTION

seccomp_rule_add 是添加一条规则

c
int seccomp_rule_add(scmp_filter_ctx ctx,
             uint32_t action, int syscall, unsigned int arg_cnt, ...);

arg_cnt 表明是否需要对对应系统调用的参数做出限制以及指示做出限制的个数,如果仅仅需要允许或者禁止所有某个系统调用,arg_cnt 直接传入 0 即可,seccomp_rule_add (ctx, SCMP_ACT_KILL, SCMP_SYS (execve), 0) 即禁用 execve,不管其参数如何。

# 3. 使用 seccomp-tools

命令:
seccomp-tools dump ./pwn

此处用 buuctf 例题 orw 说明:

# 4. 绕过

一般运用 orw 函数的方式进行读取 flag

# 5. 注意

当使用了 prctl (4, 0) 时

4 对应的宏是 PR_SET_DUMPABLE ,第二个参数为 0 的话则不能被 dump,为 1 可以被 dump。换句话说,该程序我们无法使用 seccomp-tools dump ./pwn 来查看其系统调用情况,只能靠阅读代码分析。同时在进行 gdb 调试时,执行到 prctl (4, 0) 这行时程序也会被终止。不过只要以 root 身份执行,那么 secccomp-tools 和 gdb 调试都没问题,估计应该是限制了一般身份用户的 dump 行为

参考:
https://zhuanlan.zhihu.com/p/363174561