샌드박스(sandbox)는 외부의 공격으로부터 시스템을 보호하기 위해 설게된 기법입니다.
샌드박스는 Allow List와 Deny List 두 가지를 선택해 적용할 수 있으며, 꼭 필요한 시스템 콜 실행, 파일의 접근만 허용합니다.
다음 장에서 Sandbox 메커니즘 중 하나인 SECCOMP 기술에 대해서 알아보겠습니다.
SECure COMPuting mode (SECCOMP)는 리눅스 커널에서 프로그램의 샌드박싱 메커니즘을 제공하는 컴퓨터 보안 기능입니다.
SECCOMP를 사용하면 애플리케이션에서 불필요한 시스템 콜의 호출을 방지할 수 있습니다.
예를 들어 애플리케이션이 외부의 시스템 명령을 실행하지 않는다면, execve와 같은 시스템 콜은 굳이 실행될 필요가 없습니다.
그래서 execve 시스템 콜의 실행을 방지하는 정책을 적용하면 애플리케이션의 취약점이 존재해도 외부의 공격으로부터 피해를 최소화 할 수 있습니다.
해당 모드는 read, write, exit, sigreturn 시스템 콜의 호출만 허용하고 이외의 시스템 콜은 요청이 들어오면 SIGKILL 시그널을 발생시켜 프로그램을 종료합니다.
해당 모드는 원하는 시스템 콜의 호출을 허용하거나 거부할 수 있습니다.
적용 방법은 라이브러리 함수를 이용한 방법과 Berkeley Packet Filter (BPF) 문법을 통해 적용하는 방법 두 가지로 나뉘어집니다.
# SECCOMP 설치 명령어
$ apt install libseccomp-dev libseccomp2 seccomp
read, write, exit, sigreturn 시스템 콜의 호출만 허용하고 이외의 시스템 콜은 요청이 들어오면 SIGKILL 시그널을 발생시키고 프로그램을 종료합니다.
prctl 함수를 사용하여 해당 모드를 적용할 수 있습니다.
// Name: strict_mode.c
// Compile: gcc -o strict_mode strict_mode.c
#include <fcntl.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <unistd.h>
void init_filter() { prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT); }
int main() {
char buf[256];
int fd = 0;
init_filter();
write(1, "OPEN!\n", 6);
fd = open("/bin/sh", O_RDONLY);
write(1, "READ!\n", 6);
read(fd, buf, sizeof(buf) - 1);
write(1, buf, sizeof(buf));
return 0;
}
kali@kali ~/dreamhack/SECCOMP ./strict_mode
OPEN!
[1] 16498 killed ./strict_mode
model_syscall는 read, write, exit, sigreturn 시스템 콜의 번호를 저장하고 있는 변수이며, 애플리케이션 호환 모드에 따라서 각 비트에 맞는 시스템 콜 번호를 저장합니다.
이후 애플리케이션에서 시스템 콜이 호출되면 __secure_computing 함수에 먼저 진입합니다. 해당 함수는 전달된 시스템 콜 번호가 model_syscalls 또는 model_syscalls_32에 미리 정의된 번호와 일치하는지 검사하고 일치하지 않으면 SIGKILL 시그널을 전달하고 SECCOMP_RET_KILL을 반환합니다.
static const int mode1_syscalls[] = {
__NR_seccomp_read,
__NR_seccomp_write,
__NR_seccomp_exit,
__NR_seccomp_sigreturn,
-1, /* negative terminated */
};
#ifdef CONFIG_COMPAT
static int mode1_syscalls_32[] = {
__NR_seccomp_read_32,
__NR_seccomp_write_32,
__NR_seccomp_exit_32,
__NR_seccomp_sigreturn_32,
0, /* null terminated */
};
#endif
static void __secure_computing_strict(int this_syscall) {
const int *allowed_syscalls = mode1_syscalls;
#ifdef CONFIG_COMPAT
if (in_compat_syscall()) allowed_syscalls = get_compat_mode1_syscalls();
#endif
do {
if (*allowed_syscalls == this_syscall) return;
} while (*++allowed_syscalls != -1);
#ifdef SECCOMP_DEBUG
dump_stack();
#endif
seccomp_log(this_syscall, SIGKILL, SECCOMP_RET_KILL_THREAD, true);
do_exit(SIGKILL);
}
원하는 시스템 콜의 호출을 허용하거나 거부할 수 있습니다.
SCMP_ACT_KILL을 통해서 모든 시스템 콜의 호출을 금지하는 규칙을 생성합니다.
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL);
그 이후 원하는 시스템 콜을 seccomp_rule_add 함수를 통해 허용하는 규칙을 적용합니다.
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
// Name: libseccomp_alist.c
// Compile: gcc -o libseccomp_alist libseccomp_alist.c -lseccomp
#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <unistd.h>
void sandbox() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL);
if (ctx == NULL) {
printf("seccomp error\n");
exit(0);
}
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigreturn), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
seccomp_load(ctx);
}
int banned() { fork(); }
int main(int argc, char *argv[]) {
char buf[256];
int fd;
memset(buf, 0, sizeof(buf));
sandbox();
if (argc < 2) {
banned();
}
fd = open("/bin/sh", O_RDONLY);
read(fd, buf, sizeof(buf) - 1);
write(1, buf, sizeof(buf));
}
kali@kali ~/dreamhack/SECCOMP ./libseccomp_alist
[1] 23320 invalid system call ./libseccomp_alist
✘ kali@kali ~/dreamhack/SECCOMP ./libseccomp_alist 1
@@@@�▒▒▒�6�6@@%
SCMP_ACT_ALLOW를 통해 모든 시스템 콜의 호출을 허용하는 규칙을 생성합니다.
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
이렇게 생성된 규칙에 seccomp_rule_add 함수를 통해서 시스템 콜의 호출을 거부하는 규칙을 생성합니다.
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(open), 0);
// Name: libseccomp_dlist.c
// Compile: gcc -o libseccomp_dlist libseccomp_dlist.c -lseccomp
#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <unistd.h>
void sandbox() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
exit(0);
}
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(openat), 0);
seccomp_load(ctx);
}
int main(int argc, char *argv[]) {
char buf[256];
int fd;
memset(buf, 0, sizeof(buf));
sandbox();
fd = open("/bin/sh", O_RDONLY);
read(fd, buf, sizeof(buf) - 1);
write(1, buf, sizeof(buf));
}
kali@kali ~/dreamhack/SECCOMP ./libseccomp_dlist
[1] 25596 invalid system call ./libseccomp_dlist
BPF는 커널에서 지원하는 VM으로, 임의 데이터를 비교하고, 결과에 따라 특정 구문으로 분기하는 명령어를 제공합니다.
특정 시스템 콜 호출 시에 어떻게 처리할지 명령어를 통해 구현할 수 있습니다.
BPF 코드를 직접 입력하지 않고 편리하게 원하는 코드를 실행할 수 있게끔 매크로를 제공합니다.
operand에 해당하는 값을 명시한 opcode로 값을 가져옵니다. opcode는 인자로 전달된 값에서 몇 번째 인덱스에서 가져올 것인지를 지정할 수 있습니다.
BPF_STMT(opcode, operand)
BPF_STMT 매크로를 통해 저장한 값과 operand를 opcode에 정의한 코드로 비교하고, 비교 결과에 따라 특정 오프셋으로 분기합니다.
BPF_JUMP(opcode, operand, true_offset, false_offset)
현재 아키텍처가 x86_64라면 다음 코드로 분기하고, 다른 아키텍처라면 SECCOMP_RET_KILL을 반환하고 프로그램을 종료합니다.
#define arch_nr (offsetof(struct seccomp_data, arch))
#define ARCH_NR AUDIT_ARCH_X86_64
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0),
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),
호출된 시스템 콜의 번호를 저장하고, ALLOW_SYSCALL 매크로를 호출합니다. 해당 매크로는 호출된 시스템 콜이 인자로 전달된 시스템 콜과 일치하는지 비교하고 같다면 SECCOMP_RET_ALLOW를 반환합니다. 만약 다른 시스템 콜이라면 KILL_PROCESS를 호출해 SECCOMP_RET_KILL을 반환하고 프로그램을 종료합니다.
#define ALLOW_SYSCALL(name) \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW
#define KILL_PROCESS \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr),
ALLOW_SYSCALL(rt_sigreturn),
ALLOW_SYSCALL(open),
ALLOW_SYSCALL(openat),
ALLOW_SYSCALL(read),
ALLOW_SYSCALL(write),
ALLOW_SYSCALL(exit_group),
KILL_PROCESS,
// Name: secbpf_alist.c
// Compile: gcc -o secbpf_alist secbpf_alist.c
#include <fcntl.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <unistd.h>
#define ALLOW_SYSCALL(name) \
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW)
#define KILL_PROCESS BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL)
#define syscall_nr (offsetof(struct seccomp_data, nr))
#define arch_nr (offsetof(struct seccomp_data, arch))
/* architecture x86_64 */
#define ARCH_NR AUDIT_ARCH_X86_64
int sandbox() {
struct sock_filter filter[] = {
/* Validate architecture. */
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, arch_nr),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARCH_NR, 1, 0),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),
/* Get system call number. */
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
/* List allowed syscalls. */
ALLOW_SYSCALL(rt_sigreturn),
ALLOW_SYSCALL(open),
ALLOW_SYSCALL(openat),
ALLOW_SYSCALL(read),
ALLOW_SYSCALL(write),
ALLOW_SYSCALL(exit_group),
KILL_PROCESS,
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
perror("prctl(PR_SET_NO_NEW_PRIVS)\n");
return -1;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) {
perror("Seccomp filter error\n");
return -1;
}
return 0;
}
void banned() { fork(); }
int main(int argc, char* argv[]) {
char buf[256];
int fd;
memset(buf, 0, sizeof(buf));
sandbox();
if (argc < 2) {
banned();
}
fd = open("/bin/sh", O_RDONLY);
read(fd, buf, sizeof(buf) - 1);
write(1, buf, sizeof(buf));
return 0;
}
kali@kali ~/dreamhack/SECCOMP ./secbpf_alist
[1] 2230 invalid system call ./secbpf_alist
✘ kali@kali ~/dreamhack/SECCOMP ./secbpf_alist 1
@@@@�▒▒▒�6�6@@%
현재 아키텍처가 X86_64면 다음 코드로 분기하고, 다른 아키텍처라면 SECCOMP_RET_KILL을 반환하고 프로그램을 종료합니다.
#define arch_nr (offsetof(struct seccomp_data, arch))
#define ARCH_NR AUDIT_ARCH_X86_64
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0),
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),
호출된 시스템 콜의 번호를 저장하고, DENY_SYSCALL 매크로를 호출합니다. 해당 매크로는 호출된 시스템 콜이 인자로 전달된 시스템 콜과 일치하는지 비교하고 같다면 SECCOMP_RET_KILL을 반환해 프로그램을 종료합니다.
#define DENY_SYSCALL(name) \
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)
#define MAINTAIN_PROCESS \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW)
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr),
DENY_SYSCALL(open),
DENY_SYSCALL(openat),
MAINTAIN_PROCESS,
// Name: secbpf_dlist.c
// Compile: gcc -o secbpf_dlist secbpf_dlist.c
#include <fcntl.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <unistd.h>
#define DENY_SYSCALL(name) \
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL)
#define MAINTAIN_PROCESS BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW)
#define syscall_nr (offsetof(struct seccomp_data, nr))
#define arch_nr (offsetof(struct seccomp_data, arch))
/* architecture x86_64 */
#define ARCH_NR AUDIT_ARCH_X86_64
int sandbox() {
struct sock_filter filter[] = {
/* Validate architecture. */
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, arch_nr),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARCH_NR, 1, 0),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),
/* Get system call number. */
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
/* List allowed syscalls. */
DENY_SYSCALL(open),
DENY_SYSCALL(openat),
MAINTAIN_PROCESS,
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
perror("prctl(PR_SET_NO_NEW_PRIVS)\n");
return -1;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) {
perror("Seccomp filter error\n");
return -1;
}
return 0;
}
int main(int argc, char* argv[]) {
char buf[256];
int fd;
memset(buf, 0, sizeof(buf));
sandbox();
fd = open("/bin/sh", O_RDONLY);
read(fd, buf, sizeof(buf) - 1);
write(1, buf, sizeof(buf));
return 0;
}
kali@kali ~/dreamhack/SECCOMP ./secbpf_dlist
[1] 7430 invalid system call ./secbpf_dlist
SECCOMP가 적용된 바이너리의 분석을 도울 뿐만 아니라 BPF 어셈블러/디스어셈블러를 제공하는 유용한 도구입니다.
$ sudo apt install gcc ruby-dev
$ gem install seccomp-tools
kali@kali ~/dreamhack/SECCOMP seccomp-tools dump /home/kali/dreamhack/SECCOMP/secbpf_alist
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 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x06 0x00 0x00 0x00000000 return KILL