[하루한줄] CVE-2026-31431 : 리눅스 커널에서의 Copy Fail 취약점으로 인한 커널 LPE

URL

https://copy.fail/

Target

2017년 인플레이스 최적화 도입 이후부터 2026년 4월 패치 이전까지의 메인라인 커널 및 이를 사용하는 모든 주요 리눅스 배포판 (6.12, 6.17, 6.18 커널 라인 포함)

  • 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 및 이하 버전

Explain

Background

리눅스 커널은 암호화 및 무결성 검증을 동시 수행하는 AEAD 알고리즘 모듈을 포함하고 있습니다. 이 중 일반 암호와 인증 알고리즘을 조합해 주는 헬퍼 템플릿인 authencesn 모듈(IPsec 패킷 처리용)이 존재합니다. 리눅스는 AF_ALG라는 특수한 소켓 인터페이스를 통해 이러한 커널 내부 암호화 모듈을 일반 사용자 프로세스에도 노출하고 있어, 권한이 없는 일반 사용자도 socket()을 통해 이 모듈에 입력을 주입할 수 있습니다.

Root Cause

본 취약점은 레이스 컨디션이나 타이밍, 오프셋 릭이 필요 없는 명확한 직선형 논리 결함(Straight-line Logic Flaw)입니다. 취약점은 세 가지 독립적인 커널 메커니즘의 결합으로 완성되었습니다.

  1. 2011년 (authencesn 도입 → 4bytes OOB Write):

    IPsec ESN(확장 시퀀스 번호) 지원을 위해 도입된 authencesn 모듈은 내부 HMAC 계산을 위해 64비트 시퀀스 번호의 상위/하위 바이트를 재정렬하는 과정이 필요합니다. 이때 커널은 호출자의 목적지 버퍼(dst)를 임시 스크래치 패드로 활용하는데, 내부 crypto_authenc_esn_decrypt() 함수 실행 중 세 번째 복사 연산인 scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1)가 트리거됩니다. 이 코드는 정상적인 AEAD 목적지 경계선(정상 데이터 및 인증 태그 영역)을 넘어, assoclen + cryptlen 오프셋 위치(즉, 버퍼의 OOB 영역)에 하위 시퀀스 번호(seqno_lo)에 해당하는 4바이트 데이터를 강제로 덮어쓰는 독특한 Out-Of-Bound Write 속성을 가지고 있었습니다. 연산의 성공/실패 여부와 무관하게 이 4바이트는 원본으로 복구되지 않고 그대로 잔존합니다.

  2. 2015년 (AF_ALGsplice 도입):

    splice() 시스템 콜을 통해 파일 데이터를 복사 없이 파이프를 거쳐 AF_ALG 소켓으로 전달할 수 있게 되면서, 타겟 파일의 실제 읽기 전용 페이지 캐시 물리 페이지 주소가 암호화 입력 분산리스트(src scatterlist)에 직접 매핑되는 경로가 생성되었습니다.

  3. 2017년 (인플레이스 최적화 - 최종 트리거):

    algif_aead.c에 도입된 최적화(commit: 72548b093ee3)로 인해 복호화 과정이 인플레이스(In-place)로 처리되기 시작했습니다. algif_aead는 입력을 다음과 같이 취급합니다.

    [AAD][Ciphertext][Authentication Tag]

    이때 커널은 AAD와 ciphertext 데이터 영역은 정상적인 유저 버퍼로 복사하지만, 마지막 인증 태그 영역은 메모리 복사 비용을 아끼기 위해 복사하지 않고 sg_chain()을 통해 원래의 입력 scatterlist(즉, splice로 들어온 타겟 파일의 읽기 전용 페이지 캐시 영역)를 writable 목적지 scatterlist(dst)의 맨 끝에 그대로 연결한 뒤 req->src = req->dst로 바인딩해 버렸습니다.

결과적으로 사용자가 AF_ALG 소켓과 splice()를 이용해 읽기 권한만 있는 임의의 파일(e.g, su)을 밀어 넣으면, 유저 수신 버퍼 바로 뒤에 타겟 파일의 페이지 캐시가 writable 분산리스트 체인으로 엮이게 됩니다. 이 상태에서 복호화를 트리거하면, authencesn 모듈의 고질적인 버그였던 바운더리 너머 4바이트 OOB Write 연산(assoclen + cryptlen 위치)이 유저 버퍼 공간을 뚫고 나와 바로 뒤에 바인딩된 타겟 파일의 실제 페이지 캐시 물리 주소를 덮어쓰는 치명적인 데이터 오염을 유발하게 됩니다.

PoC

VFS(Virtual File System)는 사용자의 파일 읽기·쓰기 요청을 실제 파일 시스템(ext4, XFS 등)으로 전달하는 리눅스 커널의 파일 시스템 추상화 계층입니다.

일반적인 파일 읽기 흐름에서는(ref. https://blog.naver.com/pjt3591oo/224272201239)

read("/usr/bin/su") -> VFS -> page cache hit -> 유저 공간 복사

순서로 명령이 실행됩니다.

하지만, Copy Fail의 공격 실행 흐름을 보면 앞서 말한 root cause로 인해 페이지 캐시 자체가 직접 오염됩니다.

  1. 타겟 파일의 페이지 캐시 생성

    /usr/bin/su가 한 번이라도 읽히면 바이너리의 코드 페이지(.text 영역을 포함한 파일 페이지)가 페이지 캐시에 적재

  2. scatterlist 바인딩

    splice()를 이용해 해당 페이지 캐시의 실제 파일 페이지를 AF_ALG의 scatterlist에 연결

  3. authencesn 모듈의 scratch write가 발생

    → 페이지 캐시의 해당 페이지에 4바이트 Overwrite

  4. 일반 권한의 사용자가 execve("/usr/bin/su") 실행

    → OS가 디스크가 아닌 오염된 page cache에서 그대로 바이너리 로드

  5. 메모리 상에서 변조된 su 코드 실행

    → setuid 비트에 의해 root 권한으로 프로세스 시작
    → 공격 코드에서 setreuid(0, 0) 호출로 UID/GID 0 획득
    execve("/bin/sh") 실행으로 root 셸 획득

정리하면, 일반적인 VFS 쓰기 경로를 거치지 않고 가상 메모리 매핑(kmap_local_page)을 통해 페이지 캐시를 직접 오염시키기 때문에, 커널의 쓰기 백 시스템이 작동하지 않아 페이지의 수정 여부를 나타내는 플래그인 더티 비트(Dirty Bit)가 0(Clean)으로 유지됩니다. 즉, 디스크 상의 파일 해시/무결성 검사 도구들은 오염을 전혀 감지하지 못합니다.

# 1. AF_ALG 소켓을 생성하고 authencesn 모듈에 바인딩
a = s.socket(38, 5, 0) # 38 = AF_ALG, 5 = SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))

# 2. sendmsg를 통해 컨트롤 헤더 및 AAD 주입 (bytes 4-7에 적힌 값이 4바이트 덮어쓰기 값인 seqno_lo가 됨)
u.sendmsg([b"A"*4 + payload_chunk], [cmsg_headers], MSG_MORE)

# 3. splice()를 통해 읽기 전용인 /usr/bin/su의 페이지 캐시 물리 주소를 소켓 버퍼(TX SGL -> RX SGL 끝단)로 결합
os.splice(target_fd, pipe_wr, offset)
os.splice(pipe_rd, alg_fd, offset)

# 4. recv를 호출하면 복호화가 트리거되며 authencesn의 스크래치 라이트가 실행됨
# scatterwalk가 유저 버퍼 경계를 넘어 Chained된 su의 페이지 캐시로 진입, 정해진 오프셋에 4바이트를 강제 기입함
# HMAC 검증은 실패하여 에러를 반환하지만 오염된 4바이트는 메모리에 그대로 잔존함
u.recv(...)

732 바이트의 파이썬 PoC는 루프를 돌며 이 과정을 반복하여 su 바이너리의 .text 영역 메모리를 setreuid(0,0)execve("/bin/sh")를 수행하는 160 바이트 크기의 자그마한 ELF 코드로 교체합니다. 이후 su를 실행하면 페이지 캐시의 오염된 코드가 UID 0(root) 권한으로 실행됩니다. 호스트와 컨테이너 간에 페이지 캐시가 공유되므로 이 메커니즘은 컨테이너 이스케이프 및 Kubernetes 노드 완전 장악 플러그인으로도 작동합니다.

Patch

1. 정식 커널 패치 분석 (commit: a664bf3d603d)
리눅스 커널 보안 팀은 복잡하게 얽힌 내부 상태 관리 문제를 해결하기 위해, 문제를 촉발했던 2017년의 algif_aead 인플레이스(in-place) 최적화 코드를 완전히 제거하고 이전의 아웃오브플레이스(out-of-place) 방식으로 롤백했습니다.

  • 패치 코드 변경점:

    // AS-IS: src와 dst가 동일한 scatterlist를 가리켜 페이지 캐시가 writable 영역에 노출됨
    aead_request_set_crypt(&areq->cra_u.aead_req, rsgl_src,
                           areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv);
    
    // TO-BE: src는 원래의 입력(TX SGL), dst는 별도의 유저 수신 버퍼(RX SGL)로 명확히 분리
    aead_request_set_crypt(&areq->cra_u.aead_req, tsgl_src,
                           areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv);
  • 패치 적용으로 인해 sg_chain()을 통해 페이지 캐시가 목적지 버퍼 끝에 붙는 메커니즘 자체가 완전 삭제되었으며, authencesn이 아무리 버퍼 외부 영역에 스크래치 라이트를 수행하더라도 그것은 오직 유저 공간 할당 버퍼 안에서만 맴돌 뿐 실제 페이지 캐시에는 도달할 수 없게 되었습니다.

2. 임시 완화 조치 (블랙리스트 및 모듈 제거)
즉시 패치 및 재부팅이 불가능한 프로덕션 환경의 경우, 공격의 진입점인 algif_aead 모듈을 비활성화합니다.

echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null
  • 영향도 검증: 해당 모듈을 차단해도 커널 내부 API를 직접 사용하는 dm-crypt(LUKS), kTLS, IPsec/XFRM, OpenSSL/SSH 등의 핵심 기능들은 AF_ALG 소켓 인터페이스를 거치지 않으므로 리눅스 시스템 운영에 아무런 지장을 주지 않습니다.

3. 컨테이너 인프라 가드 (Kubernetes 등)
신뢰할 수 없는 사용자의 코드가 실행되는 멀티테넌트 환경이나 CI/CD 빌드 팜 환경인 경우, 호스트 커널 패치와 더불어 컨테이너 보안 프로필(seccomp)을 통해 AF_ALG 소켓 생성 자체를 원천 차단하는 정책을 수립해야 합니다.

Reference

https://theori.io/ko/blog/copy-fail-page-cache

https://blog.naver.com/pjt3591oo/224272201239



본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.