본문 바로가기
CVE 취약점

CVE-2026-31431 "Copy Fail" — Linux 커널 algif_aead 권한 상승 취약점 분석

by WhiteGuidance 2026. 9. 29.
반응형

취약점 개요


항목 내용
CVE ID CVE-2026-31431
별칭 Copy Fail
CVSS 3.1 7.8 (HIGH) — AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
취약 컴포넌트 Linux 커널 crypto/algif_aead.c, crypto/authenc_esn.c
영향 로컬 권한 상승(LPE), 컨테이너 이스케이프
영향 커널 4.14 ~ 6.18.21 / 6.19.11 (2017년 도입, 2026년 4월 패치)

Copy Fail은 레이스 컨디션이나 타이밍 공격이 아니라, 정해진 순서대로 시스템 콜만 호출하면 100% 재현되는 결정론적 로직 버그다. 일반 권한의 로컬 사용자가 커널의 페이지 캐시에 자신이 원하는 4바이트를 원하는 위치에 써넣을 수 있고, 이를 setuid 바이너리에 적용하면 root 셸을 얻는다.


배경 지식 먼저 정리하기

소스코드로 들어가기 전에, 이 취약점을 이해하는 데 필요한 세 가지 개념을 쉽게 짚고 넘어간다.

 

AF_ALG란? Linux 커널은 자신이 가진 암호화 기능(AES, SHA 등)을 유저 프로그램이 직접 쓸 수 있도록 소켓 인터페이스로 열어준다. 이게 AF_ALG 소켓이다. 소켓을 열고 bind()로 어떤 암호 알고리즘을 쓸지 지정한 다음, sendmsg()로 데이터를 보내고 recvmsg()로 결과(암호화/복호화된 데이터)를 받는다. root 권한이 전혀 필요 없다.

 

splice()란? 보통 파일을 읽으려면 커널이 파일 내용을 유저 공간 버퍼로 복사해줘야 한다(read()). 그런데 splice()는 이 복사를 생략하고, 커널 내부의 페이지 캐시(page cache) — 즉 파일 내용이 캐싱되어 있는 커널 메모리 — 를 파이프나 소켓에 "참조"로 그대로 넘겨준다. 복사가 없으니 빠르지만, 그 대신 넘겨받은 쪽이 원본 페이지 캐시를 실수로라도 건드리면 진짜 파일의 캐시된 내용이 오염된다는 위험이 있다.

 

AEAD와 authencesn이란? AEAD(Authenticated Encryption with Associated Data)는 암호화와 무결성 검증(인증 태그)을 함께 처리하는 방식이다. authencesn은 그중에서도 IPsec이 쓰는 특수한 버전으로, ESN(Extended Sequence Number, 64비트 확장 시퀀스 번호)을 처리하기 위해 상위 32비트(seqno_hi)와 하위 32비트(seqno_lo)를 임시로 재배열하는 로직을 갖고 있다. 이 재배열 과정에서 목적지 버퍼를 "잠깐 쓰고 버리는 메모지" 취급하는 게 이번 취약점의 방아쇠다.

 

이 세 가지가 각각은 멀쩡한데, 한 곳에서 만나면서 문제가 생긴다.


근본 원인: 2017년에 추가된 "인플레이스(in-place) 최적화"

algif_aead.c의 복호화 경로는 원래 입력(source)과 출력(destination) 스캐터리스트(scatterlist, 메모리 조각들을 이어 붙인 리스트)를 따로 썼다. 그런데 2017년 커밋 72548b093ee3에서 성능을 위해 입력과 출력을 같은 메모리로 취급하는 "인플레이스" 방식이 도입됐다.

 

이때 커널은 다음과 같이 동작한다.

  1. 입력 스캐터리스트는 AAD(연관 데이터) || 암호문(CT) || 인증 태그(Tag) 순서로 구성된다.
  2. AAD와 CT는 memcpy_sglist()로 진짜 복사되어 사용자가 recvmsg()로 넘긴 수신 버퍼(RX buffer)에 담긴다. — 여기까진 안전하다. 복사본이니까.
  3. 그런데 마지막의 인증 태그(authsize 바이트)는 복사하지 않고, sg_chain()으로 "참조"만 출력 스캐터리스트 뒤에 이어붙인다.

이해하기 쉽게 그리면 이렇다.

 

그리고 커널은 req->src = req->dst로 입력과 출력을 아예 동일한 포인터로 설정해버린다.

 

이 설계 자체는 "AEAD 알고리즘이라면 당연히 자기 출력 영역 안에서만 쓰기를 하겠지"라는 암묵적 전제 위에 서 있다. 문제는 이 전제를 커널이 코드로 강제하지 않는다는 점이다. 그리고 하필 authencesn이 이 전제를 깬다.


트리거: authencesn의 "스크래치 라이트"

authencesn의 복호화 함수(crypto_authenc_esn_decrypt)는 ESN 재배열을 위해 목적지 버퍼(dst)를 임시 메모장처럼 사용한다. 개념적으로 재구성하면 다음과 같은 3단계 호출로 이뤄진다.

 

한 줄씩 쉽게 풀면:

  • ①, ②번은 AAD 영역 안에서 값을 잠깐 바꿨다가 나중에 원상 복구하는 거라 문제가 없다.
  • ③번이 핵심이다. assoclen + cryptlen은 "AAD 길이 + 암호문 길이", 즉 정확히 인증 태그가 시작하는 바로 그 지점이다. authencesn은 이 위치에 4바이트를 쓰는데, 이건 커널의 AEAD 출력 계약(destination에는 AAD || 평문만 있어야 함)을 넘어선 영역이다. 게다가 이 4바이트는 나중에 복원되지 않는다. crypto_authenc_esn_decrypt_tail()이 다시 읽어가긴 하지만, "원래 거기 있던 값"을 되돌려 쓰는 코드는 없다. 한 번 덮어쓰면 그걸로 끝이다.

GCM, CCM, 일반 authenc 등 다른 모든 AEAD 알고리즘은 자기 출력 영역 안에서만 쓴다. authencesn만 유일하게 경계 밖에 쓴다. 평상시(입출력이 분리된 경우)에는 이 4바이트가 그냥 유저의 RX 버퍼 끝부분에 떨어질 뿐이라 아무 해가 없었다. 그런데 2017년 인플레이스 최적화 이후로는, 이 4바이트 쓰기가 sg_chain()으로 이어붙인 페이지 캐시 영역까지 걸어 들어간다.

 

scatterwalk_map_and_copy()는 스캐터리스트를 순서대로 따라가며 kmap_local_page()로 페이지를 매핑해 쓰기 때문에, RX 버퍼를 넘어서면 그다음 이어진 페이지 — 즉 splice로 들여온 파일의 진짜 페이지 캐시 — 에 그대로 쓰기가 들어간다.

공격자 입장에서 통제 가능한 값은 세 가지다.

  • 대상 파일: 현재 사용자가 읽을 수 있는 아무 파일 (setuid 바이너리 포함)
  • 쓰기 위치: splice할 파일 오프셋, splice 길이, assoclen 값을 조합해 assoclen + cryptlen이 파일의 어느 4바이트에 떨어질지 정확히 계산 가능
  • 쓰여지는 값: sendmsg()로 보내는 AAD의 4~7번째 바이트(seqno_lo)가 그대로 쓰여짐

즉 "어느 파일의, 어느 위치에, 어떤 4바이트를" 전부 공격자가 정할 수 있는 임의 쓰기(arbitrary write) 프리미티브다.


공격 흐름 개념도

아래는 이 취약점이 root 셸까지 이어지는 흐름을 정리한 다이어그램이다.

다이어그램은 5단계로 구성된다.

  1. AF_ALG 소켓 생성 — 권한 없이 authencesn(hmac(sha256),cbc(aes))로 바인딩
  2. sendmsg()로 AAD 전송 — 4~7번째 바이트에 "쓰고 싶은 4바이트" 삽입
  3. splice()로 대상 파일 연결 — /usr/bin/su 같은 setuid 바이너리를 파이프 → AF_ALG 소켓으로 스플라이스
  4. recvmsg() 트리거 — 복호화가 실행되며 authencesn이 페이지 캐시에 4바이트를 씀 (HMAC 검증은 실패하지만 쓰기는 이미 반영됨)
  5. 반복 후 execve() — 필요한 만큼 4바이트씩 여러 번 반복해 셸코드를 심은 뒤 오염된 바이너리를 실행 → setuid로 인해 root 권한 획득

여기서 중요한 점은, 커널이 이 페이지를 "더티(dirty)"로 표시하지 않는다는 것이다. 일반적인 파일 쓰기는 VFS 쓰기 경로를 거치며 더티 플래그가 설정되고 디스크에 반영(writeback)되지만, 이번 쓰기는 그 경로를 완전히 우회한다. 그 결과:

  • 디스크상의 원본 파일은 전혀 변경되지 않는다 (체크섬 비교로는 탐지 불가)
  • 하지만 실제로 실행되거나 읽히는 건 항상 페이지 캐시이므로, 오염된 내용이 시스템 전체에 즉시 반영된다
  • 페이지 캐시는 호스트 전체에서 공유되므로, 컨테이너 환경에서는 컨테이너 경계를 넘어 호스트 커널에 영향을 줄 수 있다

 


어쩌다 9년간 아무도 몰랐나 — 코드 히스토리로 보는 원인

이 버그는 세 개의 독립적인 커밋이 시간차를 두고 겹치면서 만들어졌다. 각각은 커밋 시점에는 전혀 문제가 없었다.


연도 커밋 변경 내용 당시 영향
2011 a5079d084f8b authencesn 추가, ESN 재배열을 위해 dst를 스크래치로 사용 당시 유일한 호출자가 커널 내부 xfrm 계층뿐이라 무해
2015 104880a6b470 authencesn을 신규 AEAD 인터페이스로 전환, assoclen+cryptlen 오프셋 쓰기 도입 AF_ALG는 아직 입출력 분리 방식(out-of-place)이라 무해
2015 (algif_aead 최초 도입) AF_ALG에 AEAD + splice 지원 추가 페이지 캐시가 크립토 경로에 들어올 수 있게 됨
2017 72548b093ee3 성능을 위해 algif_aead를 인플레이스로 전환, req->src = req->dst 여기서 위 세 가지가 겹쳐지며 취약점 완성

즉 authencesn의 "경계 밖 쓰기" 습관, splice의 "페이지 캐시 참조 전달", algif_aead의 "인플레이스 처리"라는 세 조각이 각자 합리적인 이유로 추가됐지만, 아무도 이 셋의 조합이 만드는 결과까지는 검토하지 않았다.


패치 분석: 다시 out-of-place로

패치 커밋 a664bf3d603d는 2017년의 인플레이스 최적화를 사실상 되돌린다.

 

패치 후에는 req->src가 TX SGL(페이지 캐시가 들어올 수 있는 쪽), req->dst가 RX SGL(유저 버퍼)로 완전히 분리된다. AAD만 src에서 dst로 복사되고, 태그 페이지를 dst 체인에 참조로 엮던 sg_chain() 로직 자체가 제거됐다. 이제 authencesn이 아무리 assoclen + cryptlen 위치에 스크래치 쓰기를 해도, 그 자리에는 더 이상 페이지 캐시가 아니라 유저 버퍼만 존재한다.

 

커밋 메시지도 이유를 명확히 밝힌다: "algif_aead에서 인플레이스로 동작할 이유가 없다. 어차피 source와 destination은 서로 다른 메모리 매핑에서 온다." 애초에 이 최적화가 실질적인 이득 없이 위험만 키운 설계였다는 뜻이다.


완화 방법 (패치 적용 전 임시 조치)

커널 패치를 즉시 적용할 수 없는 환경이라면, algif_aead 모듈 자체를 막아 공격 표면을 없앨 수 있다.

 

컨테이너/멀티테넌트 환경이라면 seccomp 프로파일로 AF_ALG 소켓 생성(socket(AF_ALG, ...)) 자체를 차단하는 것도 유효한 방어선이다.

반응형