- 本文转载于跳跳糖安全社区,原文链接为https://tttang.com/archive/1640/
0x0 前言
这是WMI的第三篇文章,本文主要调式分析WMI消费者的工作原理,进而提出WMI的检测思路。本文首先介绍了本次分析所需要了解的WMI基本组件和底层协议(RPC),然后通过调式网上的RPC客户端和服务端的通信,了解RPC的原理,接着通过分析两个典型的WMI利用(查询数据,执行函数),了解WMI的检测,由于WMI调试相关资料过少,没有进行自我订正,可能存在错误,或者重大错误,希望有了解的大佬积极斧正。
0x1 WMI组件介绍
这一节内容截取于软件调试补编。
WMI大多数文件都保存在%system32%\wbem文件夹下,其中下面文件是本次调试分析中使用到的
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
函数是一个不定参数函数,从第三个参数开始,传入的是调用的服务端函数所需要的参数。
在MulNdrpInitializeContextFromProc+0x4B处,将参数堆栈保存在pStubMsg.pContext结构体中。
NdrpClientMarshal函数相当于格式化参数等所需要的数据,便于远程调用,在函数调用之前,可以看到RpcMsg->Buffer并不存在数据,但是在调用NdrpClientMarshal之后,已经将[In]参数传入RpcMsg->Buffer中(可能_RPC_MESSAGE结构的地址不一样是因为这是两次不同的调试)。
在经过NdrpClientMarshal函数序列化之后,调用NdrpSendReceive函数发送NDR数据
在OSF_CCALL::SendReceiveHelper+0x48处,将RpcMsg->Buffer赋值到OSF_CCALL类偏移0x100处,接着调用OSF_CCALL::FastSendReceive函数继续发送数据。
0x2.2 服务端接收数据
从相关介绍中,我了解到NdrServerCall2作为服务端入口函数存在的,但是服务端并不直接调用NdrServerCall2接收和传送客户端的数据。有关服务端在进行PRC调用的时候,接收,调用,以及返回数据的函数堆栈如下:
在OSF_SCALL::DispatchHelper函数中,会调用RPC_INTERFACE::DispatchToStub函数,其中第二个参数应该为_RPC_MESSAGE结构体(堆栈中应为第一个,因为又在this指针)
接着,调用DispatchToStubInCNoAvrf函数,其目的是将_RPC_MESSAGE传入NdrServerCall2。然后调用NdrStubCall2函数。
其实,NdrStubCall2函数主要作用是根据_RPC_MESSAGE提供的pRpcMsg->ProcNum信息,获取服务端内对应的函数,根据pRpcMsg->Buffer获取参数,继而调用Invoke函数。以下是部分代码。另外,Marshal NDR数据的时候,也是和之前客户端相反的,客户端先Marshal成NDR数据,然后发送,等接收后在UnMarshal。而服务端是先UnMarshal,然后在执行,最后Marshal。
在Invoke函数,显然可以看到将两个参数传入需要被调用函数中。
很显然,当调用完NdrpServerMarshal之后,便在pRpcMsg->Buffer中保存了结果
0x2.3 服务端发送数据
在OSF_SCALL::DispatchHelper函数中,在执行完RPC_INTERFACE::DispatchToStub函数(执行Invoke函数)之后,便会调用OSF_SCALL::Send函数,第二个参数(this + 196)是不是很熟悉,保存的就是_RPC_MESSAGE结构体。看来在传输过程中,RPC主要传输的是_RPC_MESSAGE。
0x2.4 客户端接收数据
在OSF_CCALL::SendNextFragment中,调用OSF_CCONNECTION::SendFragment函数,其中,这里的a4,对应的其实是pContext,
在OSF_CCALL::FastSendReceive函数中,接着程序会调用OSF_CCALL::ActuallyProcessPDU
函数,其中Src保存的是pContxt,跟准确的表达也就是[InOut]参数,在调用之前,可以看到BufferLength为4,即传入了一个参数的大小,当调用完成之后,BufferLength变为了48,且buffer中也有了返回的结果。
经过NdrpSendReceive函数之后,_RPC_MESSAGE.buffer(同_MIDL_STUB_MESSAGE.Buffer)却存储参数堆栈
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结构体则是一个包含域名,用户名密码的结构体。
|
|
最终调用CDCOMTrans::DoActualCCI
函数,在CDCOMTrans::DoActualCCI
函数中,最终会调用CoCreateInstanceEx函数,CoCreateInstanceEx函数可以在指定的远程计算机上创建与给定 CLSID 关联的单个未初始化对象。而CoCreateInstance也可以创建一个实例,但是CoCreateInstance与CoCreateInstanceEx函数的区别在于CoCreateInstanceEx可以创建远程计算机的实例。 CoCreateInstanceEx函数的第一个参数是CLSID,表示要实例化对象的CLSID。在上一篇文章中,检测远程WMI连接的CLSID的值为8BC3F05E-D86B-11D0-A075-00C04FB68820。这就是为什么只要针对这个CLSID检测就可以判断是WMI远程连接了。
0x3.2 WMI查询
WMI查询操作,是通过wminet_utils模块的ExecQueryWmi
函数调用fastprox模块的CWbemSvcWrapper::XWbemServices::ExecQuery函数,而CWbemSvcWrapper::XWbemServices::ExecQuery的第二第三个参数分别表示执行的查询语句的类型,和SQL语句的内容。WMI拥有自己的查询语句,即WQL。
最终经过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表示数组首地址,该数组有多个元素构成。
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个参数是指向传入参数的类。
微软官方对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结构。
CWbemInstance::Put
底层主要通过CWbemInstance::SetPropValue
函数实现,首先判断是否是系统属性名,然后通过CClassPart::FindPropertyInfo
函数获取Property信息,接着依次调用CInstancePart::SetActualValue
函数,CUntypedValue::LoadFromCVar
函数。和CFastHeap::AllocateString
函数。并在CFastHeap::AllocateString
完成属性值的设置。
调用CInstancePart::SetActualValue
函数,其类是CInstancePart,父类为CWbemInstance类,显然可以得出在CWbemInstance+0x68的偏移处为CInstancePart
类。
接着调用CUntypedValue::LoadFromCVar
,其中,第三个参数为(this + 0x6C)
,其实这是一个CFastHeap类,其位于CInstancePart
类的第0x6C的偏移处。
最终调用CFastHeap::AllocateString
函数,完成对InParameters对象的赋值。v6其实就是等于[this]+pIndex。
简单总结一下,假设CWbemInstance位于0x06505990,通过调用CInstancePart::SetActualValue
函数,可知CInstancePart的地址位于CWbemInstance + 0x68
即0x065059f8。然后通过调用CUntypedValue::LoadFromCVar
函数可知,CFastHeap的地址位于CInstancePart + 0x6C
即06505ar64的地址。通过获取对CFastHeap取值,就可以知道参数的地址。
0x4.4 总结
根据上述,我们可知,IWbemServices::ExecMethod
函数的第一,第二,第五个参数分别表示的是Class名,函数名,参数类。其中参数可以通过参数类[__PARAMETERS+0x68+0x6C]获取。由此如果需要检测WMI通过Invoke-Method的方法进行创建函数,设置注册表等行为,可以通过检测IWbemServices::ExecMethod
的调用实现。