Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[System_Hacking] AD: stage2_SECCOMP 본문

Hack/DreamHack(로드맵)

[System_Hacking] AD: stage2_SECCOMP

CIDY 2022. 7. 10. 19:55

*sandbox

샌드박스는 외부의 공격으로부터 시스템을 보호하기 위해 설계된 보호 기법이다. -> Allow ListDeny list 두 가지를 선택 적용할 수 있다. -> 프로그램의 기능 수행에 있어 꼭 필요한 시스템 콜 실행과 필수적인 파일에 대한 접근만을 허용함. -> 외부 공격 최소화

 

프로그램마다 목적과 기능이 제각기 다르기 떄문에 샌드박스는 개발자가 직접 명시해야 함 -> 오용할 경우 서비스 접근성을 과하게 해치거나 목적한 기능의 정상적 작동이 이루어지지 않을 수 있음.

 

 

*SECCOMP

secure computing mode -> 리눅스 커널에서 프로그램의 샌드박싱 매커니즘을 제공하는 컴퓨터 보안 기능. -> 불필요한 시스템 콜의 호출을 방지할 수 있다. 예를 들어 어플에서 외부의 시스템 명령어를 실행하는 일이 아니면 execve와 같은 시스템 콜은 굳이 실행될 필요가 없다. 오히려 공격 시에 많이 이용되는 시스템 콜임 -> execve 시스템 콜이 실행될 경우 어플을 즉각 종료하도록 할 수 있음. -> 어플 자체에 취약점이 있더라도 외부의 공격으로부터 피해 최소화 가능

 

SECCOMP에는 두 가지 모드를 선택 적용할 수 있다.

 

-STRICT_MODE : 이 모드는 read, write, exit, sigreturn 이 네 가지의 시스템 콜 호출만을 허용함 -> 이외의 시스템 콜이 호출될 경우 SIGKILL 시그널 발생 + 프로그램 종료

 

-FILTER_MODE : 이 모드는 원하는 시스템 콜의 호출을 허용하거나 거부할 수 있다. -> 라이브러리 함수를 이용한 적용 방법과 Berkeley Packet Filter(BPF)문법을 통해 적용하는 방법이 있다.

 

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

참고로 각 모드에 대한 매크로는 위와 같다.

 

SECCOMP설치 명령어 :

apt install libseccomp-dev libseccomp2 seccomp

 

 

*STRICT_MODE

read, write, exit, sigreturn 시스템 콜의 호출만을 허용 -> 이외의 시스템 콜 호출 요청이 들어올 경우 SIGKILL 시그널 발생 + 프로그램을 종료시킨다 -> 그만큼 기능 제한 -> 다양한 기능을 수행하는 어플에서는 적용할 수 없다.

 

seccomp기능은 prctl()이라는 함수로 사용할 수 있다. 

 

*prctl은 프로세스 관리 함수인데, 아래와 같이 정의된다.

int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

해당 함수의 option 위치에 어떤 인자를  넣는가에 따라 기능이 상이한데, 아래와 같이 PR_SET_SECCOMP를 넣으면 seccomp설정 함수로 기능하는 것이다.

 

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

 

stricr_mode를 적용한 예시 코드인데, 

 

prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

 

부분에서 strict_mode로 설정해 준 것이다.

 

$ ./strict_mode
OPEN!
Killed

위 코드를 실행시키면 이렇게 된다. (SIGKILL발생)

 

-> 그럼 이 모드는 어떻게 특정 시스템 콜만을 허용할까? -> model_syscalls 구조체에서는 허용되는 네 가지 시스템 콜의 번호를 저장하고 있다. -> 시스템 콜 호출 시 __secure_computing함수로 시스템 콜 번호가 전달되는데, 이 전달된 번호가 model_syscalls나 model_syscall_32에 미리 정의된 번호와 일치하는지 검사 -> 불일치 시 SIGKILL -> SECCOMP_RET_KILL반환

 

 

*FILTER_MODE
시스템 콜의 호출을 허용하거나 거부할 수 있는 모드이다. 어플 기능에 맞춰 strict모드보다 좀 더 유연하게 시스템 콜을 허용 혹은 거부할 수 있다.

 

seccomp에서는 몇 가지 함수를 제공하는데, (seccomp.h라이브러리에 들어있는 듯. 아래는 라이브러리 함수를 이용한 seccomp 적용이다.)

 

-seccomp_init : seccomp모드의 기본값을 설정하는 함수. 아무 시스템 콜이 호출되면 여기 명시된 이벤트가 발생

-seccomp_rule_add : SECCOMP의 규칙을 추가함. 임의의 시스템 콜을 허용 혹은 거부 가능

-seccomp_load : 앞서 적용한 규칙을 애플리케이션에 반영함

 

위 함수들을 순차적으로 이용하여 allow/deny list를 만들 수 있다.

 

 

*Allow list

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

 

위 코드는 seccomp 라이브러리 함수를 사용해 지정한 시스템 콜의 호출만을 허용하는 예시 코드이다. sandbox함수에서 seccomp_init으로 시스템 콜의 기본값을 SCMP_ACT_KILL으로 해뒀다. ->  모든 시스템 콜의 호출을 기본적으로 허용하지 않음(allow list) -> 그리고 seccomp_rule_add 함수에서 세 번째 인자로 전달된 시스템 콜의 호출을 허용(SCMP_ACT_ALLOW)하도록 하고 있다.

 

(아마 ctx라는 변수에 규칙 내용을 쭉 추가한 다음 마지막에 seccomp_rule_load함수에 넣어 애플리케이션에 반영하도록 하는 것 같다.)

 

-> main을 보면 sandbox();로 앞에서 설정해둔 규칙을 적용한 뒤, 인자 개수에 따라 fork함수의 호출을 결정 -> fork함수가 호출되면 (banned) 규칙에 따라 함수의 호출을 허용하지 않음. 

 

$ ./libseccomp_alist
Bad system call (core dumped)
$ ./libseccomp_alist 1
ELF> J@X?@8	@@@?888h?h? P?P?!

 

위와 같이 인자를 하나만 주면 바로 종료된다.

 

 

*Deny List

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

 

이 코드는 seccomp라이브러리 함수를 사용해 지정한 시스템 콜을 호출하지 못하도록 하는 예제 코드이다. sandbox함수에서 SCMP_ACT_ALLOW를 통해 모든 시스템 콜의 호출을 허용하는 초기 규칙 생성 -> seccomp_rule_add 함수를 이용해 세 번째 인자로 전달된 시스템 콜의 호출을 거부(SCMP_ACT_KILL)하는 규칙 생성

 

main에서 sandbox()해주면 앞의 규칙들을 적용하는 것임. -> 규칙상 호출을 거부하도록 설정해둔 open이 호출되므로 바로 종료됨.

 

 

*FILTER_MODE : BPF

Berkeley Packet Filter (BPF)를 사용해 seccomp을 적용할 수 있다. bpf는 커널에서 지원하는 virtual machine으로, 원래 네트워크 패킷을 분석 및 필터링하는 목적으로 사용했는데, 데이터를 비교하고 그 결과에 따라 특정 구문으로 분기하는 명령어를 제공한다. -> 앞서 라이브러리 함수를 통해 규칙을 정의한 것 처럼, 특정 시스템 콜 호출 시 어떻게 처리할지 명령어를 통해 구현할 수 있다. -> vm이라 다양한 명령어와 타입이 존재함

 

BPF_LD : 인자로 전달된 값을 누산기에 복사 -> 값을 복사 후 비교 구문에서 비교할 수 있다.

BPF_JMP : 지정한 위치로 분기함

BPF_JEQ : 설정한 비교 구문이 일치할 경우 지정 위치로 분기함

BPF_RET : 인자로 전달된 값을 반환

 

*BPF macro : BPF코드를 직접 입력하지 않고 편리하게 원하는 코드를 실행할 수 있게 매크로 제공(아래 두 개가 매크로)

 

BPF_STMT : 오퍼랜드에 해당하는 값을 명시한 옵코드로 값을 가져옴. 옵코드는 인자로 전달된 값에서 몇 번째 인덱스로부터 몇 바이트를 가져올 것인지를 지정할 수 있다.

BPF_STMT(opcode, operand)

BPF_JUMP : BPF_STMT매크로를 통해 저장한 값과 오퍼랜드를 옵코드에 정의한 코드로 비교하고, 비교 결과에 따라 특정 오프셋으로 분기함. (true or flase)

BPF_JUMP(opcode, operand, true_offset, false_offset)

 

 

*Allow List

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

 

위 코드는 BPF를 통해 지정한 시스템 콜의 호출만을 허용하는 예제 코드이다. sandbox함수의 filter구조체에 BPF코드가 작성되어 있는 것을 확인할 수 있다. 

 

아 라이브러리 함수를 이용한 필터링 방식보다 읽기 귀찮게 생겼다.. 무엇보다 매크로가 너무 많다. 그래도 직관적으로 allow list를 형성하고 있음을 알 수 있다.

 

 

#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이면 다음 코드로 분기하고, 다른 아키텍쳐라면 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,

이 부분은 시스템 콜을 검사하는 것이다. 호출된 시스템 콜의 번호를 저장하고, ALLOW_SYSCALL매크로를 호출 -> 호출된 시스템 콜이 인자로 전달된 시스템 콜과 일치하는지 비교하고, 같을 경우 SECCOMP_RET_ALLOW를 반환 -> 다른 시스템 콜이면 KILL_PROCESS호출 -> SECCOMP_RET_KILL반환 후 프로그램 종료

 

 

$ ./secbpf_alist
Bad system call (core dumped)
$ ./secbpf_alist 1
ELF> J@X?@8	@@@?888h?h? P?P?!

 

코드의 실행 결과는 위와 같다.

 

 

*Deny List

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

 

위 코드는 BPF를 통해 지정한 시스템 콜을 호출하지 못하도록 하는 예제 코드이다. 이것도 allow list와 같이 아키텍쳐 검사와 시스템 콜 검사 과정을 거치는데, 시스템 콜 검사 과정에서 호출된 시스템 콜 번호를 저장 후 -> DENY_SYSCALL매크로 호출 -> 호출된 시스템 콜이 인자로 전달된 시스템 콜과 일치하는지 비교 -> 일치할 경우 SECCOMP_RET_KILL -> 프로그램 종료

 

 

*seccomp_tools

seccomp규칙이 복잡하면 바이너리 분석이 힘들어진다. C코드로 작성된 BPF문법은 이해가 쉬운 편이지만, 컴파일 된 바이너리의 경우 바이트 코드를 변환해야 하기에 어려움 -> seccomp-tools는 SECCOMP가 적용된 바이너리의 분석을 돕고 BPF 어셈블러/디스어셈블러를 지원한다.

 

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