[Dreamhack] Background: SECCOMP

#코딩노예#·2022년 7월 22일
0

Dreamhack - System Hacking

목록 보기
38/49

Sandbox

샌드박스(sandbox)는 외부의 공격으로부터 시스템을 보호하기 위해 설게된 기법입니다.

샌드박스는 Allow List와 Deny List 두 가지를 선택해 적용할 수 있으며, 꼭 필요한 시스템 콜 실행, 파일의 접근만 허용합니다.

다음 장에서 Sandbox 메커니즘 중 하나인 SECCOMP 기술에 대해서 알아보겠습니다.



SECCOMP

SECure COMPuting mode (SECCOMP)는 리눅스 커널에서 프로그램의 샌드박싱 메커니즘을 제공하는 컴퓨터 보안 기능입니다.

SECCOMP를 사용하면 애플리케이션에서 불필요한 시스템 콜의 호출을 방지할 수 있습니다.

예를 들어 애플리케이션이 외부의 시스템 명령을 실행하지 않는다면, execve와 같은 시스템 콜은 굳이 실행될 필요가 없습니다.

그래서 execve 시스템 콜의 실행을 방지하는 정책을 적용하면 애플리케이션의 취약점이 존재해도 외부의 공격으로부터 피해를 최소화 할 수 있습니다.

STRICT_MODE

해당 모드는 read, write, exit, sigreturn 시스템 콜의 호출만 허용하고 이외의 시스템 콜은 요청이 들어오면 SIGKILL 시그널을 발생시켜 프로그램을 종료합니다.

FILTER_MODE

해당 모드는 원하는 시스템 콜의 호출을 허용하거나 거부할 수 있습니다.

적용 방법은 라이브러리 함수를 이용한 방법과 Berkeley Packet Filter (BPF) 문법을 통해 적용하는 방법 두 가지로 나뉘어집니다.

# SECCOMP 설치 명령어
$ apt install libseccomp-dev libseccomp2 seccomp

STRICT_MODE

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

prctl 함수를 사용하여 해당 모드를 적용할 수 있습니다.

STRICT_MODE: 예제 코드

// 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

STRICT_MODE 동작 원리

model_syscallread, write, exit, sigreturn 시스템 콜의 번호를 저장하고 있는 변수이며, 애플리케이션 호환 모드에 따라서 각 비트에 맞는 시스템 콜 번호를 저장합니다.

이후 애플리케이션에서 시스템 콜이 호출되면 __secure_computing 함수에 먼저 진입합니다. 해당 함수는 전달된 시스템 콜 번호가 model_syscalls 또는 model_syscalls_32에 미리 정의된 번호와 일치하는지 검사하고 일치하지 않으면 SIGKILL 시그널을 전달하고 SECCOMP_RET_KILL을 반환합니다.

STRICT_MODE 처리 과정

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);
}

FILTER_MODE: 라이브러리 함수

원하는 시스템 콜의 호출을 허용하거나 거부할 수 있습니다.

SECCOMP 함수


ALLOW LIST

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);

FILTER_MODE: ALLOW LIST 예제 코드

// 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@@%  

DENY LIST

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);

FILTER_MODE: 라이브러리 함수 사용 DENY LIST 예제

// 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

FILTER_MODE: BPF

BPF는 커널에서 지원하는 VM으로, 임의 데이터를 비교하고, 결과에 따라 특정 구문으로 분기하는 명령어를 제공합니다.

특정 시스템 콜 호출 시에 어떻게 처리할지 명령어를 통해 구현할 수 있습니다.


BPF Macro

BPF 코드를 직접 입력하지 않고 편리하게 원하는 코드를 실행할 수 있게끔 매크로를 제공합니다.

BPF_STMT

operand에 해당하는 값을 명시한 opcode로 값을 가져옵니다. opcode는 인자로 전달된 값에서 몇 번째 인덱스에서 가져올 것인지를 지정할 수 있습니다.

BPF_STMT(opcode, operand)

BPF_JUMP

BPF_STMT 매크로를 통해 저장한 값과 operandopcode에 정의한 코드로 비교하고, 비교 결과에 따라 특정 오프셋으로 분기합니다.

BPF_JUMP(opcode, operand, true_offset, false_offset)

ALLOW LIST

아키텍처 검사

현재 아키텍처가 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,

FILTER_MODE: BPF ALLOW LIST 예제

// 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
@@@@�▒▒▒�66@@%   

DENY LIST

아키텍처 검사

현재 아키텍처가 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,

FILTER_MODE: BPF DENY LIST 예제

// 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-tools

SECCOMP가 적용된 바이너리의 분석을 도울 뿐만 아니라 BPF 어셈블러/디스어셈블러를 제공하는 유용한 도구입니다.

seccomp-tools 설치

$ sudo apt install gcc ruby-dev
$ gem install seccomp-tools

dump

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

0개의 댓글