点击蓝字 / 关注我们
0x0 前言
这是WMI的第三篇文章,本文主要调式分析WMI消费者的工作原理,进而提出WMI的检测思路。本文首先介绍了本次分析所需要了解的WMI基本组件和底层协议(RPC),然后通过调式网上的RPC客户端和服务端的通信,了解RPC的原理,接着通过分析两个典型的WMI利用(查询数据,执行函数),了解WMI的检测,由于WMI调试相关资料过少,没有进行自我订正,可能存在错误,或者重大错误,希望有了解的大佬积极斧正。
0x1 WMI组件介绍
这一节内容截取于软件调试补编。
WMI大多数文件都保存在%system32%wbem文件夹下,其中下面文件是本次调试分析中使用到的
wbemcore.dll WMI核心模块
Wbemprox.dll WBEM代理,供 WMI应用程序连接WMI服务,包含了IWbemLocator接口的实现(Clocator类)。
Fastprox.dll 包含了用于进程间调用和RPC通信的类和函数,又称为Microsoft WBEMFast Call Context。
CIM对象管理器(CIM Object Manager,简称CIMOM)是WMI的核心部件。它负责管理和维护系统中的类和对象,也是 WMI管理程序(消耗器)和 WMI提供器之间进行交互的桥梁。从进程的角度看,CIMOM是工作在WMI服务器进程中的一系列动态链接库,它们利用COM/DCOM 技术相互协作。对外也是以COM接口的形式公开它们的服务。
WBEMCORE.DLL中的CWbemInstance类是描述和管理CIM类实例的一个内部类。包括读取实例的类名(class name)、修改或读取实例的属性值、复制实例数据等。MSDN中公开的IWbemClassObject 接口定义了操作WMI类和实例的基本方法,通过该接口,WMI应用程序可以访问相应的WMI类或实例。可以认为CWbemClass类和CWbemInstance类为实现这一接口的方法而提供的支持类。
WMI应用程序利用DCOM技术来使用WMI服务进程内的WMI服务。DCOM是分布式组件模型的简称,是对COM技术的扩展,目的是使不同计算机上的COM对象可以相互通信。DCOM协议又被称为对象RPC (Object Remote Procedure Call),是基于标准RPC协议而制定的。
0x2 RPC调试原理
0x2.1 客户端发送数据
RPC客户端使用NdrClientCall2
函数发送和接收数据,NdrClientCall2
函数是客户端入口的一个存根函数。NdrClientCall2
函数是一个不定参数函数,从第三个参数开始,传入的是调用的服务端函数所需要的参数。
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
在MulNdrpInitializeContextFromProc+0x4B处,将参数堆栈保存在pStubMsg.pContext结构体中。
0:000> dt _MIDL_STUB_MESSAGE 0055f60c
Client!_MIDL_STUB_MESSAGE
+0x000 RpcMsg : 0x0055f5e0 _RPC_MESSAGE
....
+0x0c4 pContext : 0x0055f70c _NDR_PROC_CONTEXT
....
0:000> dc 0055f70c +0x18
0055f724 0055fadc 00000232 00000000 003d0cb4 ..U.2.........=.
0055f734 00000000 00000000 00000000 00000000 ................
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U.
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
NdrpClientMarshal函数相当于格式化参数等所需要的数据,便于远程调用,在函数调用之前,可以看到RpcMsg->Buffer并不存在数据,但是在调用NdrpClientMarshal之后,已经将[In]参数传入RpcMsg->Buffer中(可能_RPC_MESSAGE结构的地址不一样是因为这是两次不同的调试)。
0:000> dt _RPC_MESSAGE 0055f5e0
Client!_RPC_MESSAGE
+0x000 Handle : (null)
+0x004 DataRepresentation : 0x22c
+0x008 Buffer : (null)
....
0:000> dt _RPC_MESSAGE 0040f3b4
Client!_RPC_MESSAGE
+0x000 Handle : 0x004dbb78 Void
+0x004 DataRepresentation : 0x22c
+0x008 Buffer : 0x004dc2d0 Void
....
0:000> dc 004dc2d0
004dc2d0 00000040 00010000 551d88b0 4283b831 @..........U1..B
004dc2e0 6527cda1 289fe459 00000001 8a885d04 ..'eY..(.....]..
004dc2f0 11c91ceb 0008e89f 6048102b 00000002 ........+.H`....
004dc300 00010001 551d88b0 4283b831 6527cda1 .......U1..B..'e
004dc310 289fe459 00000001 6cb71c2c 45409812 Y..(....,..l..@E
004dc320 00000003 00000000 00000001 baadf00d ................
004dc330 baadf00d baadf00d baadf00d baadf00d ................
004dc340 baadf00d baadf00d baadf00d baadf00d ................
在经过NdrpClientMarshal函数序列化之后,调用NdrpSendReceive函数发送NDR数据
0:000> kn
# ChildEBP RetAddr
00 0040f334 77090da2 RPCRT4!OSF_CCALL::SendReceiveHelper
01 0040f35c 7704b313 RPCRT4!OSF_CLIENT_MESSAGE_SENDER::SendReceive+0x35
02 0040f36c 770373f9 RPCRT4!OSF_CCALL::SendReceive+0x13
03 0040f37c 770380bb RPCRT4!I_RpcSendReceive+0x28
04 0040f390 7703808a RPCRT4!NdrSendReceive+0x31
05 0040f39c 770d0149 RPCRT4!NdrpSendReceive+0x9
在OSF_CCALL::SendReceiveHelper+0x48处,将RpcMsg->Buffer赋值到OSF_CCALL类偏移0x100处,接着调用OSF_CCALL::FastSendReceive函数继续发送数据。
*(this + 64) = a2->Buffer; // 会将参数列表复制到buffer中
v6 = OSF_CCALL::FastSendReceive(this, a2, a3);
0x2.2 服务端接收数据
从相关介绍中,我了解到NdrServerCall2作为服务端入口函数存在的,但是服务端并不直接调用NdrServerCall2接收和传送客户端的数据。有关服务端在进行PRC调用的时候,接收,调用,以及返回数据的函数堆栈如下:
0:003> kn
# ChildEBP RetAddr
00 00cff3cc 77055a57 Server!Add
01 00cff3ec 770d05f1 RPCRT4!Invoke+0x2a
02 00cff7f0 770d104e RPCRT4!NdrStubCall2+0x2ea
03 00cff80c 77055fe3 RPCRT4!NdrServerCall2+0x19
04 00cff844 77056483 RPCRT4!DispatchToStubInCNoAvrf+0x46
05 00cff89c 7705635d RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x158
06 00cff8c0 77097ddd RPCRT4!RPC_INTERFACE::DispatchToStub+0x90
07 00cff94c 7709812c RPCRT4!OSF_SCALL::DispatchHelper+0x23f
08 00cff960 77098371 RPCRT4!OSF_SCALL::DispatchRPCCall+0xf5
09 00cff98c 77098910 RPCRT4!OSF_SCALL::ProcessReceivedPDU+0x223
0a 00cff9ac 77098b0c RPCRT4!OSF_SCALL::BeginRpcCall+0x123
0b 00cffa08 770a749f RPCRT4!OSF_SCONNECTION::ProcessReceiveComplete+0x1e1
0c 00cffa1c 770bbfbf RPCRT4!ProcessConnectionServerReceivedEvent+0x1c
在OSF_SCALL::DispatchHelper函数中,会调用RPC_INTERFACE::DispatchToStub函数,其中第二个参数应该为_RPC_MESSAGE结构体(堆栈中应为第一个,因为又在this指针)
v13 = RPC_INTERFACE::DispatchToStub(v2, (this + 196), 0, v16, &v18);// <-----this+196为_RPC_MESSAGE
0:004> dc esp
00d5facc 003b192c 00000000 00000000 00d5faec ,.;.............
00d5fadc 003b1670 003b1868 00000000 003acca8 p.;.h.;.......:.
00d5faec 003b17f0 00000000 00000400 00d5fb04 ..;............
0:004> dt _RPC_MESSAGE 003b192c
Server!_RPC_MESSAGE
+0x000 Handle : 0x003b1868 Void
+0x004 DataRepresentation : 0x10
+0x008 Buffer : 0x003b1ad0 Void
+0x00c BufferLength : 4
+0x010 ProcNum : 0
.....
0:004> dc 003b1ad0
003b1ad0 00000040 00010000 551d88b0 4283b831 @..........U1..B
00d5fadc 003b1670 003b1868 00000000 003acca8 p.;.h.;.......:.
00d5faec 003b17f0 00000000 00000400 00d5fb04 ..;.............
...
接着,调用DispatchToStubInCNoAvrf函数,其目的是将_RPC_MESSAGE传入NdrServerCall2。然后调用NdrStubCall2函数。
RPCRT4!DispatchToStubInCNoAvrf:
77055fcc 6a0c push 0Ch
77055fce 68f85f0577 push offset RPCRT4!_imp_load__FreeAddrInfoW+0x1a8 (77055ff8)
77055fd3 e83506feff call RPCRT4!_SEH_prolog4 (7703660d)
77055fd8 33f6 xor esi,esi
77055fda 8975fc mov dword ptr [ebp-4],esi
77055fdd ff750c push dword ptr [ebp+0Ch]
77055fe0 ff5508 call dword ptr [ebp+8] ss:002b:00d5fa50={Server!ILT+4690(_NdrServerCall2 (01310257)}
77055fe3 c745fcfeffffff mov dword ptr [ebp-4],0FFFFFFFEh
其实,NdrStubCall2函数主要作用是根据_RPC_MESSAGE提供的pRpcMsg->ProcNum信息,获取服务端内对应的函数,根据pRpcMsg->Buffer获取参数,继而调用Invoke函数。以下是部分代码。另外,Marshal NDR数据的时候,也是和之前客户端相反的,客户端先Marshal成NDR数据,然后发送,等接收后在UnMarshal。而服务端是先UnMarshal,然后在执行,最后Marshal。
DispatchTable_ = pRpcMsg_[8];
if ( !DispatchTable_ )
DispatchTable_ = DispatchTable;
pFunc = DispatchTable_[ProcNum];
ArgNum = StackSize >> 2;
v26 = StackSize >> 2;
if ( StackSize >> 2 && (OptFlags->Unused & 4) != 0 && (v42 & 8) == 0 )
v26 = --ArgNum;
pArgBuffer_ = pArgBuffer;
returnValue = Invoke(pFunc, pArgBuffer, ArgNum);// <------
if ( (OptFlags->Unused & 4) == 0 )
goto LABEL_26;
if ( (v42 & 8) == 0 )
*&pArgBuffer_[4 * ArgNum] = returnValue;
在Invoke函数,显然可以看到将两个参数传入需要被调用函数中。
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
0
很显然,当调用完NdrpServerMarshal之后,便在pRpcMsg->Buffer中保存了结果
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
1
0x2.3 服务端发送数据
在OSF_SCALL::DispatchHelper函数中,在执行完RPC_INTERFACE::DispatchToStub函数(执行Invoke函数)之后,便会调用OSF_SCALL::Send函数,第二个参数(this + 196)是不是很熟悉,保存的就是_RPC_MESSAGE结构体。看来在传输过程中,RPC主要传输的是_RPC_MESSAGE。
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
2
0x2.4 客户端接收数据
在OSF_CCALL::SendNextFragment中,调用OSF_CCONNECTION::SendFragment函数,其中,这里的a4,对应的其实是pContext,
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
3
在OSF_CCALL::FastSendReceive函数中,接着程序会调用OSF_CCALL::ActuallyProcessPDU
函数,其中Src保存的是pContxt,跟准确的表达也就是[InOut]参数,在调用之前,可以看到BufferLength为4,即传入了一个参数的大小,当调用完成之后,BufferLength变为了48,且buffer中也有了返回的结果。
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
4
经过NdrpSendReceive函数之后,_RPC_MESSAGE.buffer(同_MIDL_STUB_MESSAGE.Buffer)却存储参数堆栈
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
5
0x3 WMI调试1——检索信息
本部分以Get-WmiObject -class Win32_Process为例。
0x3.1 WMI连接
ConnectServerWmi函数
的作用是链接WMI服务器,就像之前所说的,位于wminet_utils模块的函数,只是起到存根函数的作用,其最终会调用wbemprox的CLocator::ConnectServe
函数。
在CLocator::ConnectServe
函数中,最终会调用CDCOMTrans::DoActualConnection
函数,其调用堆栈如下。
CDCOMTrans::DoActualConnection
函数的主要作用是初始化_COSERVERINFO结构体,或者_COAUTHIDENTITY结构体。_COSERVERINFO结构体是一个包含激活功能的结构体。_COAUTHIDENTITY结构体则是一个包含域名,用户名密码的结构体。
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
6
最终调用CDCOMTrans::DoActualCCI
函数,在CDCOMTrans::DoActualCCI
函数中,最终会调用CoCreateInstanceEx函数,CoCreateInstanceEx函数可以在指定的远程计算机上创建与给定 CLSID 关联的单个未初始化对象。而CoCreateInstance也可以创建一个实例,但是CoCreateInstance与CoCreateInstanceEx函数的区别在于CoCreateInstanceEx可以创建远程计算机的实例。CoCreateInstanceEx函数的第一个参数是CLSID,表示要实例化对象的CLSID。在上一篇文章中,检测远程WMI连接的CLSID的值为8BC3F05E-D86B-11D0-A075-00C04FB68820。这就是为什么只要针对这个CLSID检测就可以判断是WMI远程连接了。
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
7
0x3.2 WMI查询
WMI查询操作,是通过wminet_utils模块的ExecQueryWmi
函数调用fastprox模块的CWbemSvcWrapper::XWbemServices::ExecQuery函数,而CWbemSvcWrapper::XWbemServices::ExecQuery的第二第三个参数分别表示执行的查询语句的类型,和SQL语句的内容。WMI拥有自己的查询语句,即WQL。
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
8
最终经过ole32!ObjectStubless函数调用RPCRT4!NdrClientCall2进行RPC调用。在RPCRT4!NdrClientCall2函数中,经过Marshal,会把参数保存在RPCMSG->Buffer中。这样做的好处是方便数据的传输。
将windbg附加到WmiPrvSE.exe进程即WMI提供程序进程。因为WMI原理简单来说就是WMI消费程序通过WMI核心架构,向WMI提供程序请求数据,WMI提供程序返回相关结果。WmiPrvSE.exe进程其实是X64进程,当windbg中断在call RPCRT4!Invoke
,根据x64函数的调用约定,rcx应该是需要invoke的函数,rdx应该是参数的缓冲区,r9d是参数个数。可以看到,服务端WmiPrvSE.exe接收到了数据,并准备调用CreateInstanceEnumAsync函数实例化Win32_Process。0x3.3 Get方法获取属性值
Get函数最终是通过调用fastprox模块的CWbemObject::Get
方法实现的,CWbemObject::Get
主要有调用了两个方法,分别是CWbemInstance::GetProperty
和CWbemInstance::GetPropertyType
。
CWbemInstance::GetProperty
函数主要是为了获取指定属性名的属性值,在其底层主要调用了CWbemObject::GetSystemPropertyByName
或者CWbemInstance::GetNonsystemPropertyValue
函数,前者主要获取的是系统属性值,而后者是获取非系统属性的属性值,在CSystemProperties::FindName
函数中,可以看到系统属性有哪些。
在调用CWbemObject::GetSystemPropertyByName
函数之前,在堆栈中看到需要查看的属性名为__PATH,调用结束后,可以看到返回的是一个CVar结构。CVAR偏移为0x00表示变量的类型,CVAR偏移为0x08,则表示变量的值。
0x3.4 GetNames方法获取属性值
GetNames函数最终调用fastprox模块的CWbemObject::GetNames
方法。CWbemObject::GetNames
函数主要是获取系统和非系统的属性名。通过flag标记,判断是获取系统属性名,亦或是非系统属性名,如果lFlags为0x30,则获取系统属性名,如果为0x40,则仅获取非系统属性名,因为将结果保存SAFEARRAY结构。SAFEARRAY+0x00表示数组的维度,可知这是一个一维数组,然后偏移+0x0C表示数组首地址,该数组有多个元素构成。
0:000> dc esp
0055f9e4 003d0d70 003d0caa 0055fadc 0055fc20 p.=...=...U. .U.
0055f9f4 0055fc28 7efde000 cccccccc cccccccc (.U....~........
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U. <----0055fbd8调用完成后填入返回值,40作为传入的参数
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0:000> dc 0055fbd8
0055fbd8 00000000 00000000 00000000 00000000 ................
0055fbe8 00000000 00000000 00000000 00000000 ................
0055fbf8 00000000 00000000 00000000 00000000 ................
0055fc08 00000000 00000000 00000000 00000000 ................
....
9
0x3.5 总结获取进程信息原理
其获取进程数据的主要原理是第一次通过调用GetNames方法,获取系统属性名,然后依次调用Get方法获取属性名的属性值,接着第二次调用GetNames方法获取非系统属性值,然后依次调用Get方法获取属性值。
0x4 WMI调试2——执行函数
本节使用的语句为Invoke-WmiMethod -class Win32_Process -Name Create calc.exe。
0x4.1 初始分析
在分析这一部分的时候,因为wminet_utils模块中没有ExecMethod相关的函数,并没有像上面一样通过wminet_utils模块来寻求突破。我们都知道WMI底层是客户端通过RPC协议远程调用服务端的函数,并接收返回值。所以我决定在RPC底层,通过中断NdrClientStub2函数,然后追溯栈回溯的方法确定调用方。最终发现其直接调用了fastprox模块的IWbemServices::ExecMethod
函数。
0x4.2 调用ExecMethod方法
IWbemServices::ExecMethod
函数的原型如下,第一个参数是Object的名字,第二个参数为函数名,第5个参数是指向传入参数的类。
0:000> dt _MIDL_STUB_MESSAGE 0055f60c
Client!_MIDL_STUB_MESSAGE
+0x000 RpcMsg : 0x0055f5e0 _RPC_MESSAGE
....
+0x0c4 pContext : 0x0055f70c _NDR_PROC_CONTEXT
....
0:000> dc 0055f70c +0x18
0055f724 0055fadc 00000232 00000000 003d0cb4 ..U.2.........=.
0055f734 00000000 00000000 00000000 00000000 ................
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U.
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
0
微软官方对pInParams的解释是*如果执行方法不需要输入参数,则可能为NULL。否则,它指向一个 IWbemClassObject*,并从https://docs.microsoft.com/en-us/windows/win32/wmisdk/creating-parameters-objects-in-c--这里了解到更详细的介绍。
根据微软的介绍,创建__PARAMETERS的实例的步骤如下:
• 确定包含方法定义的类的类路径。
•使用从IWbemProviderInit::Initialize传入的类路径和IWbemServices指针,调用IWbemClassObject::GetMethod来检索输入和输出参数类。GetMethod方法返回一个IWbemClassObject用于访问每个类的指针。
•使用输出类的IWbemClassObject指针,调用IWbemClassObject::SpawnInstance以创建类的实例。
•通过设置与输出值对应的属性来填充类实例,如果方法有返回值,则设置ReturnValue属性。
•通过IWbemObjectSink::Indicate方法将__PARAMETERS实例传递回调用者。
根据上述描述,我了解到了如果要创建这个一个__PARAMETERS实例,首先需要调用IWbemClassObject::SpawnInstance以创建类的实例,然后设置与输出值对应的属性来填充类实例。这里填充类实例是使用了CWbemInstance::Put
函数。最终把SpawnInstance创建创建类的实例传入IWbemServices::ExecMethod的pInParams参数。
0x4.3 填充类实例
WMI使用fastprox模块的CWbemInstance::Put
函数设置属性值,函数原型如下,第二个参数是待修改的属性名,而第4个参数是属性值。这是一个VARIANT结构。
0:000> dt _MIDL_STUB_MESSAGE 0055f60c
Client!_MIDL_STUB_MESSAGE
+0x000 RpcMsg : 0x0055f5e0 _RPC_MESSAGE
....
+0x0c4 pContext : 0x0055f70c _NDR_PROC_CONTEXT
....
0:000> dc 0055f70c +0x18
0055f724 0055fadc 00000232 00000000 003d0cb4 ..U.2.........=.
0055f734 00000000 00000000 00000000 00000000 ................
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U.
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
1
CWbemInstance::Put
底层主要通过CWbemInstance::SetPropValue
函数实现,首先判断是否是系统属性名,然后通过CClassPart::FindPropertyInfo
函数获取Property信息,接着依次调用CInstancePart::SetActualValue
函数,CUntypedValue::LoadFromCVar
函数。和CFastHeap::AllocateString
函数。并在CFastHeap::AllocateString
完成属性值的设置。
调用CInstancePart::SetActualValue
函数,其类是CInstancePart,父类为CWbemInstance类,显然可以得出在CWbemInstance+0x68的偏移处为CInstancePart
类。
0:000> dt _MIDL_STUB_MESSAGE 0055f60c
Client!_MIDL_STUB_MESSAGE
+0x000 RpcMsg : 0x0055f5e0 _RPC_MESSAGE
....
+0x0c4 pContext : 0x0055f70c _NDR_PROC_CONTEXT
....
0:000> dc 0055f70c +0x18
0055f724 0055fadc 00000232 00000000 003d0cb4 ..U.2.........=.
0055f734 00000000 00000000 00000000 00000000 ................
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U.
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
2
接着调用CUntypedValue::LoadFromCVar
,其中,第三个参数为(this + 0x6C)
,其实这是一个CFastHeap类,其位于CInstancePart
类的第0x6C的偏移处。
0:000> dt _MIDL_STUB_MESSAGE 0055f60c
Client!_MIDL_STUB_MESSAGE
+0x000 RpcMsg : 0x0055f5e0 _RPC_MESSAGE
....
+0x0c4 pContext : 0x0055f70c _NDR_PROC_CONTEXT
....
0:000> dc 0055f70c +0x18
0055f724 0055fadc 00000232 00000000 003d0cb4 ..U.2.........=.
0055f734 00000000 00000000 00000000 00000000 ................
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U.
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
3
最终调用CFastHeap::AllocateString
函数,完成对InParameters对象的赋值。v6其实就是等于[this]+pIndex。
0:000> dt _MIDL_STUB_MESSAGE 0055f60c
Client!_MIDL_STUB_MESSAGE
+0x000 RpcMsg : 0x0055f5e0 _RPC_MESSAGE
....
+0x0c4 pContext : 0x0055f70c _NDR_PROC_CONTEXT
....
0:000> dc 0055f70c +0x18
0055f724 0055fadc 00000232 00000000 003d0cb4 ..U.2.........=.
0055f734 00000000 00000000 00000000 00000000 ................
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U.
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
4
简单总结一下,假设CWbemInstance位于0x06505990,通过调用CInstancePart::SetActualValue
函数,可知CInstancePart的地址位于CWbemInstance + 0x68
即0x065059f8。然后通过调用CUntypedValue::LoadFromCVar
函数可知,CFastHeap的地址位于CInstancePart + 0x6C
即06505ar64的地址。通过获取对CFastHeap取值,就可以知道参数的地址。
0:000> dt _MIDL_STUB_MESSAGE 0055f60c
Client!_MIDL_STUB_MESSAGE
+0x000 RpcMsg : 0x0055f5e0 _RPC_MESSAGE
....
+0x0c4 pContext : 0x0055f70c _NDR_PROC_CONTEXT
....
0:000> dc 0055f70c +0x18
0055f724 0055fadc 00000232 00000000 003d0cb4 ..U.2.........=.
0055f734 00000000 00000000 00000000 00000000 ................
...
0:000> dc 0055fadc
0055fadc 0055fbd8 00000040 0055fd7c 0055fc28 ..U.@...|.U.(.U.
0055faec 7efde000 cccccccc cccccccc cccccccc ...~............
...
5
0x4.4 总结
根据上述,我们可知,IWbemServices::ExecMethod
函数的第一,第二,第五个参数分别表示的是Class名,函数名,参数类。其中参数可以通过参数类[__PARAMETERS+0x68+0x6C]获取。由此如果需要检测WMI通过Invoke-Method的方法进行创建函数,设置注册表等行为,可以通过检测IWbemServices::ExecMethod
的调用实现。
0x5 参考文献
• [Creating Parameters Objects in C++] https://docs.microsoft.com/en-us/windows/win32/wmisdk/creating-parameters-objects-in-c--
• 软件调试补篇
• https://github.com/dotnet/docs/blob/main/docs/framework/unmanaged-api/wmi/index.md
• Windows RPC Demo实现
跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...