項目地址:https://github.com/0xlane/com-process-inject
Process Injection via Component Object Model (COM) IRundown::DoCallback().
該技術由 @modexpblog 挖掘發現,在我對該技術進行深入研究過程中,將原項目mdsecactivebreach/com_inject(https://github.com/mdsecactivebreach/com_inject)使用了 Rust 重寫,希望對使用 Rust 的安全人員在 COM 接口調用、進程注入等方面有所幫助。
對于此項注入技術原理的更保真的解釋,還請參考 https://www.mdsec.co.uk/2022/04/process-injection-via-component-object-model-com-irundowndocallback/,這里只記錄一下工具用法和過程中遇到的一些問題和想法。
PS D:\rust\com-inject> .\target\release\com-inject.exe -hcom-inject (1.0) - REInjectA process injection tool via COMCommands: inject Inject special dll or shellcode to target process list List interface instance in special or all process help Print this message or the help of the given subcommand(s)Options: -h, --help Print help -V, --version Print version#############################################################PS D:\rust\com-inject> .\target\release\com-inject.exe inject -hInject special dll or shellcode to target processUsage: com-inject.exe inject [OPTIONS] Arguments: Target process idOptions: -m, --method Use CoGetObject instead of CoUnmarshalInterface to establish channel -d, --dll Inject DLL into target, specify full path -s, --shellcode Inject shellcode into target process -h, --help Print help#############################################################PS D:\rust\com-inject> .\target\release\com-inject.exe list -hList interface instance in special or all processUsage: com-inject.exe list [OPTIONS] [PID]Arguments: [PID] Target process idOptions: -v, --verbose Dispaly all interface, default only IRundown -h, --help Print help
Tips:
- DLL 和 Shellcode 文件路徑使用絕對路徑;
- 不論是 list 操作還是 inject 操作,都會嘗試開啟 DEBUG 權限;
- 避免對同一進程交替進行 DLL 注入和 shellcode 注入或者重復進行 DLL 注入,可能會報錯 “被調用的對象已與其客戶端斷開連接。(0x80010108)”,貌似是多次調用后遠程接口會被釋放掉;
- 如果報錯 “不支持此接口 (0x80004002)”,就多試幾遍;
- 并不是任何進程都能注入,只能對 list 動作顯示出來的進程進行注入
技術原理。
先說一下如何使用 Rust 對 COM 接口調用,調用過程可以分這幾個步驟:
- 接口定義
- 調用 CoInitializeEx 初始化
- 調用 CoGetObject 或其他類似 API 獲取接口指針
- 使用接口指針調用接口方法
- 調用 CoUninitialize 結束
重點在接口定義,后面幾步都是 API 調用,對于一些有文檔記錄的接口一般都有對應的頭文件或 IDL,直接用就行,但是對于其他 COM 接口,調用之前先要定義一個包含方法虛表的結構體/接口,這個虛表的內存偏移、方法順序需要保證和接口實現一致,后面拿到接口指針才能正確調用對應的方法,c++ 里的接口定義示例:
const IIDIID_IRundown = { 0x00000134, 0x0000, 0x0000, {0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}};
MIDL_INTERFACE("00000134-0000-0000-C000-000000000046")IRundown : public IUnknown { STDMETHOD(RemQueryInterface) ( REFIPID ripid, ULONG cRefs, USHORT cIids, IID *iids, REMQIRESULT **ppQIResults);
STDMETHOD(RemAddRef) ( unsigned short cInterfaceRefs, REMINTERFACEREF InterfaceRefs[], HRESULT *pResults);
STDMETHOD(RemRelease) ( USHORT cInterfaceRefs, REMINTERFACEREF InterfaceRefs[]);};
所有 COM 接口的祖先就是 IUnknown,大部分接口直接繼承自 IUnknown,還有部分通過繼承 IDispatch 或其他接口間接的繼承自 IUnknown。繼承在內存布局上實際上就是在父類的內存結構基礎上進行新增,所以不繼承直接將 IUnknown 中的方法搬過來也行。
由于 Rust 里面接口、類全部都以 struct 的形式表達,并且和 C++ 中的 struct 內存布局是有區別的,所以在定義接口虛表時,全部需要加上 #[repr(C)],代表該結構體內存布局和 C 完全一致。
C 里面有 IUnknown,Rust 里也不需要我們從 IUnknown 開始實現,實際上在 windows-rs 和 winapi 這兩個 crate 中都有實現,但是實現方式上有所不同。主要體現在對 “接口指針” 的定義上,下面是使用 C、winapi、windows-rs 各自如何聲明一個接口指針變量:
聲明方式C IUnknown *p = NULL; winapi let p: *mut IUnknown = ptr::null_mut(); windows-rs let p: IUnknown; |
可以看出來 winapi 的接口定義方式更符合 c 的接口調用風格,而 windows-rs 從聲明上則看不出來是一個指針,指針被隱藏在了內部:
#[repr(transparent)]pub struct IUnknown(std::ptr::NonNull);
transparent 可以理解為透傳,相當于:pub type IUnknown = std::ptr::NonNull,所以 let p: IUnknown 等價于 let p: std::ptr::NonNull,這樣才能看出來是個指針了。
對于這塊暫時解釋到這里,想更進一步理解具體怎么用 Rust 定義一個接口的話,可以借鑒我代碼里對 IRundown 接口的實現方式。
接下來理解 COM 接口方法的調用過程,COM 實際上可以理解為 RPC 的一種上層實現,所以還是 RPC,調用接口的程序稱為客戶端,真正處理執行調用請求的稱為服務端。
之前列出的調用過程步驟中的第 3 步,使用 CoGetObject、CoCreateInstance、CoGetObjectContext 這些 API 獲取接口指針,如果獲取成功就相當于和服務端連接成功,當通過指針調用方法后,相當于發起一個請求到服務端了。
所以回到該技術中,該技術使用了一個名為 IRundown 的接口,此接口中包含一個可以執行回調的方法 DoCallback,定義如下:
pub DoCallback: unsafe extern "system" fn(this: *mut ::core::ffi::c_void, pParam: *mut XAptCallback) -> windows::core::HRESULT,
XAptCallback 參數設置回調地址和參數地址:
#[repr(C)]#[derive(Clone, Copy)]pub struct tagXAptCallback { pub pfnCallback: PTRMEM, // what to execute. e.g. LoadLibraryA, EtwpCreateEtwThread pub pParam: PTRMEM, // parameter to callback. pub pServerCtx: PTRMEM, // combase!g_pMTAEmptyCtx pub pUnk: PTRMEM, // Not required pub iid: windows::core::GUID, // Not required pub iMethod: i32, // Not required pub guidProcessSecret: windows::core::GUID // combase!CProcessSecret::s_guidOle32Secret}pub type XAptCallback = tagXAptCallback;
pfnCallback 為回調函數指針,pParam 為參數指針。加上之前說的 C/S 架構,接口調用請求實際上是在服務端處理的,所以當服務端進程接收到執行回調的請求后,觸發回調執行完成代碼注入。
大致的技術利用原理就這些,其他的都是一些細節問題,比如如何獲取到該接口指針、如何注入到任意進程中去,這兩個實際上是一個問題,前面說過成功獲取接口指針即是連接到目標進程,所以對于此類問題的根本是 “哪些進程屬于這個接口的服務進程”。
好像目前唯一好用的查看 COM 進程信息的工具就是 OleViewDotNet(https://github.com/tyranid/oleviewdotnet) 了,需要提前使用 windbg 或者其他調試器把 combase.dll 的符號下載到本地,然后配置到 OleViewDotNet 里,否則是查不到任何結果的:

然后在 Processes -> All Proccess -> By Name 打開 COM 進程列表,搜索 IRundown:

相當于執行 com-inject.exe list -v。
這些進程中存在 IRundown 接口指針,由于 IRundown 接口的實現者是 combase.dll,所以加載 combase.dll 的進程都有可能。
windbg 里面可以直接按下面的方式找 IRundown 接口虛表:
0:004> x /D /d combase!*CRemoteUnknown* A B C D E F G H I J K L M N O P Q R S T U V W X Y Z00007fff`637008c8 combase!CRemoteUnknown::`vftable' = *[13]0:004> dx -r1 (*((combase!void (__cdecl*(*)[13])())0x7fff637008c8))(*((combase!void (__cdecl*(*)[13])())0x7fff637008c8)) [Type: void (__cdecl* [13])()] [0] : 0x7fff6353e790 : combase!CRemoteUnknown::QueryInterface+0x0 [Type: void (__cdecl*)()] [1] : 0x7fff635ae3b0 : [Type: void (__cdecl*)()] [2] : 0x7fff635ae3b0 : [Type: void (__cdecl*)()] [3] : 0x7fff63520600 : combase!CRemoteUnknown::RemQueryInterface+0x0 [Type: void (__cdecl*)()] [4] : 0x7fff6351a390 : combase!CRemoteUnknown::RemAddRef+0x0 [Type: void (__cdecl*)()] [5] : 0x7fff6352f2b0 : combase!CRemoteUnknown::RemRelease+0x0 [Type: void (__cdecl*)()] [6] : 0x7fff6355ad50 : combase!CRemoteUnknown::RemQueryInterface2+0x0 [Type: void (__cdecl*)()] [7] : 0x7fff6355afa0 : combase!CRemoteUnknown::AcknowledgeMarshalingSets+0x0 [Type: void (__cdecl*)()] [8] : 0x7fff636765a0 : combase!CRemoteUnknown::RemChangeRef+0x0 [Type: void (__cdecl*)()] [9] : 0x7fff6358ee90 : combase!CRemoteUnknown::DoCallback+0x0 [Type: void (__cdecl*)()] [10] : 0x7fff6358ee80 : combase!CRemoteUnknown::DoNonreentrantCallback+0x0 [Type: void (__cdecl*)()] [11] : 0x7fff634d29b0 : combase!CRemoteUnknown::GetInterfaceNameFromIPID+0x0 [Type: void (__cdecl*)()] [12] : 0x7fff6355b140 : combase!CRemoteUnknown::RundownOid+0x0 [Type: void (__cdecl*)()]0:004> u 7fff6358ee90combase!CRemoteUnknown::DoCallback [onecore\com\combase\dcomrem\remoteu.cxx @ 1843]:00007fff`6358ee90 48895c2408 mov qword ptr [rsp+8],rbx00007fff`6358ee95 57 push rdi00007fff`6358ee96 4883ec40 sub rsp,40h00007fff`6358ee9a 0f104234 movups xmm0,xmmword ptr [rdx+34h]00007fff`6358ee9e 488bda mov rbx,rdx00007fff`6358eea1 488d542430 lea rdx,[rsp+30h]00007fff`6358eea6 f30f7f442430 movdqu xmmword ptr [rsp+30h],xmm000007fff`6358eeac e83b000000 call combase!CProcessSecret::VerifyMatchingSecret (00007fff`6358eeec)0:004> bp 7fff6358ee90
其他細節或者挖掘思路直接看一下大佬的文章解惑吧 https://www.mdsec.co.uk/2022/04/process-injection-via-component-object-model-com-irundowndocallback/。
原項目運行后可能會遇到一些問題,在重寫時簡單處理了一下,問題如下:
我在 find_ipid_table 中加了些條件,然后就沒遇到過著個問題了:
if (*cpage)._pgalloc._cPages <= 0 || (*cpage)._pgalloc._cEntries <= 0 { continue;}
IPID 是一個 GUID,是對接口指針的標識,具有一定的格式:xxxxxxxx-yyyy-zzzz-xxxx-xxxxxxxxxxxx,yyyy 的位置代表進程 PID,zzzz 的位置代表線程 TID,如果線程 ID 無效會導致獲取的 server context 不正確,最后雖然這個接口指針的狀態雖然不是 IPIDF_DISCONNECTED,但是最終調用 DoCallback 時依然返回錯誤:“被調用的對象已與其客戶端斷開連接。(0x80010108)”。
所以我在獲取接口指針時,加了些過濾,優先使用 TID 有效的 IPID:
let x: Vec<_> = entries.iter().filter(|x| x.ipid.tid > 0x0 && x.ipid.tid < 0xffff).collect();let y: Vec<_> = entries.iter().filter(|x| x.ipid.tid == 0x0).collect();if x.len() > 0 { (*rc).ipid = x[0].ipid; (*rc).oxid = x[0].oxid; (*rc).oid = x[0].oid;} else if y.len() > 0 { (*rc).ipid = y[0].ipid; (*rc).oxid = y[0].oxid; (*rc).oid = y[0].oid;} else { (*rc).ipid = entries[0].ipid; (*rc).oxid = entries[0].oxid; (*rc).oid = entries[0].oid;}
未解決的問題
- 每次注入都會消耗掉目標進程中的一個接口指針,不確定為什么會自動釋放掉,當用完之后就會一直注入失敗了;
- 對于 TID 為 0x0000 或 0xFFFF 時總是注入失敗,怎么解決;
- 通用于 x86 和 x86_64 的 COM 進程。
中國網絡空間安全協會
雷石安全實驗室
威脅棱鏡
LemonSec
一顆小胡椒
合天網安實驗室
0x00實驗室
系統安全運維
看雪學苑
數世咨詢
看雪學苑
看雪學苑