XFRM ESP-in-TCP SKBFL_SHARED_FRAG privesc (CVE-2026-46300)¶
"Fragnesia":
skb_try_coalesce()içinde düşürülen birSKBFL_SHARED_FRAGmarker'ı, ESP-in-TCP'nin page-cache-backed fragment'lar üzerinde in-place decrypt yapmasına izin veriyor; bu da unprivileged bir local user'a read-only dosyaların page cache'ine arbitrary write veriyor (ör./usr/bin/su).
Mechanism¶
Invariant: shared frag'lar asla in-place decrypt edilmemeli
IPsec ESP input path'i, bir skb'nin data'sını kopyalamaktan (skb_cow_data())
yalnızca buffer'ın page'lerini doğrudan mutate etmek güvenli olduğunda
kaçınabilir. Bu güvenlik kararı skb_shared_info içindeki SKBFL_SHARED_FRAG
flag'ine dayanır; bu flag, page fragment'ları externally owned olan bir
skb'yi işaretler — örneğin normal bir dosyadan splice edilen file-cache
page'leri. skb_has_shared_frag() böyle bir frag olup olmadığını bildirir;
true dönerse ESP, cleartext başka bir subsystem'in sahibi olduğu bir page'e
asla düşmesin diye decrypt etmeden önce data'yı duplicate etmek zorundadır.
Bug şu: skb_try_coalesce() — TCP'nin receive-side fragment merger'ı —
frag'ları bir skb'den diğerine taşır ama ortaya çıkan buffer hâlâ
page-cache-backed fragment'lar içeriyorken bile SKBFL_SHARED_FRAG
marker'ını düşürür. Coalescing'den sonra skb_has_shared_frag(), aslında
shared file-cache page'lerine işaret eden bir skb için false döner. ESP-in-TCP
o zaman skb_cow_data()'yı atlar ve AES-GCM decryption'ını in-place çalıştırır,
keystream'i doğrudan page cache'e XOR'lar. Attacker ESP key'ini sağladığı için
(xfrm state onun kurduğu bir state'tir), "decryption" aslında kontrollü bir
write'tır: keystream XOR ciphertext = seçilen plaintext, record başına ~192 byte.
Sonuç, read-only bir dosyanın page cache'ine güvenilir, race-free bir arbitrary
write'tır. On-disk byte'lar dokunulmadan kalır, ama execve()'in kullandığı
in-memory kopya modifiye edilir — cache'te /usr/bin/su'yu patch'lemek bir
sonraki çağrıda root verir.
Not: "page-level UAF" bu bug için dolaşan bir alias'tır ama yanıltıcıdır — kök
primitive bir UAF değil, yukarıda tarif edilen deterministik page-cache write'tır.
Aday fix, daha önceki Dirty Frag remediation'ını (f4c50a4034e6) açıkça anar;
yani o hardening bu latent path'i açığa çıkardı ve Dirty Frag patch'leri Fragnesia'yı
düzeltmez.
Walkthrough¶
Sadece yetkili test
Aşağıdaki adımlar, disclosure ve vendor FAQ'larından gelen public, kavramsal reproduction path'idir. Sadece sahibi olduğun ya da test etmeye yetkili olduğun sistemlerde, ideal olarak unpatched bir kernel üzerinde disposable bir VM'de çalıştır.
Public PoC (William Bowling, V12 Security) dört iyi bilinen building block'u zincirler. Üst seviyede:
- Host privilege olmadan
CAP_NET_ADMINelde et — çoğu distro'da default olarak açık olan unprivileged namespace'ler üzerinden:
/* CAP_NET_ADMIN inside the new netns is enough to drive NETLINK_XFRM */
unshare(CLONE_NEWUSER | CLONE_NEWNET);
- Transport-mode bir ESP-in-TCP security association kur —
NETLINK_XFRMüzerinden, attacker'ın seçtiği bir key ile AES-GCM kullanarak (böylece keystream — ve dolayısıyla in-place write — tamamen kontrol edilebilir). ESP-in-TCP, TCP encapsulation (ULP) kullanır, dolayısıyla SA bir encap config taşır.
# conceptual equivalent of the netlink state the PoC builds
ip xfrm state add src <a> dst <b> proto esp spi 0x... \
mode transport \
aead 'rfc4106(gcm(aes))' 0x<known-key> 128 \
encap espintcp <sport> <dport> 0.0.0.0
- Target page'i cache'te shared frag olarak hazırla. Read-only privileged bir
binary (PoC
/usr/bin/sukullanır) page cache'e okunur, sonra file-backed page'lerisplice()ile bir TCP stream'e kuyruğa alınır. Normal bir dosyadan bir socket'e splice etmek, tam olarakSKBFL_SHARED_FRAGfrag'larını üreten şeydir.
int f = open("/usr/bin/su", O_RDONLY);
/* splice file pages -> pipe -> TCP socket carrying the ESP-in-TCP stream */
splice(f, &off, pipe_w, NULL, len, SPLICE_F_MOVE);
splice(pipe_r, NULL, tcp_sock, NULL, len, SPLICE_F_MOVE);
- Coalescing + in-place decrypt'i tetikle. Özel hazırlanmış TCP segment'leri,
skb_try_coalesce()'in shared frag'ları artıkSKBFL_SHARED_FRAG'i temizlenmiş bir skb'ye merge etmesine yol açar. ESP inputskb_has_shared_frag() == falsegörür,skb_cow_data()'yı atlar ve attacker-keyed ESP payload'ını in-place decrypt eder —/usr/bin/su'nun page-cache byte'larını seçilen içerikle overwrite eder. Write primitive'i, record başına "AES-GCM keystream üzerinden 192-byte XOR" olarak tarif edilir.
Beklenen sonuç
$ ls -l /usr/bin/su # on-disk file unchanged, still root:root 4755
-rwsr-xr-x 1 root root 71912 ... /usr/bin/su
$ ./fragnesia # patches the cached copy
[*] CAP_NET_ADMIN via userns+netns
[*] ESP-in-TCP SA installed (aes-gcm, known key)
[*] splicing /usr/bin/su pages into TCP stream
[*] coalesce -> SHARED_FRAG dropped -> in-place decrypt
[+] page cache of /usr/bin/su patched
$ su # runs the modified cached image
# id
uid=0(root) gid=0(root) ...
Detection¶
- Syscall chain üzerinde behavioral telemetry:
unshare(CLONE_NEWUSER | CLONE_NEWNET)ardındanNETLINK_XFRMSA-install operasyonları. - Normal bir dosyadan bir TCP socket'e
splice(), hemen ardından bir ULP (espintcp) değişikliği, özellikle tight loop'larda. - Tight loop'larda AF_ALG / crypto interface kullanımı.
- Privileged binary'ler için page-cache ve on-disk arasında divergence: cached
page'leri on-disk içeriğinden farklı olan bir dosya (legitimate bir writer
olmadan) güçlü bir göstergedir —
posix_fadvise(POSIX_FADV_DONTNEED)cache drop'larıyla periyodik re-read, tampering'i ortaya çıkarabilir.
Mitigation¶
- Patch: merge edilen buffer hâlâ shared frag'lar tuttuğunda
skb_try_coalesce()'inSKBFL_SHARED_FRAGmarker'ını propagate etmesini sağlayan kernel fix'ini uygula. Dikkat: daha önceki "Dirty Frag" patch'leri Fragnesia'yı düzeltmez — kendi patch'ine ihtiyacı var. - IPsec kullanılmıyorsa ESP modüllerini blacklist'le:
printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' \
> /etc/modprobe.d/fragnesia.conf
rmmod esp4 esp6 rxrpc 2>/dev/null || true
- Unprivileged namespace'leri kısıtla,
CAP_NET_ADMINfoothold'unu ortadan kaldırarak:
Bu, Dirty Frag page-cache write'in in-TCP kardeşidir; aynı controlled-write-to-page-cache pivot'u Dirty Pipe page-cache write'in de temelini oluşturur.