Seccomp从0到1

 

作者:at0de@星盟

介绍

Seccomp(全称:secure computing mode)在2.6.12版本(2005年3月8日)中引入linux内核,将进程可用的系统调用限制为四种:read,write,_exit,sigreturn。最初的这种模式是白名单方式,在这种安全模式下,除了已打开的文件描述符和允许的四种系统调用,如果尝试其他系统调用,内核就会使用SIGKILL或SIGSYS终止该进程。Seccomp来源于Cpushare项目,Cpushare提出了一种出租空闲linux系统空闲CPU算力的想法,为了确保主机系统安全出租,引入seccomp补丁,但是由于限制太过于严格,当时被人们难以接受。

2005年的Cpushare项目原文及评论:https://lwn.net/Articles/120647/

 

实例

我们可以写一个实例验证一下:

ps:如果没有头文件可以直接用apt安装

sudo apt install libseccomp-dev libseccomp2 seccomp

代码如下:

#include <stdio.h>
#include <sys/prctl.h> 
#include <linux/seccomp.h>

int main() {
    //prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
    char *buf = "hello world!n";
    write(0,buf,0xc);
    printf("%s",buf);
}

未使用seccomp:hello world!hello world!

使用seccomp:hello world!Killed

初代的seccomp就是这样暴力,禁用白名单之外的所有函数!

 

Seccomp-BPF

介绍

尽管seccomp保证了主机的安全,但由于限制太强实际作用并不大。在实际应用中需要更加精细的限制,为了解决此问题,引入了Seccomp – Berkley Packet Filter(Seccomp-BPF)。Seccomp-BPF是Seccomp和BPF规则的结合,它允许用户使用可配置的策略过滤系统调用,该策略使用Berkeley Packet Filter规则实现,它可以对任意系统调用及其参数(仅常数,无指针取消引用)进行过滤。Seccomp-BPF在3.5版(2012年7月21日)的Linux内核中(用于x86 / x86_64系统)和Linux内核3.10版(2013年6月30日)被引入Linux内核。

Seccomp我们已经知道了,那么BPF规则是什么?

最初构想提出于1992年,其目的是为了提供一种过滤包的方法,并且要避免从内核空间到用户空间的无用的数据包复制行为。它最初是由从用户空间注入到内核的一个简单的字节码构成,它在那个位置利用一个校验器进行检查 —— 以避免内核崩溃或者安全问题 —— 并附着到一个套接字上。其简化的语言以及存在于内核中的即时编译器(JIT),使 BPF 成为一个性能卓越的工具。

两者是如何结合的?

seccomp在过滤系统调用(调用号和参数)的时候,借助了BPF定义的过滤规则,以及处于内核的用BPF language写的mini-program。此外,BPF使得seccomp的用户无法遭受系统调用插入框架中常见的检查时间-使用时间(TOCTOU)攻击。BPF程序不能取消引用指针,这将所有过滤器限制为直接评估系统调用参数。

Seccomp-BPF在原来的基础上增加了过滤规则,大致流程如下:

现在我们逐步分析代码。

step 1:定义过滤规则

先看看禁用execve函数的规则:

struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4), //前面两步用于检查arch
BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),    //将帧的偏移0处,取4个字节数据,也就是系统调用号的值载入累加器
BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),    //当A == 59时,顺序执行下一条规则,否则跳过下一条规则,这里的59就是x64的execve系统调用
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),     //返回KILL
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),    //返回ALLOW
};

我从filter.h中找到了结构体和宏指令,BPF的过滤规则就是由两个指令宏组成的指令序列完成的,这个序列是一个结构体数组。

/*
 *    Try and keep these values and structures similar to BSD, especially
 *    the BPF code definitions which need to match so you can share filters
 */

struct sock_filter {    /* Filter block */
    __u16    code;   /* Actual filter code */
    __u8    jt;    /* Jump true */
    __u8    jf;    /* Jump false */
    __u32    k;      /* Generic multiuse field */
};

struct sock_fprog {    /* Required for SO_ATTACH_FILTER. */
    unsigned short        len;    /* Number of filter blocks */
    struct sock_filter __user *filter;
};

/* ret - BPF_K and BPF_X also apply */
#define BPF_RVAL(code)  ((code) & 0x18)
#define         BPF_A           0x10

/* misc */
#define BPF_MISCOP(code) ((code) & 0xf8)
#define         BPF_TAX         0x00
#define         BPF_TXA         0x80

/*
 * Macros for filter block array initializers.
 */
#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

bpf_common.h中有BPF_STMT和BPF_JUMP这两个操作指令参数的介绍,中文注释来自看雪论坛okchenshuo师傅

#define BPF_CLASS(code) ((code) & 0x07)            //首先指定操作的类别
#define        BPF_LD        0x00                                        //将操作数装入A或者X
#define        BPF_LDX        0x01                   
#define        BPF_ST        0x02                                        //拷贝A或X的值到内存
#define        BPF_STX        0x03
#define        BPF_ALU        0x04                                        //用X或常数作为操作数在累加器上执行算数或逻辑运算
#define        BPF_JMP        0x05                                        //跳转指令
#define        BPF_RET        0x06                                        //终止过滤器并表明报文的哪一部分保留下来,如果返回0,报文全部被丢弃
#define        BPF_MISC     0x07

/* ld/ldx fields */
#define BPF_SIZE(code)  ((code) & 0x18)         //在ld时指定操作数的大小
#define        BPF_W        0x00                //双字
#define        BPF_H        0x08                //单字
#define        BPF_B        0x10                //单字节
#define BPF_MODE(code)  ((code) & 0xe0)         //操作数类型
#define        BPF_IMM        0x00
#define        BPF_ABS        0x20                        //绝对偏移                   
#define        BPF_IND        0x40                        //相对偏移
#define        BPF_MEM        0x60
#define        BPF_LEN        0x80
#define        BPF_MSH        0xa0
/* alu/jmp fields */
#define BPF_OP(code)    ((code) & 0xf0)         //当操作码类型为ALU时,指定具体运算符
#define        BPF_ADD        0x00                    //到底执行什么操作可以看filter.h里面的定义
#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                    //当操作码类型是JMP时指定跳转类型
#define        BPF_JEQ        0x10
#define        BPF_JGT        0x20
#define        BPF_JGE        0x30
#define        BPF_JSET        0x40
#define BPF_SRC(code)   ((code) & 0x08)        
#define        BPF_K        0x00                    //常数
#define        BPF_X        0x08

有师傅写了函数库seccomp-bpf.h,用来快速添加规则

#define VALIDATE_ARCHITECTURE 
    BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr), 
    BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0), 
    BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)

#define EXAMINE_SYSCALL 
    BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr)

#define ALLOW_SYSCALL(name) 
    BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), 
    BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW)

#define KILL_PROCESS 
    BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)

我们只需要按照结构体去创建规则就可以使用它,可以有多条过滤规则,seccomp会从第0条开始逐条执行,直到遇到BPF_RET返回,决定是否允许该操作以及做某些修改。

step 2:定义prog参数

prog结构体过滤规则个数与规则数组起始位置。

struct sock_fprog prog = { 
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),    /* Number of filter blocks */
.filter = filter, /* Pointer to array of BPF instructions */
};

step 3:使用prctl函数

prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);

prctl函数原型如下:

int prctl(int option, unsigned long arg2, unsigned long arg3,unsigned long arg4, unsigned long arg5);

prctl()用法

我们这里用到了两种

prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);

为了保证安全性,需要将PR_SET_NO_NEW_PRIVSW位设置位1。这个操作能保证seccomp对所有用户都能起作用,并且会使子进程即execve后的进程依然受控,意思就是即使执行execve这个系统调用替换了整个binary权限不会变化,而且正如其名它设置以后就不能再改了,即使可以调用ptctl也不能再把它禁用掉。

prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);

PR_SET_SECCOMP指明我们正在为进程设置seccomp;
SECCOMP_MODE_FILTER将seccomp的过滤规则指向&prog;
&prog就是我们定义的过滤规则

实例

代码:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <malloc.h>
#include <sys/prctl.h>
#include <seccomp.h>
#include <linux/seccomp.h>
#include <linux/filter.h>

int main() {
    struct sock_filter filter[] = {
    BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
    BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
    BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
    BPF_JUMP(BPF_JMP+BPF_JEQ,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 = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
    .filter = filter,
    };
    prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
    prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
    printf("start!n");
    system("id");
    return 0;
}

添加多条规则建议使用seccomp-bpf.h库,简单方便

例如

#define _GNU_SOURCE 1
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <malloc.h>
#include <sys/prctl.h>
#include <seccomp.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include "seccomp-bpf.h"

static int install_syscall_filter(void)
{
    struct sock_filter filter[] = {
        /* Validate architecture. */
        VALIDATE_ARCHITECTURE,
        /* Grab the system call number. */
        EXAMINE_SYSCALL,
        /* List allowed syscalls. */
        ALLOW_SYSCALL(rt_sigreturn),
#ifdef __NR_sigreturn
        ALLOW_SYSCALL(sigreturn),
#endif
        ALLOW_SYSCALL(exit_group),
        ALLOW_SYSCALL(exit),
        ALLOW_SYSCALL(read),
        ALLOW_SYSCALL(write),
        KILL_PROCESS,
    };
    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
        .filter = filter,
    };

    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl(NO_NEW_PRIVS)");
        goto failed;
    }
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
        perror("prctl(SECCOMP)");
        goto failed;
    }
    return 0;

failed:
    if (errno == EINVAL)
        fprintf(stderr, "SECCOMP_FILTER is not available. :(n");
    return 1;
}

int main()
{

    if (install_syscall_filter())
        return 1;


    return 0;
}

 

seccomp库函数

seccomp库可以提供一些函数实现prctl类似的效果,库中封装了一些函数,可以不用了解BPF规则而实现过滤。

需要安装一些库文件

sudo apt install libseccomp-dev libseccomp2 seccomp

实例

#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>

int main(void){
    scmp_filter_ctx ctx;
    ctx = seccomp_init(SCMP_ACT_ALLOW);
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
    seccomp_load(ctx);
    char * str = "/bin/sh";
    write(1,"hello worldn",12);
    syscall(59,str,NULL,NULL);//execve
    return 0;
}

scmp_filter_ctx是过滤器的结构体

seccomp_init对结构体进行初始化,若参数为SCMP_ACT_ALLOW,则过滤为黑名单模式;若为SCMP_ACT_KILL,则为白名单模式,即没有匹配到规则的系统调用都会杀死进程,默认不允许所有的syscall。

seccomp_rule_add用来添加一条规则,arg_cnt为0,表示我们直接限制execve,不管参数是什么,如果arg_cnt不为0,那arg_cnt表示后面限制的参数的个数,也就是只有调用execve,且参数满足要求时,才会拦截

seccomp_load是应用过滤器,如果不调用seccomp_load则上面所有的过滤都不会生效

注意:编译的时候要在最后面加 -lseccomp

 

seccomp-tools

项目地址:https://github.com/david942j/seccomp-tools

可以用来查看过滤规则

$ seccomp-tools dump ./a.out 
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x02 0xffffffff  if (A != 0xffffffff) goto 0007
 0005: 0x15 0x01 0x00 0x0000003b  if (A == execve) goto 0007
 0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0007: 0x06 0x00 0x00 0x00000000  return KILL

还有其他几个功能,可以自己探索

 

seccomp在ctf中的应用

seccomp在ctf中大多用于禁用execve函数,解决办法就是构造shellcode,用open->read->write的方式读flag

做题这部分网上的例题和参考资料很多,我就不说了

我讲一下如何出seccomp类型的题:

1.栈类型的题使用ptrcl或seccomp库都行;

2.堆类型需要用ptrcl函数,因为seccomp库会留下堆的使用痕迹,如下图所示;

我查看seccomp.c的源码,发现了内核空间使用的kmalloc、kfree等函数

3.出堆题时将seccomp库的规则放到最后

[GKCTF2020]Domo 这道题的师傅是这样做的,但是会出现非预期解

 

总结

seccomp的pwn题近两年算是一个热点,搞懂这种机制运行原理很重要,希望能对大家有帮助。

 

参考资料

https://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt

https://www.kernel.org/doc/html/v4.16/userspace-api/seccomp_filter.html

https://ajxchapman.github.io/linux/2016/08/31/seccomp-and-seccomp-bpf.html

https://zhougy0717.github.io/2019/11/30/seccomp%E4%BB%8B%E7%BB%8D/

https://blog.betamao.me/2019/01/23/Linux%E6%B2%99%E7%AE%B1%E4%B9%8Bseccomp/#seccomp

https://darkwing.moe/2019/08/08/seccomp/

https://bbs.pediy.com/thread-258146.htm

(完)