異常檢查此外,Roshtyak包含設置自定義向量異常處理程序的檢查,並有意觸發各種異常,以確保它們都按預期得到處理。
Roshtyak使用RtlAddVectoredExceptionHandler設置了一個向量異常處理程序。此處理程序包含針對所選異常代碼的自定義處理程序。頂級異常處理程序也使用SetUnhandledExceptionFilter註冊。不應該在目標執行環境中調用此處理程序(任何有意觸發的異常都不應該通過向量異常處理程序)。因此,這個頂級處理程序只包含一個對TerminateProcess的調用。有趣的是,Roshtyak還使用ZwSetInformationProcess使用ProcessDefaultHardErrorMode類來設置SEM_FAILCRITICALERRORS。這確保了即使異常以某種方式被傳遞到默認異常處理程序,Windows也不會顯示標準漏洞消息框,這可能會提醒受害者發生了可疑的事情。
當一切都設置好之後,Roshtyak開始生成異常。第一個異常是由一條popf指令生成的,後面直接跟著一條cpuid指令(如下所示)。 popf指令彈出的值被精心設計為設置漏洞標誌,而該標誌又會引發一個單步異常。在物理設備上,異常會在cpuid指令之後立即觸發。然後,自定義向量異常處理程序將接管並將指令指針從標記為無效指令的C7 B2 操作碼中移走。但是,在許多管理程序下,不會引發單步異常。這是因為cpuid 指令強制VM 退出,這可能會延遲跟踪標誌的效果。如果是這種情況,處理器將在嘗試執行無效操作碼時引發非法指令異常。如果向量異常處理程序遇到這樣的異常,它就知道它是在管理程序下運行的。 Palo Alto Networks 的一篇文章描述了這種技巧的一種變體。
基於異常的檢查使用popf 和cpuid 來檢測管理程序
另一個異常是使用雙字節int 3指令(CD 03)生成的。這條指令後面是垃圾操作碼。這裡的int 3 引發一個斷點異常,該異常由向量異常處理程序處理。向量異常處理程序實際上並沒有做任何處理異常的事情,這很有趣。這是因為默認情況下,Windows 在處理兩個字節的int 3 指令時,會將指令指針留在兩個指令字節之間,指向03 字節。當從這個03 字節反彙編時,垃圾操作碼突然開始變得有意義。我們認為這是對一些急於求成的調試器的檢查,這些調試器可能會將指令指針“修復”到03字節之後的指針。
此外,向量異常處理程序檢查線程的CONTEXT 並確保寄存器Dr0 到Dr3 為空。如果不是,則使用硬件斷點調試進程。雖然這種檢查在惡意軟件中比較常見,但CONTEXT 通常是通過調用GetThreadContext 之類的函數來獲取的。此時,惡意軟件開發者利用CONTEXT 作為參數傳遞給異常處理程序,因此他們不需要調用任何額外的API 函數。
大型可執行映射下一次檢查很有趣,主要是因為我們不確定它真正應該檢查什麼。首先,Roshtyak創建一個大小為0x386F000的大型PAGE_EXECUTE_READWRITE映射。然後它將這個映射映射到自己的地址空間9次。在這之後,它將映射設置為0x42 (inc edx的操作碼),除了最後六個字節,它們被四個inc ecx指令和jmp dword ptr [ecx]填充。接下來,它將映射視圖的9個基址放入一個數組中,後面是單個ret指令的地址。最後,它將ecx指向這個數組並調用第一個映射視圖,這將導致依次調用所有映射視圖,直到最後的ret指令。返回後,Roshtyak 驗證edx 恰好增加了0x1FBE6FCA 倍(9 * (0x386F000 - 6))。
大映射段的結尾,jmp dword ptr [ecx] 指令應該跳轉到下一個映射視圖的開頭
我們最好的猜測是,這是另一個反模擬器檢查。例如,在某些模擬器中,映射段可能沒有完全實現,因此寫入映射視圖的一個實例的指令可能不會傳播到其他八個實例。另一種理論是,這種檢查可以用於請求模擬器可能無法提供的大量內存。畢竟,所有視圖的總和幾乎是標準32位用戶模式地址空間的一半。
中止檢測過程這個技巧濫用NtCreateThreadEx 中未記錄的線程創建標誌來檢測Roshtyak 的主進程何時被外部掛起(這可能意味著附加了調試器)。這個標誌實際上允許線程即使在PsSuspendProcess被調用時也能繼續運行。這與另一個濫用線程掛起計數器是帶符號的8位值這一事實的技巧相結合,這意味著它的最大值為127。 Roshtyak生成兩個線程,其中一個線程持續掛起另一個線程,直到達到掛起計數器的限制。在此之後,第一個線程會定期掛起另一個線程,並檢查對NtSuspendThread 的調用是否繼續失敗並顯示STATUS_SUSPEND_COUNT_EXCEEDED。如果沒有,則該線程必須被外部掛起並恢復,這將使掛起計數器保持在126,因此對NtSuspendThread 的下一次調用將成功。如果沒有得到這個漏洞代碼,那麼Roshtyak就會停止使用TerminateProcess。 Secret Club在一篇博文中詳細描述了這一技巧。我們相信這就是Roshtyak的作者得到這個技巧的原因。值得一提的是,Roshtyak只在Windows版本18323 (19H1)及以後的版本中使用了這種技術,因為在之前的版本中沒有實現無文檔記錄的線程創建標誌。
間接註冊表寫入Roshtyak 執行許多可疑的註冊表操作,例如,設置RunOnce 項以實現持久性。由於可能會監控對此類密鑰的修改,因此Roshtyak 試圖繞過監控。它首先生成一個隨機註冊表項名稱,並使用ZwRenameKey 將RunOnce 項臨時重命名為隨機名稱。重命名後,Roshtyak 會在臨時密鑰中添加一個新的持久性條目,然後最終將其重命名為RunOnce。這種寫入註冊表的方法很容易被檢測到,但它可能會繞過一些簡單的基於掛鉤的監控方法。
同樣,Roshtyak 使用多種方法來釋放文件。除了對NtDeleteFile 的明顯調用之外,Roshtyak 還可以通過在對ZwSetInformationFile 的調用中設置FileDispositionInformation 或FileRenameInformation 來有效地釋放文件。然而,與註冊表修改方法不同的是,這似乎不是為了逃避檢測而實現的。相反,如果對NtDelete文件的初始調用失敗,Roshtyak將嘗試這些替代方法。
檢查VBAWarnings
VBAWarnings 註冊表值控制用戶打開包含嵌入VBA 宏的文檔時Microsoft Office 的行為方式。如果此值為1,則意味著“啟用所有宏”,則默認執行宏,甚至不需要任何用戶交互。這是沙盒的常見設置,旨在自動觸發惡意文檔。另一方面,此設置對於普通用戶來說並不常見,他們通常不會隨意更改設置,使自己更容易受到攻擊(至少他們中的大多數人不會)。因此,Roshtyak 使用此檢查來區分沙箱和普通用戶,如果VBAWarnings 的值為1,則拒絕進一步運行。有趣的是,這意味著無論出於何種原因以這種方式降低安全性的用戶都不會受Roshtyak 的影響。
命令行清除Roshtyak 的核心是用非常可疑的命令行執行的,例如RUNDLL32.EXE SHELL32.DLL,ShellExec_RunDLL REGSVR32.EXE -U /s 'C:\Users\\AppData\Local\Temp\dpcw.etl.'。這些命令行看起來不是特別合法,因此Roshtyak 試圖在執行期間隱藏它們。它通過清除從各種來源收集的命令行信息來做到這一點。它首先調用GetCommandLineA 和GetCommandLineW 並清除兩個返回的字符串。然後它會嘗試清除PEB-ProcessParameters-CommandLine 指向的字符串(即使它指向一個已經被清除的字符串)。由於Roshtyak 經常在WoW64 下運行,它還調用NtWow64QueryInformationProcess64 來獲取指向PEB64 的指針,以清除通過遍歷這個“第二個”PEB 獲得的ProcessParameters-CommandLine。雖然清除命令行可能是為了讓Roshtyak 看起來更合法,但完全清除任何命令行也是極不尋常的。 Red Canary 研究人員在他們的博客文章中註意到了這一點,他們提出了一種基於這些可疑的空命令行的檢測方法。
Roshtyak 的核心流程,如Process Explorer 所示,注意可疑的空命令行。
附加技巧除了到目前為止描述的技巧之外,Roshtyak 還使用了許多其他惡意軟件中常見的不太複雜的技巧。這些包括:
使用ThreadHideFromDebugger 隱藏線程,並使用NtQueryInformationThread 驗證線程是否真的被隱藏了;
在ntdll 中修補DbgBreakPoint;
使用GetLastInputInfo 檢測用戶活動情況;
檢查來自PEB 的字段(BeingDebugged、NtGlobalFlag);
檢查來自KUSER_SHARED_DATA 的字段(KdDebuggerEnabled、ActiveProcessorCount、NumberOfPhysicalPages);
檢查所有正在運行的進程的名稱(一些通過哈希比較,一些通過模式比較,一些通過字符分佈比較);
哈希所有加載模塊的名稱,並根據硬編碼的黑名單檢查它們;
驗證主進程名不需要太長時間,而且與沙盒中使用的已知名稱不匹配;
使用cpuid指令檢查hypervisor信息和處理器標誌;
使用沒有紀錄的COM 接口;
根據硬編碼的黑名單檢查用戶名和計算機名;
正在檢查是否存在已知的沙盒誘餌文件;
根據硬編碼的黑名單檢查自己的適配器的MAC 地址;
檢查ARP 表中的MAC 地址(使用GetBestRoute 填充它並使用GetIpNetTable 來檢查它);
使用ProcessDebugObjectHandle、ProcessDebugFlags 和ProcessDebugPort 調用ZwQueryInformationProcess;
檢查顯示設備的DeviceId(使用EnumDisplayDevices);
檢查\\.\PhysicalDrive0 的ProductId(使用IOCTL_STORAGE_QUERY_PROPERTY);
檢查虛擬硬盤(使用NtQuerySystemInformation 和SystemVhdBootInformation);
檢查原始SMBIOS 固件表(使用NtQuerySystemInformation 和SystemFirmwareTableInformation);
設置Defender 排除項(針對路徑和進程);
刪除與惡意軟件使用的進程名相關的IFEO註冊表項;
混淆我們展示了許多旨在防止Roshtyak 在不良執行環境中觸發的反分析技巧。就單個技巧來說,都很容易被修復或識別。分析Roshtyak 之所以困難的原因,是所有這些技巧被組合起來了,同時被重度混淆和多層包裝。這使得靜態研究反分析技巧並弄清楚如何通過所有檢查以使Roshtyak 自行解包變得非常困難。此外,即使是主要的有效載荷也收到了相同的混淆,這意味著靜態分析Roshtyak 的核心功能也需要大量的去混淆。
接下來,我們將介紹Roshtyak使用的主要混淆技術。
來自Roshtyak的隨機代碼片段,可以看到,這種混淆使得Hex-Rays反編譯器的原始輸出實際上難以理解
controlflowflatterning(控制流平坦化)控制流扁平化是Roshtyak 採用的最引人注目的混淆技巧之一。它以一種不同尋常的方式實現,使Roshtyak 函數的控制流圖具有獨特的外觀(見下文)。控制流扁平化的目標是混淆各個代碼塊之間的控制流關係。
控制流由一個32 位控制變量引導,該變量跟踪執行狀態,識別要執行的代碼塊。這個控制變量在每個函數開始時被初始化以引用起始代碼塊(通常是一個nop 塊)。然後在每個代碼塊的末尾修改控制變量,以識別應該執行的下一個代碼塊。修改是使用一些算術指令執行的,例如add、sub 或xor。
有一個調度程序使用控制變量將執行路由到正確的代碼塊中。這個調度程序由if/else塊組成,這些塊被循環鏈接到一個循環中。每個調度程序塊接受控制變量,並使用算術指令屏蔽它,以檢查是否應該將執行路由到它所保護的代碼塊中。有趣的是,從代碼塊到調度程序循環有多個入口點,使控制流圖在IDA 中呈現鋸齒狀外觀。
使用包含imul 指令的特殊代碼塊執行分支。它依賴於前一個塊來計算分支標誌。使用imul 指令將該分支標誌與一個隨機常數相乘,並將結果與新的控制變量相加、替換或異或。這意味著在分支塊之後,控制變量將識別兩個可能的後續代碼塊中的一個,這取決於為分支標誌計算的值。
用控制流扁平化混淆的函數的控制流圖
函數激活項Roshtyak 的混淆函數需要一個額外的參數,我們稱之為激活密鑰。此激活密鑰用於解密所有局部常量、字符串、變量等。如果使用漏洞的激活密鑰調用函數,則解密會產生垃圾明文,這很可能導致Roshtyak陷入控制流分配器內部的無限循環中。這是因為調度程序使用的所有常量(控制變量的初始值、調度程序守衛使用的掩碼以及用於跳轉到下一個代碼塊的常量)都使用激活密鑰加密。如果沒有正確的激活密鑰,調度程序根本不知道如何調度。
如果不知道正確的激活密鑰,對函數進行逆向工程實際上是不可能的。所有字符串、緩衝區和局部變量/常量都保持加密狀態,所有交叉引用都丟失了,更糟糕的是,沒有控制流信息。只剩下單獨的代碼塊,沒有辦法知道它們之間是如何關聯的。
每個混淆函數都必須從某個地方調用,這意味著調用該函數的代碼必須提供正確的激活密鑰。但是,獲取激活密鑰並不是那麼容易。首先,調用目標也使用激活密鑰加密,因此如果不知道正確的激活密鑰,就不可能找到調用函數的位置。其次,即使提供的激活密鑰也被調用函數的激活密鑰加密。並且該激活密鑰被下一個調用函數的激活密鑰加密。以此類推,一直到入口點函數。
這給去混淆帶來了很多麻煩。入口點函數的激活密鑰必須以明文形式存在。使用這個激活密鑰,可以解密直接從這個入口點函數調用的函數的調用目標和激活密鑰。遞歸地應用此方法使我們能夠重構完整的調用圖以及所有函數的激活項。唯一的例外是從未調用過且由編譯器保留的函數。這些函數可能仍然是個謎,但由於示例沒有使用它們,因此從惡意軟件分析師的角度來看,它們並不那麼重要。