WMI调试与检测

HaCky 2022-08-09 09:56:00

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:004> dc esp
00d5f5dc  003b1188 00000040 00000202 00000002  ..;.@...........

很显然,当调用完NdrpServerMarshal之后,便在pRpcMsg->Buffer中保存了结果

0:002> dt _MIDL_STUB_MESSAGE  00a2f1f0
Server!_MIDL_STUB_MESSAGE
   +0x000 RpcMsg           : 0x003b192c _RPC_MESSAGE
   +0x004 Buffer           : 0x003b0f48  ""
   +0x008 BufferStart      : 0x003b1fd0  "???"
   +0x00c BufferEnd        : 0x003b1fd8  ".???"
0:002> dt _RPC_MESSAGE 003b192c 
Server!_RPC_MESSAGE
   +0x000 Handle           : 0x003b1868 Void
   +0x004 DataRepresentation : 0x10
   +0x008 Buffer           : 0x003b0f40 Void
   +0x00c BufferLength     : 0x24
   +0x010 ProcNum          : 1
   +0x014 TransferSyntax   : 0x003accd4 _RPC_SYNTAX_IDENTIFIER
   +0x018 RpcInterfaceInformation : 0x003accbc Void
   +0x01c ReservedForRuntime : 0x003b1958 Void
   +0x020 ManagerEpv       : (null) 
   +0x024 ImportContext    : 0xbaadf00d Void
   +0x028 RpcFlags         : 0
0:002> dc 003b0f40
003b0f40  00000004 00000000 00000000 00000000  ................
003b0f50  00000000 00000000 00000000 00000000  ................
003b0f60  00000000 00000000 00000000 00000000  ...............

0x2.3 服务端发送数据

在OSF_SCALL::DispatchHelper函数中,在执行完RPC_INTERFACE::DispatchToStub函数(执行Invoke函数)之后,便会调用OSF_SCALL::Send函数,第二个参数(this + 196)是不是很熟悉,保存的就是_RPC_MESSAGE结构体。看来在传输过程中,RPC主要传输的是_RPC_MESSAGE。

OSF_SCALL::Send(this, (this + 196));      // <------发送
0:002> dc esp
00a2f6b4  003b192c 003b1670 003b1868 00000000  ,.;.p.;.h.;.....
00a2f6c4  003acca8 003b17f0 00000000 00000000  ..:...;.........
....
0:002> dt _RPC_MESSAGE 003b192c 
Server!_RPC_MESSAGE
   +0x000 Handle           : 0x003b1868 Void
   +0x004 DataRepresentation : 0x10
   +0x008 Buffer           : 0x003b0f40 Void
   +0x00c BufferLength     : 8
   +0x010 ProcNum          : 1
   +0x014 TransferSyntax   : 0x003accd4 _RPC_SYNTAX_IDENTIFIER
   +0x018 RpcInterfaceInformation : 0x003accbc Void
   +0x01c ReservedForRuntime : 0x003b1958 Void
   +0x020 ManagerEpv       : (null) 
   +0x024 ImportContext    : 0xbaadf00d Void
   +0x028 RpcFlags         : 0
0:002> dc 003b0f40
003b0f40  00000004 00000000 00000000 00000000  ................
003b0f50  00000000 00000000 00000000 00000000  ................
003b0f60  00000000 00000000 00000000 00000000  ................
003b0f70  00000000 00000000 00000000 00000000  ................

0x2.4 客户端接收数据

在OSF_CCALL::SendNextFragment中,调用OSF_CCONNECTION::SendFragment函数,其中,这里的a4,对应的其实是pContext,

v6 = OSF_CCONNECTION::SendFragment(
     *(this + 40),
     v14,
     this,
     v23,
     v24,
     *(this + 54),
     v21,
     *(this + 53),
     *(this + 57),
     *(*(this + 40) + 88) & 0x40,
     v22,
     v25,
     a4,
     a5);
0:000> dc esi
0040f308  004dc550 00000000 6d4f9985 0040f334  P.M.......Om4.@.
0:000> dc 004dc550 
004dc550  03020005 00000010 00000060 00000002  ........`.......
004dc560  00000048 00000000 00000040 53435052  H.......@...RPCS
004dc570  65767265 00000072 00000000 00000000  erver...........
0:000> dc 004dc550+18
004dc568  00000040 53435052 65767265 00000072  @...RPCServer...

在OSF_CCALL::FastSendReceive函数中,接着程序会调用OSF_CCALL::ActuallyProcessPDU函数,其中Src保存的是pContxt,跟准确的表达也就是[InOut]参数,在调用之前,可以看到BufferLength为4,即传入了一个参数的大小,当调用完成之后,BufferLength变为了48,且buffer中也有了返回的结果。

v6 = OSF_CCALL::ActuallyProcessPDU(this, Src, v46, a2, 0, 0);
0:000> dt _RPC_MESSAGE 0040f3b4
Client!_RPC_MESSAGE
   +0x000 Handle           : 0x004dbb78 Void
   +0x004 DataRepresentation : 0x22c
   +0x008 Buffer           : 0x004dc2d0 Void
   +0x00c BufferLength     : 4
   +0x010 ProcNum          : 0
   +0x014 TransferSyntax   : 0x00000004 _RPC_SYNTAX_IDENTIFIER
   +0x018 RpcInterfaceInformation : 0x00110d28 Void
   +0x01c ReservedForRuntime : 0x77ac59bc Void
   +0x020 ManagerEpv       : 0x00000008 Void
   +0x024 ImportContext    : 0x77a39a3a Void
   +0x028 RpcFlags         : 0
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
//调用完成之后
0:000> dt _RPC_MESSAGE 0040f3b4
Client!_RPC_MESSAGE
   +0x000 Handle           : 0x004dbb78 Void
   +0x004 DataRepresentation : 0x10
   +0x008 Buffer           : 0x004dc568 Void
   +0x00c BufferLength     : 0x48
   +0x010 ProcNum          : 0
   +0x014 TransferSyntax   : 0x00000004 _RPC_SYNTAX_IDENTIFIER
   +0x018 RpcInterfaceInformation : 0x00110d28 Void
   +0x01c ReservedForRuntime : 0x77ac59bc Void
   +0x020 ManagerEpv       : 0x00000008 Void
   +0x024 ImportContext    : 0x77a39a3a Void
   +0x028 RpcFlags         : 0x1000
0:000> dc 004dc568
004dc568  00000040 53435052 65767265 00000072  @...RPCServer...
004dc578  00000000 00000000 00000000 00000000  ................
004dc588  00000000 00000000 00000000 00000000  ................
004dc598  00000000 00000000 00000000 00000000  ................
004dc5a8  00000000 00000000 baadf00d baadf00d  ................
004dc5b8  baadf00d baadf00d baadf00d baadf00d  ................
004dc5c8  baadf00d baadf00d baadf00d baadf00d  ................
004dc5d8  baadf00d baadf00d baadf00d baadf00d  ................

经过NdrpSendReceive函数之后,_RPC_MESSAGE.buffer(同_MIDL_STUB_MESSAGE.Buffer)却存储参数堆栈

0:000> dt _MIDL_STUB_MESSAGE 0040f3e0
Client!_MIDL_STUB_MESSAGE
   +0x000 RpcMsg           : 0x0040f3b4 _RPC_MESSAGE
   +0x004 Buffer           : 0x004dc568  "@"
0:000> dc 004dc568 
004dc568  00000040 53435052 65767265 00000072  @...RPCServer...
004dc578  00000000 00000000 00000000 00000000  ................
004dc588  00000000 00000000 00000000 00000000  ................
004dc598  00000000 00000000 00000000 00000000  ................
004dc5a8  00000000 00000000 baadf00d baadf00d  ................

0x3 WMI调试1——检索信息

本部分以Get-WmiObject -class Win32_Process为例。

0x3.1 WMI连接

ConnectServerWmi函数的作用是链接WMI服务器,就像之前所说的,位于wminet_utils模块的函数,只是起到存根函数的作用,其最终会调用wbemprox的CLocator::ConnectServe函数。
mark

CLocator::ConnectServe函数中,最终会调用CDCOMTrans::DoActualConnection函数,其调用堆栈如下。
mark

CDCOMTrans::DoActualConnection函数的主要作用是初始化_COSERVERINFO结构体,或者_COAUTHIDENTITY结构体。_COSERVERINFO结构体是一个包含激活功能的结构体。_COAUTHIDENTITY结构体则是一个包含域名,用户名密码的结构体。

typedef struct _COSERVERINFO {
  DWORD      dwReserved1;
  LPWSTR     pwszName;
  COAUTHINFO *pAuthInfo;
  DWORD      dwReserved2;
} COSERVERINFO;
typedef struct _COAUTHIDENTITY {
  USHORT *User;
  ULONG  UserLength;
  USHORT *Domain;
  ULONG  DomainLength;
  USHORT *Password;
  ULONG  PasswordLength;
  ULONG  Flags;
} COAUTHIDENTITY;

mark

最终调用CDCOMTrans::DoActualCCI函数,在CDCOMTrans::DoActualCCI函数中,最终会调用CoCreateInstanceEx函数,CoCreateInstanceEx函数可以在指定的远程计算机上创建与给定 CLSID 关联的单个未初始化对象。而CoCreateInstance也可以创建一个实例,但是CoCreateInstance与CoCreateInstanceEx函数的区别在于CoCreateInstanceEx可以创建远程计算机的实例。 CoCreateInstanceEx函数的第一个参数是CLSID,表示要实例化对象的CLSID。在上一篇文章中,检测远程WMI连接的CLSID的值为8BC3F05E-D86B-11D0-A075-00C04FB68820。这就是为什么只要针对这个CLSID检测就可以判断是WMI远程连接了。

HRESULT CoCreateInstanceEx(
  [in]      REFCLSID     Clsid,
  [in]      IUnknown     *punkOuter,
  [in]      DWORD        dwClsCtx,
  [in]      COSERVERINFO *pServerInfo,
  [in]      DWORD        dwCount,
  [in, out] MULTI_QI     *pResults
);
v10 = CoCreateInstanceEx(&CLSID_WbemLevel1Login, 0, 0x14u, (a3 == 0 ? a2 : 0), 1u, &pResults);

mark

0x3.2 WMI查询

WMI查询操作,是通过wminet_utils模块的ExecQueryWmi函数调用fastprox模块的CWbemSvcWrapper::XWbemServices::ExecQuery函数,而CWbemSvcWrapper::XWbemServices::ExecQuery的第二第三个参数分别表示执行的查询语句的类型,和SQL语句的内容。WMI拥有自己的查询语句,即WQL。

v12 = pCurrentNamespace->lpVtbl->ExecQuery(pCurrentNamespace, strQueryLanguage, strQuery, lFlags, pCtx, ppEnum);

mark

最终经过ole32!ObjectStubless函数调用RPCRT4!NdrClientCall2进行RPC调用。在RPCRT4!NdrClientCall2函数中,经过Marshal,会把参数保存在RPCMSG->Buffer中。这样做的好处是方便数据的传输。
mark

将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。
mark
mark
mark

0x3.3 Get方法获取属性值

Get函数最终是通过调用fastprox模块的CWbemObject::Get方法实现的,CWbemObject::Get主要有调用了两个方法,分别是CWbemInstance::GetPropertyCWbemInstance::GetPropertyType

CWbemInstance::GetProperty函数主要是为了获取指定属性名的属性值,在其底层主要调用了CWbemObject::GetSystemPropertyByName或者CWbemInstance::GetNonsystemPropertyValue函数,前者主要获取的是系统属性值,而后者是获取非系统属性的属性值,在CSystemProperties::FindName函数中,可以看到系统属性有哪些。
mark

在调用CWbemObject::GetSystemPropertyByName函数之前,在堆栈中看到需要查看的属性名为__PATH,调用结束后,可以看到返回的是一个CVar结构。CVAR偏移为0x00表示变量的类型,CVAR偏移为0x08,则表示变量的值。

mark
mark

0x3.4 GetNames方法获取属性值

GetNames函数最终调用fastprox模块的CWbemObject::GetNames方法。CWbemObject::GetNames函数主要是获取系统和非系统的属性名。通过flag标记,判断是获取系统属性名,亦或是非系统属性名,如果lFlags为0x30,则获取系统属性名,如果为0x40,则仅获取非系统属性名,因为将结果保存SAFEARRAY结构。SAFEARRAY+0x00表示数组的维度,可知这是一个一维数组,然后偏移+0x0C表示数组首地址,该数组有多个元素构成。

typedef struct tagSAFEARRAY {
  USHORT         cDims;
  USHORT         fFeatures;
  ULONG          cbElements;
  ULONG          cLocks;
  PVOID          pvData;
  SAFEARRAYBOUND rgsabound[1];
} SAFEARRAY, *LPSAFEARRAY;

mark
mark
mark
mark
mark
mark

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个参数是指向传入参数的类。

HRESULT ExecMethod(
  [in]  const BSTR       strObjectPath,
  [in]  const BSTR       strMethodName,
  [in]  long             lFlags,
  [in]  IWbemContext     *pCtx,
  [in]  IWbemClassObject *pInParams,
  [out] IWbemClassObject **ppOutParams,
  [out] IWbemCallResult  **ppCallResult
);

mark

微软官方对pInParams的解释是如果执行方法不需要输入参数,则可能为NULL。否则,它指向一个 IWbemClassObject,并从https://docs.microsoft.com/en-us/windows/win32/wmisdk/creating-parameters-objects-in-c--这里了解到更详细的介绍。

根据微软的介绍,创建__PARAMETERS的实例的步骤如下:

    1. 确定包含方法定义的类的类路径。
    1. 使用从IWbemProviderInit::Initialize传入的类路径和IWbemServices指针,调用IWbemClassObject::GetMethod来检索输入和输出参数类。GetMethod方法返回一个IWbemClassObject用于访问每个类的指针。
    1. 使用输出类的IWbemClassObject指针,调用IWbemClassObject::SpawnInstance以创建类的实例。
    1. 通过设置与输出值对应的属性来填充类实例,如果方法有返回值,则设置ReturnValue属性。
    1. 通过IWbemObjectSink::Indicate方法将__PARAMETERS实例传递回调用者。

根据上述描述,我了解到了如果要创建这个一个__PARAMETERS实例,首先需要调用IWbemClassObject::SpawnInstance以创建类的实例,然后设置与输出值对应的属性来填充类实例。这里填充类实例是使用了CWbemInstance::Put函数。最终把SpawnInstance创建创建类的实例传入IWbemServices::ExecMethod的pInParams参数。

0x4.3 填充类实例

WMI使用fastprox模块的CWbemInstance::Put函数设置属性值,函数原型如下,第二个参数是待修改的属性名,而第4个参数是属性值。这是一个VARIANT结构。

int __stdcall CWbemInstance::Put(CWbemInstance *this, LPCWSTR wszName, LONG lFlags, VARIANT *pVal, CIMTYPE vtType)

mark
mark

CWbemInstance::Put底层主要通过CWbemInstance::SetPropValue函数实现,首先判断是否是系统属性名,然后通过CClassPart::FindPropertyInfo函数获取Property信息,接着依次调用CInstancePart::SetActualValue函数,CUntypedValue::LoadFromCVar函数。和CFastHeap::AllocateString函数。并在CFastHeap::AllocateString完成属性值的设置。

调用CInstancePart::SetActualValue函数,其类是CInstancePart,父类为CWbemInstance类,显然可以得出在CWbemInstance+0x68的偏移处为CInstancePart类。

CInstancePart::SetActualValue((this + 0x68), v5, value);// this+0x68===>CInstancePart

接着调用CUntypedValue::LoadFromCVar,其中,第三个参数为(this + 0x6C),其实这是一个CFastHeap类,其位于CInstancePart类的第0x6C的偏移处。

v8 = CUntypedValue::LoadFromCVar(v16, a3, v15, (this + 0x6C), &v17, v6);// this + 0x6C ====>CFastHeap

最终调用CFastHeap::AllocateString函数,完成对InParameters对象的赋值。v6其实就是等于[this]+pIndex。

CFastHeap::AllocateString(fastheap, *(v35 + 2), &a2)

mark

简单总结一下,假设CWbemInstance位于0x06505990,通过调用CInstancePart::SetActualValue函数,可知CInstancePart的地址位于CWbemInstance + 0x68即0x065059f8。然后通过调用CUntypedValue::LoadFromCVar函数可知,CFastHeap的地址位于CInstancePart + 0x6C即06505ar64的地址。通过获取对CFastHeap取值,就可以知道参数的地址。

CWbemInstance ====> 0x06505990
CWbemInstance + 0x68 ====> CInstancePart:065059f8
CInstancePart + 0x6C ====> CFastHeap:06505ar64

mark
mark
mark

0x4.4 总结

根据上述,我们可知,IWbemServices::ExecMethod函数的第一,第二,第五个参数分别表示的是Class名,函数名,参数类。其中参数可以通过参数类[__PARAMETERS+0x68+0x6C]获取。由此如果需要检测WMI通过Invoke-Method的方法进行创建函数,设置注册表等行为,可以通过检测IWbemServices::ExecMethod的调用实现。

0x5 参考文献

评论

H

HaCky

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

memcache安全 文章:1 篇
运维安全 文章:62 篇
PHP安全 文章:45 篇
无线安全 文章:27 篇
Ruby安全 文章:2 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

路人甲@@

https://github.com/ice-doom/CodeQLRule 代

S

shungli923

师傅,催更啊!!!

1

1ue1uekin8

6

素十八

F

f0zz

给大佬点赞

目录