Skip to content

第四部分 DCOM 与 WMI

第 6 章 DCOM 与 WMI

6.1 DCOM 编程基础 — COM / ORPC / 对象引用

RPC 是一种计算机通信协议:它允许运行于一台计算机的程序调用另一个地址空间(通常是开放网络中一台计算机)的子程序,对程序员而言就像调用本地程序一样。DCOM 则是基于 RPC 的远程 COM 对象调用。

DCOM协议栈

DCOM 协议概述

对象 RPC 调用和 PDU 正文请求

对象 RPC 调用和 PDU 主体响应

首先我们来介绍下dcom编程涉及的一些关键词

  • activation:在DCOM协议中,客户端提供对象类的CLSID并 从该 对象类或能够创建此类对象的类工厂获取对象的机制
  • CID:每个 ORPC 调用都在 ORPCTHIS 结构中携带一个。延续已有因果链(causality)的新调用复用该链的 CID;开启新因果链的调用分配新的 CID。CID(因果标识符)用于防止 ORPC 调用中的死锁。
  • class factory:一个对象,其目的是从特定对象类创建对象
  • CLSID:标识DCOM对象类或COM类的id,常用CLSID如下即为dcomrt.py开头的静态变量
名称 GUID 用途 章节
CLSID_ActivationContextInfo {000001a5-0000-0000-c000-000000000046} ActivationContextInfoData 的激活属性CLSID 2.2.22.2.5
CLSID_ActivationPropertiesIn {00000338-0000-0000-c000-000000000046} ActivationPropertiesIn 的 OBJREF_CUSTOM 解封器 CLSID 3.1.2.5.2.3.23.1.2.5.2.3.3
CLSID_ActivationPropertiesOut {00000339-0000-0000-c000-000000000046} ActivationPropertiesOut 的 OBJREF_CUSTOM 解封器 CLSID 3.1.2.5.2.3.23.1.2.5.2.3.3
CLSID_CONTEXT_EXTENSION {00000334-0000-0000-c000-000000000046} 上下文 (2) ORPC 扩展的 ORPC_EXTENT 标识符 2.2.21.4
CLSID_ContextMarshaler {0000033b-0000-0000-c000-000000000046} 上下文的 OBJREF_CUSTOM 解封器 CLSID (2) 2.2.20
CLSID_ERROR_EXTENSION {0000031c-0000-0000-c000-000000000046} 错误信息 ORPC 扩展的 ORPC_EXTENT 标识符 2.2.21.1
CLSID_ErrorObject {0000031b-0000-0000-c000-000000000046} 用于错误信息的 OBJREF_CUSTOM 解封器 CLSID 2.2.21.2
CLSID_InstanceInfo {000001ad-0000-0000-c000-000000000046} InstanceInfoData 的激活属性 CLSID 2.2.22.2.3
CLSID_InstantiationInfo {000001ab-0000-0000-c000-000000000046} InstantiationInfoData 的激活属性 CLSID 2.2.22.2.1
CLSID_PropsOutInfo {00000339-0000-0000-c000-000000000046} PropsOutInfo 的激活属性 CLSID 2.2.22.2.9
CLSID_ScmReplyInfo {000001b6-0000-0000-c000-000000000046} ScmReplyInfoData 的激活属性 CLSID 2.2.22.2.8
CLSID_ScmRequestInfo {000001aa-0000-0000-c000-000000000046} ScmRequestInfoData 的激活属性 CLSID 2.2.22.2.4
CLSID_SecurityInfo {000001a6-0000-0000-c000-000000000046} SecurityInfoData 的激活属性 CLSID 2.2.22.2.7
CLSID_ServerLocationInfo {000001a4-0000-0000-c000-000000000046} LocationInfoData 的激活属性 CLSID 2.2.22.2.6
CLSID_SpecialSystemProperties {000001b9-0000-0000-c000-000000000046} SpecialPropertiesData 的激活属性 CLSID 2.2.22.2.2
IID_IActivation {4d9f4ab8-7d1c-11cf-861e-0020af6e7c57} IActivation 的RPC接口 UUID 3.1.2.5.2.1
IID_IActivationPropertiesIn {000001A2-0000-0000-C000-000000000046} pActProperties OBJREF 结构的iid字段的值 3.1.2.5.2.3.23.1.2.5.2.3.3
IID_IActivationPropertiesOut {000001A3-0000-0000-C000-000000000046} ppActProperties OBJREF 结构的iid字段的值 3.1.2.5.2.3.23.1.2.5.2.3.3
IID_IContext {000001c0-0000-0000-C000-000000000046} 上下文结构的iid字段的值。 2.2.20
IID_IObjectExporter {99fcfec4-5260-101b-bbcb-00aa0021347a} IObjectExporter 的 RPC 接口 UUID 3.1.2.5.1
IID_IRemoteSCMActivator {000001A0-0000-0000-C000-000000000046} IRemoteSCMActivator 的 RPC 接口 UUID 3.1.2.5.2.2
IID_IRemUnknown {00000131-0000-0000-C000-000000000046} IRemUnknown 的 RPC 接口 UUID 3.1.1.5.6
IID_IRemUnknown2 {00000143-0000-0000-C000-000000000046} IRemUnknown2 的 RPC 接口 UUID 3.1.1.5.7.1
IID_IUnknown {00000000-0000-0000-C000-000000000046} IUnknown 的 RPC 接口 UUID 3.1.1.5.8
  • COM:一种面向对象的编程模型,它定义了对象如何在单个进程内或进程之间进行交互.在COM中,客户端可以通过在对象上实现的接口访问对象

关于com编程的概念理解,可以看知乎上灵剑的回答和一篇com编程入门

https://www.zhihu.com/question/49433640/answer/115952604

https://blog.51cto.com/u_15075510/3505281

简单讲一下:COM 是一种规范,而不是实现。当使用 C++ 实现时,COM 组件就是一个 C++ 类,COM 接口就是继承自 IUnknown 的纯虚类,COM 组件就是实现相应 COM 接口的 C++ 类。COM 规范规定,任何组件或接口都必须从 IUnknown 接口继承而来。IUnknown 定义了 3 个重要函数:QueryInterface、AddRef 和 Release。其中 QueryInterface 负责组件对象上的接口查询,AddRef 用于增加引用计数,Release 用于减少引用计数。引用计数是 COM 中非常重要的概念,它很好地解决了组件对象的生命周期问题,即 COM 组件何时被销毁、由谁来销毁。COM 规范规定,每个组件都必须实现一个与之对应的类工厂(Class Factory)。类工厂也是一个 COM 组件,它实现了 IClassFactory 接口。在 IClassFactory 的接口函数 CreateInstance 中,才能使用 new 操作生成一个 COM 组件类对象实例。

实现一个COM组件,需要完成以下工作:

+ COM组件接口
COM组件接口是一个继承IUnknown的抽象类

+ COM组件实现类
就是具体的功能实现类,一个 COM 组件实现类可以同时实现多个 COM 接口

+ COM组件创建工厂
通过类工厂来创建com组件实现类的实例

+ COM组件注册
COM组件需要使用regsvr32工具注册到注册表
DllGetClassObject:用于获得类工厂指针 
DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载COM组件
DllRegisterServer:将COM组件注册到注册表中 
DllUnregisterServer:删除注册表中的COM组件的注册信息 
DLL还有一个可选的入口函数DllMain,可用于初始化和释放全局变量 
DllMain:DLL的入口函数,在LoadLibrary和FreeLibrary时都会调用
regsvr32 ComTest_Server.dll
COM 组件的使用流程:初始化 COM 库;CoCreateInstance 通过 CLSID 获取 COM 组件对象实例(返回默认的 IID_IUnknown 接口);调用 QueryInterface 通过接口 IID 获取接口指针;最后调用接口方法。QueryInterface 负责查找接口,并通过第二个 OUT 参数返回实现该接口的对象指针。
CoInitialize(NULL);    // COM 库初始化
// ...
IUnknown *pUnk = NULL;
IObject *pObj = NULL;
// 创建组件对象,CLSID_XXX 为 COM 组件类的 GUID(class id),返回默认 IID_IUnknown 接口
HRESULT hr = CoCreateInstance(CLSID_XXX, NULL, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown, (void **)&pUnk);
if (S_OK == hr)
{
    // 获取接口,IID_XXX 为组件接口的 GUID(interface id)
    hr = pUnk->QueryInterface(IID_XXX, (void **)&pObj);
    if (S_OK == hr)
    {
        // 调用接口方法
        pObj->DoXXX();
    }
    // 释放组件对象
    pUnk->Release();
}
// ...
// 释放 COM 库
CoUninitialize();
DCOM实现
创建接口和对象
使用 MIDL 脚本定义自定义接口。
使用 MIDL 编译器生成 C++ 头文件和编组代码。
在实现 COM 对象时,选择嵌套或继承技术。
实现(编码)接口。
按照身份规则编写IUnknown::QueryInterface方法。
按照生命周期规则编写IUnknown::AddRef和IUnknown::Release方法。
实现自己的接口方法。
创建类工厂
决定是否要公开自定义或标准工厂接口。
对于标准工厂接口,实现CreateInstance和LockServer方法。
如果需要支持动态调用,实现IDispatch接口。
如果您希望支持任何其他自定义或标准接口,请实现它们。
  • context:执行环境的上下文属性或表示资源与一组在客户端和服务器之间交换的消息之间的关联
  • context identifier:标识上下文的guid
  • 动态端点:在运行时请求和分配的特定于网络的服务器地址
  • endpoint:用于远程过程调用的远程过程调用 (RPC) 服务器进程的网络特定地址。终结点的实际名称和类型取决于 正在使用的RPC协议序列例如,对于 TCP 上的 RPC(RPC 协议序列 ncacn_ip_tcp),端点可能是 TCP 端口 1025。对于服务器消息块上的 RPC(RPC 协议序列 ncacn_np),端点可能是命名管道的名称
  • SPN:客户端用来标识服务以进行相互身份验证的名称,SPN由两部分或三部分组成,每个部分由正斜杠 ('/') 分隔。第一部分是服务类,第二部分是主机名,第三部分(如果存在)是服务名称。例如,“ldap/dc-01.fabrikam.com/fabrikam.com”是一个由三部分组成的SPN,其中“ldap”是服务类名称,“dc-01.fabrikam.com”是主机名,“ fabrikam.com”是服务名称。
  • envoy context:作为获取对象引用的结果被封送并返回给客户端的上下文
  • interface:接口,组件对象模型 (COM)服务器中的规范,描述如何访问类的方法
  • IDL:接口定义语言,描述接口的语法
  • IID:标识接口的guid
  • object:对象,在[DCOM协议中,实现一个或多个对象远程协议 (ORPC) 接口并在object exporter范围内由对象标识符 (OID)唯一标识
  • IRemUnknown接口:一个 ORPC 接口,包含用于调用远程对象上的 QueryInterface、AddRef 和 Release 的方法。
  • IRemUnknown2 接口:扩展 IRemUnknown 功能的 ORPC 接口。
  • object exporter : object容器,每个object exporter实例必须为其IRemUnknown接口创建一个IPID条目。如果对象导出器实例处于COMVERSION5.6 或更高版本,它还必须为IRemUnknown2接口创建一个 IPID 条目。对象导出器实例必须创建其 IPID 条目,如下所示:
  • 它必须分配一个 IPID 并将其设置在 IPID 条目中。
  • 它必须将IPID 条目中的IID设置为 IRemUnknown 接口或 IRemUnknown2 接口的 IID。
  • 它必须指示 RPC 侦听 IRemUnknown 接口或 IRemUnknown2 接口,如 [C706] 部分 3.1.20 (rpc_server_register_if) 中所指定。
  • 它必须将条目中的对象指针设置为对象导出器实现 IRemUnknown 接口或 IRemUnknown2 接口的对象指针。
  • 它必须将 IPID 条目中的 OID 和 OXID 设置为从解析器获得的相应值。
  • 它必须将 IPID 条目添加到 IPID 表中。
  • object class:在DCOM协议中,由CLSID标识的一类对象,可以通过activation CLSID获得 其成员。对象类通常与一组公共接口相关联,这些接口由对象类中的所有对象实现
在COM编程中,一个接口包含若干相关方法,一个对象实现若干接口,类工厂是创建或实例化其他COM对象的特殊COM对象。
  • object exporter ID(OXID):64位数字,用于唯一标识对象服务器中的object exporter
  • OXID 解析:获取与对象导出器通信所需的远程过程调用 (RPC) 绑定信息的过程。对象解析器服务实现以下RPC接口:
  • IObjectExporter 方法。
  • IActivation:包含用于创建对象和类工厂的方法。
  • IRemoteSCMActivator:包含更多用于创建对象和类工厂的方法。
  • 对象标识符 (OID):标识对象的唯一64位数字

在 Internet 或 Intranet 网络环境下,ORPC 仍使用标准 RPC 数据包,附加 DCOM 专用信息——接口指针标识符(IPID,interface pointer identifier)、版本信息和扩展信息——作为调用与返回的附加参数传送,其中 IPID 标识调用被处理的远程机器上特定对象的特定接口。DCOM 客户程序必须周期性地 ping 远程机器上的对象,以保证客户与对象一直处于连接状态。

  • IPID(接口指针标识符)表:

IPID 标识特定进程内特定对象实例上的一个特定接口(接口指针)。

由 IPID 键控的对象接口条目表。每个条目必须包含:

  • 接口的IPID。
  • 接口的IID。
  • 对象的 OID。
  • 对象导出器的OXID
  • 对象引用的公共引用计数
  • 对象引用的私有引用计数。

  • OXID 表:客户端已知的对象导出器的条目表,由 OXID 键控。每个条目必须包含:

  • 对象导出器的 OXID。

  • 对象导出器的RPC绑定信息。
  • 对象导出器的 IRemUnknown 接口的 IPID。
  • 对象导出器的身份验证级别提示。

  • OID 表:客户端已知对象的条目表,由 OID 键控。每个条目必须包含:

  • 对象的 OID。

  • 对象上接口的 IPID 列表。
  • 对象导出器的 OXID。
  • STDOBJREF中包含的 saResAddr 字段的 STRINGBINDING 的实现定义散列。
  • 如果对象参与ping,则必须设置为 True 的布尔垃圾收集标志;请参阅第 2.2.18.2 节中的 SORF_NOPING标志。

  • 解析器表:客户端已知的对象解析器的条目表,由 STRINGBINDING 哈希键控。每个条目必须包含:

  • 一个 STRINGBINDING 散列。

  • 对象解析器的 DUALSTRINGARRAY。
  • 包含对象解析器的 ping 集标识符的 SETID。
  • 对象解析器的 RPC 绑定信息。

  • SETID 表:客户端引用的ping 集条目表,由 SETID 键控。每个条目必须包含:

  • ping 集的 SETID。

  • ping 集中的OID列表。
  • 序列号

  • 对象引用(object reference):在 DCOM 协议中对对象的引用,在线路上表示为 OBJREF。对象引用使对象能被该对象的对象导出器(object exporter)之外的实体访问。

  • OBJREF:对象引用的封送形式。

以上关于对象引用和对象编组的描述来自微软官方文档的直译。通俗地讲:一个接口指针本质上是本机内存中的一个地址,想把它传给客户端调用,就需要附上 flag(标识对象引用类型)、IID(唯一标识接口)、对象引用(接口指针)等信息,打包成 OBJREF 块进行传输。服务端把它发给客户端,客户端解包后获得可用的接口指针,即可进行远程对象调用。标准编组的 OBJREF 中包含在网络中唯一定位接口指针所需的全部信息(例如 OXID、OXID 解析器地址等)。客户端接收到 OBJREF 后,COM 会将其解组为指向本地代理的接口指针;客户端在接口指针上的任何方法调用,都会经本地代理转发到关联的远程存根(stub),再由存根交给服务器端的目标对象。

OBJREF 的结构

  • OXID Resolve:
  • 它存储与远程对象连接所需的RPC字符串绑定,并将其提供给本地客户端。
  • 它向本地计算机仍持有客户端引用的远程对象发送 ping 消息,并接收在本地计算机上运行的对象的 ping 消息。OXID 解析器的这一职责支撑了 COM 的垃圾回收机制。

6.2 dcomrt.py — DCOM 运行时(DCOMConnection / INTERFACE / IActivation)

有了上面的基础,接着看 dcomrt 模块:文件开头定义了常用 DCOM 类的 CLSID 静态常量和错误处理函数,随后定义了 DCOM 通信(ORPC 协议)中使用的数据类型结构体与各类 flag 值。

紧接着定义了上下文的句柄类和DCOM协议版本类

class handle_t(NDRSTRUCT):
    ......
class COMVERSION(NDRSTRUCT):
    .....
class PCOMVERSION(NDRPOINTER):
    .....

下面是 ORPC 通信中二进制大对象(BLOB)、数据编码、激活、OXID 解析、创建远程对象等结构体的定义:

class ORPC_EXTENT(NDRSTRUCT):
    ....
class BYTE_ARRAY(NDRUniConformantArray):
    ....
class OBJREF(NDRSTRUCT):
    ....

之后是 DCOM 连接类,可通过它建立 DCOM 通信连接、创建远程对象、对服务器进行 ping:

class DCOMConnection:
    """
    This class represents a DCOM Connection. It is in charge of establishing the 
    DCE connection against the portmap, and then launch a thread that will be 
    pinging the objects created against the target.
    In theory, there should be a single instance of this class for every target
    """
    PINGTIMER = None
    OID_ADD = {}
    OID_DEL = {}
    OID_SET = {}
    PORTMAPS = {}

    def __init__(self, target, username='', password='', domain='', lmhash='', nthash='', aesKey='', TGT=None, TGS=None,
                 authLevel=RPC_C_AUTHN_LEVEL_PKT_PRIVACY, oxidResolver=False, doKerberos=False, kdcHost=None):
        self.__target = target
        self.__userName = username
        self.__password = password
        self.__domain = domain
        self.__lmhash = lmhash
        self.__nthash = nthash
        self.__aesKey = aesKey
        self.__TGT    = TGT
        self.__TGS    = TGS
        self.__authLevel = authLevel
        self.__portmap = None
        self.__oxidResolver = oxidResolver
        self.__doKerberos = doKerberos
        self.__kdcHost = kdcHost
        self.initConnection()
        .....
def pingServer(cls):
    ......
def CoCreateInstanceEx(self, clsid, iid):
    ......

ORPCTHIS 实例类

ORPCTHIS 结构是在 ORPC 请求 PDU 中发送的第一个(隐式)参数,用于将 ORPC 扩展数据发送到服务器。ORPCTHIS 结构也作为激活 RPC 请求中的显式参数发送。

 typedef struct tagORPCTHIS {
   COMVERSION version;
   unsigned long flags;
   unsigned long reserved1;
   CID cid;
   [unique] ORPC_EXTENT_ARRAY* extensions;
 } ORPCTHIS;

ORPCTHIS 主要用于构造 RPC 激活请求:

classInstance = CLASS_INSTANCE(ORPCthis, stringBindings)
        return IRemUnknown2(INTERFACE(classInstance, b''.join(resp['ppInterfaceData'][0]['abData']), ipidRemUnknown,target=self.__portmap.get_rpc_transport().getRemoteHost()))

INTERFACE 接口类

初始化interface接口通信必要的参数

  if interfaceInstance is not None:
            self.__target = interfaceInstance.get_target()
            self.__iPid = interfaceInstance.get_iPid()
            self.__oid  = interfaceInstance.get_oid()
            self.__oxid = interfaceInstance.get_oxid()
            self.__cinstance = interfaceInstance.get_cinstance()
            self.__objRef = interfaceInstance.get_objRef()
            self.__ipidRemUnknown = interfaceInstance.get_ipidRemUnknown()

process_interface 函数处理对象引用数据包的封送格式。

connect 函数的逻辑:

  • 如果已存储连接信息,则根据 target 和 oxid 复用当前线程的连接,并按 iid 通过 alter_ctx 绑定远程 RPC 接口、建立上下文;
  • 如果不存在 oxid 连接信息,则解析绑定地址,通过 DCERPC 工厂类绑定接口建立 TCP 连接,并设置 credentials 凭证与 Kerberos 信息,最终保存连接信息。
 def connect(self, iid = None):
        if (self.__target in INTERFACE.CONNECTIONS) is True:
            if current_thread().name in INTERFACE.CONNECTIONS[self.__target] and \
                            (self.__oxid in INTERFACE.CONNECTIONS[self.__target][current_thread().name]) is True:
                dce = INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['dce']
                currentBinding = INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['currentBinding']
                if currentBinding == iid:
                    # We don't need to alter_ctx
                    pass
                else:
                    newDce = dce.alter_ctx(iid)
                    INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['dce'] = newDce
                    INTERFACE.CONNECTIONS[self.__target][current_thread().name][self.__oxid]['currentBinding'] = iid
            else:
                stringBindings = self.get_cinstance().get_string_bindings()
                # No OXID present, we should create a new connection and store it
                stringBinding = None
                isTargetFQDN = self.is_fqdn()
                .....
                .....
                if binding.upper().find(self.get_target().upper()) >= 0:
                stringBinding = 'ncacn_ip_tcp:' + strBinding['aNetworkAddr'][:-1]                         .....

IRemUnknown 远程Unknown接口类

实现了 RemQueryInterface(根据 IPID 查询接口)、RemAddRef、RemRelease 三个方法。

IObjectExporter类

实现了ObjectExporter,包含解析oxid,ping,检查ServerAlive功能

def ResolveOxid(self, pOxid, arRequestedProtseqs):
    ....
def SimplePing(self, setId):
    ....
def ServerAlive(self):
    ....

IActivation 激活类

IActivation 是由服务控制管理器(SCM)暴露的 RPC 接口(不是 COM 接口;旧文献中也称 IRemoteActivation)。SCM 运行在每台计算机上,进程名为 RPCSS.EXE。IActivation 只有一个方法 RemoteActivation:

 error_status_t RemoteActivation(
   [in] handle_t hRpc,
   [in] ORPCTHIS* ORPCthis,
   [out] ORPCTHAT* ORPCthat,
   [in] GUID* Clsid,
   [in, string, unique] wchar_t* pwszObjectName,
   [in, unique] MInterfacePointer* pObjectStorage,
   [in] DWORD ClientImpLevel,
   [in] DWORD Mode,
   [in, range(1, MAX_REQUESTED_INTERFACES)] 
     DWORD Interfaces,
   [in, unique, size_is(Interfaces)] 
     IID* pIIDs,
   [in, range(0, MAX_REQUESTED_PROTSEQS)] 
     unsigned short cRequestedProtseqs,
   [in, size_is(cRequestedProtseqs)] 
     unsigned short aRequestedProtseqs[],
   [out] OXID* pOxid,
   [out] DUALSTRINGARRAY** ppdsaOxidBindings,
   [out] IPID* pipidRemUnknown,
   [out] DWORD* pAuthnHint,
   [out] COMVERSION* pServerVersion,
   [out] HRESULT* phr,
   [out, size_is(Interfaces), disable_consistency_check] 
     MInterfacePointer** ppInterfaceData,
   [out, size_is(Interfaces), disable_consistency_check] 
     HRESULT* pResults
 );

它被设计用来激活远程计算机上的COM对象。这是一个非常强大的功能,但在纯RPC中没有提供.通过IRemoteActivation接口,一台机器上的SCM与另一台机器上的SCM联络,要求它激活一个对象,即客户机上的SCM调用服务器上SCM的IRemoteActivation::RemoteActivation,要求它激活以CLSID(方法第四个参数)为标识的对象。RemoteActivation返回一个激活对象的封送接口指针和两个特殊的值:接口指针标识(IPID)和对象对外联络标识(OXID)。每种支持的网络协议,都有一个熟知的 SCM 端口,每个端口都标识了一个基于网络协议的虚拟通讯通道。例如,经典 DCOM 教材记载使用 TCP 或 UDP 时端口为 1066、命名管道为 \pipe\mypipe;现代 Windows 上 RPC 端点实际由端点映射器(TCP 135)动态解析。SCM 常用协议如下表。

Constant/value Description
ncacn_nb_tcpConnection-oriented NetBIOS over Transmission Control Protocol (TCP) Client only: MS-DOS, Windows 3.x Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT
ncacn_nb_ipxConnection-oriented NetBIOS over Internet Packet Exchange (IPX) Client only: MS-DOS, Windows 3.x Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT
ncacn_nb_nbConnection-oriented NetBIOS Enhanced User Interface (NetBEUI) Client only: MS-DOS, Windows 3.x Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT, Windows Me, Windows 98, Windows 95
ncacn_ip_tcpConnection-oriented Transmission Control Protocol/Internet Protocol (TCP/IP) Client only: MS-DOS, Windows 3.x, and Apple Macintosh Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT, Windows Me, Windows 98, Windows 95
ncacn_npConnection-oriented named pipes Client only: MS-DOS, Windows 3.x, Windows 95 Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT
ncacn_spxConnection-oriented Sequenced Packet Exchange (SPX) Client only: MS-DOS, Windows 3.x Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT, Windows Me, Windows 98, Windows 95
ncacn_dnet_nspConnection-oriented DECnet transport Client only: MS-DOS, Windows 3.x
ncacn_at_dspConnection-oriented AppleTalk DSP Client: Apple Macintosh Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT
ncacn_vns_sppConnection-oriented Vines scalable parallel processing (SPP) transport Client only: MS-DOS, Windows 3.x Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT
ncadg_ip_udpDatagram (connectionless) User Datagram Protocol/Internet Protocol (UDP/IP) Client only: MS-DOS, Windows 3.x Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT
ncadg_ipxDatagram (connectionless) IPX Client only: MS-DOS, Windows 3.x Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT
ncadg_mqDatagram (connectionless) over the Microsoft Message Queue Server (MSMQ) Client only: Windows Me/98/95 Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT Server 4.0 with SP3 and later
ncacn_httpConnection-oriented TCP/IP using Microsoft Internet Information Server as HTTP proxy Client only: Windows Me/98/95 Client and Server: Windows Server 2003, Windows XP, Windows 2000
ncalrpcLocal procedure call Client and Server: Windows Server 2003, Windows XP, Windows 2000, Windows NT, Windows Me, Windows 98, Windows 95

函数功能实现和所需参数如下

    def RemoteActivation(self, clsId, iid):
        # Only supports one interface at a time
        self.__portmap.bind(IID_IActivation)
        ORPCthis = ORPCTHIS()  # ORPCthis,扩展必须为 null
        ORPCthis['cid'] = generate()
        ORPCthis['extensions'] = NULL
        ORPCthis['flags'] = 1

        request = RemoteActivation()
        request['Clsid'] = clsId  # 指定要创建对象的 CLSID
        request['pwszObjectName'] = NULL  # 用于初始化对象的字符串
        request['pObjectStorage'] = NULL  # 用于初始化对象的 objref
        request['ClientImpLevel'] = 2  # 该值在接收时被忽略
        request['Mode'] = 0  # 激活类工厂时为 0xFFFFFFFF,否则为 0
        request['Interfaces'] = 1  # pIID 元素数量

        _iid = IID()
        _iid['Data'] = iid

        request['pIIDs'].append(_iid)  # 要创建对象上请求的接口 id 数组
        request['cRequestedProtseqs'] = 1  # aRequestedProtseqs 中元素的数量,必须在 1 和 MAX_REQUESTED_PROTSEQS 之间
        request['aRequestedProtseqs'].append(7)  # 客户端支持的 RPC 协议序列标识符

        resp = self.__portmap.request(request)

        # Now let's parse the answer and build an Interface instance

        ipidRemUnknown = resp['pipidRemUnknown']  # 对象导出器 IRemUnknown 的 IPID

        Oxids = b''.join(pack('<H', x) for x in resp['ppdsaOxidBindings']['aStringArray'])  # object exporter 的 OXID 绑定数据
        strBindings = Oxids[:resp['ppdsaOxidBindings']['wSecurityOffset']*2]
        securityBindings = Oxids[resp['ppdsaOxidBindings']['wSecurityOffset']*2:]

        done = False
        stringBindings = list()
        while not done:
            if strBindings[0:1] == b'\x00' and strBindings[1:2] == b'\x00':
                done = True
            else:
                binding = STRINGBINDING(strBindings)  # 对象导出器支持的字符串与安全绑定(不能为 NULL,应包含端点)
                stringBindings.append(binding)
                strBindings = strBindings[len(binding):]

        done = False
        while not done:
            if len(securityBindings) < 2:
                done = True
            elif securityBindings[0:1] == b'\x00' and securityBindings[1:2 ]== b'\x00':
                done = True
            else:
                secBinding = SECURITYBINDING(securityBindings)
                securityBindings = securityBindings[len(secBinding):]

        classInstance = CLASS_INSTANCE(ORPCthis, stringBindings)
        return IRemUnknown2(INTERFACE(classInstance, b''.join(resp['ppInterfaceData'][0]['abData']), ipidRemUnknown,
                                      target=self.__portmap.get_rpc_transport().getRemoteHost()))

IRemoteSCMActivator 远程SCM激活类

实现了RemoteGetClassObject和RemoteCreateInstance

客户端使用 RemoteGetClassObject (Opnum 3) 方法为类工厂对象创建对象引用。

 HRESULT RemoteGetClassObject(
   [in] handle_t rpc,
   [in] ORPCTHIS* orpcthis,
   [out] ORPCTHAT* orpcthat,
   [in, unique] MInterfacePointer* pActProperties,
   [out] MInterfacePointer** ppActProperties
 );

客户端使用 RemoteCreateInstance (Opnum 4) 方法为实际对象创建对象引用。

 HRESULT RemoteCreateInstance(
   [in] handle_t rpc,
   [in] ORPCTHIS* orpcthis,
   [out] ORPCTHAT* orpcthat,
   [in, unique] MInterfacePointer* pUnkOuter,
   [in, unique] MInterfacePointer* pActProperties,
   [out] MInterfacePointer** ppActProperties
 );

MInterfacePointer 是一个 NDR 封包结构

 typedef struct tagMInterfacePointer {
   unsigned long ulCntData;
   [size_is(ulCntData)] byte abData[];
 } MInterfacePointer;

ulCntData: 这必须指定abData参数的大小(以字节为单位)。

在impacket中因为wmi功能基于dcom协议,所以主要是在wmiquery功能实现和wmiexec和dcomexec中建立dcom连接使用

之后调用ShellWindows\ShellBrowserWindow等com组件实现命令执行和shell

# eg.examples/dcomexec.py
from impacket.dcerpc.v5.dcomrt import DCOMConnection, COMVERSION
    .......
       dcom = DCOMConnection(addr, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash,
                              self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost)
        try:
            dispParams = DISPPARAMS(None, False)
            dispParams['rgvarg'] = NULL
            dispParams['rgdispidNamedArgs'] = NULL
            dispParams['cArgs'] = 0
            dispParams['cNamedArgs'] = 0

            if self.__dcomObject == 'ShellWindows':
                # ShellWindows CLSID (Windows 7, Windows 10, Windows Server 2012R2)
                iInterface = dcom.CoCreateInstanceEx(string_to_bin('9BA05972-F6A8-11CF-A442-00A0C90A8F39'), IID_IDispatch)
                iMMC = IDispatch(iInterface)
                resp = iMMC.GetIDsOfNames(('Item',))
                resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_METHOD, dispParams, 0, [], [])
                iItem = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData']))
                resp = iItem.GetIDsOfNames(('Document',))
                resp = iItem.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], [])
                pQuit = None
            elif self.__dcomObject == 'ShellBrowserWindow':
                # ShellBrowserWindow CLSID (Windows 10, Windows Server 2012R2)
                iInterface = dcom.CoCreateInstanceEx(string_to_bin('C08AFD90-F2A1-11D1-8455-00A0C91F3880'), IID_IDispatch)
                iMMC = IDispatch(iInterface)
                resp = iMMC.GetIDsOfNames(('Document',))
                resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], [])
                pQuit = iMMC.GetIDsOfNames(('Quit',))[0]
            elif self.__dcomObject == 'MMC20':
                iInterface = dcom.CoCreateInstanceEx(string_to_bin('49B2791A-B1AE-4C90-9B8E-E860BA07F889'), IID_IDispatch)
                iMMC = IDispatch(iInterface)
                resp = iMMC.GetIDsOfNames(('Document',))
                resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], [])
                pQuit = iMMC.GetIDsOfNames(('Quit',))[0]
            else:
                logging.fatal('Invalid object %s' % self.__dcomObject)
                return

6.3 dcom 子模块 — oaut / comev / scmp / vds / wmi

6.3.1 [MS-OAUT] oaut.py

对象链接与嵌入(OLE)自动化是 Microsoft 公司的 OLE 2.0 体系结构的组成部分。借助 OLE 自动化,无论使用哪种语言来编写应用程序,应用程序都可以在 OLE 自动化对象中公布它们的属性和方法。于是,其他应用程序(例如 MSSQL或 Microsoft Exchange)可以通过 OLE 自动化来利用这些属性和方法,从而集成这些对象。公布属性和方法的应用程序被称为 OLE 自动化服务器或对象,而访问那些属性和方法的应用程序被称为 OLE 自动化控制器。比如MSSQL可以通过sp_configure 开启OLE Automation Procedures通过Transact-SQL 批处理中实例化 OLE 自动化对象,OLE 自动化服务器是实现了 OLE IDispatch 接口的 COM 组件(对象)。OLE 自动化控制器是 COM 客户机,它通过 IDispatch 接口与自动化服务器进行通信。COM 是 OLE 的基础.

OLE Automation 的核心是 IDispatch,调用流程如下:

通用自动化调用

IDispatch::GetIDsOfNames的response返回的是想要调用的方法的DISPID,如果知道id的话也可以直接调用

优化的自动化调用

常见接口id如下

常量/值 描述
CLSID_RecordInfo{0000002F-0000-0000-C000-000000000046} RecordInfoData的 OBJREF_CUSTOM 解封器 CLSID 第 2.2.31 节)
IID_IRecordInfo{0000002F-0000-0000-C000-000000000046} pRecInfo OBJREF 结构的IID字段的值(参见2.2.28.2.1节)。
IID_IDispatch{00020400-0000-0000-C000-000000000046} 与 IDispatch接口关联的 GUID (请参阅第3.1节)。
IID_ITypeComp{00020403-0000-0000-C000-000000000046} 与 ITypeComp 接口关联的 GUID(请参阅第3.5节)。
IID_ITypeInfo{00020401-0000-0000-C000-000000000046} 与 ITypeInfo 接口关联的 GUID(请参阅第3.7节)。
IID_ITypeInfo2{00020412-0000-0000-C000-000000000046} 与 ITypeInfo2 接口关联的 GUID(请参阅第3.9节)。
IID_ITypeLib{00020402-0000-0000-C000-000000000046} 与 ITypeLib 接口关联的 GUID(请参阅第3.11节)。
IID_ITypeLib2{00020411-0000-0000-C000-000000000046} 与 ITypeLib2 接口关联的 GUID(请参阅第3.13节)。
IID_IUnknown{00000000-0000-0000-C000-000000000046} 与 IUnknown 接口关联的 GUID。
IID_IEnumVARIANT{00020404-0000-0000-C000-000000000046} 与 IEnumVARIANT 接口关联的 GUID(请参阅第3.3节)。
IID_NULL{00000000-0000-0000-0000-000000000000} 标识 NULL 值的 GUID(如 [C706] 部分 A1 nil UUID中指定)。

在模块中IDispatch类实现的方法如下

GetTypeInfoCount  确定自动化服务器是否提供类型描述信息
GetTypeInfo      提供对自动化服务器公开的类型描述信息的访问
GetIDsOfNames    将单个成员名称方法或属性名称和一组可选的参数名称映射到一组相应的整数DISPIDs可用于对IDispatch::Invoke的后续调用
Invoke           提供对自动化服务器公开的属性和方法的访问


 HRESULT GetIDsOfNames(
   [in] REFIID riid,
   必须等于 IID_NULL
   [in, size_is(cNames)] LPOLESTR* rgszNames
   必须是要映射的字符串数组数组中的第一个字符串必须指定服务器支持的方法或属性的名称任何附加字符串必须包含第一个字符串中的值指定的方法或属性的所有参数的名称映射必须不区分大小写
   [in, range(0,16384)] UINT cNames,
   必须等于要映射的名称的数量并且必须介于 0  16384 之间
   [in] LCID lcid,
   必须等于解释名称的区域设置 ID
   [out, size_is(cNames)] DISPID* rgDispId
   必须是由服务器填写的 DISPID 数组每个 DISPID 按位置对应于rgszNames中传递的名称之一
   如果严重性位设置为 0则该方法成功完成
   如果严重性位设置为 1 并且整个 HRESULT DWORD 与下表中的值不匹配则发生致命故障
   如果严重性位设置为 1 并且整个 HRESULT DWORD 与下表中的值匹配则发生故障
 );

  HRESULT Invoke(
   [in] DISPID dispIdMember,必须等于要调用的方法或属性的DISPID 
   [in] REFIID riid,必须等于 IID_NULL
   [in] LCID lcid,必须等于自动化服务器支持的区域设置 ID
   [in] DWORD dwFlags,必须是下表中指定的位标志的组合
   [in] DISPPARAMS* pDispParams,
 指针必须指向 定义传递给方法的参数的DISPPARAMS结构参数必须以pDispParams->rgvarg相反的顺序存储以便第一个参数是数组中索引最高的那个Byref 参数必须在此数组中标记为 VT_EMPTY 条目并改为存储在rgVarRef中 
   [out] VARIANT* pVarResult,指向将填充方法或属性调用结果的 VARIANT指针
   [out] EXCEPINFO* pExcepInfo,如果该值不为空且返回值为 DISP_E_EXCEPTION则该结构必须由自动化服务器填充否则它必须为scode 和wCode字段指定一个 0 并且必须在接收时忽略它
    [out] UINT* pArgErr,如果此值不为空且返回值为 DISP_E_TYPEMISMATCH  DISP_E_PARAMNOTFOUND则此参数必须等于 pDispParams->rgvarg 中第一个有错误的参数的索引否则接收方必须忽略该参数
   [in] UINT cVarRef,必须等于pDispParams中传递的 byref 参数的数量
    [in, size_is(cVarRef)] UINT* rgVarRefIdx,必须包含 cVarRef 个无符号整数每个整数是 pDispParams->rgvarg 中标记为 VT_EMPTY  byref 参数的索引
       [in, out, size_is(cVarRef)] VARIANT* rgVarRef 必须包含客户端在调用时设置的 byref 参数以及从调用成功返回时由服务器设置的参数此数组中的参数也必须以相反的顺序存储以便第一个 byref 参数在数组中具有最高索引
 );
含义
DISPATCH_METHOD 0x00000001 该成员作为方法调用。
DISPATCH_PROPERTYGET0x00000002 该成员作为属性或数据成员检索。
DISPATCH_PROPERTYPUT0x00000004 该成员被更改为属性或数据成员。
DISPATCH_PROPERTYPUTREF0x00000008 成员通过引用赋值而不是值赋值进行更改。仅当属性接受对对象的引用时,此标志才有效。
DISPATCH_zeroVarResult0x00020000 必须指定客户端对实际的 pVarResult [out] 参数不感兴趣。返回时,pVarResult参数必须指向一个 VT_EMPTY 变体,所有保留字段都设置为 0。
DISPATCH_zeroExcepInfo0x00040000 必须指定客户端对实际的 pExcepInfo [out] 参数不感兴趣。返回时, pExcepInfo必须指向一个 EXCEPINFO 结构,所有标量字段设置为 0,所有 BSTR 字段设置为 NULL。
DISPATCH_zeroArgErr0x00080000 必须指定客户端对实际的 pArgErr [out] 参数不感兴趣。返回时,pArgErr必须设置为 0。

核心方法是Invoke和GetIDsOfNames

    def GetIDsOfNames(self, rgszNames, lcid = 0):
        request = IDispatch_GetIDsOfNames()
        request['riid'] = IID_NULL
        for name in rgszNames:
            tmpName = LPOLESTR()
            tmpName['Data'] = checkNullString(name)
            request['rgszNames'].append(tmpName)
        request['cNames'] = len(rgszNames)
        request['lcid'] = lcid
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        IDs = list()
        for id in resp['rgDispId']:
            IDs.append(id)

        return IDs

    def Invoke(self, dispIdMember, lcid, dwFlags, pDispParams, cVarRef, rgVarRefIdx, rgVarRef):
        request = IDispatch_Invoke()
        request['dispIdMember'] = dispIdMember
        request['riid'] = IID_NULL
        request['lcid'] = lcid
        request['dwFlags'] = dwFlags
        request['pDispParams'] = pDispParams
        request['cVarRef'] = cVarRef
        request['rgVarRefIdx'] = rgVarRefIdx
        request['rgVarRef'] = rgVarRefIdx
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        return resp

在 dcomexec 的 ShellWindows 组件调用中就使用了 Invoke 函数:

class DCOMEXEC:
    ......
    def getInterface(self, interface, resp):
        # Now let's parse the answer and build an Interface instance
        objRefType = OBJREF(b''.join(resp))['flags']
        objRef = None
        if objRefType == FLAGS_OBJREF_CUSTOM:
            objRef = OBJREF_CUSTOM(b''.join(resp))
        elif objRefType == FLAGS_OBJREF_HANDLER:
            objRef = OBJREF_HANDLER(b''.join(resp))
        elif objRefType == FLAGS_OBJREF_STANDARD:
            objRef = OBJREF_STANDARD(b''.join(resp))
        elif objRefType == FLAGS_OBJREF_EXTENDED:
            objRef = OBJREF_EXTENDED(b''.join(resp))
        else:
            logging.error("Unknown OBJREF Type! 0x%x" % objRefType)

        return IRemUnknown2(
            INTERFACE(interface.get_cinstance(), None, interface.get_ipidRemUnknown(), objRef['std']['ipid'],
                      oxid=objRef['std']['oxid'], oid=objRef['std']['oxid'],
                      target=interface.get_target()))

    def run(self, addr, silentCommand=False):
        ......
        # ① 建立 DCOM 连接(内部完成 EPM 解析、SCM 激活与 OXID 解析)
        dcom = DCOMConnection(addr, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash,
                              self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost)
        try:
            dispParams = DISPPARAMS(None, False)   # Invoke 调用的通用参数容器
            ......

            if self.__dcomObject == 'ShellWindows':
                # ② 按 CLSID 激活 DCOM 组件,拿到默认的 IDispatch 接口
                # ShellWindows CLSID (Windows 7, Windows 10, Windows Server 2012R2)
                iInterface = dcom.CoCreateInstanceEx(string_to_bin('9BA05972-F6A8-11CF-A442-00A0C90A8F39'), IID_IDispatch)
                iMMC = IDispatch(iInterface)
                # ③ GetIDsOfNames:方法/属性名 → DISPID;Invoke:按 DISPID 调用
                resp = iMMC.GetIDsOfNames(('Item',))
                resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_METHOD, dispParams, 0, [], [])
                # ④ 属性返回的是封送接口指针,经 getInterface 解 OBJREF 还原为新接口
                iItem = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData']))
                resp = iItem.GetIDsOfNames(('Document',))
                resp = iItem.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], [])
                pQuit = None
            elif self.__dcomObject == 'ShellBrowserWindow':
                # ShellBrowserWindow CLSID (Windows 10, Windows Server 2012R2)
                iInterface = dcom.CoCreateInstanceEx(string_to_bin('C08AFD90-F2A1-11D1-8455-00A0C91F3880'), IID_IDispatch)
                iMMC = IDispatch(iInterface)
                resp = iMMC.GetIDsOfNames(('Document',))
                resp = iMMC.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], [])
                pQuit = iMMC.GetIDsOfNames(('Quit',))[0]
            elif self.__dcomObject == 'MMC20':
                ......

            iDocument = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData']))

            if self.__dcomObject == 'MMC20':
                # ⑤ MMC20:Document.ActiveView.ExecuteShellCommand(...) 执行命令
                resp = iDocument.GetIDsOfNames(('ActiveView',))
                resp = iDocument.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], [])

                iActiveView = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData']))
                pExecuteShellCommand = iActiveView.GetIDsOfNames(('ExecuteShellCommand',))[0]
                self.shell = RemoteShellMMC20(self.__share, (iMMC, pQuit), (iActiveView, pExecuteShellCommand), smbConnection, self.__shell_type, silentCommand)
            else:
                # ⑤ ShellWindows/ShellBrowserWindow:Document.Application.ShellExecute(...) 执行命令
                resp = iDocument.GetIDsOfNames(('Application',))
                resp = iDocument.Invoke(resp[0], 0x409, DISPATCH_PROPERTYGET, dispParams, 0, [], [])

                iActiveView = IDispatch(self.getInterface(iMMC, resp['pVarResult']['_varUnion']['pdispVal']['abData']))
                pExecuteShellCommand = iActiveView.GetIDsOfNames(('ShellExecute',))[0]
                self.shell = RemoteShell(self.__share, (iMMC, pQuit), (iActiveView, pExecuteShellCommand), smbConnection, self.__shell_type, silentCommand)
            ......

摘录说明:以上保留 getInterface 的 OBJREF 分发与 run() 中三个 DCOM 组件的激活、属性链与命令执行路径。dcomexec 源码中另有的 RemoteShell / RemoteShellMMC20 半交互 shell(约 300 行,负责命令回显的 SMB 文件读写、编解码与提示符管理)与本文教学点无关,此处略去,完整实现见 examples/dcomexec.py。

0x409 是美式英文的区域 ID(LCID)。

dcomexec 调用 ShellBrowserWindow COM 对象时使用 Document.Application 属性,并在 Document.Application.Parent 返回的对象上调用 ShellExecute 方法,以执行命令。

6.3.2 [MS-COMEV] comev.py

com+协议的实现模块,com+协议用于存储和管理远程计算机上事件发布者及其各自订阅者的配置数据。该协议还指定了如何获取有关发布者及其订阅者的特定信息。发布者-订阅者框架 允许应用程序发布其他应用程序可能感兴趣的历史信息。发布信息的应用程序称为发布者,而订阅信息的应用程序称为订阅者。发布者在称为事件的离散集中指定此信息。同样,订阅者可以通过为事件创建订阅来订阅事件 。COM+ 事件系统协议提供了一种在远程计算机上管理事件 及其各自订阅的方法。该协议作为一组 DCOM [MS-DCOM] 接口公开。 使用该协议,发布者可以发布、更新或删除远程机器上的事件。同样,订阅者 可以使用该协议为远程机器上的事件创建订阅。它还可以修改、查询或删除远程计算机上事件的订阅。 订阅者可以指定它希望接收特定类型的事件或事件集合。这是通过指定 过滤标准来定义的。简单来说就是远程事件管理。COM+ 事件系统协议使用 DCOM [MS-DCOM] 通过网络进行通信并验证针对基础结构发出的所有请求。与 DCOM 一起,此协议还通过使用IDispatch 接口 中的数据类型 BSTR 和 VARIANT使用 OLE 自动化协议[MS-OAUT] 。 [MS-COMA]中描述的协议 可用于为COM+ 事件系统协议使用的事件类和订户 DCOM 组件执行类型库的注册。它还可用于发现在服务器上注册的订阅者 DCOM 组件以创建订阅。

event:应用程序公开的可能与其他应用程序相关的历史数据的离散单元。事件的一个示例是特定用户登录到计算机。

event类:使用发布应用程序指定的标准组合在一起的历史数据集合。

event接口:事件方法的集合。一个事件类 包含一个或多个事件接口。

event方法:发布者 应用程序生成事件时由发布者-订阅者框架调用的方法。

filter条件:订阅者指定的一组规则,作为订阅 的一部分, 用于定义它想要接收的历史数据类型。

模块开头提供了event相关的com组件clsid并定义了通信中所需的数据结构,在IEventClass*类中提供了进行事件查询修改的函数

class IEventClass3(IEventClass2):
    def __init__(self, interface):
        IEventClass2.__init__(self,interface)
        self._iid = IID_IEventClass3

    def get_EventClassPartitionID(self):
        request = IEventClass3_get_EventClassPartitionID()
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        resp.dump()
        return resp

    def put_EventClassPartitionID(self, bstrEventClassPartitionID):
        request = IEventClass3_put_EventClassPartitionID()
        request['bstrEventClassPartitionID '] = bstrEventClassPartitionID
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        resp.dump()
        return resp

    def get_EventClassApplicationID(self):
        request = IEventClass3_get_EventClassApplicationID()
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        resp.dump()
        return resp
        ............

6.3.3 [MS-SCMP] scmp.py

SCMP卷影副本管理协议,它以编程方式枚举卷影副本并在远程计算机上配置卷影副本存储

卷影复制服务的架构示意图

卷影副本: 在定义明确的时刻,卷上保存的数据副本。

卷影副本提供程序:服务器上的一个软件组件,提供创建、枚举、删除和管理卷影副本的本地服务。

卷影副本集:同时创建并由公共 ID 标识的卷影副本的集合。

卷影副本存储:存储来自原始卷的差异数据的存储位置,以便维护指定原始卷的所有卷影副本。该位置可以是同一卷或不同卷上的一个文件或一组文件。

shadow copy storage association :原始卷与卷影副本存储所在卷的关系。

卷影副本存储卷:卷影副本存储所在的卷。

snapshot :制作卷影副本的时间点。

就像给虚拟机打快照一样给系统卷打个快照,攻击者可借此提取 ntds.dit、执行命令等。

卷影副本创建过程
若要创建卷影副本,请求程序、编写程序和提供程序将执行以下操作:
请求程序要求卷影复制服务枚举编写程序,收集编写程序元数据,并准备创建卷影副本。
每个编写程序都会为需要备份的组件和数据存储创建 XML 描述,并将其提供给卷影复制服务。 编写器还定义了用于所有组件的还原方法。 卷影复制服务向请求程序提供编写程序的描述,而请求程序则选择要备份的组件。
卷影复制服务通知所有编写程序准备数据以进行卷影复制。
每个编写程序都会根据需要准备数据,例如完成所有未结束事务、滚动(rolling,截断/归档)事务日志和刷新缓存。 当数据准备好进行卷影复制时,编写程序将通知卷影复制服务。
卷影复制服务通知编写程序将应用程序写入 I/O 请求暂时冻结几秒钟(仍然可以执行读取 I/O 请求),创建卷的卷影副本需要这几秒的时间。 应用程序冻结的时间不允许超过 60 秒。 卷影复制服务刷新文件系统缓冲区,然后冻结文件系统,从而确保正确记录文件系统元数据,并以一致的顺序写入要进行卷影复制的数据。
卷影复制服务通知提供程序创建卷影副本。 卷影副本创建周期不超过 10 秒,在此期间,对文件系统的所有写入 I/O 请求都将保持冻结状态。
卷影复制服务释放文件系统写入 I/O 请求。
VSS 通知编写程序解除冻结应用程序写入 I/O 请求。 此时,应用程序可以继续将数据写入正在进行卷影复制的磁盘

卷影复制服务的工作原理示意图

卷影副本和支持卷影副本的卷:客户端获取的第一个接口是IVssSnapshotMgmt 接口。客户端调用 IVssSnapshotMgmt::QueryVolumesSupportedForSnasphots(原文如此,微软文档中即为此拼写)方法来获取可以进行卷影复制的卷的集合。服务器必须响应一个IVssEnumMgmtObject 接口,客户端可以在该接口上调用方法来遍历集合。客户端调用 IVssSnapshotMgmt::QuerySnapshotsByVolume 获取已存在于指定卷上的卷影副本集合。服务器必须响应一个 IVssEnumObject 接口,客户端可以在该接口上调用方法来遍历集合。客户端调用 IVssSnapshotMgmt::GetProviderMgmtInterface 方法获取IVssDifferentialSoftwareSnapshotMgmt 接口。服务器必须响应一个 IVssDifferentialSoftwareSnapshotMgmt 接口,客户端可以在该接口上调用方法来管理卷影副本存储关联。

卷影副本存储关联:用于管理卷影副本存储关联的接口是通过 IVssSnapshotMgmt::GetProviderMgmtInterface 获取的。客户端调用 IVssDifferentialSoftwareSnapshotMgmt::QueryVolumesSupportedForDiffArea 方法来获取可用于存储卷影副本差异数据的卷集合。服务器必须响应一个 IVssEnumMgmtObject 接口,客户端可以在该接口上调用方法来遍历集合。客户端调用 IVssDifferentialSoftwareSnapshotMgmt::QueryDiffAreasForVolume 以获取已存在的卷影副本存储关联的集合,以存储特定原始卷的卷影副本差异数据. 服务器必须响应一个 IVssEnumMgmtObject 接口,客户端可以在该接口上调用方法来遍历集合。客户端调用 IVssDifferentialSoftwareSnapshotMgmt::QueryDiffAreasOnVolume 以获取用于在特定卷上存储差异数据的卷影副本存储关联的集合。服务器必须响应一个 IVssEnumMgmtObject 接口,客户端可以在该接口上调用方法来遍历集合。

模块开始定义了IVssSnapshotMgmt等接口的CLSID和卷影协议需要的数据结构如VSS_ID等等

随后实现了枚举卷影副本等查询功能。虽然名为“卷影副本管理协议”,但该协议只能查询,并没有创建卷影副本的功能。

class IVssSnapshotMgmt(IRemUnknown2):
    def __init__(self, interface):
        IRemUnknown2.__init__(self, interface)
        self._iid = IID_IVssSnapshotMgmt

    def GetProviderMgmtInterface(self, providerId = IID_ShadowCopyProvider, interfaceId = IID_IVssDifferentialSoftwareSnapshotMgmt):
        req = GetProviderMgmtInterface()
        classInstance = self.get_cinstance()
        req['ORPCthis'] = classInstance.get_ORPCthis()
        req['ORPCthis']['flags'] = 0
        req['ProviderId'] = providerId
        req['InterfaceId'] = interfaceId
        resp = self.request(req, self._iid, uuid = self.get_iPid())
        return IVssDifferentialSoftwareSnapshotMgmt(INTERFACE(classInstance, ''.join(resp['ppItf']['abData']), self.get_ipidRemUnknown(), target = self.get_target()))

有趣的是,examples/secretsdump.py 也确实没有调用 scmp 模块,而是通过远程执行 vssadmin 命令来枚举卷影副本——协议虽然实现了,但整个 impacket 都没有用到它:

  def __getLastVSS(self, forDrive=None):
        if forDrive:
            command = '%COMSPEC% /C vssadmin list shadows /for=' + forDrive
        else:
            command = '%COMSPEC% /C vssadmin list shadows'
        self.__executeRemote(command)
        time.sleep(5)
        tries = 0
        while True:
            try:
                self.__smbConnection.getFile('ADMIN$', 'Temp\\__output', self.__answer)
                break
            except Exception as e:
                if tries > 30:
                    # We give up
                    raise Exception('Too many tries trying to list vss shadows')
                if str(e).find('SHARING') > 0:
                    # Stuff didn't finish yet.. wait more
                    time.sleep(5)
                    tries +=1
                    pass
                else:
                    raise

6.3.4 [MS-VDS] vds.py

虚拟磁盘服务 (VDS) 远程协议是一组分布式组件对象模型 (DCOM)接口,用于管理计算机上的磁盘存储配置。虚拟磁盘服务远程协议处理详细的低级操作系统和存储概念。

模块主要定义了协议实现所需的变量和添加 / 删除虚拟磁盘等接口功能,目前 impacket 还没有调用该模块的脚本:

class IVdsService(IRemUnknown2):
    def __init__(self, interface):
        IRemUnknown2.__init__(self, interface)

    def IsServiceReady(self):
        # 样板模式:request → 异常时从异常对象取响应包(e.get_packet())
        request = IVdsService_IsServiceReady()
        request['ORPCthis'] = self.get_cinstance().get_ORPCthis()
        request['ORPCthis']['flags'] = 0
        try:
            resp = self.request(request, uuid = self.get_iPid())
        except Exception as e:
            resp = e.get_packet()
        return resp 

    ......

    def QueryProviders(self, masks):
        # 查询提供程序:返回可枚举的 IEnumVdsObject,
        # 后续枚举出的每个对象同样是 IRemUnknown2 子类接口
        request = IVdsService_QueryProviders()
        request['ORPCthis'] = self.get_cinstance().get_ORPCthis()
        request['ORPCthis']['flags'] = 0
        request['masks'] = masks
        resp = self.request(request, uuid = self.get_iPid())
        return IEnumVdsObject(INTERFACE(self.get_cinstance(), ''.join(resp['ppEnum']['abData']), self.get_ipidRemUnknown(), target = self.get_target()))

6.3.5 [MS-WMI] wmi.py

WMI 协议

Windows Management Instrumentation 远程协议使用 DCOM 远程协议通过网络进行通信并验证针对基础结构发出的所有请求。DCOM 远程协议实际上是 Windows Management Instrumentation 远程协议的基础,用于完成以下任务:

  • 建立协议。
  • 确保沟通渠道。
  • 验证客户端。
  • 在客户端和服务器之间实现可靠的通信。

这意味着 DCOM 远程协议实现提供并使用所有底层协议。除 DCOM 之外,WMI 远程协议还使用 [MS-WMIO] 中定义的特殊编码,通过网络传输 [DMTF-DSP0004] 中定义的信息。WMI 远程协议用于传达符合公共信息模型(CIM)的管理数据,用户可以使用 WMI 管理本地和远程计算机。另一种方式是使用 Windows 远程管理(WinRM),它基于 SOAP 获取远程 WMI 管理数据。

img

这张图描述了 WMI 基础结构与 WMI 提供者(provider)、托管对象、WMI 使用者(可使用 wmic、wbemtest 工具、WMI Scripting API 或直接使用 COM 接口;.NET 使用 System.Management 相关功能)的关系。

WMI提供者是一个监控一个或者多个托管对象的COM组件(对象)。一个托管对象是一个逻辑或者物理组件,比如硬盘驱动器、网络适配器、数据库系统、操作系统、进程或者服务。和驱动相似,WMI提供者通过托管对象提供的数据向WMI服务提供数据,同时将WMI服务的请求传递给托管对象。

在这里插入图片描述

下表列出了操作系统 WMI 提供程序,wmi的功能都是基于操作系统各功能的provider提供的

提供程序 描述
活动目录供应商 将 Active Directory 对象映射到 WMI。通过访问 WMI 中的轻型目录访问协议 (LDAP) 命名空间,您可以在 Active Directory 中引用对象或使对象成为别名。
BitLocker 驱动器加密 (BDE) 提供程序 为硬盘驱动器上的存储区域提供配置和管理,由Win32_EncryptableVolume的实例表示,可以使用加密进行保护。
BizTalk 供应商 提供对 WMI 类表示的 BizTalk 管理对象的访问。
引导配置数据 (BCD) 提供程序 通过 Root\WMI 命名空间中的 BCD 提供程序类提供对引导配置数据的访问。有关详细信息,请参阅BCD 参考
CIMWin32 WMI 供应商 支持在 CimWin32.dll 中实现的类。这些包括核心 CIM WMI 类、这些类的 Win32 实现和电源管理事件。
分布式文件系统 (DFS) 提供程序 提供分布式文件系统(DFS)功能,在多个服务器上对共享进行逻辑分组,并将它们透明地链接到单个命名空间中的树状结构中。
分布式文件系统复制 (DFSR) 提供程序 创建用于配置和监视分布式文件系统 (DFS)服务的工具。有关详细信息,请参阅DFSR WMI 类
DNS 供应商 使管理员和程序员能够使用 Windows Management Instrumentation (WMI) 配置域名系统 (DNS) 资源记录 (RR) 和 DNS 服务器。
磁盘配额供应商 允许管理员控制每个用户存储在 NTFS 卷上的数据量。
事件日志提供程序 提供从事件日志服务到事件通知的数据访问。
Hyper-V WMI 提供程序 (V2) 使开发人员和脚本编写人员能够为虚拟化平台快速构建自定义工具、实用程序和增强功能。
Hyper-V WMI 提供程序 使开发人员和脚本编写人员能够为虚拟化平台快速构建自定义工具、实用程序和增强功能。
互联网信息服务 (IIS) 公开可用于查询和配置 IIS 元数据库的编程接口。
IP路由供应商 提供网络路由信息。
作业对象提供者 提供对命名内核作业对象上的数据的访问。
智能平台管理接口 (IPMI) 与 WMI IPMI 提供程序一起工作,将底板管理控制器 (BMC) 操作的数据提供给操作系统。
Live Communications Server 2003 提供程序 提供 WMI 类,用于创建、注册、配置和管理带有Live Communications Server 2003的自定义会话启动协议 (SIP) 应用程序。
网络负载平衡 (NLB) 允许应用程序通过 WMI 与网络负载平衡集群交互。
Ping 供应商 为 WMI 提供对标准ping命令提供的状态信息的访问。
政策提供者 提供对组策略的扩展,并允许改进策略的应用。
电源管理事件提供者 通过对 Windows 电源管理协议建模,向Win32_PowerManagementEvent类提供信息以描述因电源状态更改而导致的电源管理事件。
远程桌面服务 WMI 提供程序 在远程桌面服务环境中启用一致的服务器管理。
报告服务提供商 定义 WMI 类,允许您编写脚本和代码来修改报表服务器和报表管理器的设置。
策略结果集 (RSoP) 提供程序 提供在假设情况下计划和调试策略设置的方法。这些方法允许管理员轻松确定适用于或将适用于用户或计算机的策略设置组合。这称为策略结果集 (RSoP)。有关详细信息,请参阅关于 RSoP WMI 方法提供程序RSoP WMI 类
安全供应商 检索或更改控制文件、目录和共享的所有权、审核和访问权限的安全设置。
服务器集群供应商 定义一组用于访问群集对象、属性和事件的 WMI 类。
会话提供者 管理网络会话和连接。
卷影复制供应商 为共享文件夹功能的卷影副本提供管理功能。
SNMP 供应商 将管理信息库 (MIB) 架构对象中定义的简单网络管理协议 (SNMP) 对象映射到 WMI CIM 类。此提供程序未预安装。有关详细信息,请参阅设置 WMI SNMP 环境
系统中心端点保护 (SCEP) 定义启用 System Center Endpoint Protection (SCEP) 管理的 WMI 类。
系统注册提供商 使管理应用程序能够检索和修改系统注册表中的数据;并在发生变化时收到通知。64 位平台上有两个版本的系统注册表提供程序。
系统还原供应商 提供配置和使用系统还原功能的类。有关详细信息,请参阅配置系统还原系统还原 WMI 类
值得信赖的平台模块供应商 提供对有关安全设备的数据的访问,由Win32_TPM的实例表示,这是 Microsoft Windows 可信平台计算机系统的信任根。
信任供应商 提供有关域信任的访问信息。
查看供应商 基于其他类的实例创建新的实例和方法。64 位平台上有两个版本的视图提供程序。
波分供应商 提供对符合 Windows 驱动程序模型 (WDM) 的硬件驱动程序的类、实例、方法和事件的访问。
Win32 供应商 提供来自 Windows 系统的访问和更新数据,例如环境变量的当前设置和逻辑磁盘的属性。
Windows Defender的 定义启用 Windows Defender 管理的 WMI 类。
Windows 安装程序提供程序 提供从与 Windows Installer 兼容的应用程序收集的访问信息,并使 Windows Installer 过程可远程使用。
Windows 产品激活提供商 通过使用 WMI 接口支持 Windows 产品激活 (WPA) 管理,并提供一致的服务器管理。Windows 产品激活不适用于基于 Itanium 的 Windows 操作系统版本。
WMIPerfClass 提供者 创建 WMI性能计数器类。数据由 WMIPerfInst 提供程序动态提供给这些 WMI 性能类。WMIPerfClass 和 WMIPerfInst 提供程序替换了ADAP函数。
WmiPerfInst 供应商 从 WMI性能计数器类定义动态提供原始和格式化的性能计数器数据。

为便于理解,可以提前看下 examples/wmiexec.py——它调用 CIMWin32 提供程序 Win32_Process 类的 Create 方法启动进程,执行 cmd 或 powershell 命令(或反弹 shell):

# eg./examples/wmiexec.py

    ...................
dcom = DCOMConnection(addr, self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash,
                              self.__aesKey, oxidResolver=True, doKerberos=self.__doKerberos, kdcHost=self.__kdcHost)
        try:
            iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
            iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
            iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
            iWbemLevel1Login.RemRelease()

            win32Process, _ = iWbemServices.GetObject('Win32_Process')

            self.shell = RemoteShell(self.__share, win32Process, smbConnection, self.__shell_type, silentCommand)
    ...............

class RemoteShell(cmd.Cmd):
    def __init__(self, share, win32Process, smbConnection, shell_type, silentCommand=False):
        cmd.Cmd.__init__(self)
        self.__share = share
        self.__output = '\\' + OUTPUT_FILENAME
        self.__outputBuffer = str('')
        self.__shell = 'cmd.exe /Q /c '
        self.__shell_type = shell_type
        self.__pwsh = 'powershell.exe -NoP -NoL -sta -NonI -W Hidden -Exec Bypass -Enc '
        self.__win32Process = win32Process
        self.__transferClient = smbConnection
        self.__silentCommand = silentCommand
        self.__pwd = str('C:\\')
        self.__noOutput = False
        self.intro = '[!] Launching semi-interactive shell - Careful what you execute\n[!] Press help for extra shell commands'
    .....................

以下是Win32_Process类中的方法

Method Description
AttachDebugger Launches the currently registered debugger for a process.
Create 创建一个新进程。
GetAvailableVirtualSize 计算当前进程可用的虚拟地址容量
GetOwner 检索运行该进程的用户名和域名。
GetOwnerSid 检索进程所有者的安全标识符 (SID)。
SetPriority 更改进程的执行优先级。
Terminate 终止进程及其所有线程。

Win32_Process的属性如下

属性 数据类型 说明
CommandLine 字符串 用于启动特定进程的命令行(如适用)
CreationClassName 字符串 创建实例时使用的类或子类名称,与其他关键属性一起可唯一标识该类及其子类的所有实例。继承自 CIM_Process
CreationDate 日期时间 进程开始执行的日期。继承自 CIM_Process
CSCreationClassName 字符串 作用域计算机系统的创建类名称。继承自 CIM_Process
CSName 字符串 作用域计算机系统的名称。继承自 CIM_Process
...... 更多属性见官方文档

更多属性见 https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-process 。

全部列出意义不大——实际渗透中先明确要远程实现什么功能,再查询相应的 provider / class / 方法 / 属性进行调用即可。

这里再看一个例子

再看一个例子:Windows Defender WMIv2 API 中 MSFT_MpPreference 类的 Add 方法(调用时默认会寻求用户确认,指定 -Force 则不确认):

uint32 Add(
  [in] string  ExclusionPath[],          // 排除路径,允许管理员明确禁止扫描检查列出的任何路径
  [in] string  ExclusionExtension[],     // 允许管理员明确禁止扫描检查列出的任何扩展
  [in] string  ExclusionProcess[],       // 允许管理员明确禁止扫描检查列出的任何进程
  [in] sint64  ThreatIDDefaultAction_Ids[],      // 检测到时不应对其采取默认操作的威胁 ID
  [in] uint8   ThreatIDDefaultAction_Actions[],  // 对上述威胁采取的操作,顺序须与 Ids 中指定的顺序一致
  [in] boolean Force
);

同一提供程序下的 MSFT_MpComputerStatus 类可查看当前安全软件及版本信息,便于免杀前的信息收集(原文本地截图已失效)。

WMI 委派

另一个有意思的点:根据官方文档(https://learn.microsoft.com/en-us/windows/win32/wmisdk/connecting-to-a-3rd-computer-delegation)的描述,在本地系统上运行从远程系统获取数据的脚本时,WMI 只需要 Impersonate 模拟级别就会把凭据提供给远程系统上的数据提供者;但如果脚本连接到远程系统的 WMI 后,还要继续访问第三台机器(例如打开其他远程系统上的日志文件),就会失败——除非模拟级别为 Delegate。因此可以对我们创建的账户配置 WMI 委派,从而通过 WMI 对第三台机器(如 DC)进行控制。这方面的公开资料不多,该委派机制或许可用作后门思路。

这里放一个实现wmi委派的ps脚本及使用方法

https://github.com/grbray/PowerShell/blob/main/Windows/Set-WMINameSpaceSecurity.ps1

https://techcommunity.microsoft.com/t5/core-infrastructure-and-security/delegate-wmi-access-to-domain-controllers/ba-p/259535

模块代码分析

从前面wmiexec的脚本中我们可以看到调用wmi的方式是通过IWbemLevel1Login接口

IWbemLevel1Login 接口允许用户连接到特定名称空间中的管理服务接口。接口必须由UUID {F309AD18-D86A-11d0-A075-00C04FB68820} 唯一标识

IWbemLevel1Login包含四个方法

方法 说明 Opnum
EstablishPosition 不执行任何操作,主要进行 NTLMLogin 之前的区域协商 3
RequestChallenge 不执行任何操作 4
WBEMLogin 不执行任何操作 5
NTLMLogin 将用户连接到指定命名空间中的管理服务接口 6

主要看下 NTLMLogin 的参数:

 HRESULT NTLMLogin(
   [in, unique, string] LPWSTR wszNetworkResource:代表返回的 IWbemServices 对象所关联的服务器上的命名空间。此参数不得为 NULL
   [in, unique, string] LPWSTR wszPreferredLocale,一个指向字符串的指针,该字符串必须以首选顺序指定语言环境值,以逗号分隔。如果客户端不提供它,服务器会创建一个特定于实现的默认列表
   [in] long lFlags,必须为 0
   [in] IWbemContext* pCtx,必须是指向IWbemContext 接口的指针,它必须包含客户端发送的附加信息。如果pCtx 为 NULL,则必须忽略该参数。
   [out] IWbemServices** ppNamespace如果调用成功,ppNamespace 必须返回一个指向 IWbemServices接口指针的指针。当发生错误时,此参数必须设置为 NULL。
 );
为响应 IWbemLevel1Login::NTLMLogin 方法,服务器必须返回对应于 wszNetworkResource 参数的 IWbemServices 接口。当调用成功时,服务器必须创建一个 IWbemServices 对象,并将 wszPreferredLocale 存储在对象中。服务器必须在 NamespaceConnectionTable 中查找与 wszNetworkResource 对应的 NamespaceConnection 对象,并将其引用存储在 IWbemServices 对象中。服务器必须将 GrantedAccess 设置为命名空间安全描述符授予客户端的一组访问权限。请求本地化信息的所有后续 IWbemServices 方法调用必须以 wszPreferredLocale 中指定的语言返回信息。当首选语言环境为 NULL 时,服务器应该使用特定于实现的逻辑来决定语言环境。成功的方法执行必须使用 IWbemServices 接口指针填充 ppNamespace 参数,并且必须返回 WBEM_S_NO_ERROR。

可以看到IWbemLevel1Login类对ntlmlogin方法的实现

class IWbemLevel1Login(IRemUnknown):
    ........    
    def NTLMLogin(self, wszNetworkResource, wszPreferredLocale, pCtx):
        request = IWbemLevel1Login_NTLMLogin()
        request['wszNetworkResource'] = checkNullString(wszNetworkResource)
        request['wszPreferredLocale'] = checkNullString(wszPreferredLocale)
        request['lFlags'] = 0
        request['pCtx'] = pCtx
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        return IWbemServices(
            INTERFACE(self.get_cinstance(), b''.join(resp['ppNamespace']['abData']), self.get_ipidRemUnknown(),
                      target=self.get_target()))

之后则是通过IWbemServices完成对provider的调用

IWbemServices接口的方法如下

Method Description
OpenNamespace 为客户端提供作用域为请求的命名空间的 IWbemServices 接口指针
CancelAsyncCall 取消当前 IWbemObjectSink 指针标识的异步方法调用
QueryObjectSink 获取允许客户端直接向服务器发送事件的通知处理程序
GetObject 检索CIM 类CIM 实例
GetObjectAsync IWbemServices::GetObject 方法的异步版本
PutClass 在与当前 IWbemServices 接口关联的命名空间中创建新类或更新现有类
PutClassAsync IWbemServices::PutClass 方法的异步版本
DeleteClass 从与当前 IWbemServices 接口关联的命名空间中删除指定的类
DeleteClassAsync IWbemServices::DeleteClass 方法的异步版本。
CreateClassEnum 创建类枚举。
CreateClassEnumAsync IWbemServices::CreateClassEnum 方法的异步版本。
PutInstance 创建或更新现有类的实例
PutInstanceAsync PutInstance 方法的异步版本。
DeleteInstance 删除现有类的实例
DeleteInstanceAsync IWbemServices::DeleteInstance 方法的异步版本。
CreateInstanceEnum 创建满足选择条件的所有类实例的实例枚举
CreateInstanceEnumAsync IWbemServices::CreateInstanceEnum 方法的异步版本。
ExecQuery 基于查询返回 IWbemClassObject 接口对象的可枚举集合。
ExecQueryAsync IWbemServices::ExecQuery 方法的异步版本。
ExecNotificationQuery 当客户端调用请求订阅事件时,服务器运行查询以接收事件。
ExecNotificationQueryAsync IWbemServices::ExecNotificationQuery 方法的异步版本。
ExecMethod 执行 由 CIM 类或从 IWbemServices 接口检索的CIM实例实现的CIM 方法。
ExecMethodAsync IWbemServices::ExecMethod 方法的异步版本

WMI 远程管理的主要功能方法都在这里,模块中实现了每一个方法:

class IWbemServices(IRemUnknown):
    def __init__(self, interface):
        IRemUnknown.__init__(self,interface)
        self._iid = IID_IWbemServices

    def OpenNamespace(self, strNamespace, lFlags=0, pCtx = NULL):
        request = IWbemServices_OpenNamespace()
        request['strNamespace']['asData'] = strNamespace
        request['lFlags'] = lFlags
        request['pCtx'] = pCtx
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        resp.dump()
        return resp

    def CancelAsyncCall(self,IWbemObjectSink ):
        request = IWbemServices_CancelAsyncCall()
        request['IWbemObjectSink'] = IWbemObjectSink
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        return resp['ErrorCode']

    def QueryObjectSink(self):
        request = IWbemServices_QueryObjectSink()
        request['lFlags'] = 0
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        return INTERFACE(self.get_cinstance(), b''.join(resp['ppResponseHandler']['abData']), self.get_ipidRemUnknown(),
                         target=self.get_target())

    def GetObject(self, strObjectPath, lFlags=0, pCtx=NULL):
        request = IWbemServices_GetObject()
        request['strObjectPath']['asData'] = strObjectPath
        request['lFlags'] = lFlags
        request['pCtx'] = pCtx
        resp = self.request(request, iid = self._iid, uuid = self.get_iPid())
        ppObject = IWbemClassObject(
            INTERFACE(self.get_cinstance(), b''.join(resp['ppObject']['abData']), self.get_ipidRemUnknown(),
                      oxid=self.get_oxid(), target=self.get_target()), self)
        if resp['ppCallResult'] != NULL:
            ppcallResult = IWbemCallResult(
                INTERFACE(self.get_cinstance(), b''.join(resp['ppObject']['abData']), self.get_ipidRemUnknown(),
                          target=self.get_target()))
        else:
            ppcallResult = NULL
        return ppObject, ppcallResult
    ..........

方法调用的例子可以看 examples/wmipersist.py:先通过 DeleteInstance 删除现有实例(REMOVE 模式),再通过 GetObject 获取 ActiveScriptEventConsumer、__EventFilter、__IntervalTimerInstruction 等类,通过 SpawnInstance 派生新实例,最后通过 PutInstance 写入:

 if self.__options.action.upper() == 'REMOVE':
            # 卸载:逐个删除四类实例(Consumer / Filter / Timer / Binding)
            self.checkError('Removing ActiveScriptEventConsumer %s' % self.__options.name,
                            iWbemServices.DeleteInstance('ActiveScriptEventConsumer.Name="%s"' % self.__options.name))
            ......   # 同样方式删除 __EventFilter、__IntervalTimerInstruction、__FilterToConsumerBinding

        else:
            # 安装:① GetObject 取类定义 → SpawnInstance 派生实例 → 填属性 → PutInstance 写入
            activeScript, _ = iWbemServices.GetObject('ActiveScriptEventConsumer')
            activeScript = activeScript.SpawnInstance()
            activeScript.Name = self.__options.name
            activeScript.ScriptingEngine = 'VBScript'          # 以 VBScript 作为事件触发载荷
            activeScript.CreatorSID = [1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0]   # Creator SID
            activeScript.ScriptText = options.vbs.read()       # 恶意 VBS 脚本体
            self.checkError('Adding ActiveScriptEventConsumer %s'% self.__options.name, 
                iWbemServices.PutInstance(activeScript.marshalMe()))

            if options.filter is not None:
                ......   # __EventFilter:指定 WQL 查询与所在命名空间 root\cimv2
            else:
                wmiTimer, _ = iWbemServices.GetObject('__IntervalTimerInstruction')
                wmiTimer = wmiTimer.SpawnInstance()
                wmiTimer.TimerId = 'TI_%s' % self.__options.name
                wmiTimer.IntervalBetweenEvents = int(self.__options.timer)    # 定时器间隔
                self.checkError('Adding IntervalTimerInstruction',
                    iWbemServices.PutInstance(wmiTimer.marshalMe()))

                eventFilter,_ = iWbemServices.GetObject('__EventFilter')
                eventFilter =  eventFilter.SpawnInstance()
                eventFilter.Name = 'EF_%s' % self.__options.name
                eventFilter.Query = 'select * from __TimerEvent where TimerID = "TI_%s" ' % self.__options.name
                eventFilter.QueryLanguage = 'WQL'
                eventFilter.EventNamespace = r'root\subscription'
                ......

            # ② 绑定 Filter → Consumer,事件发生时执行脚本
            filterBinding, _ = iWbemServices.GetObject('__FilterToConsumerBinding')
            filterBinding = filterBinding.SpawnInstance()
            filterBinding.Filter = '__EventFilter.Name="EF_%s"' % self.__options.name
            filterBinding.Consumer = 'ActiveScriptEventConsumer.Name="%s"' % self.__options.name
            filterBinding.CreatorSID = [1, 2, 0, 0, 0, 0, 0, 5, 32, 0, 0, 0, 32, 2, 0, 0]

            self.checkError('Adding FilterToConsumerBinding',
                iWbemServices.PutInstance(filterBinding.marshalMe()))

这里派生使用的是wbemcli.h中的IWbemClassObject::SpawnInstance 方法

HRESULT SpawnInstance(
  [in]  long             lFlags,
  [out] IWbemClassObject **ppNewInstance
);

当前对象必须是使用 IWbemServices::GetObject、IWbemServices::CreateClassEnum 或 IWbemServices::CreateClassEnumAsync 从 WMI 获取的类定义,然后使用此类定义创建新实例。需要调用 IWbemServices::PutInstance 才能将实例实际写入 WMI。如果要在调用 IWbemServices::PutInstance 之前放弃对象,只需调用 IWbemClassObject::Release 即可。请注意,支持从实例生成实例,但返回的实例将为空。

脚本中调用的3个类作用如下

  • ActiveScriptEventConsumer 类在事件传递到该脚本时,以任意脚本语言运行预定义脚本。

  • __IntervalTimerInstruction系统类以时间间隔生成事件,类似于Windows编程中的WM_TIMER消息。 事件使用者通过创建引用此类的事件查询来注册以接收间隔计时器事件。 由于操作系统行为,无法保证事件将以精确请求的时间间隔传送。

  • 永久事件使用者的注册需要 __EventFilter 系统类的实例。

wmipersist.py 的作用是构造 WQL 事件查询监听事件,当事件发生时执行攻击者预置的脚本。

除了以上介绍的两个类,wmi模块中还实现了以下接口:

  • IEnumWbemClassObject接口:对CIM对象集合进行枚举或克隆

  • IWbemCallResult 接口:用于从 返回单个CIM 对象的半同步调用中返回调用结果

  • IWbemFetchSmartEnum 接口:一个帮助程序接口,用于检索网络优化的枚举器接口
  • IWbemWCOSmartEnum接口:旨在为 IEnumWbemClassObject 提供 CIM 对象的备用同步枚举
  • IWbemLoginClientID 接口:SetClientInfo 方法将客户端 NetBIOS 名称和客户端生成的唯一编号传递给服务器。
  • IWbemLoginHelper 接口:SetEvent 方法在服务器上触发指定名称的事件。

以上就是wmi模块的全部内容