十五年無人察覺!
摘要
1. 簡介
Linux kernel 的 real-time mutex(rtmutex) 子系統實作了優先權繼承 (Priority Inheritance, PI),以防止多執行緒環境中的優先權反轉(Priority inversion)情境 [1] 。當高優先權任務因被低優先權任務 Lock 而受阻時,PI 會暫時提升 lock holder 的排程優先權,以確保資源能及時釋放。FUTEX_WAIT_REQUEUE_PI 與 FUTEX_CMP_REQUEUE_PI 機制將此典範延伸至使用者空間的 futex 操作,允許執行緒原子地等待(Atomically wait)一個 futex,並被重新排隊到一個優先權繼承的 futex [2] 。
GhostLock 代表 rtmutex 內部代理鎖定機制清理路徑中的一個嚴重缺陷。此漏洞是在提交 8161239a8bcc 進行 rtmutex 重構時引入,並在約十五年間未被發現,影響了所有主要的 Linux 發行版 [1] 。此錯誤僅需 CONFIG_FUTEX_PI=y,且不需要特殊權限或使用者命名空間能力,使其在受影響的系統上普遍可利用。
先前關於 Linux kernel Use-After-Free 漏洞利用的研究主要集中在 Heap-Based 的漏洞,像是 Heap Feng Shui、Cross-Cache 攻擊和 Physmap Spraying 等技術主導了該領域 [3] [4] 。Kernel 中的 Stack-Based UAF 漏洞受到的關注相對較少,部分原因是 Kernel Stack 框架通常生命週期短暫且難以預測地回收。GhostLock 證明了當結合能將大型受控緩衝區放置在可預測 Stack 深度的系統呼叫時,Stack UAF 的可利用性不亞於 Heap UAF。
2. 漏洞分析
2.1 根本原因:remove_waiter() 中錯誤的任務參照
Kernel漏洞存在於 kernel/locking/rtmutex.c 中的 remove_waiter() 輔助函式。此函式最初是為單一情境設計:一個執行緒代表自己 block,隨後自行清理。在此假設下,current 巨集(識別當前執行的任務)總是對應到擁有正在 dequeue 的 waiter 物件的任務 [1] 。
然而,Requeue-PI 代理路徑(proxy path)打破了這個基本假設。透過 rt_mutex_start_proxy_lock(),remove_waiter() 輔助函式被呼叫來代表另一個正在睡眠的執行緒進行清理。在此代理路徑上,current 指的是發出 FUTEX_CMP_REQUEUE_PI 的執行緒,而非實際的等待任務。當 __rt_mutex_start_proxy_lock() 因偵測到 deadlock cycle 而回傳 -EDEADLK 時,復原路徑會呼叫 remove_waiter(),然後該函式錯誤地清除 current->pi_blocked_on,而非 waiter->task->pi_blocked_on [1] 。
以下程式碼片段說明了此有漏洞的代理鎖定啟動路徑:
- // Code from kernel/locking/rtmutex.c (vulnerable version)
- // This function initiates proxy locking on behalf of 'task',
- // not the currently executing task.
- int __sched rt_mutex_start_proxy_lock(struct rt_mutex_base *lock,
- struct rt_mutex_waiter *waiter,
- struct task_struct *task)
- {
- int ret;
- raw_spin_lock_irq(&lock->wait_lock);
- ret = __rt_mutex_start_proxy_lock(lock, waiter, task);
- // When ret == -EDEADLK (deadlock detected), remove_waiter is called
- // to roll back the operation. However, remove_waiter assumes 'current'
- // is the waiter task, which is FALSE on the proxy path.
- if (unlikely(ret))
- remove_waiter(lock, waiter); // BUG: clears wrong task's pi_blocked_on
- raw_spin_unlock_irq(&lock->wait_lock);
- return ret;
- }
接著 remove_waiter() 函式執行了錯誤的清理:
- // Vulnerable remove_waiter() implementation
- // The function locks current->pi_lock and clears current->pi_blocked_on.
- // On the proxy path, 'current' is the REQUEUER, not the WAITER.
- static void __sched remove_waiter(struct rt_mutex_base *lock,
- struct rt_mutex_waiter *waiter)
- {
- ...
- raw_spin_lock(¤t->pi_lock); // Locks WRONG task's pi_lock
- rt_mutex_dequeue(lock, waiter); // Dequeues waiter from lock tree
- current->pi_blocked_on = NULL; // Clears WRONG task's pi_blocked_on
- // Should be: waiter->task->pi_blocked_on
- raw_spin_unlock(¤t->pi_lock);
- ...
- }
結果是睡眠中的等待任務保留著 pi_blocked_on,指向其自身 Stack 上的 rt_mutex_waiter 物件。當等待任務返回使用者空間時,該 Stack 框架被彈出——在邏輯意義上被釋放——然而 Kernel透過任務的 pi_blocked_on 欄位保留了一個 Dangling pointer [1] 。
2.2 觸發 Deadlock 路徑
要觸發 -EDEADLK 復原,需要使用三個 futex words 和三個執行緒來建構一個 PI 依賴循環(dependency cycle)。其順序如下 [1] :
(1) 等待者 (waiter) 執行緒取得 f_pi_chain(一個 PI futex),然後在 FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target) 中 block。其 rt_mutex_waiter 物件現在配置於其 Kernel Stack 上。
(2) 持有者 (owner) 執行緒取得 f_pi_target(Requeue 的目標 PI futex),然後在 f_pi_chain 上 block ,而這個 chain 由等待者持有。
(3) 主 (main) 執行緒呼叫 FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)。
Requeue 操作嘗試將等待者代理到 f_pi_target 上。f_pi_target 的持有者已經透過 f_pi_chain block 在等待者之後,因此 PI chain 進入閉環:等待者 -> f_pi_target -> 持有者 -> f_pi_chain -> 等待者。這個 chain 被偵測到此循環並回傳 -EDEADLK,觸發 remove_waiter() 中有錯誤的復原 [1] 。
復原之後,等待任務被喚醒並返回使用者空間,但其 pi_blocked_on 仍指向現已釋放的 Stack 框架。UAF window 實際上是無期限的——等待者可以停留在使用者空間,而 Dangling pointer 則持續存在於 Kernel狀態中。
(current = Main Thread, not Waiter) K->>W: Wakeup W->>W: Return to userspace Note right of W: Stack frame popped,
but pi_blocked_on still points to freed stack Note over W,M: Stage 4: UAF Trigger M->>K: sched_setattr() on Waiter K->>K: PI chain walk dereferences dangling pi_blocked_on Note right of K: Use-After-Free on kernel stack
3. 漏洞利用操作與 Stack 回收
3.1 Dangling pointer 操作
一旦三個 futex 的循環被建構好,等待任務就持有一個指向其舊有 FUTEX_WAIT_REQUEUE_PI Stack 框架的 Dangling Kernel 指標。任何透過此任務觸發 PI chain 走訪的操作——例如 sched_setattr()——都會將 pi_blocked_on 作為 rt_mutex_waiter 指標進行 dereference [1] 。這為攻擊者提供了一條進入已釋放 Stack 記憶體的受控 dereference 路徑。
rt_mutex_waiter 結構定義如下:
- // Structure definition from kernel/locking/rtmutex.c
- // This object lives on the kernel stack during FUTEX_WAIT_REQUEUE_PI
- struct rt_mutex_waiter {
- struct rt_waiter_node tree; // rb node, embedded in lock->waiters tree
- struct rt_waiter_node pi_tree; // rb node in task's pi_waiters tree
- struct task_struct *task; // Pointer to the waiting task
- struct rt_mutex_base *lock; // Pointer to the lock being waited on
- unsigned int wake_state; // Wakeup state for the waiter
- struct ww_acquire_ctx *ww_ctx; // Wound-wait context (NULL for PI futex)
- };
3.2 透過 PR_SET_MM_MAP 進行 Stack 回收
利用 Stack UAF 的關鍵挑戰在於,在 Dangling pointer 被 dereference 之前,要以受控資料回收已釋放的 Stack 框架。GhostLock 漏洞利用透過 prctl(PR_SET_MM, PR_SET_MM_MAP, ...) 來實現,此呼叫會將使用者提供的輔助向量 (auxiliary vector, auxv) 複製到 prctl_set_mm_map() 內部一個固定大小的 unsigned long user_auxv[AT_VECTOR_SIZE] Stack 緩衝區中 [1] 。
此緩衝區大約位於與已釋放之 rt_mutex_waiter 框架相同的 Stack 深度,提供了一個大型、自然對齊、無命名空間干擾的受控 qword 區塊,直接覆蓋了舊物件。auxv 的佈局經過精心設計,使得重疊的 qword 變為:
- tree :一個精心製作的 rb 節點,使得在刪除它時會將一個選定的子節點指標 (W0_BASE) 提升為樹根。
- task :設定為 &init_task,一個有效的 task_struct,以確保 chain 走訪能安全地進行 dereference。
- lock :設定為 &inet6_protos[IPPROTO_UDP] - 8,即受限制的寫入目標。
- wake_state :設定為 0。
此漏洞利用引入了一個 Race condition,方法是將 auxv 備份在一個 memfd 中,並將複製位置設定在跨越頁面邊界之處。一個 sibling thread 在 prctl 期間對後續頁面進行 fallocate(PUNCH_HOLE) 競爭,拉長了 copy_from_user window,確保偽造的 waiter 在 consumer thread 於另一個 CPU 上觸發 sched_setattr() 時,仍然存活在 Stack 上 [1] 。
3.3 從偽造 Waiter 到受限制寫入
控制偽造的 waiter 並不能直接產生任意寫入能力。PI chain 走訪會執行以下順序 [1] :
- // The chain walk dereferences task->pi_blocked_on (our fake waiter),
- // then accesses fake_waiter->lock (our target pointer).
- // rt_mutex_dequeue() performs rb_erase on lock->waiters.
- task->pi_blocked_on -> fake waiter
- fake waiter->lock -> fake rt_mutex_base
- rt_mutex_dequeue(lock, waiter) // rb_erase on lock->waiters
rt_mutex_dequeue() 操作是一個 rb-tree 刪除。當刪除一個只有單一子節點的根節點時, Kernel會將該子節點指標寫入根節點位置 (rb_root.rb_node)。透過將 lock 指向 target - 8,rt_mutex_base 的欄位會對齊在目標指標周圍的資料上:
- // Memory layout at the fake lock target:
- // target - 8 -> raw_spinlock_t wait_lock (must read as "unlocked")
- // target -> waiters.rb_root.rb_node (this slot gets WRITTEN)
- // target + 8 -> waiters.rb_leftmost
- // target + 16 -> owner
- // The write primitive is a single constrained store:
- *(uint64_t *)target = W0_BASE;
限制條件很嚴格:目標之前的 qword 必須讀起來像是一個未鎖定的 spinlock(低 4 位元組為零),且後續的 qword 不能將走訪導向不受控的記憶體 [1] 。選擇的寫入目標是 inet6_protos[IPPROTO_UDP],這是一個可寫入 Kernel資料中的函式指標表,其相鄰進入點(entry)自然能滿足這些限制。
4. 控制流劫持與權限提升
4.1 透過 Prefetch 時序繞過 KASLR
在利用受限制寫入之前,攻擊者必須先擊敗核心位址空間佈局隨機化 (Kernel Address Space Layout Randomization, KASLR)。此漏洞利用採用利用 Prefetch 的 side-channel 技術,測量已對應與未對應位址之間的循環差異 [1] 。由於 Linux 對 Kernel image base 採用相對較弱的隨機化(x86 上約 9 位元的熵),透過多次測量取平均,可以近乎 100% 的可靠度還原出 KASLR 偏移。
CPU Entry Area (CEA) 提供了第二個關鍵操作。在 Kernel 6.2 之前,CEA 位於一個完全固定的虛擬位址。在引入強隨機化之後,CEA 的 direct-map 別名仍然可預測,因為其實體偏移是固定的。direct-map 位址可從 physmap base 推算,而 physmap base 同樣可透過 prefetch 時序洩漏 [1] :
- // CEA direct-map alias computation
- // physmap_base is leaked via prefetch side-channel
- // CPU1_CEA_BASE is a fixed physical offset depending on kernel version
- // For kernelCTF LTS 6.12.80 with 3.5G boot: 0x11c517000(+0x1f58)
- cea_direct = physmap_base + CPU1_CEA_BASE
4.2 函式表覆寫
受限制的寫入目標是 inet6_protos[IPPROTO_UDP],這是 IPv6 協定堆疊中的一個函式指標表。此目標周圍的佈局自然能滿足 spinlock 與 rb-tree 的限制 [1] :
- // inet6_protos array layout (indices shown):
- inet6_protos[16] == NULL // fake wait_lock -> reads as unlocked
- inet6_protos[17] == &udpv6_protocol // <- target (IPPROTO_UDP = 17)
- inet6_protos[18] == NULL // fake rb_leftmost
- inet6_protos[19] == NULL // fake owner
寫入之後,inet6_protos[IPPROTO_UDP] 指向 CEA 頁面。 Kernel期望在此位置有一個 inet6_protocol 結構:
- // Expected structure at the overwritten pointer:
- struct inet6_protocol {
- int (*handler)(struct sk_buff *skb); // First pivot gadget address
- int (*err_handler)(...); // Unused
- unsigned int flags; // INET6_PROTO_NOPOLICY | INET6_PROTO_FINAL
- };
觸發一個 loopback IPv6 UDP 封包(先 connect 再寫入 ::1)會導致 Kernel dereference handler 指標,將控制權轉移到攻擊者選擇的程式碼 [1] 。
4.3 DirtyMode:用於權限翻轉的最小 ROP
在 kernelCTF 目標 (lts-6.12.80) 上,沒有方便的單指令 Stack Pivot 可用。漏洞攻擊鏈多執行一次載入/呼叫,將 CEA 位址送入 rbp,接著執行 mov rsp, rbp; pop rbp; ret 來 pivot 到位於 CEA 中的 ROP Stack [1] 。
漏洞利用沒有構建一個用於完整任意寫入的冗長 ROP 鏈,而是採用了 DirtyMode:一個單一寫入操作,翻轉 core_pattern sysctl 的 mode 欄位權限位元。coredump_sysctls 表格位於可寫入的 Kernel資料中,並與 Kernel映像共用相同的 KASLR 偏移 [1] :
- // Target structure in kernel writable data:
- static struct ctl_table coredump_sysctls[] = {
- ...
- { .procname = "core_pattern",
- .data = core_pattern,
- .maxlen = CORENAME_MAX_SIZE,
- .mode = 0644, // <- Permission bits to flip
- .proc_handler = proc_dostring_coredump },
- ...
- };
一個簡短的 ROP 序列將一個寬鬆的值寫入 coredump_sysctls [1] .mode,使 /proc/sys/kernel/core_pattern 變成 world-writable。被劫持的執行緒隨後透過 msleep 安全地停放。無特權的攻擊者開啟 core_pattern,寫入 |/proc/%P/fd/666 %P,然後使一個輔助處理程序 crash,以 root 身分執行任意程式碼 [1] 。
5. 緩解措施與修補程式分析
5.1 官方修補程式
修補方案提交為 3bfdc63936dd,修改了 remove_waiter(),使其透過 waiter->task 而非 current 來正確識別等待任務。此修補程式同時更新了 rt_mutex_adjust_prio_chain(),以傳遞正確的任務來進行優先權鏈調整 [1] :
- // Patched remove_waiter() implementation
- // The fix extracts waiter_task from waiter->task and operates on it.
- static void __sched remove_waiter(struct rt_mutex_base *lock,
- struct rt_mutex_waiter *waiter)
- {
- bool is_top_waiter = (waiter == rt_mutex_top_waiter(lock));
- struct task_struct *owner = rt_mutex_owner(lock);
- struct task_struct *waiter_task = waiter->task; // CORRECT: get task from waiter
- struct rt_mutex_base *next_lock;
- lockdep_assert_held(&lock->wait_lock);
- // Use scoped_guard to lock waiter_task->pi_lock, not current->pi_lock
- scoped_guard(raw_spinlock, &waiter_task->pi_lock) {
- rt_mutex_dequeue(lock, waiter);
- waiter_task->pi_blocked_on = NULL; // CORRECT: clear waiter's state
- }
- ...
- // Pass waiter_task instead of current to priority chain adjustment
- rt_mutex_adjust_prio_chain(owner, RT_MUTEX_MIN_CHAINWALK, lock,
- next_lock, NULL, waiter_task);
- }
5.2 縱深防禦
幾個 Kernel強化設定會影響可利用性。RANDOMIZE_KSTACK_OFFSET 對 Kernel Stack 偏移引入隨機化,使得 Stack 重用步驟變成機率性(大約 1/32 的機率)而非確定性 [1] 。kernelCTF 中的緩解目標啟用了此選項,迫使漏洞利用尋找替代路徑。
STATIC_USERMODE_HELPER 會關閉特定的 DirtyMode 路徑,阻止任意使用者模式輔助程式執行。然而,底層的寫入操作可以被重新導向到其他 sysctl 表格或可寫入的 Kernel資料結構 [1] 。
安全研究人員提倡多層防禦,包括 seccomp 設定檔、SELinux/AppArmor 強制執行、積極丟棄能力,以及 Kernel遙測來偵測異常的系統呼叫模式 [5] 。這些措施不能取代修補,但能降低未來本機 Kernel錯誤演變為完整 root 入侵的可能性。
6. 結論
GhostLock 說明了輔助函式中內嵌的假設,當這些函式在其原始設計者從未預期的環境中被重複使用時,會如何變成災難性的錯誤。remove_waiter() 函式對 current == waiter task 的隱含依賴在 self-blocking 路徑上維持了十五年的有效性,而 Requeue-PI 代理路徑則悄悄地違反了這個不變條件。
此漏洞攻擊鏈展示了克服現代 Kernel保護的複雜技術:利用 Prefetch 的 KASLR 繞過、用於已知位址受控記憶體的 CEA direct-map 別名、透過 PR_SET_MM_MAP 進行的 Stack 框架回收、透過 rb-tree 操作進行的受限制寫入,以及透過 DirtyMode 進行權限提升的最小 ROP。97% 的利用穩定性與 92,337 美元的 kernelCTF 獎勵,凸顯了此漏洞類別的嚴重性 [1] 。
未來的 Kernel開發應優先採用明確的環境傳遞,而非在清理路徑中使用隱含的 current 參照,特別是在代理操作中,當執行執行緒與受影響執行緒不同時。能夠追蹤 rtmutex 操作中任務身分的靜態分析工具,或許能在多年前就偵測到此不一致。
參考文獻
- IonStack part II: GhostLock, a stack-UAF that has existed in ALL Linux distributions for 15 years — NEBUSEC Research, VEGA Team, 2026。
- Requeue-PI: Making Glibc Condvars PI-Aware — Darren Hart, IBM Linux Technology Center, 2011。
- Take a Step Further: Understanding Page Spray in Linux Kernel Exploitation — arXiv:2406.02624, 2024。
- WHEN GOOD KERNEL DEFENSES GO BAD: Reliable and Stable Kernel Exploits via Defense-Amplified TLB Side-Channel Leaks — Lukas Maar ..., USENIX Security 2025。
- [ Kernel防禦失效?拆解 Ubuntu 中的三種繞過技術]