ctf Pwnable 문제를 풀다 보면 seccomp가 적용된 바이너리를 자주 만날 수 있다. 보통은 소스코드 없이 바이너리만 주어지기 때문에 decompile 하여 코드를 보게 되는데, argument들이 정수로 표시되어 있어 해석이 불편할 때가 많아 따로 정리해두려고 한다.
seccomp는 SECure COMPuting의 약자로, 커널이 제공하는 수많은 system call 중에 사용되지 않는 것들을 필터링 함으로써 attack surface를 줄이는 역할을 한다. seccomp 필터링을 사용하려면 process가 자신이 사용할 system call들을 필터로 정의해야 하는데, 이 필터는 BPF(Berkeley Packet Filter) 프로그램 형식으로 정의된다. (BPF는 일종의 socket 필터로, seccomp에서 사용될 때는 system call 번호와 argument들에 대해 필터링을 수행한다: 하단 참조) seccomp 필터를 위반하는 system call 호출이 일어나면 커널은 SIGKILL 시그널을 발생시켜서 프로세스를 종료한다.
seccomp 필터는 한번 적용되면 제거가 불가능하도록 설계되어 있는데, seccomp 필터를 사용한다는 것 자체가 이후에 실행되는 코드들에 대한 불신을 의미하기 때문이다.
seccomp는 prctl()과 seccomp()의 두 가지 인터페이스를 통해 사용할 수 있다.
prctl()은 프로세스에 대한 여러가지 파라미터들을 변경하거나 확인하기 위한 인터페이스이다. 다양한 기능을 제공하지만, 여기서는 seccomp를 설정하기 위한 부분만 다룬다.
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
prctl()을 이용해서 seccomp를 설정하기 위해서는 PR_SET_SECCOMP 옵션을 이용해야 한다. arg2를 통해서 SECCOMP_MODE_STRICT와 SECCOMP_MODE_FILTER의 두 가지 모드를 지정할 수 있다.
SECCOMP_MODE_STRICT 모드는 read, write, exit, sigreturn을 제외한 모든 system call을 제약한다.
SECCOMP_MODE_FILTER 모드는 허용되는 system call을 지정할 수 있는데, 이 때 arg3는 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 { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
instruction들이 수행될 때 BPF 프로그램은 아래와 같이 구성된 호출 정보를 제공받는다.
struct seccomp_data {
int nr; /* System call number */
__u32 arch; /* AUDIT_ARCH_* value
(see <linux/audit.h>) */
__u64 instruction_pointer; /* CPU instruction pointer */
__u64 args[6]; /* Up to 6 system call arguments */
};
seccomp 필터가 fork를 허용하는 경우, fork를 통해 생성된 자식 프로세스는 부모의 seccomp 모드를 상속한다. execve가 허용된 경우에도 생성된 프로세스에 seccomp 모드가 유지된다.
seccomp() 인터페이스는 prctl() 인터페이스의 seccomp 관련 기능에 대한 확장 인터페이스이다. 주로 thread 관련 기능이 추가되었다.
int seccomp(unsigned int operation, unsigned int flags, void *args);
operation 인자를 통해서 SECCOMP_SET_MODE_STRICT와 SECCOMP_SET_MODE_FILTER의 두 가지 모드를 지정할 수 있다.
SECCOMP_SET_MODE_STRICT 모드는 prctl()의 strict 모드와 마찬가지로 read, write, exit, sigreturn을 제외한 모든 system call을 제약한다. 이 때, flag와 args는 각각 0과 NULL이어야 한다. seccomp(SECCOMP_SET_MODE_STRICT, 0, NULL)은 prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)와 동일한 기능을 제공한다.
SECCOMP_SET_MODE_FILTER 모드는 arg 인자를 통해 허용되는 system call들의 목록을 정의한다.
flags에는 아래의 값들을 사용할 수 있다.
- SECCOMP_FILTER_FLAG_TSYNC: 해당 프로세스 내의 모든 thread에 동일한 seccomp filter를 적용한다.
- SECCOMP_FILTER_FLAG_LOG: SECCOMP_RET_ALLOW를 제외한 모든 필터의 return action이 logging 된다.(/proc/sys/kernel/seccomp/actions_logged)
- SECCOMP_FILTER_FLAG_SPEC_ALLOW: Spectre와 Meltdown 방어를 위한 mitigation을 disable한다.
flag가 0인 경우, prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, args)와 동일하게 동작한다. 즉, seccomp() 인터페이스는 위의 flag들을 사용하기 위해 개발되었다.
seccomp() filter mode example (from here)
static int user_trap_syscall(int nr, unsigned int flags)
{
struct sock_filter filter[] = {
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_USER_NOTIF),
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)ARRAY_SIZE(filter),
.filter = filter,
};
return seccomp(SECCOMP_SET_MODE_FILTER, flags, &prog);
}
seccomp() 대신 scmp_filter_ctx, seccomp_rule_add(), seccomp_load()를 이용해서 seccomp를 구성할 수도 있다. (참조)
Constants
- linux/include/uapi/linux/seccomp.h
/* Valid values for seccomp.mode and prctl(PR_SET_SECCOMP, <mode>) */
#define SECCOMP_MODE_DISABLED 0 /* seccomp is not in use. */
#define SECCOMP_MODE_STRICT 1 /* uses hard-coded filter. */
#define SECCOMP_MODE_FILTER 2 /* uses user-supplied filter. */
/* Valid operations for seccomp syscall. */
#define SECCOMP_SET_MODE_STRICT 0
#define SECCOMP_SET_MODE_FILTER 1
#define SECCOMP_GET_ACTION_AVAIL 2
#define SECCOMP_GET_NOTIF_SIZES 3
/* Valid flags for SECCOMP_SET_MODE_FILTER */
#define SECCOMP_FILTER_FLAG_TSYNC (1UL << 0)
#define SECCOMP_FILTER_FLAG_LOG (1UL << 1)
#define SECCOMP_FILTER_FLAG_SPEC_ALLOW (1UL << 2)
#define SECCOMP_FILTER_FLAG_NEW_LISTENER (1UL << 3)
- linux/include/uapi/linux/prctl.h
/* Get/set process seccomp mode */
#define PR_GET_SECCOMP 21
#define PR_SET_SECCOMP 22
BPF는 네트워크 패킷 모니터링 도구인 tcpdump 프로그램에서 파생되었다. tcpdump에서 처리해야 할 패킷들이 너무 많아지자 모든 패킷을 user space까지 올리는 것이 비효율적이 되었고, (user space에서는 관심 있는 패킷만 처리할 수 있도록) 필터링 작업을 kernel space에서 수행하기 위해 만들어진 것이 BPF이다.
seccomp 개발자들이 seccomp에 대한 요구사항을 정리하고 설계를 진행하다 보니 BPF와 매우 비슷한 task가 필요한 것으로 판단되어서 BPF를 system call 필터링에도 사용할 수 있도록 확장하였다.
BPF 프로그램은 커널 안에 존재하는 작은 virtual machine에서 처리된다고 생각하면 된다. BPF 프로그램은 최대 4096개의 instruction을 가질 수 있으며, kernel에 로딩될 때 verification 과정을 거친다. 이 VM에서 동작하는 코드는 몇가지 제약 사항이 있는데, 대표적으로 branch는 허용 되지만 이전의 instruction으로의 jump는 허용하지 않기 때문에 loop의 구현이 불가능하다는 점이 있다. 또, 모든 branch가 return으로 종료되어야 하기 때문에 BPF 프로그램은 항상 종료되며 이 때 정확한 return 값이 반환된다는 것이 보장된다.
BPF는 하나의 accumulator register와 data 영역을 갖는데, seccomp 필터링 시에 data 영역에는 system call에 대한 정보가 저장된다. 모든 instruction은 64bit 크기인데, 16bit opcode와 8bit 크기의 두 개의 jump 주소, 그리고 opcode에 따라 용도가 정해지는 32bit 크기의 field로 구성된다. jump 주소 두개를 갖고 있을 수 있기 때문에 conditional jump를 간단히 구성할 수 있다. jump 주소는 offset이기 때문에 0은 no jump를 의미하게 된다.
BPF를 확장한 eBPF(extended BPF)도 개발되었는데, 이는 BPF의 필터링 대상을 tracepoints, raw sockets, perf event 등으로 확장한 것이다. 참고로, Spectre와 Meltdown 취약점에서 커널에 공격 코드를 올리기 위해 사용했던 것이 eBPF이다.