專案地址:https://github.com/0xlane/com-process-inject
Process Injection via Component Object Model (COM) IRundown::DoCallback().
該技術由 @modexpblog 挖掘發現,在我對該技術進行深入研究過程中,將原專案 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 -h
com-inject (1.0) - REInject
A process injection tool via COM
Commands:
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 -h
Inject special dll or shellcode to target process
Usage: com-inject.exe inject [OPTIONS] <PID>
Arguments:
<PID> Target process id
Options:
-m, --method Use CoGetObject instead of CoUnmarshalInterface to establish channel
-d, --dll <PATH> Inject DLL into target, specify full path
-s, --shellcode <PATH> Inject shellcode into target process
-h, --help Print help
#############################################################
PS D:\rust\com-inject> .\target\release\com-inject.exe list -h
List interface instance in special or all process
Usage: com-inject.exe list [OPTIONS] [PID]
Arguments:
[PID] Target process id
Options:
-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 介面呼叫,呼叫過程可以分這幾個步驟:
重點在介面定義,後面幾步都是 API 呼叫,對於一些有檔案記錄的介面一般都有對應的標頭檔案或 IDL,直接用就行,但是對於其他 COM 介面,呼叫之前先要定義一個包含方法虛表的結構體/介面,這個虛表的記憶體偏移、方法順序需要保證和介面實現一致,後面拿到介面指標才能正確呼叫對應的方法,c++ 裡的介面定義範例:
const IID
IID_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<std::ffi::c_void>);
transparent
可以理解為透傳,相當於:pub type IUnknown = std::ptr::NonNull<std::ffi::c_void>
,所以 let p: IUnknown
等價於 let p: std::ptr::NonNull<std::ffi::c_void>
,這樣才能看出來是個指標了。
對於這塊暫時解釋到這裡,想更進一步理解具體怎麼用 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 了,需要提前使用 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 Z
00007fff`637008c8 combase!CRemoteUnknown::`vftable' = <function> *[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 7fff6358ee90
combase!CRemoteUnknown::DoCallback [onecore\com\combase\dcomrem\remoteu.cxx @ 1843]:
00007fff`6358ee90 48895c2408 mov qword ptr [rsp+8],rbx
00007fff`6358ee95 57 push rdi
00007fff`6358ee96 4883ec40 sub rsp,40h
00007fff`6358ee9a 0f104234 movups xmm0,xmmword ptr [rdx+34h]
00007fff`6358ee9e 488bda mov rbx,rdx
00007fff`6358eea1 488d542430 lea rdx,[rsp+30h]
00007fff`6358eea6 f30f7f442430 movdqu xmmword ptr [rsp+30h],xmm0
00007fff`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;
}
0x0000
或 0xFFFF
,導致回撥失敗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;
}
0x0000
或 0xFFFF
時總是注入失敗,怎麼解決x86
和 x86_64
的 COM 程序