Linux 커널의 authencesn 암호화 템플릿에 존재하는 논리 버그가 읽기 가능한 임의 파일의 페이지 캐시에 제어 가능한 4바이트 쓰기를 가능하게 하며, 이를 통해 주요 Linux 배포판 전반에서 로컬 권한 상승이 가능해진다.
Copy Fail: 모든 주요 Linux 배포판에서 732바이트로 루트 획득.
Xint Code Research Team
Copy Fail(CVE-2026- 31431 )는 Linux 커널의 authencesn 암호화 템플릿에 존재하는 논리 버그입니다. 이 버그는 권한이 없는 로컬 사용자가 시스템에서 읽을 수 있는 임의 파일의 페이지 캐시에 결정적이고 제어 가능한 4바이트 쓰기를 일으키도록 합니다. 단 732바이트짜리 Python 스크립트 하나로 setuid 바이너리를 수정하고, 사실상 2017년 이후 출시된 거의 모든 Linux 배포판에서 루트를 획득할 수 있습니다.
커널은 손상된 페이지를 writeback 대상으로 dirty 표시하지 않으므로, 디스크 상의 파일은 변경되지 않으며 일반적인 디스크 체크섬 비교로는 이 변조를 놓치게 됩니다. 그러나 실제로 파일 접근 시 읽히는 것은 페이지 캐시이므로, 손상된 메모리 내 버전은 즉시 시스템 전체에서 보이게 됩니다. 권한이 없는 로컬 사용자는 setuid 바이너리의 페이지 캐시를 손상시켜 이를 루트 획득으로 이어지게 할 수 있습니다. 같은 원리는 페이지 캐시가 호스트 전반에서 공유되기 때문에 컨테이너 경계도 넘습니다.
이 발견은 AI의 도움을 받았지만, 시작점은 Theori 연구원 Taeyang Lee의 통찰이었습니다. 그는 Linux crypto 서브시스템이 페이지 캐시 기반 데이터와 어떻게 상호작용하는지 연구하고 있었습니다. 그는 Xint Code를 사용해 연구 범위를 crypto 서브시스템 전체로 확장했고, Copy Fail은 그 보고서에서 가장 치명적인 결과물이었습니다.
이 글은 2부작 시리즈의 첫 번째입니다:
1부(이 글): 버그와 로컬 권한 상승
2부: Kubernetes 컨테이너 탈출
Linux 커널에는 이전에도 주목받는 권한 상승 버그가 있었습니다. Dirty Cow(CVE-2016-5195)는 VM 서브시스템의 copy-on-write 경로에서 경쟁 조건을 이겨내야 했습니다. 보통 여러 번의 시도가 필요했고 때로는 시스템을 크래시시키기도 했습니다. Dirty Pipe(CVE-2022-0847)는 특정 버전에만 해당했고 정밀한 pipe buffer 조작이 필요했습니다.
Copy Fail은 직선적인 논리 결함입니다. 경쟁 조건, 재시도, 크래시를 유발하기 쉬운 타이밍 윈도 없이 트리거됩니다.
이식성. 완전히 동일한 스크립트가 Ubuntu, Amazon Linux, RHEL, SUSE를 포함해 테스트한 모든 배포판과 아키텍처에서 동작합니다. 배포판별 오프셋이 없습니다. 재컴파일도 없습니다. 익스플로잇 안에 버전 검사도 없습니다.
작음. 전체 익스플로잇은 표준 라이브러리 모듈(os, socket, zlib)만 사용하는 짧은 Python 스크립트입니다. os.splice를 위해 Python 3.10+가 필요합니다. 컴파일된 페이로드도, 의존성 설치도 없습니다.
은밀함. 이 쓰기는 일반적인 VFS 쓰기 경로를 우회합니다. 손상된 페이지는 커널의 writeback 메커니즘에 의해 dirty로 표시되지 않습니다. 디스크 상 체크섬을 비교하는 표준 파일 무결성 도구는 이를 놓칩니다. 디스크 상 파일은 바뀌지 않기 때문입니다. 손상되는 것은 메모리 내 페이지 캐시뿐입니다.
컨테이너 간 영향. 페이지 캐시는 컨테이너 경계를 포함해 시스템의 모든 프로세스 사이에서 공유됩니다. Copy Fail은 단순한 로컬 권한 상승이 아닙니다. 컨테이너 탈출 원시 기능이며 Kubernetes 노드 장악 벡터입니다(2부).
AF_ALG는 커널의 crypto 서브시스템을 권한 없는 사용자 공간에 노출하는 소켓 타입입니다. 사용자는 소켓을 열고 임의의 AEAD(Authenticated Encryption with Associated Data) 템플릿에 bind한 뒤, 임의 데이터에 대해 암호화 또는 복호화를 호출할 수 있습니다. 권한은 필요 없습니다.
이 버그의 기반이 되는 핵심 원시 기능은 splice()입니다. 이 기능은 복사 없이 파일 디스크립터와 pipe 사이에서 데이터를 전달하며, 페이지 캐시 페이지를 참조로 넘깁니다. 사용자가 파일을 pipe로 splice한 다음 AF_ALG 소켓으로 splice하면, 소켓의 입력 scatterlist는 해당 파일의 커널 캐시 페이지를 직접 참조하게 됩니다. 페이지는 복제되지 않습니다. scatterlist 항목은 그 파일의 모든 read(), mmap(), execve()를 뒷받침하는 동일한 물리 페이지를 가리킵니다.
AEAD 복호화에서 입력은 AAD(associated authenticated data) || ciphertext || authentication_tag입니다. algif_aead.c 내부에서 recvmsg()는 작업을 in-place로 설정합니다. 즉 같은 scatterlist가 crypto 알고리즘의 입력과 출력 양쪽 역할을 합니다.
AAD와 ciphertext 데이터는 memcpy_sglist를 통해 입력 scatterlist에서 출력 버퍼로 바이트 단위 복사됩니다. 이것은 실제 복사입니다. 페이지 캐시 페이지는 읽기만 됩니다. 그러나 입력 scatterlist의 마지막 authsize 바이트인 authentication tag는 복사되지 않습니다. 커널은 tag에 대한 scatterlist 항목을 유지한 채 sg_chain()을 사용해 이를 출력 scatterlist의 끝에 연결합니다:
Input SGL: AAD || CT || Tag
| | ^
| copy | | sg_chain (여전히 페이지 캐시 페이지를 참조)
v v |
Output SGL: AAD || CT -----+
이제 출력 scatterlist는 두 구역을 갖습니다. 사용자의 recvmsg 버퍼(복사된 AAD와 ciphertext 포함) 뒤에, 여전히 대상 파일의 원래 페이지 캐시 페이지를 참조하는 연결된 tag 페이지가 따라옵니다. 커널은 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)
이 in-place 설계가 취약점의 근본 원인입니다. 이는 페이지 캐시 페이지를 쓰기 가능한 scatterlist 안에 배치하며, 정당한 쓰기 영역과는 오직 오프셋 경계만으로 분리됩니다. 설계는 모든 AEAD 알고리즘이 자신의 쓰기를 의도된 목적지 안으로만 제한할 것이라고 가정하지만, API 어디에도 이를 강제하는 장치는 없고, 요구사항으로 문서화된 부분도 없습니다.
불행히도, 한 AEAD 알고리즘이 이 조용한 불변식을 깨뜨립니다.
커널의 AEAD API는 복호화에 대해 명확한 출력 계약을 정의합니다. 대상 버퍼는 정확히 assoclen + (cryptlen - authsize) 바이트의 AAD || plaintext를 받습니다.
authencesn은 Extended Sequence Number(ESN) 지원을 위해 IPsec에서 사용하는 AEAD 래퍼입니다. IPsec은 64비트 시퀀스 번호를 상위 절반(seqno_hi, AAD의 바이트 0–3)과 하위 절반(seqno_lo, 바이트 4–7)으로 나눠 사용합니다. 와이어 형식에는 seqno_lo만 들어가고 seqno_hi는 암묵적입니다. HMAC 계산을 위해 authencesn은 이 바이트들을 재배열해야 합니다. 해시 입력의 맨 앞에는 seqno_hi를, 맨 끝에는 seqno_lo를 둡니다.
이 재배열은 호출자의 대상 버퍼를 스크래치 공간으로 사용하는 방식으로 수행됩니다. 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 쓰기
처음 두 호출은 AAD 영역 내부에서 ESN 바이트를 섞는 동작이며, 임시 수정 후 복원됩니다. 세 번째 호출은 AEAD tag를 지난 위치인 오프셋 assoclen + cryptlen에 4바이트를 씁니다. 이 알고리즘은 자신이 소유하지 않은 메모리를 스크래치 패드로 사용하고 있습니다.
그 위치의 원래 바이트는 영구적으로 사라집니다. crypto_authenc_esn_decrypt_tail()은 AAD를 재구성하기 위해 seqno_lo를 다시 읽어오지만, 원래 내용을 dst[assoclen + cryptlen]에 다시 쓰지는 않습니다. 연산의 성공 여부와 무관하게, 그 위치는 소모 가능한 스크래치 공간으로 취급됩니다.
커널의 다른 표준 AEAD 알고리즘은 이런 동작을 하지 않습니다. GCM, CCM, 일반 authenc은 모두 자신의 쓰기를 정당한 출력 영역 안으로 제한합니다. 경계를 넘는 쓰기를 하는 것은 authencesn뿐입니다.
AF_ALG의 in-place 경로에서는 이 쓰기가 출력 버퍼에서 연결된 페이지 캐시 tag 페이지로 넘어갑니다. scatterwalk_map_and_copy는 RX 버퍼를 지나 페이지 캐시 페이지를 kmap_local_page로 매핑한 뒤, seqno_lo를 대상 파일의 커널 캐시 사본에 직접 씁니다. 그 다음 HMAC 계산이 수행되고 실패합니다(ciphertext는 조작된 값이기 때문입니다). 따라서 recvmsg()는 오류를 반환하지만, 제어 가능한 4바이트 쓰기는 그대로 남습니다.
중요한 점은 공격자가 세 가지를 제어할 수 있다는 것입니다:
어떤 파일인지: 현재 사용자가 읽을 수 있는 임의 파일.
어떤 오프셋인지: tag 영역은 splice된 파일 데이터의 마지막 authsize 바이트에 대응합니다. 공격자는 splice 파일 오프셋, splice 길이, assoclen을 선택함으로써 파일의 페이지 캐시 안에서 정확히 어떤 4바이트가 덮어써질지 결정할 수 있습니다.
어떤 값인지: 4바이트 덮어쓰기 값(seqno_lo)은 sendmsg()에서 공격자가 구성한 AAD의 바이트 4–7에서 옵니다.
2011년, authencesn이 IPsec ESP의 64비트 Extended Sequence Numbers(RFC 4303)를 지원하기 위해 커널에 추가되었습니다(a5079d084f8b). 처음부터 이 코드는 ESN 바이트 재배열을 위해 호출자의 대상 scatterlist를 스크래치 공간으로 사용했습니다. 당시에는 이것이 무해했습니다. 구형 AEAD 인터페이스에서는 associated data가 별도의 scatterlist에 있었고, 유일한 호출자는 커널 내부 xfrm 계층뿐이었기 때문입니다. 중간 쓰기를 외부에서 관찰하는 주체가 없었습니다.
4년 뒤인 2015년, AF_ALG는 AEAD 지원(algif_aead.c)을 얻게 되었고, splice() 경로를 통해 페이지 캐시 페이지를 crypto scatterlist로 전달할 수 있게 되었습니다. 같은 해 authencesn은 새로운 AEAD 인터페이스로 변환되었고(104880a6b470), 이 과정에서 출력 경계를 넘어 쓰는 assoclen + cryptlen 오프셋이 도입되었습니다. 하지만 이 시점의 AF_ALG는 out-of-place 동작을 사용했습니다. req->src와 req->dst는 서로 다른 scatterlist였습니다. 페이지 캐시 페이지는 src에 있었고(읽기 전용), 스크래치 쓰기는 dst(사용자 버퍼)로 갔습니다. 아직은 악용 가능하지 않았습니다.
그리고 2017년, algif_aead.c에 AEAD 연산을 in-place로 수행하기 위한 최적화가 추가되었습니다(72548b093ee3). 복호화에서 코드는 TX SGL의 AAD와 ciphertext 데이터를 RX 버퍼로 복사했지만, tag 페이지는 sg_chain()으로 참조만 연결했습니다. 이후 req->src = req->dst를 설정했습니다. 이제 splice에서 온 페이지 캐시 페이지가 쓰기 가능한 대상 scatterlist 안에 들어오게 되었습니다. 그러자 authencesn의 dst[assoclen + cryptlen] 쓰기가 이 연결된 tag 페이지로 걸어 들어가며 이 버그가 만들어졌습니다.
아무도 2017년의 in-place 최적화를 authencesn의 스크래치 쓰기, 혹은 splice 경로의 페이지 캐시 페이지 사용과 연결하지 못했습니다. 각각의 변화는 고립해서 보면 타당했습니다. 취약점은 이 세 요소가 만나는 지점에 존재했고, 거의 10년 동안 조용히 악용 가능 상태로 있었습니다.
기본 익스플로잇 경로는 /usr/bin/su를 겨냥합니다. 이 파일은 주요 Linux 배포판 전반에 널리 존재하는 setuid-root 바이너리이며, 우리가 테스트한 네 가지 환경 모두에 있었습니다.
1단계: 소켓 설정. AF_ALG 소켓을 열고 authencesn(hmac(sha256),cbc(aes))에 bind합니다. 키를 설정합니다. 요청 소켓을 accept합니다. 권한은 필요 없습니다. AF_ALG는 기본적으로 권한 없는 사용자도 사용할 수 있습니다.
2단계: 쓰기 구성. 셸코드 페이로드의 각 4바이트 청크마다 sendmsg() + splice() 쌍을 구성합니다. sendmsg는 AAD를 제공합니다. 바이트 4–7이 쓸 4바이트(seqno_lo)를 담습니다. splice는 대상 파일의 페이지 캐시 페이지를 ciphertext와 tag로 제공합니다. AEAD 매개변수(assoclen, splice 오프셋, splice 길이)는 dst[assoclen + cryptlen]이 /usr/bin/su의 .text 섹션 안의 목표 오프셋에 떨어지도록 선택됩니다.
**3단계: 쓰기 트리거.**recv()가 복호화 연산을 트리거합니다. authencesn 내부에서 커널은 AAD에서 ESN 바이트를 읽고, dst[assoclen + cryptlen]에 seqno_lo를 씁니다. scatterwalk는 출력 버퍼에서 연결된 페이지 캐시 페이지로 넘어갑니다. 4바이트가 /usr/bin/su의 커널 캐시 사본에 기록됩니다. 재배열된 데이터에 대해 HMAC이 계산되고 실패합니다. 커널은 AAD를 복원하기 위해 seqno_lo를 다시 읽어오지만, tag 위치의 원래 바이트는 복원되지 않습니다. recvmsg는 오류를 반환합니다. 페이지 캐시는 손상됩니다.
4단계: 실행. 모든 청크가 기록된 후 execve("/usr/bin/su")를 호출합니다. 커널은 페이지 캐시에서 바이너리를 적재합니다. 페이지 캐시 버전에는 삽입된 셸코드가 들어 있습니다. su는 setuid-root이므로 셸코드는 UID 0으로 실행됩니다. 루트입니다.
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
# ... 키 설정, 요청 소켓 u accept ...
u.sendmsg([b"A"*4 + payload_chunk], [cmsg_headers], MSG_MORE)
os.splice(target_fd, pipe_wr, offset)
os.splice(pipe_rd, alg_fd, offset)
u.recv(...) # decrypt 트리거 → 페이지 캐시 쓰기
우리는 동일한 스크립트를 네 가지 배포판에서 실행했고, 각각에서 루트 획득을 확인했습니다.
각 터미널은 사용자 xint(uid=1001)로 시작합니다. 동일한 732바이트 익스플로잇을 내려받아 실행합니다. 모든 터미널이 루트 셸로 끝납니다.
배포판****커널 Ubuntu 24.04 LTS 6.17.0-1007-aws Amazon Linux 2023 6.18.8-9.213.amzn2023 RHEL 10.1 6.12.0-124.45.1.el10_1 SUSE 16 6.12.0-160000.9-default
이 네 가지가 우리가 직접 테스트한 배포판과 커널 조합이며, 커널 계열 6.12, 6.17, 6.18을 포괄합니다.
패치(a664bf3d603d)는 algif_aead.c를 out-of-place 동작으로 되돌려 2017년의 in-place 최적화를 완전히 제거합니다. Fixes: 태그는 in-place 설계를 도입한 커밋 72548b093ee3를 가리키며, 이는 페이지 캐시 페이지를 쓰기 가능한 대상 scatterlist에 연결한 것이 근본 원인임을 확인해 줍니다.
취약한 코드는 req->src = req->dst를 설정해, 둘 다 splice()에서 온 페이지 캐시 페이지가 쓰기 가능한 대상에 연결된 결합 scatterlist를 가리키게 했습니다. 수정 후에는 둘이 분리됩니다:
// Before: src와 dst는 같은 scatterlist를 가리킴 (in-place)
aead_request_set_crypt(&areq->cra_u.aead_req, rsgl_src, // RX SGL
areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv); // RX SGL (동일)
// After: src는 TX SGL, dst는 RX SGL (out-of-place)
aead_request_set_crypt(&areq->cra_u.aead_req, tsgl_src, // TX SGL
areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv); // RX SGL (서로 다름)
이제 req->src는 TX SGL(여기에 splice로부터 온 페이지 캐시 페이지가 포함될 수 있음)을 가리킵니다. req->dst는 RX SGL(사용자의 recvmsg 버퍼)을 가리킵니다. AAD만 src에서 dst로 복사됩니다. 페이지 캐시 tag 페이지를 쓰기 가능한 대상 scatterlist에 연결하던 전체 sg_chain 메커니즘은 제거됩니다.
커밋 메시지는 이를 이렇게 요약합니다. "algif_aead에서 in-place로 동작할 이점은 없다. 소스와 목적지가 서로 다른 매핑에서 오기 때문이다."
커널을 패치하십시오. 수정 사항은 AF_ALG AEAD를 out-of-place 동작으로 되돌려, 쓰기 가능한 scatterlist에서 페이지 캐시 페이지를 제거합니다.
배포판의 커널 패키지를 업데이트하십시오. 주요 배포판은 일반적인 커널 패키지 업데이트를 통해 이 수정 사항을 배포해야 합니다.
즉각적인 완화책으로는 seccomp를 통해 AF_ALG 소켓 생성을 차단하거나 algif_aead 모듈을 블랙리스트에 올릴 수 있습니다:
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null
컨테이너 탈출 영향은 2부를 참고하십시오.
날짜****이벤트 [2026-03-23]Linux 커널 보안팀에 취약점 보고 [2026-03-24]초기 확인 응답 수신 [2026-03-25]패치 제안 및 검토 [2026-04-01]패치가 메인라인 커널에 커밋됨 [2026-04-22]CVE-2026-31431 할당 [2026-04-29]공개 발표(이 글)
Taeyang Lee의 이전 kernelCTF 작업은 AF_ALG 공격 표면을 이미 매핑해 두고 있었습니다. 그는 AF_ALG + splice가 권한 없는 사용자 공간이 페이지 캐시 페이지를 crypto 서브시스템으로 직접 밀어 넣을 수 있는 경로를 만든다는 점을 깨달았고, scatterlist 페이지의 출처가 충분히 탐구되지 않은 취약점 원천일 수 있다고 의심했습니다.
한편, 다른 Theori 연구원들은 Xint Code를 실행해 Android 드라이버와 XNU를 포함한 커널 코드에서 치명적 취약점을 찾고 있었습니다. 우리는 이 작업을 Linux로 확장하고자 했고, 내부 구조에 대한 기존 지식이 있었기 때문에 crypto 서브시스템은 자연스러운 출발점이었습니다.
Xint Code는 "operator prompt"를 지원합니다. 이것은 자동화된 스캔을 안내하기 위해 인간 운영자가 추가 문맥을 제공할 수 있게 해 줍니다(선택 사항). 이번 경우 operator prompt는 매우 단순했습니다:
이곳은 linux crypto/ 서브시스템입니다. 사용자 공간 syscall에서 도달 가능한 모든 코드 경로를 살펴보십시오. 한 가지 핵심 관찰을 유념하십시오: splice()는 읽기 전용 파일(setuid 바이너리 포함)의 페이지 캐시 참조를 crypto TX scatterlist로 전달할 수 있습니다.
약 1시간 후 스캔이 완료되었고, Copy Fail이 가장 높은 심각도의 결과로 나왔습니다.
CVE-2026-31431에 대한 Xint Code 스캔 결과
참고: 스캔은 또 다른 권한 상승 버그를 포함해 다른 고심각도 취약점들도 식별했습니다. 이들 다른 버그는 아직 책임 있는 공개 절차가 진행 중입니다.
Xint Code는 바로 이런 워크플로를 위해 만들어진 보안 연구 도구입니다. 연구자가 공격 표면을 식별하면, XC가 그것을 분석합니다.
다음 글: Copy Fail이 어떻게 모든 주요 클라우드 Kubernetes 플랫폼을 탈출하는지 다루는 "From Pod to Host".