Tuesday 28 April 2020

RPC NDR Engine, DCE & NDR64


RPC NDR Engine, DCE & NDR64




The Microsoft documentation for the RPC NDR Engine (https://docs.microsoft.com/en-us/windows/win32/rpc/rpc-ndr-engine) does a good job of explaining the “stubs” generated by MIDL that are used to marshal and unmarshal data for the DCE Transfer Syntax; it is however a “snapshot” of the stub functionality at a certain point in time (May 2018, according to the web pages) and does not reflect the newest MIDL generated stub output.



The “transfer syntax” or “wire protocol” is defined by chapter 14 of the technical standard “DCE 1.1: Remote Procedure Call” [C706] (https://publications.opengroup.org/c706) and this has remained stable; the new developments in the MIDL stubs has been in the area of data consistency checking/validation. [MS-RPCE] contains information about the Microsoft IDL syntax extensions and validation checks.



The RPC NDR Engine documentation states: “The format strings described in this document—and indeed all information generated by the compiler for NDR engine consumption—have always been considered an internal interface between the compiler and the engine.”. The documentation also references ndrtypes.h (“All format string characters used by MIDL and the NDR engine are defined in the Ndrtypes.h file”), but this file is no longer included in the Windows SDK, probably reflecting its “internal use only” status (ndr64types.h is present, but this file is essential for use of NDR64).



One of the newer type format descriptions (FC_RANGE) is present in the documentation but is “hidden” (i.e. is not where one would expect to find it) in the section called “User-Marshal”; the section “Correlation Descriptors” seems like a more natural home for it.



The format characters used by the NDR engine (such as FC_RANGE) are one byte in size and were originally assigned with related meanings grouped together; newer functionality is typically assigned a higher number (I say “typically” because there were/are a few lower values that were initially assigned with names like FC_UNUSED3) and so can be easily identified. MIDL emits “comments” to annotate its type format strings and these comments can be used to understand new usages.



One example is FC_SUPPLEMENT; section 3.1.1.5.3.3.1 of [MS-RPCE] discusses “Target Level 6.0”, “Additional Limitations”. If one constructs a “test” IDL/ACF to exercise “type_strict_context_handle” or “range Attribute to Limit the Range of Maximum Count of Conformant Array and String Length” functionality, one can see (commented) use of FC_SUPPLEMENT in the MIDL output and infer its format and purpose.



The major omission in the NDR engine documentation is the format of the ExprFormatString although this can be deduced from the comments in MIDL output (assuming that the IDL content adequately exercises expression evaluation) and the ndr64types.h file (the EXPR_TOKEN enumeration and the NDR64_EXPR_* structure definitions).



NDR64




The RPC NDR Engine documentation does not say much about NDR64. The two main statements are: “The NDR64 protocol is an extension to the 32-bit based NDR transfer syntax, created specifically to enable developers targeting 64-bit systems to achieve optimized performance.” and “The differences between the NDR wire format and the NDR64 wire format addresses the different size of pointers in a 64-bit environment, as well as other issues.”.



The wire format differences seem modest: full support for 64-bit pointer types and 64-bit representation of count and offset values – allowing much more data to be transferred (although it is difficult to imagine 32-bit (4GB) counts not being adequate in almost all RPC circumstances!).



The architecture of the MIDL stubs for NDR64 differs substantially from those for DCE. DCE uses strings of bytes and no effort is made to align multi-byte values; NDR64 uses a network of structure definitions (the structures are linked by pointers) and members of the structures are aligned on their natural boundaries – this may improve performance. NDR64 also abandons support for “big-endian” integer and floating point representations.



Only supporting “little-endian” data means that it is easier to decide which sequences of bytes can be bit-blitted. This also may improve performance and means that it is not necessary to include information about the individual members of a bit-blittable sequences of bytes in the NDR64 stubs. Depending on one’s perspective (reverse engineering interfaces from stubs or concealing details of an interface), this has negative or positive side-effects.



NDR64 only uses one mechanism for encoding the expressions in correlation descriptors whereas DCE uses three: explicit format codes for simple/common expressions (plus/minus one, multiply/divide by two) and the expression string for most other expressions except under limited circumstances where a callback to an expression evaluation routine is needed (for example, the documentation says: “If the first_is() attribute is applied to a conformant varying array, a callback to an expression evaluation routine is forced.”).



NDR64 uses a byte to encode the format character, just like DCE, but fewer values are assigned (because the “simple expression” formats are not used, structure padding uses one format and a count rather than 7 format characters). This allows related format characters to be grouped, with unused values between the groups – this makes it easy to spot “new” functionality and guess its application.



Range
Used
Description
0x00 – 0x1F
0x14
Simple Types
0x20 – 0x2F
0x05
Pointers
0x30 – 0x3F
0x08
Structures
0x40 – 0x4F
0x08
Arrays
0x50 – 0x5F
0x02
Unions
0x60 – 0x6F
0x06
Strings
0x70 – 0x7F
0x06
Handles
0x80 – 0x8F
0x05
Pointer Layout
0x90 – 0x9F
0x04
Structure Members
0xA0 –
0x06
Miscellaneous (Transmit-As, Represent-As, User-Marshal, Pipe, etc.)



A “fly-in-the-ointment” of this classification is the format code FC64_SYSTEM_HANDLE (0x3C), which is not structure related.



Using NDR64 in a pure C# application




The C language code produced by MIDL for both DCE and NDR64 is simple and predictable enough to be converted, with a few regular expressions, into compilable C# code. For DCE stubs, it is relatively easy to handle marshalling between managed code and calls to the native NdrClientCall2 or NdrClientCall3 routines. For NDR64 stubs, the network of structures linked together with pointers makes marshalling the data for a call to NdrClientCall3 more challenging.



Probably the most common way of using RPC from a .NET application is to use a mixed language approach (managed C++ (cl/clr) and C#, for example) and this approach works equally well with both types of stub.