I used Microsoft Interface Definition Language (MIDL) defined Remote Procedure Call interfaces many times in the past (for both defining custom interfaces and using standard interfaces), but had not used it again since 2005 – that is until the last few weeks, when exploring Kerberos Armouring. MIDL was needed to use the Directory Replication Service (DRS) Remote Protocol to obtain the keys needed to decipher armoured Kerberos tickets and to decode the claims in the PAC_CLIENT_CLAIMS_INFO within Kerberos tickets. Both of these usages required use of some information that is not easy to find; I also wanted to use C# to develop the code.
MIDL and C#
The default options for two important features of MIDL are “/Oicf” and “/protocol dce”. “/Oicf” is almost a “boilerplate” option (can/should be used under (almost) all circumstances); “/protocol” can be used to specify DCE, NDR64 or both as the “transfer syntax”. NDR64 is optimized for 64 bit applications but DCE can be used under all circumstances. The outputs of MIDL are C language source and include files and the output produced by “/protocol dce” is the easiest to incorporate in a pure C# application.
One can use the full features of MIDL (including NDR64 transfer syntax) if one creates a “mixed” application, compiling the MIDL output with a C compiler (perhaps C++/CLI) and using C# to native interoperability mechanisms (if necessary) to use the RPC functions.
Another approach is to “massage” the MIDL output into a form that can be “compiled” by C#. For a DCE client stub, one just needs to create and initialize two structs/classes (RPC_CLIENT_INTERFACE and MIDL_STUB_DESC) and two byte arrays (TypeFormatString and ProcFormatString); this is straightforward for anyone who is familiar with .NET interoperability. The only minor problem is that the client stub code uses macros to expand 2 and 4 byte integers into byte sequences in the format strings, but some simple pre-processing of the format strings with regular expressions can resolve this issue.
One way of calling NdrClientCall2 to initiate RPC is to define the RPC function as follows, using the DllImport EntryPoint attribute:
[DllImport("rpcrt4.dll", EntryPoint = "NdrClientCall2")]
static extern int GetNCChanges(MIDL_STUB_DESC stub, byte[] format, IntPtr drs,
uint inver, DRS_MSG_GETCHGREQ msgin,
out uint outver, [Out] DRS_MSG_GETCHGREPLY_NATIVE msgout);
static extern int GetNCChanges(MIDL_STUB_DESC stub, byte[] format, IntPtr drs,
uint inver, DRS_MSG_GETCHGREQ msgin,
out uint outver, [Out] DRS_MSG_GETCHGREPLY_NATIVE msgout);
This finesses the issue of variable length argument lists (with marshalling directives) for NdrClientCall2 and provides some type safety.
The ProcFormatString can be divided into individual chunks for each remote procedure, so the “public” interface to the RPC routine is constructed thus:
public static int GetNCChanges(IntPtr drs, uint inver, DRS_MSG_GETCHGREQ msgin,
out uint outver, DRS_MSG_GETCHGREPLY_NATIVE msgout)
{
byte[] format = new byte[]
{ 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x30, 0x00,
0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x24, 0x00,
0x47, 0x06, 0x0A, 0x47, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x48, 0x00,
0x08, 0x00, 0x08, 0x00, 0x0B, 0x01, 0x10, 0x00, 0x56, 0x00,
0x50, 0x21, 0x18, 0x00, 0x08, 0x00, 0x13, 0x01, 0x20, 0x00,
0xF8, 0x01, 0x70, 0x00, 0x28, 0x00, 0x08, 0x00, 0x00 };
out uint outver, DRS_MSG_GETCHGREPLY_NATIVE msgout)
{
byte[] format = new byte[]
{ 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x30, 0x00,
0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x24, 0x00,
0x47, 0x06, 0x0A, 0x47, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x4E, 0x00, 0x48, 0x00,
0x08, 0x00, 0x08, 0x00, 0x0B, 0x01, 0x10, 0x00, 0x56, 0x00,
0x50, 0x21, 0x18, 0x00, 0x08, 0x00, 0x13, 0x01, 0x20, 0x00,
0xF8, 0x01, 0x70, 0x00, 0x28, 0x00, 0x08, 0x00, 0x00 };
return GetNCChanges(Stub, format, drs, inver, msgin, out outver, msgout);
}
It was possible to create a single source file that could be copied between systems (e.g. by copying the source text to the clipboard, in a remote desktop scenario) and compiled with the local C# compiler. My generously formatted source code, including formatting/display of the acquired keys, was about 900 lines (30 kilobytes) long.
}
It was possible to create a single source file that could be copied between systems (e.g. by copying the source text to the clipboard, in a remote desktop scenario) and compiled with the local C# compiler. My generously formatted source code, including formatting/display of the acquired keys, was about 900 lines (30 kilobytes) long.
Calling IDL_DRSGetNCChanges and deciphering the result
Calling IDL_DRSGetNCChanges is mostly just a straightforward use of the RPC interface defined in [MS-DRSR], using the information in section 4.1.10.6.17 to decrypt some of the returned attributes (e.g. “supplementalCredentials”) and the information in [MS-SAMR] section 2.2.11.1 to decrypt some others (e.g. “unicodePwd”).
There is no documented method to get access to the key material needed to perform [MS-DRSR] 4.1.10.6.17, but the functions/definitions needed are present in the “include” files in the SDK (albeit in the file rpcdcep.h, the final “p” probably indicating “private”). The routine I_RpcBindingInqSecurityContext returns a security context handle that can be used as the first argument to QueryContextAttributes in a call to retrieve the attribute SECPKG_ATTR_SESSION_KEY. The bigger problem is to obtain access to a suitable RPC_BINDING_HANDLE value for use as the first argument to I_RpcBindingInqSecurityContext: an RPC server can call I_RpcGetCurrentCallHandle (during the handling of a call) to get a suitable handle, but a client has to first call RpcBindingSetOption to set a RPC_C_OPT_SECURITY_CALLBACK routine (the callback routine is invoked with a parameter that can be used in a call to I_RpcBindingInqSecurityContext). These are the methods that the NTDS service uses.
Setting the krbtgt password
I used a “lab” set-up to explore Kerberos Armouring and was quite happy to set a simple password for the krbtgt account. I found a script from Microsoft to reset the krbtgt password (New-CtmADKrbtgtKeys.ps1) that verified that one just needs to reset the password in AD (i.e. the password is not also stored somewhere else (and needs to be kept in sync)). The script generates a complex password and “sets” that as the krbtgt password. I put “sets” in quotes because, under the hood, the password value is ignored and NTDS sets a password of 512 random bytes (notionally 256 Unicode characters, but not all of the byte pairs in the 512 random bytes gives a valid Unicode character). Since I could not set a simple password or meaningfully use the generated password, I needed to obtain the keys derived from the password – hence the use of IDL_DRSGetNCChanges.
Decoding claims in Kerberos tickets
[MS-PAC] describes the PAC_CLIENT_CLAIMS_INFO and PAC_DEVICE_CLAIMS_INFO items that can be found in the Privilege Attribute Certificate (PAC) data included in some Kerberos tickets. Details of the claims data structure (CLAIMS_SET_METADATA) are defined in [MS-ADTS]. The complex claims data structures are serialized using NDR and [MS-ADTS] just describes the deserialization process in terms of an abstract function (NdrDecode) and references [MS-RPCE] for a brief description of “type serialization”.
In the hope that there might be some library function provided by Microsoft that could be used to decode/deserialize the claims, one might search the include files in the Windows SDK and indeed this does reveal some undocumented functions (defined in midles.h) that might be useful: NdrMesTypeDecode, NdrMesTypeDecode2 and NdrMesTypeDecode3.
Looking at how Windows deserializes the data (in samsrv.dll), one can see that it uses NdrMesTypeDecode3 and extract the “constant” data that it uses in conjunction with this routine: a MIDL_STUB_DESC and type format string plus a MIDL_TYPE_PICKLING_INFO structure – this is the essential additional element that is needed.
Samsrv.dll uses a MIDL_TYPE_PICKLING_INFO flag value of 7 and this flag is related to the functionality in the type format string. The value of 7 works with the type format string embedded in samsrv.dll (this type format string uses NDR correlation descriptors that contain both robust_flags and extended range conformance checks) but does not work with the type format string produced by applying midl.exe to the IDL text given in [MS-ADTS] (because this type format string has robust_flags but no extended range conformance checks); to use the midl.exe generated type format string, a flags value of 3 is appropriate. NdrMesTypeDecode presumably defaults a flags value of 0 or 1 (no robust_flags), which does not match the type format string produced by midl.exe applied to the given IDL text. NdrMesTypeDecode2 also takes a MIDL_TYPE_PICKLING_INFO argument and this routine can be used in place of NdrMesTypeDecode3.
Midl.exe does have a “/no_robust” option, but it is ineffective on 64-bit platforms, producing the warning:
midl : warning MIDL2469 : invalid command line option, ignored: -no_robust in 64bit platforms
MIDL_TYPE_PICKLING_INFO includes a Version member and samsrv.dll uses a value of 0x33205054 (the four ASCII characters “TP 3”), but this value does not appear to be checked/used by the decode routines.
midl : warning MIDL2469 : invalid command line option, ignored: -no_robust in 64bit platforms
MIDL_TYPE_PICKLING_INFO includes a Version member and samsrv.dll uses a value of 0x33205054 (the four ASCII characters “TP 3”), but this value does not appear to be checked/used by the decode routines.
Midl.exe can produce a type format string identical to that contained in samsrv.dll, but that involves changing the IDL text. The enumerations CLAIMS_SOURCE_TYPE and CLAIMS_COMPRESSION_FORMAT are removed and the USHORT type is used in their place; the “range” attribute is removed from the ValueCount and applied to the associated array instead. This is an example of the change:
[MS-ADTS]
struct {
[range(1, 10*1024*1024)] ULONG ValueCount;
[size_is(ValueCount)] ULONG64* Uint64Values;
};
samsrv.dll
struct {
ULONG ValueCount;
[size_is(ValueCount), range(1, 10*1024*1024)] ULONG64* Uint64Values;
};
Tip
It can be difficult to determine the offset of specific types in the MIDL type format string. The IDL definition for claims (given in [MS-ADTS]) just defines types, but it can be useful to add a dummy routine to the IDL that references the types that need to be deserialized – one can then easily see the type offsets because these are easily identifiable in the ProcFormatString. I extended the IDL definition thus:
// opnum 0
void
DecodeClaimsSet(
[out] PCLAIMS_SET_METADATA_WRAPPER* meta,
[out] PCLAIMS_SET_WRAPPER* claims);
DecodeClaimsSet(
[out] PCLAIMS_SET_METADATA_WRAPPER* meta,
[out] PCLAIMS_SET_WRAPPER* claims);