guxch的专栏

路漫漫其修远兮,吾将上下而求索

通过分析网络数据包来理解DCOM协议(一)

【翻译说明】最近,想看看DCOM的通讯能否在Linux平台上实现(其实是想实现OPC Client),就找了两篇文章,读了一下,发现还是翻译出来,供大家参考吧。

本文是其中一篇,原文题目《Understanding the DCOM Wire Protocol by Analyzing Network Data Packets》,作者:Guy Eddon 、HenryEddon,发表于1998年3月的MicrosoftSystems Journal原文较长,分两次登出。



我们从底层来讨论COM,通过分析网络上公开传输的数据包,你能了解COM的远程工作机制,这有助于你开发出更好的组件。【本文假设读者熟悉COM。】

 

大多数关于COM的文章都是从编程架构来描述的,它们告诉你为完成某个功能,而如何调用COM。在COM应用工作时,通过分析网络中物理传输的数据包,你能了解COM的远程工作机制,这有助于更好的理解COM编程模型,因而设计和开发更好的组件。

COM是构建交互组件的标准,DCOM是允许COM组件通过网络交互的一个高层次网络协议。我们认为DCOM是一个高层次网络协议,是因为它建立在几个已存在的协议基础之上。例如,假设一台计算机有以太网卡,并使用UDP协议,从最底层的以太网帧到最高层的DCOM,整个协议如图1所示,中间加着IP,UDP和RPC。图1只是许多可能配置中的一种,在RPC之下,可以有多种替代的协议。在服务器与客户机上,DCOM自动选择它下面的最好的协议。


图1 协议层次

以OSI七层网络模型来看看DCOM协议栈。如图2所示,OSI七层网络模型与本文的例子协议栈并列画出,注意图中是在Window平台下,其它平台实现的层次可能不同。


图2 OSI七层“蛋糕”

对协议栈中每层协议,数据在传输时,都包含一个数据头,而后是实际的数据,紧临的更上层协议将把它视为数据的一部分。例如,IP层包含一个数据头和数据体,IP数据体实际上包含UDP层数据头和该层的数据体,因此,通过网络传输的数据都包含协议栈中的每层协议的数据头和数据体(如图3)。

从图3中可以看出,DCOM不是一个独立于RPC之上的协议,它使用了RPC的结构体,与RPC共用了数据头和数据体,因此,为了表明在网络层次上DCOM与RPC的密切关系,DCOM协议经常被成为对象RPC或ORPC。ORPC高度综合了OSF DEC RPC协议的功能,例如,RPC中的身份认证,授权,信息完整性,加密等特性,在ORPC都有体现。


 图3  协议栈

ORPC在两个方面扩展了标准的RPC:怎样调用远程对象的方法和如何表达、传输和维护对象的引用。

Spying on the Network Protocol 监视网络协议

对COM编程者来说,网络协议的每一层几乎都被隐藏,最有效的方法是在DCOM客户端和组件之间监视网络传输,这时,需要一类叫网络嗅探器的特殊的软件(或硬件),有许多第三方的软件能够监视网络通讯,有一个叫“网络监视器(Network Monitor)”,如图4所示,它是微软Windows NT和SMS产品中的一个工具,SMS中所带的是全功能的,Windows NT中所带的只支持有限的协议并且只能查看服务器端的通讯。


图4 网络监视器

在“网络监视器”的Capture菜单中,选择Start,打开它的捕捉功能,接着运行DCOM测试程序,产生网络通讯,然后返回“网络监视器”,在Capture菜单中选择Stop and View,停止捕捉功能,并显示已经捕捉到的数据包。“网络监视器”是一个能理解许多网络协议的相当聪明的程序,它不仅仅能显示原始的数据包,而且能以一种智能和描述的方式显示捕捉的数据。与DCOM有关的协议中,“网络监视器”能识别和理解以太网、IP、UDP和RPC协议,在Display菜单中选择Filter,可以仅显示特定协议的数据包。

目前,“网络监视器”不支持DCOM协议,所以你只能以RPC的眼光看DCOM,这不会成为一个永远的问题,因为“网络监视器”提供了一个文档化的公开的接口,可以创建理解特定协议的DLL,开发能理解DCOM协议的DLL,留给读者作为练习。

为了分析DCOM协议,我们运行“网络监视器”来捕捉一个叫InsideDCOM的COM类产生的包,客户端运行在一台叫Thing1计算机上,它激活运行在Thing2计算机上的InsideDCOM,调用CoInitialize后, 客户端调用CoCreateInstanceEx实例化远方的类,代码如下:

 CoInitialize(NULL);

 

 COSERVERINFOServerInfo = { 0, L"Thing2", 0, 0 };

 MULTI_QI qi = {&IID_IUnknown, NULL, 0 };

 CoCreateInstanceEx(CLSID_InsideDCOM, NULL,

                   CLSCTX_REMOTE_SERVER,

                   &ServerInfo, 1, &qi);

 

调用CoCreateInstanceEx产生的网络通讯如图5所示。客户端车程序运行在Thing1上,它的IP为199.34.58.3,远程组件运行在Thing2上,它的IP为199.34.58.4。


图5  IUnknown请求包

图5中,可以很容易地看到数据包中的各层协议。在RPC数据头,你能看到接口IID是B8 4A 9F 4D 1C 7D CF 11 86 1E 00 20 AF6E 7C 57。与在“封送GUID解释”一节中一致,实际的IID为4D9F4AB8-7D1C-11CF-861E-0020AF6E7C57, 即IRemoteActivation接口。

远程激活(Activation)

IRemoteActivation是一个由Service Control Manager (SCM)暴露出来的RPC接口(不是COM接口),不要被SCM迷惑,它管理WindonwsNT服务,运行在每台计算机上,进程名称为RPCSS.EXE。IRemoteActivation只有一个方法RemoteActivation,它被设计用来激活远程计算机上的COM对象。这是一个非常强大的功能,但在纯RPC中没有提供,纯RPC中,服务器必须在客户端连接到来之间启动。Windows95和98缺少必要的安全机制支持远程启动服务器进程,但是这些平台上也能用IRemoteActivation接口和远程激活。IRemoteActivation接口的IDL定义如下所示。

 [ // no objecthere. Not a COM interface!

  uuid(4d9f4ab8-7d1c-11cf-861e-0020af6e7c57),

  pointer_default(unique)

]

interface IRemoteActivation

{

   const unsignedlong MODE_GET_CLASS_OBJECT = 0xffffffff;

 

   HRESULTRemoteActivation(

     [in] handle_t                         hRpc,

     [in]ORPCTHIS                        *ORPCthis,

     [out]ORPCTHAT                       *ORPCthat,

     [in] constGUID                       *Clsid,

     [in, string,unique] WCHAR            *pwszObjectName,

     [in, unique]MInterfacePointer        *pObjectStorage,

     [in]DWORD                           ClientImpLevel,

     [in]DWORD                            Mode,

     [in]DWORD                           Interfaces,

     [in, unique,size_is(Interfaces)] IID *pIIDs,

     [in] unsignedshort                  cRequestedProtseqs,

     [in,size_is(cRequestedProtseqs)]

          unsignedshort                  RequestedProtseqs[],

     [out]OXID                            *pOxid,

     [out]DUALSTRINGARRAY                **ppdsaOxidBindings,

     [out]IPID                           *pipidRemUnknown,

     [out]DWORD                          *pAuthnHint,

     [out]COMVERSION                     *pServerVersion,

     [out]HRESULT                         *phr,

     [out,size_is(Interfaces)]

          MInterfacePointer              **ppInterfaceData,

     [out,size_is(Interfaces)] HRESULT    *pResults

   );

}

通过IRemoteActivation接口,一台机器上的SCM与另一台机器上的SCM联络,要求它激活一个对象,即客户机上的SCM调用服务器上SCM的IRemoteActivation::RemoteActivation,要求它激活以CLSID(方法第四个参数)为标识的对象。RemoteActivation返回一个激活对象的封送接口指针和两个特殊的值:接口指针标识(IPID)和对象对外联络标识(OXID)。IPID标识了一个进程中一个对象的一个特定的实例。OXID是一个RPC字符串,绑定了与IPID标识的接口进行连接所必要的信息。我们将在后面详细讨论它们。

每种支持的网络协议,都有一个周知的SCM端口,每个端口都标识了一个基于网络协议的虚拟通讯通道。例如,当使用TCP或UDP时,这个端口是1066,当使用命名管道时,管道名称为\\pipe\mypipe,常用协议下SCM所使用的端口如图7所示。

Protocol String

Description

Endpoint

Ncadg_ip_udp

Connectionless over UDP

135

Ncacn_ip_tcp

Connection-oriented over TCP

135

Ncacn_nb_tcp

Connection-oriented using NetBIOS over TCP

135

Ncacn_http

Connection-oriented over HTTP

80

图7   SCM 终端口

封送形式的GUID解释

通过网络传输的GUID应该根据IDL的定义进行解释。
 typedef struct _GUID

 {   DWORD Data1;

     WORD  Data2;

     WORD  Data3;

     BYTE  Data4[8];

 } GUID;

由于GUID以低字节序被封送,所以再造GUID有两个步骤。第一步,重新分组在被捕捉的数据包中发现的GUID,使它看起来象一个标准的GUID,例如,一个数据包中,你定位到GUID:78 56 34 12 34 12 34 12 12 34 12 34 5678 9A BC,在第一步中,这个GUID被按标准的形式分组,如图8所示,这样就好看多了,是不是?现在低字节序的串要被整理为实际的GUID。GUID的前3部分(图8中的data1,data2,data3)需要按字节一个一个反转。GUID的最后一部分(data4)不需要修改,因为它是按简单的字符数组存储的。反转了GUID前3个部分后,经过第二步的处理,完整的GUID就是:12345678-1234-1234-1234-123456789ABC。

 

图8 标准形式的GUID

调用远程对象

对远程对象的方法调用,就是一个标准的DCE RPC调用:一个标准的请求协议数据单元(PDU)通过网络被发送,要求执行一个特定的方法。一个PDU是双方机器通讯的基本单位。请求PDU包含了要执行方法的所有输入参数([in]参数),但方法执行完后,应答客户端的PDU包含了所有输出参数([out]参数)。这看起很浅显,但实际上还是令人惊奇。一个远程的COM方法调用需要两个数据包:一个是客户端发给服务器端包含[in]参数的数据包,另一个是服务器端发给客户端包含[out]参数的数据包。19种定义的PDU类型如图9所示,注意其中某些类型是特定于面向有连接或无连接协议的。

PDU Type

Protocol

Type Value

Request

CO/CL

0

Ping

CL

1

Response

CO/CL

2

Fault

CO/CL

3

Working

CL

4

Nocall

CL

5

Reject

CL

6

Ack

CL

7

Cl_cancel

CL

8

Fack

CL

9

Cancel_ack

CL

10

Bind

CO

11

Bind_ack

CO

12

Bind_nak

CO

13

Alter_context

CO

14

Alter_context_resp

CO

15

Shutdown

CO

17

Co_cancel

CO

18

Orphaned

CO

19

图9  PDU类型

有连接的协议,如TCP,在客户端和服务器端维护一个连接,保证信息送到的顺序与发送的顺序相同,无连接的协议,如UDP,不在客户端和服务器端维护连接,不能保证客户端的信息实际送达服务器端,而且,即使送达,信息包也可能与发送时的顺序不同。缺省情况下,DCOM在Windows NT之间采用无连接的UDP,但这并不能说DCOM不可靠,采用无连接协议时,RPC利用自身的机制保证信息包顺序和到达感知。

一个RPC PDU包括3个部分,其中只需要第一部分:

·        一个PDU头,其中包含协议控制信息。

·        一个PDU体,其中包含数据。例如请求或应答PDU分别包含了操作的输入和输出参数。这个信息以Network DataRepresentation (NDR)形式存储。

·        一个身份认证检查体,其中包含了认证协议的特定数据。例如,认证协议可以包含一个加密的校验和来保证数据包的完整性。

无连接协议的PDU头部的IDL结构定义如图10所示。包类型字段(ptype)标识了PDU的类型,它的值通常是图9中定义的19个之一。ORPC用类标识符字段(objec)保存IPID。接口标识符字段(if_id)必须是COM接口的IID。这似乎有点冗余,因为object字段的IPID已经标识了这个接口,但是,把IID放在if_id字段可以使DCOM在标准的OSF DCE RPC实现上也能成功工作。在Windows平台,RPC实现已被优化,方法调用可以仅依赖于IPID的内容,而忽略IID。最后,接口版本字段(if_vers)必须是0.0,这是因为COM接口在发布之后可能永远不会修改,COM接口不支持版本化,如果修改,应定义一个新接口。所有这些字段都可以在图5中的RPC头中找到。

typedef struct

 {

     unsigned small rpc_vers = 4; // RPC protocolmajor version

     unsigned small ptype; // packet type

     unsigned small flags1; // packet flags

     unsigned small flags2; // packet flags

     byte drep[3]; // data representationformat label

     unsigned small serial_hi; // high byte ofserial number

     GUID object; // object identifier(Contains the IPID)

     GUID if_id; // interface identifier (IID)

     GUID act_id; // activity identifier

     unsigned long server_boot; // server boottime

     unsigned long if_vers; // interfaceversion

     unsigned long seqnum; // sequence number

     unsigned short opnum; // operation number

     unsigned short ihint; // interface hint

     unsigned short ahint; // activity hint

     unsigned short len; // length of packeybody

     unsigned short fragnum; // fragment number

     unsigned small auth_proto; //authentication protocol id

     unsigned small serial_lo; // low byte ofserial number

 } dc_rpc_cl_pkt_hdr_t;

图10 PDU数据头

这样那样

所有通过网络的COM方法调用,PDU请求中包含的第一个参数比较特别,它在所有参数之前,叫ORPCTHIS。如果下面所示的COM方法:HRESULT Sum(int x, int y, [out, retval] int* result)

被调用,实际PDU请求的参数为: Sum(ORPCTHISorpcthis, int x, int y)。

ORPCTHIS结构的定义如下:

 // Implicit ‘this'pointer which is the first [in]

 // parameter onevery ORPC call.

 typedef structtagORPCTHIS {

    COMVERSIONversion;  // COM version number (5.2)

    unsigned longflags; // ORPCF flags for presence of

                        // other data

    unsigned longreserved1; // set to zero

    CID  cid;                // causality id of caller

   ORPC_EXTENT_ARRAY* extensions; // [unique] extensions

 } ORPCTHIS;

 

ORPCTHIS结构第一个参数指定了这个方法调用所采用的DCOM协议版本号,Windows95 1.0版和WindowsNT4.0补丁3之前,COM版本是5.1;WindowsNT 4.0补丁3,COM版本是5.2;Windows95 1.1中的DCOM和WindowsNT 4.0补丁3之后,COM的版本是5.3。由于每次远程调用都包含有ORPCTHIS结构体,所以DCOM的版本也就传递到服务器上。在服务器上,客户端的DCOM版本与服务器端的进行比较,如果二者的主版本号不匹配,错误RPC_E_VERSION_MISMATCH会传给客户端,但允许服务器上的次版本号高于客户端,这时,服务器必须将DCOM协议的应用限制到客户端版本的允许的范围。

因果ID(causality identifier ,CID)是一个GUID,它将那些多次相关的调用联系起来。例如,如果机器A上的客户端A调用机器B上的组件B,而组件B在返回给A之前,调用机器C上的组件C,这些调用被称为有因果关系。产生一个新调用(不是处理一个进来的调用)时,根据DCOM协议,就会产生一个新CID。如果是后续调用,同一个CID会被传播,即组件B会代表客户端A使用同样的CID,即使组件B采用连接点或其他机制回调客户端A也采用同样的CID。ORPCTHIS的扩展域字段允许COM调用附加额外的数据。

目前,只有定义了两个扩展:一个是用于错误信息(IErrorInfo),另一个用于ORPC调试。对ORPCTHIS的定制扩展,可以采用一个叫通道钩(channel hooking)的没有被文档收录的技术。关于通道钩的更多的信息,请参阅January1998 installment of Don Box'sActiveX®/COM column。

在每个COM方法的应答PDU中,有一个特别的外传参数(ORPCTHAT),它被插在所有外传参数之前,因此,如果一个如下的COM方法: HRESULT Sum(int x, int y, [out, retval] int* result),它的应答PDU将是 HRESULT Sum(ORPCTHAT orpcthat, intresult)。

ORPCTHAT结构的定义如下:

 // Implicit ‘that'pointer which is the first [out]

 // parameter onevery ORPC call.

 typedef structtagORPCTHAT {

    unsignedlong  flags;    // ORPCF flags for presence

                             // of other data

   ORPC_EXTENT_ARRAY *extensions; // [unique] extensions

 } ORPCTHAT;

喵!

在DCOM网络协议中,方法参数的传送按照OSF DEC RPC所规定的网络数据描述(Network Data Representation ,NDR)格式。NDR精确地规定了所有能被IDL理解的原生数据类型是如何被封装到数据包的,DCOM对NDR的仅有扩展是对封送接口指针的支持。在接口定义中,iid是一个IDL的关键字,它可以被认为是一个新的能被封送的原生数据类型:接口指针。使用“接口指针”这个词有点问题,因为它使人在精神上想起指向vtable结构(该结构包含若干指向方法的指针)的指针,但是一旦它被封送到数据包中,情况更本不是那样,它仅是一个获取某个对象的符号表示,因而它仅仅是一个对象参考而已。被封送的接口指针的格式由MInterfacePointer结构决定:

 // Wirerepresentation of a marshaled interface

 // pointer, alwaysthe little-endian form of an OBJREF

 typedef structtagMInterfacePointer {

    ULONG             ulCntData; // size of data

    byte              abData[];  // [size_is(ulCntData)]

                                 // data

 }MInterfacePointer, *PMInterfacePointer;

跟在ulCntData之后的byte数组包含了实际的对象引用,它是一个叫OBJREF的结构(定义见图11)。OBJREF是一个用来表示对象引用的数据结构,根据采用的封送类型,OBJREF有三种形式:标准、指针或自定(standard, handler, custom)。

 

// Although thisstructure is conformant, it is always

 // marshaled inlittle-endian byte-order.

 typedef structtagOBJREF {

    unsignedlong  signature;        // Always MEOW

    unsignedlong  flags;            // OBJREF flags

    GUID           iid;              // interface identifier

 

    union {    // [switch_is(flags), switch_type(unsignedlong)]

       struct{    // [case(OBJREF_STANDARD)]

         STDOBJREF         std;        // standard objref

          DUALSTRINGARRAY   saResAddr; // resolver address

       }u_standard;

 

       struct{    // [case(OBJREF_HANDLER)]

         STDOBJREF         std;        // standard objref

         CLSID             clsid;      // Clsid of handler code

          DUALSTRINGARRAY   saResAddr; // resolver address

       } u_handler;

 

       struct{    // [case(OBJREF_CUSTOM)]

         CLSID           clsid;   // Clsid of unmarshaling code

          unsignedlong cbExtension; // size of extension data

          unsigned long  size;     // size of data thatfollows

          byte  *pData;     // extension + class specific data

                             // [size_is(size),ref]

       } u_custom;

    } u_objref;

 } OBJREF;

图11  OBJREF 结构

OBJREF的起始字段是一个无符号long,它是一个签名字段,内容是0x574F454D(十六进制),有意思的是,如果你按照低字节序重新组织一下(4D 45 4F 57),并转换成相应的ASCII码,结果是MEOW。有人推测这是Microsoft Extended Object Wirerepresentation的首字母缩写,但没人敢肯定。对MEOW结构最好的事情是,被网络监视器捕捉到大量的数据包中,我们可以很容易地找到一个对象的引用:就是找到MEOW。要注意的是,不管NDR其他部分的格式如何,一个封送的接口指针的网络格式总是小字节序的。虽然对象的引用可以有多种存储格式,不太可能,也不太希望传送一个表示字节序的标志(因而会增加数据包大小),因此COM的对象引用都是以小字节序传输。

OBJREF结构中,跟在MEOW签名字段后的是标志(flags)字段,它表示了对象引用的形式,可以被设置成OBJREF_STANDARD (1), OBJREF_HANDLER (2), or OBJREF_CUSTOM (4)。OBJREF结构最后的字段是IID,它表示了被封送的接口标识。图12是被捕捉到的,对图5请求PDU的应答数据包。图5中,调用CoCreateInstance方法请求一个对象的IUnknown接口,图12中,你能看到应答的PDU,它包含了封送的IUnknown接口指针(由“MEOW”标识)。


图12   IUnknown 应答包

The Standard Object Reference标准对象引用

上面OBJREF结构中标志(flags)字段(OBJREF_STANDARD,值为1)表示采用标准的封送方式。基于该值,OBJREF结构剩余的字段包含一个STDOBJREF字段和一个DUALSTRINGARRAY字段。STDOBJREF机构如下:

 typedef structtagSTDOBJREF {

    unsignedlong  flags;        // SORF_ flags

    unsignedlong  cPublicRefs;  // count of references

                                 // passed

    OXID           oxid; // oxid of server with thisoid

    OID            oid;  // oid of object with this ipid

    IPID           ipid; // ipid of Interface

 } STDOBJREF;

 STDOBJREF结构的第一个字段是关于对象引用的标识(flags)字段,尽管这个标识字段的大部分值都被系统保留使用,但SORF_NOPING (0x1000)这个值可以用来表明对象不需要被ping(DCOM协议采用ping方式实现复杂的垃圾回收机制,以后将涉及)。STDOBJREF结构的第二个参数cPublicRefs规定了要传输的IPID的引用次数,设置对一个接口的引用次数,避免了客户端在每次调用远程方法时都需要调用IUnknown::AddRef。

STDOBJREF结构的第三个参数规定了拥有对象的服务器的OXID。一个IPID标识了一个进程中的一个特定对象的一个特定接口,但仅仅一个IPID不能包含一次方法调用的足够信息。DCOM和RPC都采用字符串规定远程方法调用的绑定信息。RPC绑定字符串包含了诸如调用采用的网络协议,组件运行的服务器地址等信息。当DCOM准备连接一个特定的OXID时,安全绑定字符串被用来判断哪些参数要传递给RPC基础架构。OXID是一个无符号的混合变量(64位),代表了这个连接信息。调用远程方法之前,客户端将一个OXID转换成一个RPC能理解的绑定字符串。这个转换将在下面讲述。

STDOBJREF结构的第四个参数是实现封送接口对象的对象标识(OID)。OID是64位值,会被用作ping机制的一部分。STDOBJREF结构的最后字段是封送接口的真正的标识(IPID)。


(未完)

阅读更多
个人分类: C/C++
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭