Malware Detector:從事件追到程序記憶體

以 Sysmon 事件縮小範圍,再對可疑程序做記憶體掃描的 Windows 惡意行為偵測實作。

C++20 Windows Sysmon Memory Scanning

動機:從事件線索縮小記憶體檢查範圍

寒假 TeamT5 資安培訓時,我接觸到的問題是:Windows 10 上的 Sysmon 每天會產生很多事件,哪些線索值得再往程序記憶體查?只依單一 Event ID 發警報容易失去上下文;每筆事件都做深度掃描,又會把檢查成本推高。這個 Windows/C++20 專案因此把事件條件當作入口,再由掃描結果決定是否形成 finding。

課程分工中,我負責 Windows PE(Portable Executable)結構,以及 Process Injection、Process Hollowing、Reflective Injection 的攻擊手法與偵測方法簡報。以下用實際 detection 模組的資料流說明整體設計,不把團隊專案誤寫成由我一人完成。

設計:Collector、Detector、Scanner、Alertor

正規化 Sysmon 行為先由 Detector 選出觸發器,再由 ScannerOrchestrator.scan_pid 掃描目標 PID,依 finding 門檻決定是否送往 Alert sink;完整路徑如圖所示。

以 Sysmon 事件挑選目標 PID、執行記憶體掃描,再依 finding 嚴重度產生告警的 Malware Detector 架構圖

Sysmon 設定收集程序建立(EID 1)、DLL 載入(EID 7)、遠端執行緒(EID 8)、程序存取(EID 10)與檔案建立(EID 11)。這是「能不能看到事件」的 collection filter,不是最後的 alert rule;目前 detector.cpp 真正用來挑選掃描觸發器的是 EID 25、8、10、1,EID 7 和 EID 11 可以提供上下文,但不會單獨觸發這條警報。

Detector 逐筆讀取正規化事件,只保留有有效 PID 的候選,再選優先序最高者,因此同一批事件不會為每一筆線索各做一次掃描:

事件與優先序 具體條件 掃描目標
EID 25 Process Tampering(4) 事件帶有 ProcessId 即可成為最高優先序候選 ProcessId
EID 8 CreateRemoteThread(3) TargetProcessId 可轉成有效 PID;SourceImage/TargetImage 作為上下文 TargetProcessId
EID 10 ProcessAccess(2) GrantedAccess 能以十六進位解析,且是 0x001F0FFF/0x1F0FFF 全權限,或同時具 CREATE_THREAD (0x0002) 與 VM_WRITE (0x0020)/VM_OPERATION (0x0008) TargetProcessId
EID 1 ProcessCreate(1) 子程序是 system32/syswow64 下的 svchost.exe、dllhost.exe 或 rundll32.exe,且父程序路徑含 \\users\\、\\appdata\\、\\temp\\ 或 \\downloads\\ ProcessId

選出觸發器後,ScannerOrchestrator 對該 PID 執行記憶體掃描。掃描器回傳的 finding 會再依類型與嚴重度判斷是否足以輸出:

Scanner finding 被視為 strong finding 的門檻
HollowingOrTamper Medium 以上
ReflectiveInjection Medium 以上
DllSideLoading High 以上

幾個實際組合可以把「線索」和「告警」的差別說清楚:

觸發器與掃描結果 輸出
EID 25,掃描沒有 finding 仍輸出 High / SCAN-9001
EID 8,掃到 HollowingOrTamper Medium Critical / SCAN-9002
EID 10 通過 injection-like access mask,但掃描沒有 finding 抑制,不輸出告警
EID 1 的 user-writable parent heuristic,掃到 DllSideLoading High Critical / SCAN-9002

只要掃描結果沒有 finding,EID 25 仍會保留告警(因為它本身代表 Process Tampering);EID 8、EID 10、EID 1 若沒有 finding 則直接抑制,不把候選線索當成確診。存在 strong finding 時輸出 Critical / SCAN-9002,否則輸出 High / SCAN-9001。同一 PID 的 finding fingerprint 由 finding type、severity、module name、mapped path 組成;相同 fingerprint 在十秒內再次出現會被去重。

這些規則可在 detector.cpp 的 trigger、scan、dedup 和 alert 路徑逐段核對。hollowing、reflective injection 和 DLL side-loading 是掃描器嘗試辨識的行為類型,不代表每次 Sysmon 事件都確診其中一種。

結果與學到的技術:把兩層證據接成可解釋告警

這個實作讓一筆告警同時保留兩種證據:事件層說明「發生了什麼操作」,記憶體層則檢查目標程序目前的 PE 結構與記憶體屬性。事件先縮小 PID 範圍,Scanner 再回傳可列出的 finding,最後把觸發 EID、PID、來源/目標映像、各類 finding 計數、rule ID 和 severity 一起交給 alert sink。這比「看到 EID 8 就報警」多了一次驗證,也讓調查者能追問告警是由哪個線索和哪個 finding 組合而來。

我從事件 ID、程序權限遮罩一路讀到 finding fingerprint,學到偵測器的難點不是多加幾條規則,而是把 collection、觸發、掃描與輸出分層:Sysmon 設定漏掉的事件不會出現在管線裡;觸發條件排除的事件不會進入掃描;掃描器不認得的狀態也不應被包裝成已證實的攻擊。課程資料中的 Event-based Sysmon/Event Log 與 Memory-based PE/記憶體屬性,正好對應這個分層思路。

反思與邊界

專案沒有公開可重現的偵測率、誤報率或延遲數字,所以我不把規則存在解讀成已在真實網路驗證成效。路徑字串與 access mask 是可解釋的啟發式,不能單獨證明惡意;記憶體掃描也只涵蓋它實作的 finding 類型。下一步應用攻擊樣本和正常軟體分別測試每個優先序、EID 25 無 finding 的保留行為,以及十秒去重邊界,再量測噪音與漏報,而不是先宣稱涵蓋所有 Process Injection。

程式碼見 Malware-Detector GitHub,課程整理見專案簡報。