rosieblue
article thumbnail
728x90

들어가며

원격 코드 실행(Remote Code Execution)과 같은 취약점은 사전 예방 방법이 없음.

따라서 제한된 환경에서만 코드를 실행할 수 있게 하는 샌드박스 기법이 도입됨.

 

샌드박스에 대해서는 아래 포스트를 참고하자.

[Security] 샌드박스(Sandbox)와 샌드박스의 취약점

 

[Security] 샌드박스(Sandbox)와 샌드박스의 취약점

샌드박스 아래 샌드박스의 정의에 대해 쉽게 설명해 놓은 좋은 글이 있어서 이를 발췌해 보았다. "Sandboxing is a form of software virtualization that lets programs and processes run in its isolated virtual environment. Typi

hannahsecurity.tistory.com

 

간단히 샌드박스를 요약하면 외부 환경으로부터 시스템을 보호하기 위해 만들어진 제한된 가상 환경이라고 생각할 수 있다.

이 그림은 위 포스트에서도 있던 그림인데, 저 노란색 상자안에서만 프로그램이 실행된다고 보면 될 것 같다. 

우리가 오늘 다룰 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

 

 

profile

rosieblue

@Rosieblue

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!