摘要

本報告分析一個在以 Rust 為基礎的核心元件中發現的安全弱點,特別是發生於 Windows 的繪圖裝置介面(Graphics Device Interface, GDI)。該弱點是透過針對 metafile 的有針對性的模糊測試(fuzzing)攻擊發現的。本文說明了模糊測試流程的方法論、所使用的工具,以及重現與分析核心層級當機的技術面細節。此外,報告進一步探討了基於網路的模糊樣本傳輸和伺服器端收集機制,並提供了測試工具和資料處理的程式碼範例和架構見解。研究結果突顯了持續性安全測試的重要性,尤其是在像 Rust 這類新興技術被引入到關鍵系統元件時。

Rust的「安全神話」破滅?Windows Kernel GDI新漏洞深度解析 | 資訊安全新聞

1. 簡介

現代作業系統越來越多地整合先進的程式語言,以提升安全性與效能。Rust 在 Windows 核心的導入代表了朝向記憶體安全系統程式設計的一個重要轉變。然而,即使具備 Rust 的內建安全特性,仍然可能會出現弱點,特別是在與舊有系統介面互動或於核心環境中發生複雜互動時。本研究聚焦於一個在以 Rust 為基礎的 GDI 元件中發現的特定弱點,該弱點是透過大規模模糊測試攻擊所揭露。所採用的方法論不僅辨識出一個關鍵的錯誤,還促成了在困難測試環境中重現與分析核心當機的新技術發展。

2. 模糊測試方法與環境

模糊測試 (fuzzing)是一種動態軟體測試技術,透過餵入不良或意料外的輸入來揭露缺陷,例如當機、記憶體外洩或 assertion failure。本次攻擊的目標為 Windows 的繪圖元件,特別針對 metafile 的處理進行測試。有效的模糊測試需倚賴健全的測試環境,研究者使用了數個專用工具來支援測試工作。

2.1. 模糊測試工具

主要使用的 fuzzer 為 WinAFL ,這是一個為 Windows 二進位檔調整過且在揭露多項弱點方面成效顯著的模糊測試工具。為了管理中等規模的 fuzz 測試作業,研究者採用 WinAFL Pet 來簡化 fuzzing job 的建立、設定與監控。為有效分析程式當機的根本原因,則使用 BugId 來提供快速且完整的分析。

2.2. 目標選擇:Metafiles

繪圖裝置介面 (GDI) 是 Windows 的基礎元件,負責繪製圖形。本研究特別鎖定 metafile,包括 Enhanced Metafile Format (EMF) 及其變體 Enhanced Metafile Format Plus (EMF+) 。EMF 檔案包含對 GDI 函式的指令,而 EMF+ 記錄可被嵌入在 EMF 記錄中,使得複雜的繪圖指令得以表現。EMF+ 的嵌入是透過 EMR_COMMENT_EMFPLUS 記錄來實現,它可以封裝任意的私有資料。EMF+ 與其新增的 metafile 記錄所帶來的複雜性,使其成為一個重要的攻擊面。

2.3. 測試執行與「拒絕模糊測試」現象

模糊測試攻擊從 16 個初始種子檔案(包含 EMF+ 樣本)開始。在數日內辨識出多個潛在安全弱點。一項顯著的觀察是反覆出現的系統當機,研究者稱之為「Denial of Fuzzing」狀態,這指向一個核心層級的錯誤。此當機會導致 BugCheck 與系統重啟,顯示問題比一般使用者空間弱點更為深遠。接下來的挑戰從發現新弱點,轉為如何一致地重現與分析這個核心錯誤。

3. 核心錯誤重現與分析

在模糊測試環境中穩定地重現核心當機具有其獨特挑戰,尤其當 fuzzer 實例以共用記憶體模式運作並使用 RAM disk 以提升效能時。研究者採取了有系統的方法來克服這些困難。

3.1. 記憶體傾印分析(Memory Dump Analysis)

為了分析當機時作業系統的狀態,擷取記憶體傾印是必要的。然而,WinAFL 使用 RAM disk 與共用記憶體模式,使得精確找出導致當機的 fuzzed 樣本變得困難。傳統方法,例如在記憶體傾印中搜尋 EMF 簽章或使用像 Volatility FileScan DumpFiles 模組,對於大量檔案來說效率不佳。

3.2. 借助 MemProcFS

MemProcFS 工具在其 forensic 模式下,提供了對整個記憶體傾印自動識別並擷取檔案的更有效解法。透過在當機瞬間從 RAM disk 取得 fuzzer 狀態的快照,研究者得以擷取候選的種子檔案。以這些擷取的樣本為起點重新進行模糊測試,顯著縮短了重現核心錯誤所需的時間,將在一組 836 個檔案的資料集中重現時間降至 30 分鐘以內。

4. 遠端資料傳輸的模糊測試工具修改

為了進一步精簡分析流程並確保對 fuzzer 效能的影響最小,研究者修改了 fuzzing harness,使其能將變異後的測試檔案透過網路傳送至遠端伺服器。這允許集中式收集與分析導致當機的樣本,而不干擾 fuzzer 的執行。

4.1. 用戶端實作(C 程式碼)

用戶端的修改加入了一個 send_data() 函式到 fuzzing harness。該函式負責建立網路連線、傳送資料大小,然後傳輸實際的 fuzzed 樣本。包含了錯誤處理機制以確保穩健性。

  1. // Listing 1: Client-side modification to send every mutation to the server [1]
  2. int send_data(char* data, uint32_t size) {
  3. // Initialize Winsock library
  4. WSADATA wsa;
  5. // Socket descriptor
  6. SOCKET s;
  7. // Server address structure
  8. struct sockaddr_in server;
  9. // Remote server IP address (example: 192.168.1.1)
  10. wchar_t ip_address[] = L"192.168.1.1";
  11. // Set server address family and port
  12. server.sin_family = AF_INET;
  13. server.sin_port = htons(4444); // Port 4444 for communication
  14. // Initialize Winsock
  15. if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
  16. // Return error if Winsock initialization fails
  17. return 1;
  18. }
  19. // Create a socket
  20. if ((s = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
  21. // Clean up Winsock and return error if socket creation fails
  22. WSACleanup();
  23. return 1;
  24. }
  25. // Convert IP address from wide character string to network address structure
  26. if (InetPton(AF_INET, ip_address, &(server.sin_addr)) != 1) {
  27. // Close socket, clean up Winsock, and return error if IP conversion fails
  28. closesocket(s);
  29. WSACleanup();
  30. return 1;
  31. }
  32. // Connect to the remote server
  33. if (connect(s, (struct sockaddr*)&server, sizeof(server)) < 0) {
  34. // Close socket, clean up Winsock, and return error if connection fails
  35. closesocket(s);
  36. WSACleanup();
  37. return 1;
  38. }
  39. // Convert data size to network byte order and send it
  40. uint32_t size_header = htonl(size);
  41. if (send(s, (char*)&size_header, sizeof(size_header), 0) < 0) {
  42. // Close socket, clean up Winsock, and return error if sending size fails
  43. closesocket(s);
  44. WSACleanup();
  45. return 1;
  46. }
  47. // Send the actual data
  48. if (send(s, data, size, 0) < 0) {
  49. // Close socket, clean up Winsock, and return error if sending data fails
  50. closesocket(s);
  51. WSACleanup();
  52. return 1;
  53. }
  54. // Close the socket and clean up Winsock
  55. closesocket(s);
  56. WSACleanup();
  57. return 0;
  58. }

4.2. 伺服端實作(Python 程式碼)

在伺服端,一個 Python 腳本會持續監聽來自 fuzzing harness 的連線。每個連線以獨立執行緒處理,以便併發處理多個傳入樣本。腳本接收資料後將其儲存為單獨的檔案,並週期性地將蒐集到的檔案壓縮成 ZIP 檔以有效管理儲存空間。

  1. # Listing 2: Server-side Python script to receive and store fuzzed samples [1]
  2. #!/usr/bin/env python3
  3. import os
  4. import socket
  5. import zipfile
  6. import threading
  7. from concurrent.futures import ThreadPoolExecutor
  8. # Global counters for files and zip archives, protected by a lock
  9. file_counter = 0
  10. file_counter_lock = threading.Lock()
  11. zip_counter = 1
  12. def handle_client(client_socket, address):
  13. global file_counter, zip_counter
  14. try:
  15. # Receive data size (4 bytes, big-endian)
  16. data_size_bytes = client_socket.recv(4)
  17. if not data_size_bytes:
  18. return
  19. data_size = int.from_bytes(data_size_bytes, byteorder='big')
  20. # Receive the actual data in chunks
  21. data = bytearray()
  22. while len(data) < data_size:
  23. packet = client_socket.recv(min(1024, data_size - len(data)))
  24. if not packet:
  25. break
  26. data.extend(packet)
  27. # Ensure all data is received
  28. if len(data) != data_size:
  29. print(f"[!] Incomplete data received from {address}")
  30. return
  31. with file_counter_lock:
  32. file_counter += 1
  33. file_name = f"id_{file_counter:06d}"
  34. print(f"Received {file_counter} from {address}")
  35. # Save the received data to a file
  36. with open(file_name, "wb") as file:
  37. file.write(data)
  38. # Periodically zip files and clean up old ones
  39. if file_counter % 5000 == 0:
  40. zip_name = f"archive_{zip_counter:03d}.zip"
  41. with zipfile.ZipFile(zip_name, 'w') as zipf:
  42. # Zip the last 5000 files
  43. for i in range(file_counter - 4999, file_counter + 1):
  44. file_to_zip = f"id_{i:06d}"
  45. if os.path.exists(file_to_zip):
  46. zipf.write(file_to_zip)
  47. os.remove(file_to_zip) # Delete original after zipping
  48. zip_counter += 1
  49. print(f"[*] Created {zip_name} and cleaned up files.")
  50. except Exception as e:
  51. print(f"[!] Error handling client {address}: {e}")
  52. finally:
  53. client_socket.close()
  54. def main():
  55. server_ip = "0.0.0.0" # Listen on all available interfaces
  56. server_port = 4444 # Listen on port 4444
  57. server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  58. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Allow reusing the address
  59. server.bind((server_ip, server_port))
  60. server.listen(5) # Max 5 pending connections
  61. print(f"[*] Listening on {server_ip}:{server_port}")
  62. # Use a thread pool to handle multiple client connections concurrently
  63. with ThreadPoolExecutor(max_workers=20) as executor:
  64. while True:
  65. client_socket, addr = server.accept()
  66. print(f"[*] Accepted connection from {addr[0]}:{addr[1]}")
  67. executor.submit(handle_client, client_socket, addr)
  68. if __name__ == "__main__":
  69. main()

4.3. 遠端模糊測試資料蒐集架構

遠端模糊測試資料蒐集的整體架構包括在測試系統上執行的 fuzzer,持續產生並變異樣本。修改後的 harness 將這些樣本送到專用伺服器以供儲存與進一步分析。這樣的設置能將對 fuzzing 程序的 overhead 降到最低,並集中保存可能導致當機的輸入樣本。

graph TD
    A["Fuzzing Harness (WinAFL)"] -->|Mutated Samples| B{"send_data() function"};
    B -->|"Network (TCP/4444)"| C["Remote Server
(Python Script)"]; C --> D["File Storage
(id_XXXXXX files)"]; D --> E["Archiving (archive_XXX.zip)"]; subgraph Test System A end subgraph Collection Server C D E end

圖 1: 遠端模糊測試資料蒐集架構。

5. 結論

針對 Windows 中以 Rust 為基礎的 GDI 元件所做的研究顯示,即便使用現代且具記憶體安全保障的程式語言,透過模糊測試進行全面性的安全檢測仍然不可或缺。最初呈現為「Denial of Fuzzing」的核心層級弱點,凸顯出核心開發的複雜性與積極安全研究的重要性。利用像 MemProcFS 這類記憶體鑑識工具來重現核心當機,以及為變異樣本開發遠端傳輸機制,有效提升了模糊測試攻擊的效率與成效。此項工作為保護關鍵系統元件提供了寶貴的見解,並強調在作業系統發展演進過程中持續維持健全安全實務的必要性。