Saturday, 21 March 2020

NDR-RPC


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);

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 };
    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.

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.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);

Thursday, 5 March 2020

Kerberos Armouring under Windows – an example

The information carried in the encrypted parts of Kerberos messages is not very interesting and is difficult to view in an unencrypted form; the plaintext content of the messages is normally adequate to debug Kerberos related authentication problems. Some tools, such as Wireshark, can decrypt and display the information (given the necessary key information, normally in the form of a keytab file), but I suspect that this seldom happens. Kerberos Armouring adds an additional level of complexity to the encryption process and makes it unlikely that fully decrypted Kerberos armoured messages are often seen.

The following sections display the decoded messages and describes the steps and difficulties in decrypting them. Since Kerberos armouring requires a Ticket Granting Ticket (TGT) for the device (Windows client), we start there. The data structures shown are defined in RFC 4120 (Kerberos V5) and RFC 6113 (Kerberos Preauth Framework).

AS-REQ for the device, no pre-authentication


There is no encrypted information in this message.


ERROR for the device, pre-authentication required

There is no encrypted information in this message.


AS-REQ for the device, with pre-authentication


Decrypting the PA-ENC-TIMESTAMP EncryptedData gives, using the password of the device (or the appropriate key derived from the password (the aes256-cts-hmac-sha1-96 key, in this case)):

{
  [0] { 03/03/2020 17:09:58 } -- patimestamp
  [1] { 313892 } -- pausec
}


AS-REP for the device


Decrypting the final EncPart EncryptedData shown above using the password/key of the device gives:

[APPLICATION 25] { -- EncKDCRepPart
  {
    [0] { -- key
          { [0] { 18 }
            [1] { 'mMcDNF8bhHlXF+ntZ7CosH1qNaUujc6WoCyGustqEG0=' } -- base64
          }
        }
    [1] { -- last-req
          { { [0] { 0 }
              [1] { 03/03/2020 17:09:58 }
          } }
        }
    [2] { 716117998 } -- nonce
    [3] { 14/09/2037 04:48:05 } -- key-expiration
    [4] { 40-E1-00-00 } -- flags
    [5] { 03/03/2020 17:09:58 } -- authtime
    [6] { 03/03/2020 17:09:58 } -- starttime
    [7] { 04/03/2020 03:09:58 } -- endtime
    [8] { 10/03/2020 17:09:58 } -- renew-till
    [9] { 'LEIMGRUBE.CH' } -- srealm
    [10] { -- sname
           { [0] { 2 }
             [1] { { 'krbtgt' 'LEIMGRUBE.CH' } }
           }
         }
    [11] { -- caddr
           { { [0] { 20 }
               [1] { 'EC2AMAZ-EHM3HUN ' }
           } }
         }
    [12] { -- padata, RFC 6806
           { { [1] { 167 } -- PA-PAC-OPTIONS
               [2] { byte[11] => { [0] { 80-00-00-00 } } } }
             { [1] { 165 } -- PA-SUPPORTED-ENCTYPES
               [2] { 1F-00-05-00 }
           } }
         }
  }
}

 
Decrypting the ticket EncPart EncryptedData shown above using the password/key of the krbtgt account gives:

[APPLICATION 3] { -- EncTicketPart
  {
    [0] { 40-E1-00-00 } -- flags
    [1] { -- key
          { [0] { 18 }
            [1] { 'mMcDNF8bhHlXF+ntZ7CosH1qNaUujc6WoCyGustqEG0=' } -- base64
          }
        }
    [2] { 'LEIMGRUBE.CH' } -- crealm
    [3] { -- cname
          { [0] { 1 }
            [1] { { 'EC2AMAZ-EHM3HUN$' } }
          }
        }
    [4] { -- transited
          { [0] { 0 }
            [1] {  }
          }
        }
    [5] { 03/03/2020 17:09:58 } -- authtime
    [6] { 03/03/2020 17:09:58 } -- starttime
    [7] { 04/03/2020 03:09:58 } -- endtime
    [8] { 10/03/2020 17:09:58 } -- renew-till
    [10] { -- authorization-data
           { { [0] { 1 } -- AD-IF-RELEVANT
               [1] { byte[862] => { { [0] { 128 } -- AD-WIN2K-PAC
                                      [1] { byte[840] } } } } }
           }
         }
  }
}


The same “key” value is present in both encrypted parts. This enables the “session” key to be shared between the krbtgt account and the device. This key will be needed to decrypt later Kerberos armouring.

The AD-WIN2K-PAC data is just serialized data (and not encoded ASN.1). Here is a formatted dump of its contents:

PAC Type LoginInformation:
  LogonTime: 2020-03-03T16:06:52.9729666Z
  LogoffTime: 0001-01-01T00:00:00.0000000
  KickOffTime: 0001-01-01T00:00:00.0000000
  PasswordLastSet: 2020-02-17T06:58:17.9280757Z
  PasswordCanChange: 2020-02-18T06:58:17.9280757Z
  PasswordMustChange: 0001-01-01T00:00:00.0000000
  EffectiveName: EC2AMAZ-EHM3HUN$
  FullName:
  LogonScript:
  ProfilePath:
  HomeDirectory:
  HomeDirectoryDrive:
  LogonCount: 429
  BadPasswordCount: 0
  UserId: 1008
  PrimaryGroupId: 516
  GroupIds:        7 516
  UserFlags: 0x20
  UserSessionKey: 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
  LogonServer: EC2AMAZ-EHM3HUN
  LogonDomainName: LEIMGRUBE
  LogonDomainId: S-1-5-21-1700842961-3107118889-547158845
  UserAccountControl: 0x2100
  SubAuthStatus: 0x0
  LastSuccessfulILogon: 1601-01-01T00:00:00.0000000Z
  LastFailedILogon: 1601-01-01T00:00:00.0000000Z
  FailedILogonCount: 0x0
  ExtraSids: 0x7 S-1-5-9
  ExtraSids: 0x7 S-1-5-21-0-0-0-497
  ExtraSids: 0x7 S-1-18-1
PAC Type ClientInformation: 00-8F-57-2F-76-F1-D5-01 EC2AMAZ-EHM3HUN$
PAC Type UpnDns: EC2AMAZ-EHM3HUN$@Leimgrube.ch LEIMGRUBE.CH
PAC Type ClientClaimsInformation: ()
PAC Type ServerChecksum: 10-00-00-00-B0-E5-EC-FF-0A-9A-B5-8F-CD-F9-FC-E1
PAC Type KdcChecksum: 10-00-00-00-52-C2-12-E0-7E-47-C3-61-22-2D-FD-0B



AS-REQ for the user, no pre-authentication


There are 3 EncryptedData elements in the AS-REQ, but we have decrypted and shown one already, namely the EncPart of the Ticket used for the armouring (the device TGT).

Decrypting the Authenticator of the armour value AP-REQ with the session key from the device TGT gives:

[APPLICATION 2] { -- Authenticator
  {
    [0] { 5 } -- authenticator-vno
    [1] { 'LEIMGRUBE.CH' } -- crealm
    [2] { -- cname
          { [0] { 1 }
            [1] { { 'EC2AMAZ-EHM3HUN$' } }
          }
        }
    [4] { 24660 } -- cusec
    [5] { 03/03/2020 17:09:58 } -- ctime
    [6] { -- subkey
          { [0] { 18 }
            [1] { '2ft/oGqS50PtgmO0OK+3UH4KBwp4LtIUOuPS7JJ6ia8=' } -- base64
          }
        }
    [7] { 0 } -- seq-number
  }
}


Combining the subkey from above with the ticket session key, using the function KRB-FX-CF2 allows us to derive the armour key.

KRB-FX-CF2 is defined in RFC 6113 and it utilises the pseudo-random method of the encryption algorithm profile. Appendix A of RFC 6113 says “In performing interoperability testing, there was significant ambiguity surrounding [RFC3961] pseudo-random operations” and that was certainly my experience too.

aes256-cts-hmac-sha1-96 uses a simplified algorithm profile (defined in RFC 3962) and the pseudo-random method is defaulted from section 5.3 of RFC 3961. The default pseudo-random method uses the encryption function (E) of the simplified profile. Perhaps the first uncertainty is whether “encryption function” and “E” are synonymous; initially, I thought that they were, as when RFC 3961 section 5.2 refers to “encryption/decryption functions, E and D”. However section 5.3 seems to define “encryption function” in terms of “E” and other functions:

encryption function       conf = Random string of length c
                          pad  = Shortest string to bring confounder
                                 and plaintext to a length that's a
                                 multiple of m.
                          (C1, newIV) = E(Ke, conf | plaintext | pad,
                                          oldstate.ivec)
                          H1 = HMAC(Ki, conf | plaintext | pad)
                          ciphertext =  C1 | H1[1..h]
                          newstate.ivec = newIV


The biggest confusion for me was the “mode” of “E” (e.g. ECB/CBC/CTS). Even when I deduced/noticed that “No integrity checking or confounder should be included here”, I persisted with the idea that E was aes-cts and “m” (the message block size) was 1. The definition of “m” includes the statement “If a block cipher is used in a mode that can handle messages that are not multiples of the cipher block size, such as CBC mode with cipher text stealing (CTS, see [RC5]), this value would be one octet”.

Only the remark in Appendix A that “These test vectors assume that the AES pseudo-random operation is aes-ecb(trunc128(sha-1(input)))” finally made the requirements of an interoperable implementation of “pseudo-random” for aes256-cts-hmac-sha1-96 clear.
 
Decrypting the EncFastReq element with the armour key gives:


{ --  KrbFastReq
  [0] { 00-00-00-00 } -- fast-options
  [1] { -- padata
        { { [1] { 128 } -- PA-PAC-REQUEST
            [2] { byte[7] => { [0] { True } } } }
          { [1] { 167 } -- PA-PAC-OPTIONS
            [2] { byte[11] => { [0] { 80-00-00-00 } } } } }
      }
  [2] { -- req-body
        { [0] { 40-81-00-10 } -- kdc-options
          [1] { -- cname
                { [0] { 1 }
                  [1] { { 'Administrator' } } }
              }
          [2] { 'LEIMGRUBE.CH' } -- realm
          [3] { -- sname
                { [0] { 2 }
                  [1] { { 'krbtgt' 'LEIMGRUBE.CH' } } }
              }
          [5] { 13/09/2037 04:48:05 } -- till
          [6] { 13/09/2037 04:48:05 } -- rtime
          [7] { 716117995 } -- nonce
          [8] { -- etype
                { 18 23 -133 -128 24 -135 }
              }
          [9] { -- addresses
                { { [0] { 20 }
                    [1] { 'EC2AMAZ-EHM3HUN ' } } } }
              }
      }
}


ERROR for the user, pre-authentication required


Decrypting the EncFastRep element with the armour key gives:

{ -- KrbFastResponse
  [0] { -- padata
        { { [1] { 137 } -- PA-FX-ERROR
            [2] { byte[96] => [APPLICATION 30] { -- KRB-ERROR
                  { [0] { 5 } -- pvno
                    [1] { 30 } -- msg-type
                    [4] { 03/03/2020 17:09:58 } -- stime
                    [5] { 313892 } -- susec
                    [6] { 25 } -- error-code
                    [9] { 'LEIMGRUBE.CH' } -- realm
                    [10] { { [0] { 2 } [1] { { 'krbtgt' 'LEIMGRUBE.CH' } } } } -- sname
                  } } } }
          { [1] { 133 } -- PA-FX-COOKIE
            [2] { 4D-69-63-72-6F-73-6F-66-74 } }
          { [1] { 19 } -- PA-ETYPE-INFO2
     [2] { byte[60] => { { [0] { 18 } [1] { 'LEIMGRUBE.CHAdministrator' } }
                                { [0] { 23 } }
                                { [0] { -133 } }
                                { [0] { -128 } } } } }
          { [1] { 138 } -- PA-ENCRYPTED-CHALLENGE
            [2] {  } }
          { [1] { 16 } -- PA-PK-AS-REP
            [2] {  } }
          { [1] { 15 } -- PA-PK-AS-REP_OLD
            [2] {  } }
        }
      }
   [3] { 716117995 } -- nonce
}


AS-REQ for the user, with pre-authentication




Decrypting the Authenticator of the armour value AP-REQ with the session key from the device TGT gives:

[APPLICATION 2] { -- Authenticator
  {
    [0] { 5 } -- authenticator-vno
    [1] { 'LEIMGRUBE.CH' } -- crealm
    [2] { -- cname
          { [0] { 1 }
            [1] { { 'EC2AMAZ-EHM3HUN$' } }
          }
        }
    [4] { 24661 } -- cusec
    [5] { 03/03/2020 17:09:58 } -- ctime
    [6] { -- subkey
          { [0] { 18 }
            [1] { 'FI0VJO9MmOPrTFeo3Qn8g19rzjNgiVTnFegDvcrP55E=' } -- base64
          }
         }
    [7] { 0 } -- seq-number
  }
}


Decrypting the EncFastReq element with the new armour key derived from the subkey above gives:

{ --  KrbFastReq
  [0] { 00-00-00-00 } -- fast-options
  [1] { -- padata
        { { [1] { 138 } -- PA-ENCRYPTED-CHALLENGE
            [2] { byte[67] => { [0] { 18 } [2] { byte[56] } } } }
          { [1] { 128 } -- PA-PAC-REQUEST
            [2] { byte[7] => { [0] { True } } } }
          { [1] { 167 } -- PA-PAC-OPTIONS
            [2] { byte[11] => { [0] { 80-00-00-00 } } } } } }
 [2] { -- req-body
       { [0] { 40-81-00-10 } -- kdc-options
         [1] { -- cname
               { [0] { 1 }
                 [1] { { 'Administrator' } } }
             }
         [2] { 'LEIMGRUBE.CH' } -- realm
         [3] { -- sname
               { [0] { 2 }
                 [1] { { 'krbtgt' 'LEIMGRUBE.CH' } } }
             }
         [5] { 13/09/2037 04:48:05 } -- till
         [6] { 13/09/2037 04:48:05 } -- rtime
         [7] { 716117988 } -- nonce
         [8] { -- etype
               { 18 23 -133 -128 24 -135 }
             }
         [9] { -- addresses
               { { [0] { 20 }
                   [1] { 'EC2AMAZ-EHM3HUN ' } } } }
             }
     }
}


Decrypting the embedded PA-ENCRYPTED-CHALLENGE, using the client challenge armour key (derived from the client key and the armour key, using KRB-FX-CF2) gives:

{ [0] { 03/03/2020 17:09:58 } [1] { 313892 } }


AS-REP for the user


Decrypting the KrbFastArmoredRep EncryptedData using the armour key gives:

{ -- KrbFastResponse
   [0] { -- padata
         { { [1] { 19 } -- PA-ETYPE-INFO2
             [2] { byte[38] => { { [0] { 18 }
                                   [1] { 'LEIMGRUBE.CHAdministrator' } } } } }
           { [1] { 138 } -- PA-ENCRYPTED-CHALLENGE
             [2] { byte[67] => { [0] { 18 } [2] { byte[56] } } } } }
       }
   [1] { -- strengthen-key
         { [0] { 18 }
           [1] { '4aNHiHJpxx293D5zLMVKqaFkkwsQt0gHXESecGd86YM=' } } -- base64
       }
   [2] { -- finished
         { -- KrbFastFinished
           [0] { 03/03/2020 17:09:58 } -- timestamp
           [1] { 313892 } -- usec
           [2] { 'LEIMGRUBE.CH' } -- crealm
           [3] { { [0] { 1 } [1] { { 'Administrator' } } } } -- cname
           [4] { -- ticket-checksum
                 { [0] { 16 } [1] { 98-66-FC-21-FD-21-21-80-81-76-07-C4 } }
               }
         }
       }
   [3] { 716117988 } -- nonce
}


Decrypting the embedded PA-ENCRYPTED-CHALLENGE, using the KDC challenge armour key (derived from the client key and the armour key, using KRB-FX-CF2) gives:

{ [0] { 03/03/2020 17:09:58 } [1] { 313892 } }
 
Decrypting the final EncPart EncryptedData shown above using the password/key of the user, strengthened with the strengthen-key in the KrbFastResponse gives:


[APPLICATION 25] { -- EncKDCRepPart
  {
    [0] { -- key
          { [0] { 18 }
             [1] { 'lsJxd2LXykXBPBZ/jXRjCeJlfIsWanhSWIcs+eoGovk=' } -- base64
          }
        }
    [1] { -- last-req
          { { [0] { 0 }
              [1] { 03/03/2020 17:09:58 }
          } }
        }
    [2] { 716117988 } -- nonce
    [3] { 06/04/2020 23:04:57 } -- key-expiration
    [4] { 40-E1-00-00 } -- flags
    [5] { 03/03/2020 17:09:58 } -- authtime
    [6] { 03/03/2020 17:09:58 } -- starttime
    [7] { 04/03/2020 03:09:58 } -- endtime
    [8] { 10/03/2020 17:09:58 } -- renew-till
    [9] { 'LEIMGRUBE.CH' } -- srealm
    [10] { -- sname
           { [0] { 2 } [1] { { 'krbtgt' 'LEIMGRUBE.CH' } } }
         }
    [11] { -- caddr
           { { [0] { 20 } [1] { 'EC2AMAZ-EHM3HUN ' } } } }
    [12] { -- padata
           { { [1] { 167 } -- PA-PAC-OPTIONS
               [2] { byte[11] => { [0] { 80-00-00-00 } } } }
             { [1] { 165 } -- PA-SUPPORTED-ENCTYPES
               [2] { 1F-00-05-00 }
           } }
         }
  }
}


Decrypting the Ticket EncPart EncryptedData shown above using the password/key of the host/admin account gives:

[APPLICATION 3] { -- EncTicketPart
  {
    [0] { 40-E1-00-00 } -- flags
    [1] { -- key
          { [0] { 18 }
            [1] { 'lsJxd2LXykXBPBZ/jXRjCeJlfIsWanhSWIcs+eoGovk=' } -- base64
          }
        }
    [2] { 'LEIMGRUBE.CH' } -- crealm
    [3] { { [0] { 1 } [1] { { 'Administrator' } } } } -- cname
    [4] { { [0] { 0 } [1] {  } } } -- transited
    [5] { 03/03/2020 17:09:58 } -- authtime
    [6] { 03/03/2020 17:09:58 } -- starttime
    [7] { 04/03/2020 03:09:58 } -- endtime
    [8] { 10/03/2020 17:09:58 } -- renew-till
    [10] { -- authorization-data
           { { [0] { 1 } -- AD-IF-RELEVANT
               [1] { byte[846] => { { [0] { 128 } -- AD-WIN2K-PAC
                                      [1] { byte[824] } } } } }
           } }
  }
}



TGS-REQ

The EncPart of the PA-TGS-REQ ticket is the same as that in the previous section. Decrypting the Authenticator of the PA-TGS-REQ ticket with the session key of the ticket gives:

[APPLICATION 2] { -- Authenticator
  {
    [0] { 5 } -- authenticator-vno
    [1] { 'LEIMGRUBE.CH' } -- crealm
    [2] { { [0] { 1 } [1] { { 'Administrator' } } } } -- cname
    [3] { -- cksum
          { [0] { 7 }
            [1] { BB-FE-FC-99-91-8D-23-CD-F8-01-F6-56-18-E5-AF-2B }
          }
        }
    [4] { 24662 } -- cusec
    [5] { 03/03/2020 17:09:58 } -- ctime
    [6] { -- subkey
          { [0] { 18 }
            [1] { '7YGfZdDpOIgvAS6gx8CkUpwLdRlvp8M+gqz/4c0VKrY=' } -- base64
          }
        }
    [7] { 716117985 } -- seq-number
  }
}

 
Decrypting the EncFastReq element with the new armour key derived from the subkey above and the key from the PA-TGS-REQ ticket gives:

{ --  KrbFastReq
  [0] { 00-00-00-00 } -- fast-options
  [1] { -- padata
        { { [1] { 167 } -- PA-PAC-OPTIONS
            [2] { byte[11] => { [0] { C0-00-00-00 } } } } } }
  [2] { -- req-body
        { [0] { 40-81-00-00 } -- kdc-options
          [2] { 'LEIMGRUBE.CH' } -- realm
          [3] { { [0] { 2 } [1] { { 'host' 'admin' } } } } -- sname
          [5] { 13/09/2037 04:48:05 } -- till
          [7] { 716117985 } -- nonce
          [8] { { 18 17 } } -- etype
          [10] { { [0] { 18 } [2] { byte[136] } } } -- enc-authorization-data
        }
      }
}


The two EncAuthorizationData elements (in the outer ReqBody and the KrbFastReq req-body) are identical. Decrypting with the subkey gives:

{
  { [0] { 1 } -- AD-IF-RELEVANT
    [1] { byte[95] =>
           { { [0] { 141 } -- KERB_AUTH_DATA_TOKEN_RESTRICTIONS
               [1] { byte[53] => { { [0] { 0 } [1] { byte[40] } } } } }
             { [0] { 142 } -- KERB-LOCAL
               [1] { D0-23-FE-DF-21-02-00-00-30-93-AD-23-00-00-00-00 } } } } }
}


TGS-REP


Decrypting the KrbFastArmoredRep EncryptedData using the armour key gives:

{ -- KrbFastResponse
  [0] { -- padata
        {  }
      }
  [1] { -- strengthen-key
        { [0] { 18 }
          [1] { 'cNisWraV1QLW4/WuGrbZTArNS/Yx0VY9FhDI5ohbcRU=' } } -- base64
      }
  [2] { -- finished
        { -- KrbFastFinished
          [0] { 03/03/2020 17:09:58 } -- timestamp
          [1] { 313892 } -- usec
          [2] { 'LEIMGRUBE.CH' } -- crealm
          [3] { { [0] { 1 } [1] { { 'Administrator' } } } } -- cname
          [4] { -- ticket-checksum
                { [0] { 16 } [1] { 21-1F-C3-BC-95-02-B7-6D-D5-3E-8D-01 } }
              }
        }
      }
  [3] { 716117985 } -- nonce
}


Decrypting the Ticket EncPart EncryptedData shown above using the password/key of the host/admin account gives:

[APPLICATION 3] { -- EncTicketPart
  {
    [0] { 40-A1-00-00 } -- flags
    [1] { -- key
          { [0] { 18 }
            [1] { 'mvIE6JfoLACcnJcq+ZDazBstli4/ywWyeJfsKGiuQV0=' } -- base64
          }
        }
    [2] { 'LEIMGRUBE.CH' } -- crealm
    [3] { { [0] { 1 } [1] { { 'Administrator' } } } } -- cname
    [4] { { [0] { 0 } [1] {  } } } -- transited
    [5] { 03/03/2020 17:09:58 } -- authtime
    [6] { 03/03/2020 17:09:58 } -- starttime
    [7] { 04/03/2020 03:09:58 } -- endtime
    [8] { 10/03/2020 17:09:58 } -- renew-till
    [10] { -- authorization-data
           {
             { [0] { 1 } -- AD-IF-RELEVANT
               [1] { byte[846] =>
                    { { [0] { 128 } -- AD-WIN2K-PAC
                        [1] { byte[824] } } } } }
             }
             { [0] { 1 } -- AD-IF-RELEVANT
               [1] { byte[95] =>
                    { { [0] { 141 } -- KERB_AUTH_DATA_TOKEN_RESTRICTIONS
                        [1] { byte[53] => { { [0] { 0 } [1] { byte[40] } } } } }
                      { [0] { 142 } -- KERB-LOCAL
                        [1] { D0-23-FE-DF-21-02-00-00-30-93-AD-23-00-00-00-00 } } } } }
             }
           }
  }
}

 
Decrypting the final EncPart EncryptedData shown above using the subkey from the Authenticator in the TGS-REQ, strengthened with the strengthen-key in the KrbFastResponse gives:

[APPLICATION 26] { -- EncTGSRepPart
  {
    [0] { -- key
          { [0] { 18 }
            [1] { 'mvIE6JfoLACcnJcq+ZDazBstli4/ywWyeJfsKGiuQV0=' } -- base64
          }
        }
    [1] { { { [0] { 0 } [1] { 03/03/2020 17:09:58 } } } } -- last-req
    [2] { 716117985 } -- nonce
    [4] { 40-A1-00-00 } -- flags
    [5] { 03/03/2020 17:09:58 } -- authtime
    [6] { 03/03/2020 17:09:58 } -- starttime
    [7] { 04/03/2020 03:09:58 } -- endtime
    [8] { 10/03/2020 17:09:58 } -- renew-till
    [9] { 'LEIMGRUBE.CH' } -- srealm
    [10] { { [0] { 2 } [1] { { 'host' 'admin' } } } } -- sname
    [12] { -- padata
           { { [1] { 165 } -- PA-SUPPORTED-ENCTYPES
               [2] { 1F-00-00-00 } }
             { [1] { 167 } -- PA-PAC-OPTIONS
               [2] { byte[11] => { [0] { C0-00-00-00 } } }
           } }
         }
  }
}


Summary


I have nothing “profound” to say by way of summary, but I thought a distinct “end-marker” was needed after the last decrypted Kerberos message.

The most interesting part of the encrypted data is possibly the authorization data; if one can obtain a handle to a Windows token created as a result of Kerberos authentication then much of the information can be obtained via the GetTokenInformation and LsaGetLogonSessionData routines.


Sunday, 16 February 2020

Analysing LDAP Transport Channel Security via Packet Capture


It can be unexpectedly difficult to discover which security mechanisms are actually protecting an LDAP connection – the security measures that have been programmed or configured might not match what is observed via packet capture and analysis.


An LDAP connection can be protected with TLS, by using the StartTLS extended control on an existing connection or by connecting to an LDAP server listening at TCP port 636 (“ldaps” service). TLS provides sealing (also known as confidentiality or encryption) and signing (also known as integrity) for the connection.


A packet capture of a StartTLS extended operation looks like this:




An Active Directory LDAP servers supports 3 authentication methods (Simple, SASL and Sicily) and all of them can be combined with TLS. There is however a limitation with the TLS and SASL combination: SASL confidentiality and integrity mechanisms cannot be combined with TLS; attempts to combine the two fail with the LDAP unwillingToPerform (53) result code.


Combining information from RFC 2251 (LDAPv3) and [MS-ADTS], the Bind Request is defined as follows:


        BindRequest ::= [APPLICATION 0] SEQUENCE {
                version                 INTEGER (1 .. 127),
                name                    LDAPDN,
                authentication          AuthenticationChoice }



        AuthenticationChoice ::= CHOICE {
                simple                  [0] OCTET STRING,
                sasl                    [3] SaslCredentials

                sicilyPackageDiscovery  [9] OCTET STRING
                sicilyNegotiate         [10] OCTET STRING
                sicilyResponse          [11] OCTET STRING }



        SaslCredentials ::= SEQUENCE {
                mechanism               LDAPString,
                credentials             OCTET STRING OPTIONAL }


The Simple and Sicily authentication methods do not define any mechanism to encode/encapsulate any protection for the data carried on the connection which they authenticated. After authentication (the LDAP Bind operation) is complete, no protection (sealing or signing) is applied and the subsequent LDAP PDUs are carried unaltered over the LDAP connection.


Active Directory LDAP servers support 4 SASL mechanisms: GSS-SPNEGO, GSSAPI, EXTERNAL and DIGEST-MD5; only GSSAPI and GSS-SPNEGO will be described here. GSSAPI uses Kerberos for authentication and GSS-SPNEGO uses Kerberos or NTLM.


The intention to use the SASL security protection measures of “sealing” and/or “signing” is signalled via information in the authentication information; NTLM uses the NegotiateFlags field of the NEGOTIATE_MESSAGE, CHALLENGE_MESSAGE and AUTHENTICATE_MESSAGE messages whereas Kerberos uses the Authenticator Checksum Flags field in the authenticator of a KRB_AP_REQ message.


If a LDAP Bind operation is successful, the first bytes of subsequent LDAP messages reveal, to network analysis tools, whether the connection is protected: if it is protected, the first 4 bytes will be a message length (RFC 4422 (SASL), section 3.7) otherwise the message will start with the encoding of the ASN.1 representation of the LDAP operation.


For the GSSAPI SASL mechanism, the format of the SASL credentials is described in RFC 4121 (Kerberos Version 5 GSS-API) section 4.1 and defined in RFC 2743 (GSS-API) section 3.1. Credentials of the appropriate type are obtained by calling GSS_Acquire_cred (the Windows equivalent is AcquireCredentialsHandle), specifying the “Kerberos” package. The credential information is created by the GSS_Init_sec_context routine; the Windows equivalent for this routine is InitializeSecurityContext. By default (if no particular security context is requested), “signing” (integrity) is implied; to override this default, one must explicitly request no signing/integrity via the ISC_REQ_NO_INTEGRITY flag. The requested security context is conveyed in a part of the credential information that is encrypted with the Kerberos session key, so it is not easy to observe the requested context in a network trace.


If signing or sealing is requested, the data in the body of the LDAP message (after the SASL 4 byte length) is the result of passing the plain LDAP message through the GSS_Wrap routine; the Windows equivalent of this routine is EncryptMessage. GSS_Wrap/EncryptMessage is used for both signing and sealing; if only signing is required then EncryptMessage is invoked with the flag SECQOP_WRAP_NO_ENCRYPT. The format of the wrapped message is defined in RFC 4121 section 4.2.6.2.


The GSS-SPNEGO SASL mechanism is similar to the GSSAPI mechanism with the following differences. The credentials are obtained by calling AcquireCredentialsHandle, specifying the “Negotiate” package. The format of the credentials is defined in RFC 4178 (The GSS-API Negotiation Mechanism), section 4.2.


If Kerberos is negotiated, the format of subsequent LDAP messages is the same as GSSAPI/Kerberos (RFC 4121 section 4.2.6.2). If NTLM is negotiated, the format of subsequent LDAP messages is described in [MS-NLMP] section 3.4. NTLM always negotiates at least signing/integrity – this cannot be disabled with the ISC_REQ_NO_INTEGRITY flag; the NTLM SSPI package also seems to ignore the SECQOP_WRAP_NO_ENCRYPT flag, so sealing/confidentiality/encryption is performed too (regardless of whether it was requested or not).


NTLM signing is either a CRC-32 value or the first 8 bytes of an MD5 hash (depending on whether NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY is negotiated); NTLM sealing uses RC4. Kerberos uses the algorithms associated with the Kerberos session key, which might be as secure as AES-256 and SHA1-96 (first 12 bytes of an SHA-1 hash).

Thursday, 30 January 2020

LDAP Channel Binding


LDAP Channel Binding has two pre-requisites that must be fulfilled before it can be used. Firstly, the LDAP connection must flow through a TLS channel and secondly the chosen authentication information must be capable of carrying channel binding information.

The LDAP TLS channel can be established either via the LDAP StartTLS operation or by connecting to the “ldaps” port (636).

Under Windows, the core routine used by clients to both establish a TLS channel and to perform Kerberos/NTLM/Negotiate type authentication is InitializeSecurityContext; one of the outputs of this routine is a “security context handle”.

If the TLS channel establishment is successful, the client will have a security context handle that can be used in a call to the routine QueryContextAttributes to retrieve the SECPKG_ATTR_ENDPOINT_BINDINGS attribute; the value of this attribute is a structure of type SEC_CHANNEL_BINDINGS. A dump of some typical “application data” appended to this structure looks like:

  74 6c 73 2d 73 65 72 76-65 72 2d 65 6e 64 2d 70  tls-server-end-p
 6f 69 6e 74 3a f3 c1 2c-55 13 61 ae 65 13 56 dc  oint:..,U.a.e.V.
 4e de fa fa e0 12 bb 0f-dc 96 64 cf 98 84 9f a4  N.........d.....
 ee 14 de 0e d0                                   .....

The format of the data is the string “tls-server-end-point:” followed by a hash of the server-side certificate used in establishing the TLS channel. The hash function is chosen according to the procedure described in RFC 5929.

The use of the “tls-server-end-point” channel binding type rather than the stricter “tls-unique” type means that LDAP channel binding (quoting from RFC 5929) “may be used with existing TLS server-side proxies ("concentrators") without modification to the proxies”. I interpret this to mean that TLS proxies which use the same certificate and private key as the LDAP server can be used.

When generating the authentication messages to be exchanged with the LDAP server, the channel binding information can be included in the messages by providing the information in a SECBUFFER_CHANNEL_BINDINGS input to the InitializeSecurityContext routine.

The format for channel binding data in a NTLM AUTHENTICATE_MESSAGE is of the same format as that used by Kerberos (documented in RFC 4121), namely a MD5 hash of the channel binding information. This is a dump of an NTLM AUTHENTICATE_MESSAGE used in LDAP authentication, with the channel binding data highlighted:



The dump of the LDAP bind response packet above is not easy to obtain, since the channel binding information is only present if the response is sent inside a TLS channel – one needs to be able to either decrypt the TLS data or capture data inside the TLS channel (perhaps via Event Tracing for Windows (ETW), using perhaps the Microsoft-Windows-LDAP-Client provider).

Viewing the channel binding information when Kerberos is used rather than NTLM is even more difficult. The following image is the Kerberos equivalent of the NTLM data above:



The channel binding information is in the encrypted “Authenticator” and the (session) key needed to decode that is in the encrypted part of the ticket (which is encrypted with the long-term key of the service). Decrypting the ticket reveals:


flags = 40-A1-00-00
key = [23] q11qognXq26zJ5H6s75RQA==
crealm = LEIMGRUBE.CH
cname = cisco
transited = [1]
authtime = 2020-01-28T16:48:52.0000000+01:00
starttime = 2020-01-29T22:14:45.0000000+01:00
endtime = 2020-01-30T08:03:52.0000000+01:00
renew-till = 2020-02-04T16:48:52.0000000+01:00
authorization-data = [AD-IF-RELEVANT] {
  [AD-WIN2K-PAC]
    LogonTime: 2020-01-24T11:05:53.3797010Z
    LogoffTime: 0001-01-01T00:00:00.0000000
    KickOffTime: 0001-01-01T00:00:00.0000000
    PasswordLastSet: 2020-01-21T15:21:39.6988208Z
    PasswordCanChange: 2020-01-22T15:21:39.6988208Z
    PasswordMustChange: 0001-01-01T00:00:00.0000000
    EffectiveName: cisco
    FullName: Cisco
    LogonScript:
    ProfilePath:
    HomeDirectory:
    HomeDirectoryDrive:
    LogonCount: 1
    BadPasswordCount: 0
    UserId: 1113
    PrimaryGroupId: 513
    GroupIds:        7 513
    UserFlags: 0x20
    UserSessionKey: 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
    LogonServer: EC2AMAZ-EHM3HUN
    LogonDomainName: LEIMGRUBE
    LogonDomainId: S-1-5-21-1700842961-3107118889-547158845
    UserAccountControl: 0x210
    SubAuthStatus: 0x0
    LastSuccessfulILogon: 1601-01-01T00:00:00.0000000Z
    LastFailedILogon: 1601-01-01T00:00:00.0000000Z
    FailedILogonCount: 0x0
    ExtraSids: 0x7 S-1-18-1
    PAC Type ClientInformation: 00-0A-4A-70-F2-D5-D5-01 cisco
    PAC Type UpnDns: 0 cisco@Leimgrube.ch LEIMGRUBE.CH
    PAC Type ServerChecksum: 76-FF-FF-FF-0A-06-6F-27-CB-CE-81-C9-94-60-EB-E8-AA-07-42-DA
    PAC Type KdcChecksum: 10-00-00-00-76-6D-58-0C-35-85-02-54-0E-28-85-50
}

Using the session key to decrypt the authenticator reveals:

authenticator-vno = 5
crealm = LEIMGRUBE.CH
cname = cisco
cksum = [32771] bind = 44-B8-1F-E3-18-3E-1D-C2-26-73-9C-7A-03-DB-16-0F, flags = 22-00-00-00
ctime = 2020-01-29T22:22:04.0440810+01:00
subkey = [23] 5/ltpN4vZsmj+ldkN1fZ7Q==
seq-number = 737976894
authorization-data = [AD-IF-RELEVANT] {
  [AD-ETYPE-NEGOTIATION]
    { 18 17 23 }
  [KERB_AUTH_DATA_TOKEN_RESTRICTIONS]
    { { [0] { 0 }   [1] { byte[40] } } } (Flags = 0x0, TokenIL = 0x3000)
  [KERB-LOCAL]
    20-CA-25-1A-C4-02-00-00-E4-05-82-3F-00-00-00-00
  [AD-AUTH-DATA-AP-OPTIONS]
    00-40-00-00
  [144]
    ldap/dummy@LEIMGRUBE.CH
}

The “bind” data in the authenticator above matches the data in the NTLM image (after converting between hexadecimal and decimal).

Server-side verification of the channel binding data is a parallel of the operations performed on the client side. The server obtains the binding data by a call to the routine QueryContextAttributes to retrieve the SECPKG_ATTR_ENDPOINT_BINDINGS attribute; this data is then used as an input to the AcceptSecurityContext routine. The behaviour of AcceptSecurityContext in the event of missing bindings can be influenced by setting the context request flag ASC_REQ_ALLOW_MISSING_BINDINGS. If a channel binding problem is detected, the status value SEC_E_BAD_BINDINGS or SEC_E_INVALID_TOKEN is returned (depending on the nature of the problem).

The client and server must explicitly perform channel binding – TLS and Kerberos/NTLM can’t collaborate themselves to automatically perform the function. That means that code has to be explicitly added to client and server implementations of LDAP or HTTP or whatever other service that wishes to be protected by channel binding.