While there are plenty of examples to help people write dissectors for
a number of protocols, and there is example code for dissectecting
MS RPCs (eg, packet-dcerpc-svrscv.c), the code is difficult to understand
without a lot of other knowledge.
Here I will try to give you an idea of how to create a dissector for an
RPC (and not just an MS RPC, but any DCE RPC). Of course, if you have the
IDL for the RPC, you will be a long way ahead, but with MS RPC, you
usually don't have the IDL.
NOTE. It is not my goal in this little tutorial to teach you how
to write an Ethereal dissector. You should check the Ethereal source and
the documentation that comes with it for info on that. Also, this page
might be incomplete, and you will not be able to just throw together
the code fragments here to get a working dissector. However, if you
understand much of what I am saying here, you will know how to create
the rest of what is needed.
To really understand this material, you should firstly review the Network
Data Representation (NDR) spec: Network Data Representation.
Here we will look at how to create a dissector once you have a capture
of the request and reply generated by the RPC. However, you will also
need additional information, like the function prototype to get some idea
of the actual parameters. Here, MSDN can give you a great deal of help.
It can also help you to create a small program in Visual C++ that will
generate traffic to test your dissector.
We will look at two RPCs: NetWkstaGetInfo and NetWkstaUserEnum, because
they illustrate the case where relatively simple parameters are passed
(at least, no arrays of variant structures), but also the case where
arrays of variant structures are passed, which Microsoft seems to love.
The first problem you will face is in generating some RPC requests and
replies you can use to figure out what the RPC arguments look like and
so you can test that your dissector works. Here, MSDN and Visual C++ are
very useful. Using the information in MSDN you can write a simple Visual
C++ program that will generate RPC requests and replies.
The following is such a program that generates a few RPCs:
// NetWKSTA.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include
#include
#include
int main(int argc, char* argv[])
{
LPWKSTA_USER_INFO_0 pBuf = NULL;
LPWKSTA_USER_INFO_0 pTmpBuf;
LPWKSTA_INFO_102 pInfoBuf100 = NULL;
LPWKSTA_INFO_502 pInfoBuf502 = NULL;
WKSTA_INFO_502 wi;
DWORD dwLevel = 0;
DWORD dwMaxLen = -1;
DWORD dwEntriesRead = 0;
DWORD dwTotalEntries = 0;
DWORD dwResumeHandle = 0;
DWORD i;
DWORD dwTotalCount = 0;
NET_API_STATUS nStatus;
LPTSTR pszServerName = NULL;
if (argc > 2)
{
fprintf(stderr, "Usage: %s [\\\\ServerName]\n", argv[0]);
return 1;
}
// The server is not the default local computer.
//
if (argc == 2)
MultiByteToWideChar(CP_ACP, 0, argv[1], strlen(argv[1]) + 1,
wszServerName, sizeof(wszServerName)/sizeof(WCHAR));
fprintf(stdout, "\nSending RPCs to %s:\n", argv[1]);
//
// Call the NetWkstaGetInfo function, specifying level 100.
//
dwLevel = 100;
nStatus = NetWkstaGetInfo(pszServerName,
dwLevel,
(LPBYTE *)&pInfoBuf100);
// Next, call it at level 101
dwLevel = 101;
nStatus = NetWkstaGetInfo(pszServerName,
dwLevel,
(LPBYTE *)&pInfoBuf100);
// Next, call it at level 102
dwLevel = 102;
nStatus = NetWkstaGetInfo(pszServerName,
dwLevel,
(LPBYTE *)&pInfoBuf100);
dwLevel = 0;
nStatus = NetWkstaUserEnum(pszServerName,
dwLevel,
(LPBYTE*)&pBuf,
dwMaxLen,
&dwEntriesRead,
&dwTotalEntries,
&dwResumeHandle);
if ((nStatus != NERR_Success) && (nStatus != ERROR_MORE_DATA))
fprintf(stderr, "A system error has occurred: %d\n", nStatus);
return 0;
}
|
Simply build the project under Visual C++ and run it from a CMD prompt,
and have Ethereal running and capturing packets at the same time.
Once you have a capture of the traffic, we can go to work.
First, you need the basic infrastructure for your dissector.
Apart from the normal stuff you need for a dissector, you need a table
of OpNums, names and dissection routines for the RPC calls in the
interface you are interested in. Such a table would be declared something
like:
static dcerpc_sub_dissector dcerpc_wkssvc_dissectors[] = {
{ WKS_NetWkstaGetInfo, "NetWkstaGetInfo",
wkssvc_dissect_netwkstagetinfo_rqst,
wkssvc_dissect_netwkstagetinfo_reply},
{ WKS_NetWkstaSetInfo, "NetWkstaSetInfo",
wkssvc_dissect_netwkstasetinfo_rqst,
wkssvc_dissect_netwkstasetinfo_reply},
{ WKS_NetWkstaEnumUsers, "NetWkstaUserEnum",
wkssvc_dissect_netwkstaenumusers_rqst,
wkssvc_dissect_netwkstaenumusers_reply},
{ WKS_NetWkstaUnkn_003, "NetWkstaUnknown_003",
NULL,
NULL,},
{ WKS_NetWkstaUnkn_004, "NetWkstaUnknown_004",
NULL,
NULL},
{ WKS_NetWkstaTransportEnum, "NetWkstaTransportEnum",
wkssvc_dissect_netwkstatransportenum_rqst,
wkssvc_dissect_netwkstatransportenum_reply},
{0, NULL, NULL, NULL }
};
|
As you can see, this table specifies the OpNum (eg, WKS_NetWkstaGetInfo,
which will probably be declared in an include file), the name of the
routine (eg NetWkstaGetInfo), and a dissection routine for each of the
request and reply. If either is missing, the body of that request or
reply will not be dissected.
You also need to define the UUID for the interface and register a
UUID-based handoff table using the sub-dissector table above.
Here is how you specify the UUID (but you need to know the UUID):
static e_uuid_t uuid_dcerpc_wkssvc = {
0x6bffd098, 0xa112, 0x3610,
{ 0x98, 0x33, 0x46, 0xc3, 0xf8, 0x7e, 0x34, 0x5a }
};
|
The following small segment of the middle pane of an Ethereal display
of RPC packets shows what the UUID looks like.
The UUID for the interface will show up in a similar way even if Ethereal
knows nothing about the RPC interface you want to code dissectors for.
Then you must create a handoff routine like that below.
void
proto_reg_handoff_dcerpc_wkssvc(void)
{
/* Register protocol as dcerpc */
dcerpc_init_uuid(proto_dcerpc_wkssvc, ett_dcerpc_wkssvc,
&uuid_dcerpc_wkssvc, ver_dcerpc_wkssvc,
dcerpc_wkssvc_dissectors, -1);
}
|
Then you can start writing the dissectors.
Let's first look at NetWkstaGetInfo. If you look at a trace generated
from the above program, you will see that the opnum associated with the
first DCERPC request is 0. This allows you to fill in the table above
with:
{ 0, "NetWkstaGetInfo",
wkssvc_dissect_netwkstagetinfo_rqst,
wkssvc_dissect_netwkstagetinfo_reply},
|
Although you might find it better to
#define WKS_NetWkstaGetInfo 0
|
And use the symbol.
The next step is to look at the stub data that Ethereal shows and use
that, as well as information from MSDN about the prototype for
NetWkstaGetInfo
to dissect the requests and replies.
Here is what the stub data for a NetWkstaGetInfo request looks like:
00a0 5c 50 42 00 09 00 00 00 00 00 ......\P B.......
00b0 00 00 09 00 00 00 5c 00 5c 00 4e 00 54 00 34 00 ......\. \.N.T.4.
00c0 50 00 44 00 43 00 00 00 c9 11 64 00 00 00 P.D.C... É.d...
|
If you read the NDR document referred to above, you will note that
arguments are marshalled in units of 32 bits. However, what are we to
make of the stub data above. Well, firstly, we need to know what the
function prototype looks like, and then guess, by looking at the stub
data and other dissectors what we have.
The function prototype for NetWkstaGetInfo is roughly:
NET_API_STATUS NetWkstaGetInfo(
LPWSTR Server,
DWORD Level,
LPBYTE *bufptr
);
|
The MSDN entry goes on to explain that Server and Level are [in]
parameters, while bufptr is an [out] parameter. Thus, we would expect to
only see Server and Level in the request, and whatever bufptr points to
in the reply.
Now, Microsoft's stub compiler seems to regard an LPWSTR as a pointer
to a varying string, which explains this much of the stub data:
00a0 5c 50 42 00 09 00 00 00 00 00 ......\P B.......
00b0 00 00 09 00 00 00 5c 00 5c 00 4e 00 54 00 34 00 ......\. \.N.T.4.
00c0 50 00 44 00 43 00 00 00 P.D.C...
|
Those first four bytes (5c504200) are simply a REF (incase there are
several pointers to the same data), the next twelve bytes are the header
associated with a varying length string (Max Len, offset, actual length).
You will notice that the length of the string is 9 WCHARs (or UNICODE
characters), and consists of \\NT4PDC. The last pair of 00 bytes is
actually the null terminator.
This leaves the last six bytes of the stub data:
00c0 c9 11 64 00 00 00 É.d...
|
The first two bytes are simply alignment bytes (where the alignment is
32-bit alignment and is to the beginning of the stub data). The remaining
four bytes are the Level parameter.
MSDN suggests that the level parameter can take the values 100, 101, or
102. Each of these results in more data being returned from the call.
The value contained in the last four bytes is 0x64, or 100. Thus it seems
pretty clear that we have a DWORD parameter directly encoded in the stub
data stream.
Armed with this info, the dissector for requests should look like this:
static int
wkssvc_dissect_netwkstagetinfo_rqst(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
offset = dissect_ndr_str_pointer_item(tvb, offset, pinfo, tree, drep,
NDR_POINTER_UNIQUE, "Server",
hf_wkssvc_server, 0);
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_info_level, 0);
return offset;
}
|
Of course, you will have to create entries for those hf_wkssvc_* items
above, but looking at other Ethereal dissectors will give you an idea of
what is needed.
So how about the reply? Well, here is the stub data:
0080 64 00 00 00 00 fe ..\..... ..d....þ
0090 17 00 f4 01 00 00 2e fe 17 00 1a fe 17 00 04 00 ..ô....þ ...þ....
00a0 00 00 00 00 00 00 07 00 00 00 00 00 00 00 07 00 ........ ........
00b0 00 00 4e 00 54 00 34 00 50 00 44 00 43 00 00 00 ..N.T.4. P.D.C...
00c0 64 00 0a 00 00 00 00 00 00 00 0a 00 00 00 54 00 d....... ......T.
00d0 49 00 47 00 45 00 52 00 54 00 45 00 41 00 4d 00 I.G.E.R. T.E.A.M.
00e0 00 00 00 00 00 00 ......
|
The first field appears to be the Level parameter again, followed by a
bunch of stuff and then some interesting looking strings: NT4PDC and
TIGERTEAM, which happen to be the NetBIOS name of the server the request
was sent to and the domain that it lives in.
However, to understand this stub data, you have to realize that
bufptr referred to in the function prototype above is actually
a pointer to a UNION type, with one structure for each different Level
parameter that can be used. It will be defined something like the
following:
typedef union {
struct WKSTA_INFO_100 info100;
struct WKSTA_INFO_101 info101;
struct WKSTA_INFO_102 info102;
} wksta_info_union;
|
In order for the receiver to be able to distinguish what structure was
returned without retaining state in the stub, this info is returned in
stub data. A look at the NDR document mentioned above will also tell us
that a union is marshalled as the discriminator followed by the data
for the actual structure that it contains.
So, we need to write a dissector for the wksta_info_union before
we can write a dissector for the reply. However, that will simply consist
of dissecting the discriminator, which is the level byte, and then using
that in a switch statement to dissect the actual info returned. Here is
an example:
static int
wkssvc_dissect_WKS_GETINFO_UNION(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
guint32 level;
ALIGN_TO_4_BYTES;
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_info_level, &level);
switch(level){
case 100:
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_WKS_INFO_100,
NDR_POINTER_UNIQUE, "WKS_INFO_100:", -1);
break;
case 101:
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_WKS_INFO_101,
NDR_POINTER_UNIQUE, "WKS_INFO_101:", -1);
break;
case 102:
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_WKS_INFO_102,
NDR_POINTER_UNIQUE, "WKS_INFO_102:", -1);
break;
}
return offset;
}
|
Now, each of the routines to dissect the WKS_INFO_100, WKS_INFO_101, or
WKS_INFO_102 structures are going to be similar. Here is the definition
of wkssvc_dissect_WKS_INFO_100:
static int
wkssvc_dissect_WKS_INFO_100(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_platform_id, NULL);
offset = dissect_ndr_str_pointer_item(tvb, offset, pinfo, tree, drep,
NDR_POINTER_UNIQUE, "Server", hf_wkssvc_server, 0);
offset = dissect_ndr_str_pointer_item(tvb, offset, pinfo, tree, drep,
NDR_POINTER_UNIQUE, "Net Group", hf_wkssvc_net_group, 0);
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_ver_major, NULL);
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_ver_minor, NULL);
return offset;
}
|
The routines for the other structures will be similar. Refer to the
structure defininitions to see how they should be written.
Now we can dissect the reply. Another thing to realize is that
in addition to the info structure, the return code from NetWkstaGetInfo
will be encoded as the last four-byte
quantity in the stub data stream.
Here is what the dissector for the reply look like:
static int wkssvc_dissect_netwkstagetinfo_reply(tvbuff_t *tvb, int offset,
packet_info *pinfo,
proto_tree *tree,
char *drep)
{
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_WKS_GETINFO_UNION,
NDR_POINTER_REF, "Server Info", -1);
offset = dissect_doserror(tvb, offset, pinfo, tree, drep,
hf_wkssvc_rc, NULL);
return offset;
}
|
As you can see, it dissects the WKS_GETINFO_UNION first, by calling
dissect_ndr_pointer, and then dissects the error code that was
returned.
OK, now we can look at a more complex function like
NetWkstaUserEnum, where an array of structures is returned, and someone
in MS seems to have created slightly incorrect IDL, in that an [out]
parameter is sent over in the request.
If we look at the function prototype for NetWkstaUserEnum, we see it is
something like:
NET_API_STATUS NetWkstaUserEnum(
LPWSTR servername,
DWORD level,
LPBYTE *bufptr,
DWORD prefmaxlen,
LPDWORD entriesread,
LPDWORD totalentries,
LPDWORD resumehandle
);
|
Now, MSDN says that bufptr is a pointer to an array of USER_INFO
structures returned by the call. Since level can take one of several
values, bufptr will point to an array of union types. MSDN also says
that servername, level, and prefmaxlen are all [in] parameters, while
bufptr, entriesread, totalentries are [out] parameters, and resumehandle
is an [in, out] parameter.
Based on that information, let's look at the stub parameters and see if
we can recognize any of them:
00a0 5c 50 42 00 09 00 00 00 00 00 ......\P B.......
00b0 00 00 09 00 00 00 5c 00 5c 00 4e 00 54 00 34 00 ......\. \.N.T.4.
00c0 50 00 44 00 43 00 00 00 c9 11 01 00 00 00 01 00 P.D.C... É.......
00d0 00 00 30 fe 12 00 00 00 00 00 00 00 00 00 ff ff ..0þ.... ......ÿÿ
00e0 ff ff d0 fe 12 00 00 00 00 00 ÿÿÐþ.... ..
|
It is pretty clear that the first four bytes (5c 50 42 00) are a ref
parameter and they refer to the varying length string that follows, which
has the UNICODE value \\NT4PDC. That consumes the following bytes:
00a0 5c 50 42 00 09 00 00 00 00 00 ......\P B.......
00b0 00 00 09 00 00 00 5c 00 5c 00 4e 00 54 00 34 00 ......\. \.N.T.4.
00c0 50 00 44 00 43 00 00 00 P.D.C...
|
However, the next bit looks troublesome. There are two bytes of alignment
again (c9 11) but then there are far too many fields for level,
prefmaxlen and resumehandle.
The field following the filler is clearly the level parameter, and
then there are some funny fields, but towards the end we have:
00d0 ff ff ÿÿ
00e0 ff ff d0 fe 12 00 00 00 00 00 ÿÿÐþ.... ..
|
This looks like prefmaxlen, which had the value -1, followed by
resumehandle, which is a pointer to a DWORD. The question is,
what are the bytes before these last two fields?
What they are will become obvious later, so let's proceed to dealing with
the NetWkstaUserEnum reply.
Here is an example of what the stub data looks like:
0080 00 00 00 00 00 00 ..T..... ........
0090 00 00 88 29 18 00 01 00 00 00 88 05 17 00 01 00 ...).... ........
00a0 00 00 8c 05 17 00 0e 00 00 00 00 00 00 00 0e 00 ........ ........
00b0 00 00 41 00 64 00 6d 00 69 00 6e 00 69 00 73 00 ..A.d.m. i.n.i.s.
00c0 74 00 72 00 61 00 74 00 6f 00 72 00 00 00 01 00 t.r.a.t. o.r.....
00d0 00 00 98 29 18 00 00 00 00 00 00 00 00 00 ...).... ......
|
While these bytes look a bit difficult to understand, the first parameter
is the bufptr, which is a union type consisting of arrays of
USER_INFO_0
or USER_INFO_1 structures. As specified by the NDR spec, a non-encapsulated
union will contain the discriminant marshalled twice, once as the field
and once as the first part of the union representation. Following that
looks like a referent (88 05 17 00), and then the count of elements in
the array (one in this case), and then another referent (8c 05 17 00) to
the string that follows (this is actually the USER_INFO_0 structure).
After the USER_INFO_0 structure, we find the totalentries field (01 00 00
00), the resume handle (98 29 18 00 00 00 00 00) and the return code of all
zeros.
In order to dissect this, we have to know what a USER_INFO_0 and USER_INFO_1
looks like. However, MSDN helps here, and we arrive at the following code:
tatic int
wkssvc_dissect_USER_INFO_0(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
offset = dissect_ndr_str_pointer_item(tvb, offset, pinfo, tree, drep,
NDR_POINTER_UNIQUE, "User Name",
hf_wkssvc_user_name, 0);
return offset;
}
static int
wkssvc_dissect_USER_INFO_1(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
offset = dissect_ndr_str_pointer_item(tvb, offset, pinfo, tree, drep,
NDR_POINTER_UNIQUE, "User Name",
hf_wkssvc_user_name, 0);
offset = dissect_ndr_str_pointer_item(tvb, offset, pinfo, tree, drep,
NDR_POINTER_UNIQUE, "Logon Domain",
hf_wkssvc_logon_domain, 0);
offset = dissect_ndr_str_pointer_item(tvb, offset, pinfo, tree, drep,
NDR_POINTER_UNIQUE, "Other Domains",
hf_wkssvc_other_domains, 0);
offset = dissect_ndr_str_pointer_item(tvb, offset, pinfo, tree, drep,
NDR_POINTER_UNIQUE, "Logon Server",
hf_wkssvc_logon_server, 0);
return offset;
}
|
It turns out that this array of USER_INFO_0 or USER_INFO_1 structures
will be marshalled in a container that provides the number of entries
returned (entriesread) followed by the actual array. Here
is the code to dissect them:
static int
wkssvc_dissect_USER_INFO_0_array(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
offset = dissect_ndr_ucarray(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_USER_INFO_0);
return offset;
}
static int
wkssvc_dissect_USER_INFO_0_CONTAINER(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_num_entries, NULL);
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_USER_INFO_0_array, NDR_POINTER_UNIQUE,
"USER_INFO_0 array:", -1);
return offset;
}
static int
wkssvc_dissect_USER_INFO_1_array(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
offset = dissect_ndr_ucarray(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_USER_INFO_1);
return offset;
}
static int
wkssvc_dissect_USER_INFO_1_CONTAINER(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_num_entries, NULL);
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_USER_INFO_1_array, NDR_POINTER_UNIQUE,
"USER_INFO_1 array:", -1);
return offset;
}
|
Finally, the piece of glue needed to tie all this together is:
static int
wkssvc_dissect_USER_ENUM_UNION(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
guint32 level;
ALIGN_TO_4_BYTES;
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_info_level, &level);
switch(level){
case 0:
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_USER_INFO_0_CONTAINER,
NDR_POINTER_UNIQUE, "USER_INFO_0_CONTAINER:", -1);
break;
case 1:
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_USER_INFO_1_CONTAINER,
NDR_POINTER_UNIQUE, "USER_INFO_1_CONTAINER:", -1);
break;
}
return offset;
}
|
OK, so now we can see what those funny bytes were in the request. Here
they are again:
00c0 c9 11 01 00 00 00 01 00 É.......
00d0 00 00 30 fe 12 00 00 00 00 00 00 00 00 00 ..0þ.... ......
|
After the filler (c9 11) we have a uint32 value of 1, followed by another
uint32 value of 1, then what looks like a referent (30 fe 12 00) and then
two uint values of 0. That is just what you would get if bufptr,
which points to a union type and is an array of structures, was defined
in the IDL as an [in, out] parameter, and a null value was passed in the
call.
So, now we know how to dissect the request:
static int
wkssvc_dissect_netwkstaenumusers_rqst(tvbuff_t *tvb, int offset,
packet_info *pinfo, proto_tree *tree,
char *drep)
{
offset = dissect_ndr_str_pointer_item(tvb, offset, pinfo, tree, drep,
NDR_POINTER_UNIQUE, "Server",
hf_wkssvc_server, 0);
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_info_level, 0);
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_USER_ENUM_UNION,
NDR_POINTER_REF, "User Info", -1);
offset = dissect_ndr_uint32(tvb, offset, pinfo, tree, drep,
hf_wkssvc_pref_max, 0);
offset = dissect_ndr_pointer(tvb, offset, pinfo, tree, drep,
wkssvc_dissect_ENUM_HANDLE,
NDR_POINTER_UNIQUE, "Enum Handle", -1);
return offset;
}
|
Acknowledgements
I would like to thank Todd Sabin for writing a great set of
dissection routines for DCERPC, Tim Potter for making a start on
the MSRPC dissection routines, and especially Ronnie Sahlberg for
figuring out many of the details of MSRPC calls and IDL that were of
interest to us.
|