
CVE-2026-31431에 대한 개념 증명 익스플로잇으로, splice()와 조작된 소켓 옵션을 통한 Linux 커널 로컬 권한 상승을 이용해 임의의 커널 메모리 쓰기를 가능하게 하여 setuid 바이너리를 덮어씁니다.
copyFail.py는 Linux 커널의 로컬 권한 상승 취약점을 입증하는 개념 증명(PoC) 익스플로잇입니다. splice() 시스템 콜을 조작된 소켓 옵션과 결합하여 악용함으로써, 익스플로잇은 임의의 커널 메모리를 손상시킬 수 있는 능력을 얻습니다. 이 기능을 사용하여 디스크에 있는 임의의 setuid 바이너리를 사용자 정의 ELF 실행 파일로 덮어쓴 다음, 변조된 바이너리를 실행하여 상승된 권한을 획득합니다.
공격 유형: 로컬 권한 상승(LPE) 영향: 전체 루트 셸 접근(setuid-루트 바이너리를 대상으로 할 때) 복잡도: 낮음(단일 스크립트, 외부 종속성 없음)
사용법:
./copyFail_cleaned.py [대상_바이너리] # 기본값: /usr/bin/su
원본 PoC는 /usr/bin/su를 하드코딩했지만, 기본 커널 취약점은 모든 파일에 적용됩니다 — 공격자는 선택한 모든 setuid 바이너리를 덮어쓸 수 있습니다. 정리된 버전은 대상을 지정하기 위한 선택적 명령줄 인수를 허용하여 이 동작을 명시적으로 만듭니다.
Linux 커널은 두 파일 디스크립터 간에 데이터를 커널 내부에서 완전히 복사하는 splice() 시스템 콜을 제공합니다 — 먼저 사용자 공간으로 데이터를 복사하지 않습니다. 이를 "제로-카피 I/O"라고 하며 웹 서버 및 리버스 프록시와 같은 고처리량 애플리케이션의 성능을 위해 사용됩니다.
이 취약점은 커널이 지원되지 않는 프로토콜 수준 소켓 옵션으로 구성된 PACKET 소켓에 splice()가 사용될 때 특정 소켓 구조의 내부 상태를 제대로 검증하지 않기 때문에 발생합니다. 이러한 옵션을 신중하게 구성함으로써 공격자는 커널 포인터를 손상시킬 수 있으며, 그런 다음 splice()를 사용하여 커널 메모리 쓰기를 임의의 위치로 리디렉션할 수 있습니다.
간단히 말하면: 커널에는 내부적으로 데이터를 이동시키는 지름길이 있으며, 이 익스플로잇은 그 지름길을 속여 잘못된 위치 — 구체적으로 시스템 바이너리의 메모리 내 복사본 — 에 데이터를 쓰게 합니다.
익스플로잇은 명확한 일련의 단계를 따릅니다:
익스플로잇은 원시 PACKET 소켓(AF_PACKET / SOCK_RAW)을 생성하고 의도적으로 잘못된 주소에 바인딩합니다. 그런 다음 Linux가 인식하지 못하는 Solaris 수준 상수인 SOL_PNIO(프로토콜 수준 279)를 사용하여 setsockopt()를 호출합니다.
이것이 중요한 이유: 커널이 setsockopt()에서 지원되지 않는 프로토콜 수준을 만나면 데이터를 제대로 검증하지 않는 일반 처리기로 폴백합니다. 이로 인해 커널 메모리가 일관되지 않고 악용 가능한 상태로 남게 됩니다.
accept()를 통해 소켓 연결을 설정한 후, 익스플로잇은 sendmsg()를 사용하여 조작된 보조(제어) 메시지를 전송합니다. 이러한 메시지는 의도적으로 불일치하는 길이 헤더를 가지고 있습니다 — 일부는 실제보다 짧다고 주장하고, 다른 일부는 더 길다고 주장합니다.
이것이 중요한 이유: 커널의 제어 메시지 파서는 이러한 길이를 포인터 산술에 사용합니다. 불일치하는 길이는 파서가 의도된 경계를 넘어 읽거나 쓰게 하여 인접한 커널 구조 — 특히 splice()가 나중에 따를 포인터 — 를 손상시킵니다.
익스플로잇은 파이프를 생성하고 splice()를 두 번 호출합니다:
/usr/bin/su에서 파이프로 데이터를 스플라이스합니다.이것이 중요한 이유: splice() 호출은 2단계에서 심어진 손상된 커널 포인터를 따르므로 소켓 버퍼 대신 공격자가 선택한 커널 메모리 주소에 데이터를 쓰게 됩니다. 이는 익스플로잇에 임의 커널 메모리 쓰기 프리미티브를 제공합니다.
익스플로잇의 페이로드는 160바이트 ELF 실행 파일입니다(아래 참조). 익스플로잇 함수에 대한 각 호출은 정확히 4바이트를 씁니다. 따라서 메인 루프는 40회 실행됩니다(160 / 4 = 40), 매회:
모든 40회 반복 후, /usr/bin/su의 처음 160바이트가 덮어써집니다.
마지막으로 익스플로잇은 os.system("su")를 실행합니다. 커널은 페이지 캐시(이제 실제 su 바이너리 대신 공격자의 셸코드를 포함하는)에서 수정된 /usr/bin/su를 로드하고, 셸코드가 실행됩니다.
내장된 페이로드는 다음 셸코드를 포함하는 160바이트 x86-64 ELF 실행 파일로 압축 해제됩니다:
; syscall 105(execveat) 시도 — 이전 커널에서는 사용하지 못할 수 있음
xor eax, eax
xor edi, edi
mov al, 0x69 ; syscall 105
syscall
; 폴백: syscall 59(execve) — 신뢰할 수 있는 경로
lea rdi, [rip+0xf] ; RDI = "/bin/sh" 포인터
xor esi, esi ; RSI = NULL (envp)
push 0x3b ; syscall 59 번호
pop eax
cdq ; RDX = NULL (argv)
syscall
; 깨끗하게 종료
xor edi, edi
push 0x3c ; syscall 60 (exit)
pop eax
syscall
; 데이터 섹션: "/bin/sh\0\0\0"
기능: 인수 없이 /bin/sh를 생성하여 su를 실행한 프로세스의 권한을 상속합니다. 익스플로잇이 루트로 실행되면(또는 su에 루트 접근을 부여하는 기능으로), 결과 셸은 루트 셸입니다.
하지 않는 것:
이는 연구 PoC와 일치하는 단순한 일회성 권한 상승입니다.
copyFail.py
├── hex_to_bytes() — 16진수 문자열 디코더
├── exploit_splice() — 핵심 익스플로잇(소켓 설정 + 손상 + splice)
│ ├── 1단계: PACKET 소켓 생성, 바인딩, setsockopt (SOL_PNIO)
│ ├── 2단계: accept() 연결
│ ├── 3단계: 조작된 보조 메시지로 sendmsg()
│ ├── 4단계: pipe() + splice()로 커널 메모리 손상
│ └── 5단계: recv() 시도(손상 강화)
└── 메인 루프:
├── /usr/bin/su 열기(읽기 전용)
├── 내장 페이로드 압축 해제(zlib → 160바이트 ELF)
├── 루프: 반복당 4바이트 주입(총 40회 반복)
└── 변조된 su 실행 → 루트 셸
익스플로잇은 /usr/bin/su를 O_RDONLY(읽기 전용)로 엽니다. 쓰기 접근이 필요하지 않은 이유는 손상이 커널 페이지 캐시 — 커널이 변경 사항을 디스크에 플러시하기 전에 사용하는 파일의 메모리 내 복사본 — 를 통해 발생하기 때문입니다. splice() 기반 쓰기는 일반 파일 권한을 완전히 우회하여 커널 메모리로 직접 이동합니다.
/usr/bin/su 바이너리(패키지 관리자와 해시 비교)setsockopt() 호출splice() 시스템 콜sysctl을 사용하여 권한 없는 사용자의 AF_PACKET 소켓 생성 제한copyFail.py(배포된 대로)copyFail_cleaned.py(같은 디렉토리)net/packet/, fs/splice.c, net/core/sock.c이 보고서는 보안 연구 및 방어 분석 목적으로만 제공됩니다.
| 상수 | 값 | 익스플로잇에서의 용도 |
|---|
AF_PACKET | 17 | 원시 패킷 소켓 패밀리 |
SOCK_RAW | 3 | 원시 소켓 유형 |
SOL_PNIO | 279 | 지원되지 않는 프로토콜 수준(Solaris) |
MSG_DONTWAIT | 0x400 | 비차단 sendmsg 플래그 |