들어가며
원격 코드 실행(Remote Code Execution)과 같은 취약점은 사전 예방 방법이 없음.
따라서 제한된 환경에서만 코드를 실행할 수 있게 하는 샌드박스 기법이 도입됨.
샌드박스에 대해서는 아래 포스트를 참고하자.
[Security] 샌드박스(Sandbox)와 샌드박스의 취약점
간단히 샌드박스를 요약하면 외부 환경으로부터 시스템을 보호하기 위해 만들어진 제한된 가상 환경이라고 생각할 수 있다.
이 그림은 위 포스트에서도 있던 그림인데, 저 노란색 상자안에서만 프로그램이 실행된다고 보면 될 것 같다.
우리가 오늘 다룰 SECCOMP도 이 샌드박스 기법 중에 하나인데, SECCOMP에서는 이 노란색 상자 안에서 특정 시스템콜만 허용하거나, 특정 시스템 콜을 하지 못하게 한다. 이것을 어떻게 코드 상에서 구현하는지 살펴보자.
SECCOMP (SECure COMPuting mode)
seccomp은 sec+comp으로 말그대로 secure한 computing 을 지원하는 모드라고 보면된다.
그렇다면 어떻게 secure하게 지원할까?
SECCOMP : 불필요한 시스템콜을 방지하는 샌드박싱 기법
아직 어떤 샌드박싱 기법이 또 있는지는 모르지만 seccomp은 '불필요한 시스템콜'을 방지할 수 있는 기능을 가지고 있는 샌드박스 기법이다. 참고로 syscall은 유저가 직접 커널 모드로 접근할 수 없으므로 커널에게 저수준의 작업을 요청하는 명령어이다. 시스템 콜에 대해 자세히 보려면 다음 글을 참고하자. -> 시스템콜(syscall), 셸, 커널모드 vs 유저모드
실제 코드에서 seccomp을 설정하려면 prctl 함수에 PR_SET_SECCOMP를 첫번째 인자로 전달하면 된다.
구성
struct seccomp {
int mode;
struct seccomp_filter *filter;
};
/* struct seccomp - the state of a seccomp'ed process
*
* @mode: indicates one of the valid values above for controlled
* system calls available to a process.
* @filter: must always point to a valid seccomp-filter or NULL as it is
* accessed without locking during system call entry.
*
* @filter must only be accessed from the context of current as there
* is no read locking.
*/
seccomp_data
https://elixir.bootlin.com/linux/v4.1/source/include/uapi/linux/seccomp.h#L47
struct seccomp_data {
int nr; //시스템콜 번호
__u32 arch;
__u64 instruction_pointer; //at the time of the system call (sycall할 때 ip 말하는 듯)
__u64 args[6]; //아키텍처 상관없이 64bit값으로 syscall 인자들이 저장됨
};
/*
* 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.
*/
실제 코드에서 seccomp은 아래와 같이 prctl함수의 첫 인자에 PR_SET_SECCOMP를 넣어서 적용한다.
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT); }
//PR_SET_SECCOMP으로 SECCOMP 샌드박싱 사용한다고 선언, SECCOMP_MODE_STRICT로 STRICT 모드 사용한다고 선언
"prctl이란 함수는 리눅스 내부에서 프로세스를 관리할때 사용하는 함수인데 sys/prctl.h 헤더안에 선언돼있고 prctl(option, arg2, arg3, arg4, arg5)의 인자형태로 구성돼있다. 그중에서 첫번째 인자인 option으로 PR_SET_SECCOMP를 전달해 샌박을 설정하는 경우가 많은데 이 경우 arg2를 통해 seccomp 모드를 설정한다.
arg2로는 SECCOMP_MODE_STRICT, SECCOMP_MODE_FILTER 두 종류의 값이 올 수 있다"
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=mathboy7&logNo=220902956252
이때 두번째 인자로 준 모드는 무엇일까? seccomp은 아래와 같이 2가지 모드로 나누어 설정할 수 있다.
- Strict Mode: read,write,exit,sigreturn의 시스템콜만을 허용
- Filter Mode: 개발자가 원하는 시스템콜을 설정할 수 있음. (라이브러리 함수로 구현 or BPF 문법으로 구현)
/**
* seccomp_phase1() - run fast path seccomp checks on the current syscall
* @arg sd: The seccomp_data or NULL
*
* This only reads pt_regs via the syscall_xyz helpers. The only change
* it will make to pt_regs is via syscall_set_return_value, and it will
* only do that if it returns SECCOMP_PHASE1_SKIP.
*
* If sd is provided, it will not read pt_regs at all.
*
* It may also call do_exit or force a signal; these actions must be
* safe.
*
* If it returns SECCOMP_PHASE1_OK, the syscall passes checks and should
* be processed normally.
*
* If it returns SECCOMP_PHASE1_SKIP, then the syscall should not be
* invoked. In this case, seccomp_phase1 will have set the return value
* using syscall_set_return_value.
*
* If it returns anything else, then the return value should be passed
* to seccomp_phase2 from a context in which ptrace hooks are safe.
*/
u32 seccomp_phase1(struct seccomp_data *sd)
{
int mode = current->seccomp.mode; //모드
int this_syscall = sd ? sd->nr : //실행하려는 syscall 번호
syscall_get_nr(current, task_pt_regs(current));
switch (mode) { //모드에 따라 실행 함수 달라짐
case SECCOMP_MODE_STRICT:
__secure_computing_strict(this_syscall); /* may call do_exit */
return SECCOMP_PHASE1_OK;
#ifdef CONFIG_SECCOMP_FILTER
case SECCOMP_MODE_FILTER:
return __seccomp_phase1_filter(this_syscall, sd);
#endif
default:
BUG();
}
}
Strict Mode
- read, write, sigreturn, exit만 허용하는 모드
mode1_syscalls(혹 32bit인 경우 mode1_syscalls_32)에 허용가능한 syscall 번호 저장해 두고, 실행하려는 syscall이 이 배열 안에 속하는 경우는 ok, 아니면 SIGKILL
참고로 prctl에서 strict mode로 설정하면 3번째 인자(prog)은 무시된다!
static const int mode1_syscalls[] = { //64bit syscall 번호(strict 모드일 때)들 모은 배열
__NR_seccomp_read,
__NR_seccomp_write,
__NR_seccomp_exit,
__NR_seccomp_sigreturn,
-1, /* negative terminated */
};
#ifdef CONFIG_COMPAT
static int mode1_syscalls_32[] = { //32bit짜리 syscall 번호(strict 모드일때)
__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) {
// this_syscall은 실행하려는 syscall
const int *allowed_syscalls = mode1_syscalls;
#ifdef CONFIG_COMPAT //32bit인경우?
if (in_compat_syscall()) allowed_syscalls = get_compat_mode1_syscalls(); //요걸로!
#endif
do { //this_syscall이 allowed_syscalls에 있는지 검사
if (*allowed_syscalls == this_syscall) return; //있으면 return
} while (*++allowed_syscalls != -1);
//allowed_syscalls에 없으면 SIGKILL 당하는 듯
#ifdef SECCOMP_DEBUG
dump_stack();
#endif
seccomp_log(this_syscall, SIGKILL, SECCOMP_RET_KILL_THREAD, true);
do_exit(SIGKILL);
}
#ifndef CONFIG_HAVE_ARCH_SECCOMP_FILTER
void secure_computing_strict(int this_syscall)
{
int mode = current->seccomp.mode;
if (mode == 0)
return;
else if (mode == SECCOMP_MODE_STRICT)
__secure_computing_strict(this_syscall);
else
BUG();
}
#else
int __secure_computing(void)
{
u32 phase1_result = seccomp_phase1(NULL);
if (likely(phase1_result == SECCOMP_PHASE1_OK))
return 0;
else if (likely(phase1_result == SECCOMP_PHASE1_SKIP))
return -1;
else
return seccomp_phase2(phase1_result);
}
Filter Mode
- 사용할 수 있는 syscall과 사용할 수 없는 syscall을 구분해서 개발자가 filter를 만들어서 접근 제어하는 모드
#ifdef CONFIG_SECCOMP_FILTER
static u32 __seccomp_phase1_filter(int this_syscall, struct seccomp_data *sd)
{
u32 filter_ret, action;
int data;
/*
* Make sure that any changes to mode from another thread have
* been seen after TIF_SECCOMP was seen.
*/
rmb();
filter_ret = seccomp_run_filters(sd);
data = filter_ret & SECCOMP_RET_DATA;
action = filter_ret & SECCOMP_RET_ACTION;
switch (action) {
case SECCOMP_RET_ERRNO:
/* Set low-order bits as an errno, capped at MAX_ERRNO. */
if (data > MAX_ERRNO)
data = MAX_ERRNO;
syscall_set_return_value(current, task_pt_regs(current),
-data, 0);
goto skip;
case SECCOMP_RET_TRAP:
/* Show the handler the original registers. */
syscall_rollback(current, task_pt_regs(current));
/* Let the filter pass back 16 bits of data. */
seccomp_send_sigsys(this_syscall, data);
goto skip;
case SECCOMP_RET_TRACE:
return filter_ret; /* Save the rest for phase 2. */
case SECCOMP_RET_ALLOW:
return SECCOMP_PHASE1_OK;
case SECCOMP_RET_KILL:
default:
audit_seccomp(this_syscall, SIGSYS, action);
do_exit(SIGSYS);
}
unreachable();
skip:
audit_seccomp(this_syscall, 0, action);
return SECCOMP_PHASE1_SKIP;
}
#endif
라이브러리 함수로 구현하는 방법과 BPF 문법으로 구현하는 방법이 있다.
a. 라이브러리 함수로 구현
사용되는 함수들 ⬇
seccomp_init | SECCOMP 모드의 기본 값을 설정. 임의의 시스템 콜이 호출되면 이에 해당하는 이벤트가 발생. |
seccomp_rule_add | SECCOMP의 규칙을 추가. 임의의 시스템 콜을 허용하거나 거부할 수 있음. |
seccomp_load | 앞서 적용한 규칙을 애플리케이션에 반영. |
[ 예시 ]
ctx=seccomp_init(SCMP_ACT_KILL); 이런 식으로 seccomp를 설정한다.
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigreturn), 0); sigreturn 함수도 허용할 수 있게 설정함.
seccomp_load(ctx); ctx에 적용한 규칙을 반영
white list 코드
void sandbox() {
scmp_filter_ctx ctx; //scmp_filter_ctx 필터변수? ctx 선언
ctx = seccomp_init(SCMP_ACT_KILL);
//seccomp_init으로 모든 syscall을 다 막는 걸로 설정(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);
}
- filter mode로 설정하여 white list로 rt_sigreturn,exit,exit_group,read,write,open,openat syscall만 허용하는 코드
위 sandbox()함수 설정을 바탕으로 아래 main함수를 실행시켰다고 가정해보자.
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));
}
argc < 2 이면 banned()함수가 실행되어 fork syscall이 호출될 것이다. 한편 fork는 whitelist에 들어가있지 않으므로 sigkill을 내뿜으면서 죽을 것이다.
따라서 이 코드가 끝까지 잘 실행되게 하기 위해서는 argc >=2 로 실행하면 된다. 아래는 위 코드의 실행 결과이다.
$ ./libseccomp_alist #argc<2
Bad system call (core dumped)
$ ./libseccomp_alist 1 #argc>=2
ELF> J@X?@8 @@@?888h?h? P?P?!
argc>=2를 맞춰줬더니 /bin/bash를 정상적으로 orw한 것을 볼 수 있다.
blacklist 코드
아래 코드는 위 코드와 반대로 seccomp_init(SCMP_ACT_ALLOW);를 설정하여 모든 syscall을 허용하도록 초기화하였다.
이후 seccomp_rule_add(ctx, SCMP_ACT_KILL,SCMP_SYS(open),0); 등과 같이 원하지 않는 syscall들을 사용 못하게 설정하였다.
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);
}
b. BPF 문법으로 구현
BPF (Berkeley Packet Filter) : 임의 데이터들을 비교하고, 비교 결과에 따라 분기할 수 있게 하는 명령어 제공
filter 모드를 사용하기 위해선 쓰레드의 no_new_privs 비트가 설정되어 있어야 하며 다음의 코드로 설정가능하다.
prctl(PR_SET_NO_NEW_PRIVS, 1);
그 후 아래의 코드로 필터식을 전달하면 된다.
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, args);
args에는 sock_prog라는 구조체의 주소를 전달한다.
이를 위해 아래를 살펴보자.
struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter __user *filter;
};
struct sock_filter {
__u16 code; // actual filter code
__u8 jt; // jump true
__u8 jf; // jump false
__u32 k; // generic multiuse field
}
struct seccomp_filter {
atomic_t usage;
struct seccomp_filter *prev;
struct bpf_prog *prog;
};
/**
* struct seccomp_filter - container for seccomp BPF programs
*
* @usage: reference count to manage the object lifetime.
* get/put helpers should be used when accessing an instance
* outside of a lifetime-guarded section. In general, this
* is only needed for handling filters shared across tasks.
* @prev: points to a previously installed, or inherited, filter
* @len: the number of instructions in the program
* @insnsi: the BPF program instructions to evaluate
*
* seccomp_filter objects are organized in a tree linked via the @prev
* pointer. For any task, it appears to be a singly-linked list starting
* with current->seccomp.filter, the most recently attached or inherited filter.
* However, multiple filters may share a @prev node, by way of fork(), which
* results in a unidirectional tree existing in memory. This is similar to
* how namespaces work.
*
* seccomp_filter objects should never be modified after being attached
* to a task_struct (other than @usage).
*/
BPF는 어셈블리어랑 비슷하다.
code 값으로 인스트럭션을 결정하고, jt와 jf는 조건에 따른 분기점, k는 사용되는 상수값이나 기타 다양한 용도로 사용된다.
예시를 확인해보자.
bobctf2017의 megabox 문제에서 사용하는 필터식이다. (예시 출처 : LINUX] Seccomp (velog.io))
root@kali:/work/ctf/BOBCTF2017/megabox_d# seccomp-tools dump ./megabox > dump
root@kali:/work/ctf/BOBCTF2017/megabox_d# cat dump
your name... this step is for performance :) 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 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) 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: 0x06 0x00 0x00 0x00000000 return KILL
맨 위줄에 보면 code가 0x20을 가질 때는 대입 연산을 나타내며 마지막 k가 대입되는 값을 가리킨다.
k가 4일 때는 아키텍쳐가 저장되고, 0일 때는 sys_number가 저장되는 것을 알 수 있다.
마찬가지로, code가 0x15일 때는 if문을 나타낸다. k는 비교대상 값이며 비교 후에 참일 경우 jt에 해당하는 벡터만큼, 거짓일 경우 jf에 해당하는 벡터만큼 분기하는 것을 확인할 수 있다.
마지막으로 return 명령어이다. code가 0x06을 가질 때 return을 나타내며 이것은 최종적으로 실행여부를 결정하는 명령이다.
return ALLOW
는 호출된 시스템콜을 허용하겠다는 말이며
return KILL
은 종료하겠다는 의미이다.
바이너리에서 bpf 필터가 어떻게 적용되었는지에 대해서는 seccomp-tools 명령어를 사용해서 위처럼 분석할 수 있다.
BPF 명령어 & 매크로
[ 명령어 ]
- BPF_LD : 인자를 누산기로 복사. 이를 통해 비교 구문에서 해당 값을 비교할 수 있음
- BPF_JMP : 지정한 위치로 분기
- BPF_JEQ : 비교구문이 참이면 지정한 위치로 분기
- BPF_RET : 인자로 전달된 값 반환
[ 매크로 ]
BPF_STMT(opcode, operand)
operand에 있는 값을 opcode의 인자로 넣는듯.
opcode는 operand에 있는 값을 어디서 몇 바이트만큼 가져올지 지정 가능
BPF_JUMP(opcode, operand, true_offset, false_offset)
BPF_STMT 매크로를 통해 저장한 값과
operand를
opcode에 정의된 코드로 비교하고, 특정 오프셋으로 분기
아키텍처 검사 코드
#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),
아키텍처가 x86_64면 분기, 아니면 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,
호출된 시스템 콜의 번호를 저장하고, ALLOW_SYSCALL 매크로를 호출
호출된 시스템 콜이 인자로 전달된 시스템 콜과 일치하는지 비교하고 같다면 SECCOMP_RET_ALLOW를 반환
만약 다른 시스템 콜이라면 KILL_PROCESS를 호출해 SECCOMP_RET_KILL을 반환하고 프로그램을 종료
References
https://learn.dreamhack.io/263
Advanced Linux Exploitation | Dreamhack
https://velog.io/@woounnan/LINUX-Seccomp
'Linux Exploitation > Fundamentals' 카테고리의 다른 글
[HackTheBox] SickROP (1) | 2024.01.05 |
---|---|
[Pwnable] SROP (SigRetrun Oriented Programming) (0) | 2023.07.23 |
[Pwnable] .init_array, .fini_array, _rtld_global (0) | 2023.07.08 |
[Pwnable] PIE (0) | 2023.05.15 |
[gdb] 로컬/서버의 라이브러리 다른 경우 (0) | 2023.04.28 |