摘要
報告針對 Dirty Frag 漏洞提供深入的技術分析,這是一類影響 Linux 發行版的權限提升漏洞。Dirty Frag 利用兩個不同的 page-cache 寫入漏洞串連而成:xfrm-ESP Page-Cache 寫入 與 RxRPC Page-Cache 寫入。這些漏洞讓無特權的使用者可以透過確定性地修改敏感檔案(如
/etc/passwd
或
/usr/bin/su
)的 page-cache 表項,來取得 root 權限。報告詳細說明了根本原因、攻擊機制以及緩解策略,並與 Dirty Pipe、Copy Fail 等類似漏洞進行對照。特別關注的核心程式碼路徑解釋了這些攻擊如何被實現,以及後續的修補程式如何解決它們。
1. 簡介
Linux kernel 雖然具備強健的安全機制,但有時仍會潛藏導致嚴重安全事件的漏洞。Dirty Frag 漏洞正是此類關鍵缺陷的代表,它在多種 Linux 發行版上造成本機權限提升 (LPE)。該漏洞由 Hyunwoo Kim (@v4bel) [1] 發現並回報,其特別棘手之處在於能繞過針對類似漏洞的既有緩解措施,因而讓廣泛的系統暴露於風險中。這份報告目的在剖析 Dirty Frag 的技術細節,聚焦於其底層機制、攻擊鏈,以及 kernel 層級用以反制此漏洞的修補程式。
2. 背景:理解 Page-Cache 寫入漏洞
Dirty Frag 與其他著名 Linux kernel 漏洞(即 Dirty Pipe (CVE-2022-0847) 和 Copy Fail (CVE-2026-31431))在概念上有相似之處。Dirty Pipe 利用 pipe buffer 機制的缺陷來覆寫任意檔案,而 Copy Fail 則針對
struct pipe_buffer
,但 Dirty Frag 特別操縱了
struct sk_buff
中的
frag
[1]
。這些漏洞都源於 kernel 的 zero-copy 發送路徑(通常涉及
splice()
)在無意中讓攻擊者取得對應為唯讀的 page-cache pages 的寫入存取權。
在 Copy Fail 的情境中,攻擊者使用
splice(file -> pipe -> AF_ALG_fd)
將攻擊者控制的 page-cache page 注入傳輸的 scatter-gather 清單 (TX SGL)。在
recv()
操作期間,作為位元組重排的一部分,會發生一個 scratch 寫入。通常,這個寫入會針對 skb 的 tag 區域,不會構成威脅。然而,當攻擊者固定的 page 剛好位於此位置時,完整性假設就會被違反,導致對 page-cache 進行任意的 4 位元組寫入
[1]
[2]
。
Dirty Frag 擴展了此模式,在來自
splice()
的非線性
skb
的
frag
上重現了相同的漏洞。這類漏洞特別危險,因為修改發生在 RAM 中,影響檔案在記憶體中的表示方式,而不會改變其在磁碟上的對應內容。這意味著後續讀取受影響的檔案將會反映攻擊者的修改,即使磁碟本身保持不變
[1]
[2]
。
3. Dirty Frag 的技術分析
Dirty Frag 是一個複合式漏洞,串連了兩種不同的 page-cache 寫入缺陷:xfrm-ESP Page-Cache 寫入 與 RxRPC Page-Cache 寫入。兩者都利用了 kernel 在處理網路封包緩衝區 (
sk_buff
) 及其相關片段 (
frag
) 於加密操作時的行為。
3.1. CVE-2026-43284:xfrm-ESP Page-Cache 寫入
xfrm-ESP Page-Cache 寫入漏洞發生在
esp_input()
函式中,該函式負責處理 IPsec Encapsulating Security Payload (ESP) 封包。核心問題在於對
skb_cow_data()
的呼叫被繞過。理想情況下,在對非線性
skb
執行 in-place AEAD 解密之前,
esp_input()
應該配置一個新的 kernel 私有緩衝區並複製 fragment 資料,以防止對共享的 page-cache pages 進行非預期的修改。然而,
esp_input()
中的特定分支允許這個關鍵的 copy-on-write (COW) 機制被略過
[1]
。
請參考以下來自
esp_input()
[1]
的簡化程式路徑:
- static int esp_input(struct xfrm_state *x, struct sk_buff *skb)
- {
- [...] // Other code
- if (!skb_cloned(skb)) {
- if (!skb_is_nonlinear(skb)) { // <=[1]
- nfrags = 1;
- goto skip_cow;
- } else if (!skb_has_frag_list(skb)) {
- nfrags = skb_shinfo(skb)->nr_frags;
- nfrags++;
- goto skip_cow; // <=[2]
- }
- }
- err = skb_cow_data(skb, 0, &trailer);
- [...] // Other code
- }
在標籤
[1]
處,如果
skb
不是非線性,或者在標籤
[2]
處,如果它是非線性但缺少
frag_list
,程式碼會直接跳到
skip_cow
。這會繞過
skb_cow_data()
的呼叫,這代表如果攻擊者先前已透過
splice()
將 page-cache page 固定到
frag
中,那麼該 page 就會成為後續 in-place 加密操作的來源與目的地
[1]
。
實際的寫入發生在
crypto_authenc_esn_decrypt()
函式中,具體來說是一個 4 位元組的儲存操作。這個函式用於帶有 Extended Sequence Numbers (ESN) 的 ESP,它執行一個前處理步驟,將序號的高位 4 個位元組移動到來源 SGL 的末端。有漏洞的部分如下所示
[1]
:
static int crypto_authenc_esn_decrypt(struct aead_request *req)
{
[...] // Other code
/* Move high-order bits of sequence number to the end. */
scatterwalk_map_and_copy(tmp, src, 0, 8, 0);
if (src == dst) {
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // <=[3]
dst = scatterwalk_ffwd(areq_ctx->dst, dst, 4);
[...] // Other code
}
在標籤
[3]
處的 4 位元組儲存發生在目的地 SGL 的
assoclen + cryptlen
位置。透過精心建構 payload 長度,攻擊者可以確保先前經由
splice()
植入的 page (P) 正好佔據這個位置。寫入的值是來自 ESP 標頭序號的高 32 個位元,使用者可以在 SA 註冊期間透過
XFRMA_REPLAY_ESN_VAL
netlink 屬性控制該值
[1]
。
關鍵的是,這個寫入發生在 AEAD 身份驗證驗證之前。即使身份驗證失敗,page-cache 的修改仍然會持續存在,這讓攻擊者可以在不知道 SA 驗證金鑰的情況下修改敏感檔案。雖然註冊 XFRM SA 需要
CAP_NET_ADMIN
權限,但可以透過使用
unshare(CLONE_NEWUSER | CLONE_NEWNET)
將子程序隔離在一個新的 user/net namespace 中來達成此權限
[1]
。
3.2. RxRPC Page-Cache 寫入
Dirty Frag 的第二個部分涉及 RxRPC Page-Cache 寫入漏洞。在此情境中,
rxkad_verify_packet_1()
函式使用
pcbc(fcrypt)
直接在 fragment 之上執行 in-place 單區塊解密。與 xfrm-ESP 漏洞類似,這個操作會修改無特權使用者具有讀取存取權的 page-cache page,導致持續性的記憶體內變更
[1]
。
4. 攻擊流程與影響
Dirty Frag 的攻擊程式鎖定
/usr/bin/su
等敏感檔案以取得 root 權限。攻擊者會將
/usr/bin/su
的 page-cache 前 192 個位元組替換為一個靜態的 root-shell ELF。這個 ELF 被設計成在執行時會執行
setgid(0); setuid(0); setgroups(0,NULL); execve("/bin/sh", NULL, ["TERM=xterm",NULL])
,從而繞過 PAM 並獲得 root shell
[1]
。
這 192 位元組的 ELF 被分成 48 個 4 位元組的區塊寫入,利用了 ESP 變體中任意的 4 位元組 STORE 能力。攻擊步驟如下 [1] :
-
Namespace 隔離:
使用
unshare(CLONE_NEWUSER | CLONE_NEWNET)在新的 user/net namespace 中取得CAP_NET_ADMIN權限。 -
XFRM SA 註冊:
註冊 48 個 XFRM SA,每個都具有唯一的 SPI,並將 4 位元組的 shellcode 區塊嵌入到
XFRMA_REPLAY_ESN_VAL.seq_hi中。 -
觸發寫入:
對於每個區塊,會建立一對
sk_recv和sk_sendsocket。一個偽造的 ESP 線路標頭被 vmsplice 到一個 pipe 中,接著是從目標檔案(例如/usr/bin/su)偏移量i*4處讀取的 16 個位元組。然後將這個 pipe 內容 splice 到sk_send,確保/usr/bin/su的 page-cache page 被植入到發送端skb的frag[0]中。
接收端透過
udp_rcv -> xfrm4_udp_encap_rcv -> xfrm_input -> esp_input
處理
skb
。由於
skb_cow_data()
被繞過,page-cache page 被保留下來,而
crypto_authenc_esn_decrypt()
會執行 4 位元組的儲存,從而在精確的偏移量修改 page-cache。這個過程是確定性的,不需要鎖步 (lock-stepping)。一旦修改完成,page-cache 會一直持續到系統重新啟動或明確清除快取為止。當父程序執行
/usr/bin/su
時,修改後的 ELF 會被對應到記憶體中,導致 root 權限提升
[1]
。
圖 1:Dirty Frag (xfrm-ESP 變體) 的簡化攻擊流程
5. 緩解措施與修補程式分析
針對 Dirty Frag 的緩解措施主要集中在防止 page-cache pages(尤其是那些透過
splice()
固定的頁面)被直接用作 in-place 加密操作中的可寫入目的地。修補程式引入了
SKBFL_SHARED_FRAG
標記。這個標記會設定在那些源自 IPv4/IPv6 datagram 附加路徑中
splice()
的 page fragments 上
[1]
。
esp_input()
函式(及其 IPv6 對應函式
esp6_input()
)中的關鍵變更是增加對這個新標記的檢查。修改後的程式碼確保如果一個
skb
具有共享的 fragment(即
skb_has_shared_frag(skb)
回傳 true),則無論其他條件如何,它都會被強制路由到
skb_cow_data()
路徑。這會強制將資料複製到一個私有緩衝區,然後再執行任何 in-place 操作,從而防止對原始 page-cache 的修改
[1]
。
以下來自
net/ipv4/esp4.c
的修補程式片段說明了這一點
[1]
:
diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c index 6dfc0bcde..6a5febbdb 100644 --- a/net/ipv4/esp4.c +++ b/net/ipv4/esp4.c @@ -873,7 +873,8 @@ static int esp_input(struct xfrm_state *x, struct sk_buff *skb) nfrags = 1; goto skip_cow; - } else if (!skb_has_frag_list(skb)) { + } else if (!skb_has_frag_list(skb) && + !skb_has_shared_frag(skb)) { nfrags = skb_shinfo(skb)->nr_frags; nfrags++;
在條件判斷式中加入
&& !skb_has_shared_frag(skb)
確保如果
skb
包含共享的 fragment,則不再走
skip_cow
路徑。這有效地關閉了漏洞,強制對可能共享的 page-cache pages 實施 copy-on-write 行為
[1]
。
類似地,
net/ipv4/ip_output.c
(和
net/ipv6/ip6_output.c
)中的修改確保當資料透過
splice()
附加時,特別是在未指定
MSG_NO_SHARED_FRAGS
的情況下,能正確設定
SKBFL_SHARED_FRAG
標記
[1]
:
diff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c index e4790cc7b..5bcd73cbd 100644 --- a/net/ipv4/ip_output.c +++ b/net/ipv4/ip_output.c @@ -1233,6 +1233,8 @@ static int __ip_append_data(struct sock *sk, if (err < 0) goto error; copy = err; + if (!(flags & MSG_NO_SHARED_FRAGS)) + skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG; wmem_alloc_delta += copy; } else if (!zc) { int i = skb_shinfo(skb)->nr_frags;
這種全面的方法確保任何可能被攻擊者透過
splice()
操縱的 page-cache page 在加密操作之前被正確隔離,從而防止構成 Dirty Frag 漏洞的 in-place 寫入。
6. 結論
Dirty Frag 漏洞凸顯了保護複雜 kernel 子系統(尤其是涉及 zero-copy 資料路徑和加密操作的子系統)所面臨的持續挑戰。透過串連兩種不同的 page-cache 寫入漏洞(xfrm-ESP 與 RxRPC),Dirty Frag 展示了一種強效的本機權限提升方法。對其根本原因(被繞過的 copy-on-write 機制以及在 in-place 解密期間未預期的 scratch 寫入)的詳細分析,強調了在 kernel 內部進行細緻記憶體管理和資料流完整性檢查的重要性。後續的修補程式引入了
SKBFL_SHARED_FRAG
標記並強制執行適當的 copy-on-write 行為,成為防禦此類攻擊的關鍵防線,從而強化了 Linux 系統的整體安全態勢。