Home
System Hacking
🧨

Dirty Frag(CVE-2026-43284, CVE-2026-43500)란?

Type
CVE
날짜
2026/08/10
종류
Kernel
Linux
1 more property

여담

Copy Fail을 보고 나면 자연스럽게 다음 질문이 생긴다. “AF_ALG만 막으면 끝나는가?”, “splice로 들어온 page-cache page를 in-place crypto가 수정하는 다른 경로는 없는가?”
Dirty Frag는 그 질문에서 출발한 후속 vulnerability class다. Copy Fail이 AF_ALG TX scatterlist에서 page provenance를 잃었다면, Dirty Frag는 networking의 struct sk_buff.frags[]에서 같은 문제가 다시 나타난다.
이 글은 Dirty Frag를 구성하는 두 취약점, CVE-2026-43284 xfrm-ESP와 CVE-2026-43500 RxRPC를 하나의 본편으로 정리한다.
🔒
공개된 CVE, 연구자 write-up, upstream patch를 기반으로 한 분석 글이다. 실제 시스템에서는 PoC 실행보다 배포판 kernel update와 reboot가 우선이다.

읽는 순서

Copy Fail에서 이미 정리한 page cache와 splice/pipe는 다시 쓰지 않았다. 필요한 부분만 다음 순서로 이어 보면 된다.
기존 Copy Fail Background (4) splice()와 pipe Code by Code Dirty Frag Background (1) sk_buff와 paged frags Dirty Frag Background (2) XFRM, ESP와 ESN Dirty Frag Background (3) RxRPC, RXKAD와 fcrypt 본편 두 page-cache write와 patch
Plain Text
복사
Copy Fail의 authencesn sink가 기억나지 않으면 본편의 4-byte STORE 부분만 다시 보면 된다.

취약점 요약

구분
xfrm-ESP variant
RxRPC variant
CVE
CVE-2026-43284
CVE-2026-43500
취약 subsystem
esp4, esp6, IPv4/IPv6 datagram splice path
rxrpc, RXKAD packet verification
write sink
crypto_authenc_esn_decrypt()
rxkad_verify_packet_1()pcbc(fcrypt) decrypt
primitive
controlled 4-byte STORE
key-selected 8-byte STORE
namespace
일반적으로 user/net namespace와 namespace 내부 CAP_NET_ADMIN 필요
user namespace 불필요
주요 제약
unprivileged user namespace 정책
rxrpc.ko 제공·autoload 여부와 최종 PAM strategy
두 CVE의 공통 결과는 읽기만 가능한 file의 page-cache page를 networking receive path의 crypto destination으로 오인시켜 RAM에서 수정하는 것이다.
race condition이 아니라 straight-line logic bug이므로 timing window에 의존하지 않는다.

한 줄로 보는 공통 root cause

target file page cache P -> splice(file -> pipe) -> splice(pipe -> socket) -> MSG_SPLICE_PAGES -> sender skb frags[]가 P를 참조 -> loopback/network receive -> receiver가 shared frag provenance를 놓침 -> skb_to_sgvec() -> src == dst인 in-place crypto -> P에 STORE
Plain Text
복사
정상적인 zero-copy와 취약점의 경계는 page reference 전달 자체가 아니다.
핵심 invariant는 다음이다.
외부 source와 공유된 page는 packet을 수정하는 in-place path에 들어가기 전에 kernel-private copy로 분리되어야 한다.
Dirty Frag에서는 이 invariant가 SKBFL_SHARED_FRAG, skb_has_shared_frag(), skb_cow_data()/skb_unshare() 경계에서 깨졌다.

코드 읽는 지도

파일
역할
핵심 함수
fs/splice.c
pipe page를 bio_vec iterator로 만들어 socket send path에 넘기고 kernel 내부 MSG_SPLICE_PAGES를 설정한다.
splice_to_socket()
net/core/skbuff.c
iterator에서 page를 추출해 skb paged frag로 붙인다.
skb_splice_from_iter(), skb_append_pagefrags()
include/linux/skbuff.h
skb_frag_t, skb_shared_info, shared-frag flag와 ownership helper를 정의한다.
skb_has_shared_frag()
net/ipv4/ip_output.c, net/ipv6/ip6_output.c
UDP/IP datagram의 splice page를 skb에 붙인다. ESP patch가 shared flag 표시를 추가한 지점이다.
__ip_append_data(), __ip6_append_data()
net/ipv4/esp4.c, net/ipv6/esp6.c
ESP receive packet을 SGL로 바꾸고 in-place AEAD decrypt한다.
esp_input(), esp6_input()
crypto/authencesn.c
ESN high 32-bit를 재배치하며 4-byte scratch STORE를 수행한다.
crypto_authenc_esn_decrypt()
net/rxrpc/call_event.c, conn_event.c
RxRPC DATA/RESPONSE packet을 security ops로 넘기기 전에 unshare 여부를 결정한다.
rxrpc_input_call_event(), rxrpc_verify_response()
net/rxrpc/rxkad.c
RXKAD AUTH packet의 첫 8 bytes를 in-place decrypt한다.
rxkad_verify_packet_1()
crypto/fcrypt.c
RXKAD가 사용하는 AFS 전용 64-bit block cipher다.
fcrypt_decrypt() 계열

CVE-2026-43284: xfrm-ESP Page-Cache Write

Step 1. target file page를 UDP skb frag에 심는다

PoC는 ESP header와 IV를 userspace page로 pipe에 넣고, 이어서 target file의 일부를 같은 pipe에 splice한다. pipe 전체를 UDP send socket으로 splice하면 kernel이 MSG_SPLICE_PAGES 경로를 사용한다.
vmsplice(pipe_write, ESP_header_and_IV, ...); splice(target_fd, &target_off, pipe_write, NULL, 16, ...); splice(pipe_read, NULL, udp_send_socket, NULL, 24 + 16, ...);
C
복사
실제 sender skb는 다음 모양이 된다.
skb.linear ESP header + IV skb.frags[0] page = target file page-cache P offset = target file offset size = 16
Plain Text
복사
여기까지는 zero-copy send의 정상 동작이다.

Step 2. loopback packet을 ESP input으로 보낸다

수신 UDP socket에 UDP_ENCAP_ESPINUDP를 설정하면 packet은 일반 UDP payload로 queue되지 않고 XFRM receive path에 들어간다.
udp_rcv() -> xfrm4_udp_encap_rcv() -> xfrm_input() -> esp_input()
Plain Text
복사
PoC는 새 user/net namespace에서 attacker-controlled SA를 등록한다. SA의 algorithm은 authencesn(hmac(sha256),cbc(aes))이고 ESN을 활성화한다.

Step 3. esp_input이 COW를 건너뛴다

취약한 조건은 다음이다.
if (!skb_cloned(skb)) { if (!skb_is_nonlinear(skb)) { goto skip_cow; } else if (!skb_has_frag_list(skb)) { goto skip_cow; /* shared frags[]도 통과 */ } } skb_cow_data(skb, 0, &trailer);
C
복사
skb가 cloned되지 않았고 frag_list만 없으면 paged frags[]의 실제 owner를 확인하지 않는다. file page P가 그대로 보존된다.

Step 4. page P가 in-place AEAD dst가 된다

skb_to_sgvec(skb, sg, 0, skb->len); aead_request_set_crypt(req, sg, sg, elen + ivlen, iv); crypto_aead_decrypt(req);
C
복사
src == dst다. sg[] 뒤쪽 entry는 page P를 가리키므로 Crypto API가 수행하는 STORE도 P에 반영된다.

Step 5. authencesn이 controlled 4 bytes를 쓴다

crypto_authenc_esn_decrypt()은 authentication 전에 ESN high bits를 dst 끝으로 옮긴다.
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
C
복사
마지막 호출이 page P에 도달한다. 위치는 packet length로 맞추고, 값은 SA 등록 시 XFRMA_REPLAY_ESN_VAL.seq_hi에 넣은 32-bit 값으로 제어한다.
AEAD authentication이 뒤에서 -EBADMSG로 실패해도 이미 수행된 4-byte STORE는 rollback되지 않는다.

Step 6. 4-byte STORE를 LPE로 확장한다

공개 PoC는 /usr/bin/su의 첫 192 bytes를 48개의 4-byte chunk로 나눠 작은 ELF payload로 교체한다.
disk inode와 setuid bit는 그대로다. 이후 init user namespace의 parent process가 execve("/usr/bin/su")를 호출하면 kernel은 오염된 page-cache content를 매핑하고 setuid-root 권한으로 payload entry point를 실행한다.

xfrm-ESP patch

Mainline fix: f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4
패치는 양쪽 경계를 함께 고친다.
1.
IPv4/IPv6 datagram append path가 splice page를 skb에 붙일 때 SKBFL_SHARED_FRAG를 표시한다.
2.
esp_input()/esp6_input() fast path가 skb_has_shared_frag(skb)를 검사하고 shared라면 skb_cow_data()로 보낸다.
- } else if (!skb_has_frag_list(skb)) { + } else if (!skb_has_frag_list(skb) && + !skb_has_shared_frag(skb)) { goto skip_cow; }
Diff
복사
결과적으로 file page P는 crypto dst가 되지 않고 kernel-private copy가 decrypt된다.

CVE-2026-43500: RxRPC Page-Cache Write

Step 1. attacker key로 RXKAD connection을 만든다

PoC는 process keyring에 attacker-controlled RXKAD v1 token을 add_key("rxrpc", ...)로 등록한다.
가짜 UDP server가 CHALLENGE를 보내면 AF_RXRPC client kernel은 RESPONSE를 만들며 token의 session key K로 pcbc(fcrypt) security context를 초기화한다.
이 과정에는 user namespace나 CAP_NET_ADMIN이 필요하지 않다.

Step 2. target file page가 RxRPC DATA skb frag가 된다

forged DATA packet의 protocol header는 userspace buffer에서 만들고, security header로 처리될 8 bytes는 target file에서 splice한다.
loopback receive 후 skb frag가 target page-cache page P를 계속 가리키는 상태로 RxRPC input queue에 들어간다.

Step 3. cloned 검사만 통과하고 security ops에 들어간다

취약한 event path는 packet을 private copy로 만드는 조건을 skb_cloned(skb) 하나로 판단했다.
그러나 !skb_cloned(skb)인 skb도 externally-owned paged frag나 frag_list를 가질 수 있다. shared P가 그대로 RXKAD verifier에 전달된다.

Step 4. RXKAD가 P 위에서 8-byte decrypt를 수행한다

ret = skb_to_sgvec(skb, sg, sp->offset, 8); memset(&iv, 0, sizeof(iv)); skcipher_request_set_crypt(req, sg, sg, 8, iv.x); crypto_skcipher_decrypt(req);
C
복사
single block, IV=0이므로 page에 쓰이는 값은 개념적으로 다음과 같다.
plaintext P8 = fcrypt_decrypt(ciphertext C8, attacker key K)
Plain Text
복사
ESP처럼 원하는 bytes를 직접 넣는 구조는 아니다. 대신 userspace에서 K를 바꿔가며 원하는 plaintext pattern이 나오는 key를 찾는다.

Step 5. overlapping STORE로 /etc/passwd를 바꾼다

공개 PoC는 offset 4, 6, 8에 8-byte write를 겹쳐 적용한다. 각 다음 brute-force는 직전 write가 바꿔 놓은 ciphertext를 반영해야 한다.
root:x:0:0:root:... | +-- overlapping 8-byte STORE x 3 root::0:0:GGGGGG:...
Plain Text
복사
root password field가 비고 PAM nullok가 허용되는 환경에서는 su -가 password 없이 root shell로 이어질 수 있다.
여기서 8-byte page-cache write primitive는 kernel bug의 결과이고, 빈 password를 이용하는 su/PAM chain은 배포판 정책에 의존하는 LPE strategy다.

RxRPC patch

Mainline fix: aa54b1d27fe0c2b78e664a34fd0fdf7cd1960d71
패치는 DATA와 RESPONSE handler 모두에서 unshare 조건을 확장한다.
- if (skb_cloned(skb)) { + if (skb_cloned(skb) || + skb_has_frag_list(skb) || + skb_has_shared_frag(skb)) { /* make a private packet before in-place decrypt */ }
Diff
복사
kernel-private NIC/GRO frags의 zero-copy fast path는 유지하면서, splice/frag_list처럼 외부 ownership이 있는 page만 copy 대상으로 보낸다.
ℹ️
초기 연구자 문서는 PoC가 실용적으로 도달한 RxRPC 범위를 2dc334f1a63a부터 설명했다. 현재 kernel.org CVE record와 fix commit은 d0d5c0cd1e71을 Fixes 기준으로 사용한다. 운영 영향 범위는 mainline 숫자만 추측하지 말고 배포판 advisory와 backport 상태를 기준으로 확인해야 한다.

왜 두 취약점을 chain이라고 부르는가

여기서 chain은 두 bug를 한 process에서 연속 실행해야만 root가 된다는 의미라기보다, 배포판별 blind spot을 서로 보완하는 exploit package라는 의미에 가깝다.
환경
ESP
RxRPC
unprivileged user namespace 허용
도달 가능
module이 있으면 도달 가능
Ubuntu AppArmor가 userns 제한
일반적으로 차단
userns가 필요 없어 우회 경로가 될 수 있음
rxrpc.ko 미제공
ESP가 대안
도달 불가
즉 하나의 kernel에서 두 primitive를 반드시 합성하는 것이 아니라, xfrm-ESP와 RxRPC 중 해당 배포판에서 열려 있는 경로를 선택한다.

Copy Fail, Dirty Pipe와 비교

취약점
page-cache page가 들어가는 곳
잘못된 write
Dirty Pipe
pipe_buffer.page
stale PIPE_BUF_FLAG_CAN_MERGE로 pipe write가 file page에 append
Copy Fail
AF_ALG TX SGL
취약한 AEAD src/dst 구성과 authencesn 4-byte scratch STORE
Dirty Frag ESP
skb_shared_info.frags[]
COW를 건너뛴 ESP in-place AEAD와 같은 authencesn STORE
Dirty Frag RxRPC
skb_shared_info.frags[]
RXKAD pcbc(fcrypt) in-place 8-byte decrypt
Copy Fail의 algif_aead module을 blacklist해도 Dirty Frag는 막히지 않는다. ESP variant는 AF_ALG가 아니라 XFRM receive path에서 같은 authencesn sink에 도달하고, RxRPC variant는 별도 skcipher sink를 사용한다.

후속 변종: Fragnesia

CVE-2026-46300 Fragnesia는 Dirty Frag와 같은 class에 속하지만, 위 두 CVE의 일부나 단순 별칭은 아니다.
Fragnesia는 ESP-in-TCP ULP와 skb coalescing 경계에서 shared-frag 표시가 보존되지 않는 별도 logic bug다. file page가 TCP receive queue에 들어간 뒤 socket이 espintcp ULP로 전환되면, queued page를 ESP ciphertext로 보고 AES-GCM decrypt가 page cache 위에서 수행된다.
공개 PoC는 IV nonce를 조절해 원하는 keystream byte를 고르고 1-byte primitive를 반복해 /usr/bin/su의 page cache를 바꾼다.
따라서 학습 순서는 다음이 좋다.
Dirty Frag 1. shared skb frag provenance 2. ESP/RxRPC in-place decrypt 3. SKBFL_SHARED_FRAG patch Fragnesia 4. skb coalescing 과정에서 shared marker가 어떻게 유실되는가 5. ESP-in-TCP ULP가 queued skb를 어떻게 소비하는가
Plain Text
복사
Fragnesia는 이 글에서 경계만 잡고, 실제 skb_try_coalesce()/espintcp 흐름은 별도 분석 대상으로 남긴다.

Mitigation

가장 확실한 대응

배포판이 제공하는 보안 kernel로 update하고 새 kernel로 reboot한다. mainline commit 포함 여부만으로 vendor kernel 상태를 단정하면 안 된다. 배포판은 서로 다른 stable branch에 backport하기 때문이다.

임시 attack-surface reduction

사용하지 않는 환경에서만 다음 module의 load를 제한하는 방안을 검토할 수 있다.
esp4, esp6: xfrm-ESP와 Fragnesia 경로
rxrpc: RxRPC 경로
이 조치는 IPsec VPN이나 AFS/RxRPC workload를 깨뜨릴 수 있으므로 운영 의존성을 먼저 확인해야 한다.

부분 mitigation의 한계

algif_aead blacklist: Copy Fail만 줄이고 Dirty Frag에는 충분하지 않다.
unprivileged user namespace 제한: ESP variant의 진입 조건은 줄이지만 RxRPC variant는 남는다.
read-only mount, immutable bit, file permission: 일반 file write가 아니라 kernel page-cache STORE이므로 근본 방어가 아니다.

이미 page cache가 오염된 경우

공개 PoC 실행 후에는 target executable이나 /etc/passwd의 in-memory view가 계속 변조된 상태일 수 있다. 분석 VM에서는 reboot하거나 root 권한으로 page cache를 drop해 정리한다. host나 운영 시스템에서 PoC를 실행하면 안 된다.

GDB에서 볼 지점

환경을 붙일 때는 다음 순서로 breakpoint를 잡으면 data provenance가 보인다.
공통 splice_to_socket skb_splice_from_iter skb_append_pagefrags skb_to_sgvec ESP __ip_append_data esp_input esp_input_set_header crypto_authenc_esn_decrypt scatterwalk_map_and_copy RxRPC rxrpc_input_call_event rxrpc_verify_response rxkad_verify_packet_1 crypto_skcipher_decrypt
Plain Text
복사
확인할 값은 다음이다.
skb_shinfo(skb)->flagsnr_frags
skb_cloned(skb), skb_has_frag_list(skb), skb_has_shared_frag(skb)
skb_frag_page(&skb_shinfo(skb)->frags[i])
sg_page(&sg[i])
crypto request의 src/dst SGL pointer identity
ESP의 XFRM_SKB_CB(skb)->seq.input.hi
RxRPC의 sp->offset와 8-byte ciphertext
목표는 crash가 아니라 target file page P가 pipe, skb frag, SGL을 거쳐 동일한 struct page로 유지되는 것을 증명하는 것이다.

정리

Dirty Frag는 “network crypto가 위험하다”는 취약점이 아니다. zero-copy로 외부 page를 skb frag에 연결한 뒤 그 provenance를 놓치고, kernel-private packet이라는 잘못된 전제 아래 in-place decrypt를 수행한 ownership bug다.
핵심 흐름은 다음 한 줄로 기억하면 된다.
file page cache -> pipe -> skb frag -> SGL -> in-place crypto -> page-cache STORE
Plain Text
복사
xfrm-ESP는 Copy Fail과 같은 authencesn 4-byte sink를 재사용하고, RxRPC는 pcbc(fcrypt) 8-byte decrypt로 별도 primitive를 만든다. 두 경로는 배포판별 namespace/module 차이를 서로 보완한다.

Reference