如何利用COM繞過AppLocker CLM
一、前言
約束語言模式(Constrained Language Mode,CLM)是限制Shell/">PowerShell的一種方法,可以限制PowerShell訪問類似 Add-Type
之類的功能或者許多反射(reflective)方法(攻擊者可以通過這些方法,在後續滲透工具中利用PowerShell執行時作為攻擊媒介)。
結合微軟的描述,這個功能通常可以作為一種安全控制方案,防禦方能夠利用該功能阻止執行類似 Invoke-Mimikatz
之類的工具,因為這些工具非常依賴於反射技術。
當我準備滲透部署了CLM的目標環境時,我需要快速瞭解有哪些潛在方法能夠繞過這種保護措施。我搭建了一個Windows 10測試環境,通過預設設定的規則來部署CLM。在本文中,我會與大家分享我的研究結果,介紹如何以非管理員使用者身份繞過這種保護機制。
二、進入正題
在測試環境中,我們要做的第一件事就是啟用AppLocker。在本文中我們將使用Windows預設部署的規則來限制指令碼執行。執行Application Identity服務後,我們可以使用如下命令確保本機已啟用CLM:
$ExecutionContext.SessionState.LanguageMode
這裡我們可以看到該命令的返回結果為 ConstrainedLanguage
,表明現在我們處於受限環境中。我們可以嘗試使用PowerShell中的受限命令來驗證這一點:
Add-Type "namespace test { }"
系統的確啟用了CLM,那麼我們如何才能繞過這個障礙?
三、AppLocker CLM中的New-Object
令人驚訝的是,當我開始搜尋CLM存在的攻擊面時,我發現當通過AppLocker啟用CLM時, New-Object
依然可用(儘管仍受到一些限制)。這似乎與預期的場景不一致,但我們的確可以使用如下命令來確認這一點:
New-Object -ComObject WScript.Shell
這給我留下了一種完美方式,可以從PowerShell內部來操控PowerShell程序,這是因為COM物件由DLL託管,而DLL會被載入到呼叫程序中。那麼我們如何才能建立可以被載入的COM物件?如果我們使用ProcMon工具來觀察 New-Object -ComObject xpntest
的呼叫過程時,我們可以看到該過程會多次請求 HKEY_CURRENT_USER
登錄檔項:
仔細觀察後,我們可以使用如下指令碼來建立 HKCU
中所需的登錄檔鍵值:
現在如果嘗試載入我們的COM物件,可以看到我們的DLL已被載入到PowerShell程序空間中:
非常好,現在我們已經可以在受限的上下文環境中,將任意DLL載入PowerShell程序中,無需呼叫動作太大的 CreateRemoteThread
或者 WriteProcessMemory
。但我們的目標是繞過CLM,如何利用我們的非託管(unmanaged)DLL載入方式來完成這個任務?我們可以利用.NET CLR,或者更確切一點,我們可以通過非託管DLL載入.NET CLR來呼叫.NET assembly(程式集)。
四、從非託管DLL到託管DLL到反射
現在我們可以使用類似Cobalt Strike之類的工具,該工具提供了 Execute-Assembly
功能,可以將CLR載入到非託管程序中,操作起來非常方便。之前我在 ofollow,noindex" target="_blank">GIST 上公開了一份程式碼,可以不依賴Cobalt Strike來完成這個任務:
這裡我不會討論程式碼的詳細內容(如果大家感興趣可以去閱讀微軟的官方 示例 ),該程式碼可以讓DLL載入.NET CLR,然後載入.NET assembly,然後將執行權傳遞給特定的方法。
該過程完成後,我們就可以訪問.NET,更重要的一點是,我們可以訪問.NET的反射功能。接下來我們需要找到啟用/關閉CLM的具體位置。
反彙編組成PowerShell的.NET assembly(即 System.Management.Automation.dll
)後,我們可以看到程式集的 System.Management.Automation.Runspaces.RunspaceBase.LanguageMode
屬性中有一個地方可以標識當前的語言模式。由於我們要使用反射技術,因此需要找到引用 Runspace
的變數,在執行時修改該變數。我發現要完成該任務,最好的 辦法 就是通過 Runspaces.Runspace.DefaultRunspace.SessionStateProxy.LanguageMode
,如下所示:
將程式碼編譯成.NET assembly,現在我們就可以通過反射方式禁用CLM,然後只需要建立一個 PowerShell指令碼 加以執行即可:
這樣就能大功告成,詳細過程可參考 演示視訊 。
五、背後原理
那麼為什麼COM可以繞過這種保護機制,PowerShell如何處理COM載入過程?
我們可以在 SystemPolicy.IsClassInApprovedList
方法中找到答案,該方法用來檢查是否允許我們向 New-Object
提供的CLSID。深入分析該方法,我們可以看到主要工作由如下關鍵程式碼負責:
if (SystemPolicy.WldpNativeMethods.WldpIsClassInApprovedList(ref clsid, ref wldp_HOST_INFORMATION, ref num, 0u) >= 0 && num == 1) { ... }
該呼叫只是 WldpIsClassInApprovedList
函式(位於 wldp.dll
中)的一個封裝函式,而後者用來檢查CLSID是否匹配DeviceGuard(現在為Windows Defender Application Control)策略。由於該方法沒有考慮到AppLocker,這意味著通過檢查的任何CLSID都可以暢行無阻。
六、奇怪的場景
結合前面分析,當我測試這種技術時,我陷入了一個奇怪的場景,當我們使用如下方法設定CLM時,這種技術將無法正常工作:
$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
這讓我有點困惑,因為之前我經常使用如上命令來測試載荷,此時我們面臨的環境又有什麼不同呢?回到我們的反彙編程式碼上,我們可以在 Microsoft.Powershell.Commands.Utility.dll
程式集中找到問題的答案,具體路徑位於 NewObjectCommand
類的 BeginProcessing
方法中:
這裡我們可以看到程式碼中存在2條路徑,具體取決於CLM的啟用方式。如果 SystemPolicy.GetSystemLockdownPolicy
返回 Enfore
,那麼就會執行第1條路徑,此時AppLocker或者DeviceGuard處於啟用狀態(並非我們使用 ExecutionContext.SessionState.LanguageMode
的場景)。如果直接設定這個屬性,我們會直接進入 if (!flag)…
程式碼段,此時就會丟擲異常。從這裡我們可以看到,CLM的行為實際上會根據具體的啟用方法(是通過AppLocker、DeviceGuard啟用還是通過 LanguageMode
屬性來啟用)而有所不同。
本文介紹的方法並不是繞過CLM的唯一方法,即使粗略分析PowerShell,我們也能找到實現類似效果的其他潛在方法。如果大家想了解其他技巧,可以參考Oddvar Moe在Debycon上的精彩 演講 。