Skip to content

XFRM ESP-in-TCP SKBFL_SHARED_FRAG privesc (CVE-2026-46300)

"Fragnesia": skb_try_coalesce() içinde düşürülen bir SKBFL_SHARED_FRAG marker'ı, 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:

  1. Host privilege olmadan CAP_NET_ADMIN elde 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);
  1. Transport-mode bir ESP-in-TCP security association kurNETLINK_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
  1. Target page'i cache'te shared frag olarak hazırla. Read-only privileged bir binary (PoC /usr/bin/su kullanır) page cache'e okunur, sonra file-backed page'leri splice() ile bir TCP stream'e kuyruğa alınır. Normal bir dosyadan bir socket'e splice etmek, tam olarak SKBFL_SHARED_FRAG frag'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);
  1. Coalescing + in-place decrypt'i tetikle. Özel hazırlanmış TCP segment'leri, skb_try_coalesce()'in shared frag'ları artık SKBFL_SHARED_FRAG'i temizlenmiş bir skb'ye merge etmesine yol açar. ESP input skb_has_shared_frag() == false gö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) ...
On-disk hiçbir modifikasyon olmaz, dolayısıyla diske karşı yapılan file-integrity check'leri geçer.

Detection

  • Syscall chain üzerinde behavioral telemetry: unshare(CLONE_NEWUSER | CLONE_NEWNET) ardından NETLINK_XFRM SA-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()'in SKBFL_SHARED_FRAG marker'ı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_ADMIN foothold'unu ortadan kaldırarak:
echo 'user.max_user_namespaces=0' > /etc/sysctl.d/fragnesia.conf
sysctl --system

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.

References