//////////////////////////////////////////////////////////////////////////////// // // Microsoft Research Singularity // // Copyright (c) Microsoft Corporation. All rights reserved. // // File: NtlmSupplicant.cs // // Note: // // This file contains an implementation of the NTLM v1 authentication // protocol. // // This implementation contains only the logic for building, parsing, and // validating, etc. NTLM messages and hashes. This code does not handle // anything related to a specific application of NTLM; it does not handle // storing or retrieving credentials, nor does it handle transmitting // NTLM messages to or from remote applications. All of that logic depends // on the situation in which NTLM is used, and so is omitted. // // The NtlmSupplicant class provides methods for computing the NTLM v1 // hashes (both Lan Man and NT hashes), given a username, password, and // a challenge (nonce) received from the authenticator (server). These // methods can be used in two different ways: they can act directly on // already-parsed fields (useful when the application carries the fields // separately, as basic SMB authentication does), or they can act on // NTLMSSP messages. // // NTLMSSP messages are those that are produced and consumed by the Windows // NTLM SSPI package. These messages are treated as opaque byte blobs by // application protocols, and the NTLM SSPI package handles building and // parsing these messages. This simplifies integration of NTLM into // application protocols, and also enables NTLM to be used with the SPNEGO // multi-protocol negotiation protocol. // // // TODO: // // * Only the supplicant logic is provided. Need to provide the methods that // an authenticator would need. // // * Byte-order cleanup. The protocol is little-endian. // // * NTLM v2 // /////////////////////////////////////////////////////////////////////////////// using System; using System.Diagnostics; using System.Text; using Microsoft.Singularity; using System.Security.Cryptography; using Utils; namespace System.Security.Protocols.Ntlm { /// // // // Implements the NTLM authentication protocol. // // // This is a "static" class; there are no instance fields or methods. // None of the methods of this class have any side-effects, outside of // operating on the parameters passed to them. // // // public /*static*/ sealed class NtlmSupplicant { private NtlmSupplicant() {} /// // // Builds an NTLMSSP "Negotiate" message, which begins an NTLM authentication exchange. // The caller supplies negotiation flags, which request certain behaviors, as well as // the domain name and workstation name of the client computer. // // // // // The caller can also provide the domain name and workstation name of the client. // These values are not required, and are usually used for event logging. // // // This method does not have any side-effects. // // // // // Enables certain NTLM options. See the NtlmNegotiateFlags enumerated type for more info. // // // The domain name of the client computer. If the caller does not want // to provide this information, pass an empty string. // // // The name of the client computer. If the caller does not want to provide this information, // then pass an empty string. // // // A buffer containing the encoded NTLMSSP "Negotiate" message. This message should be sent // to any application that supports NTLM authentication, using whatever transport mechanism // is appropriate for the application. // // public static byte[]! GetNegotiate(NtlmNegotiateFlags flags, string! domain, string! workstation) { Encoding encoding = Encoding.Unicode; // Build the fixed-length header of the message. Also, compute the offsets and lengths // of the variable-length fields (the strings). NtlmNegotiateMessage negotiate; negotiate.Header.MessageType = (uint)NtlmMessageType.Negotiate; negotiate.Header.Signature = NtlmConstants.MessageSignature64Le; negotiate.NegotiateFlags = (uint)(flags | NtlmNegotiateFlags.RequestTarget | NtlmNegotiateFlags.NegotiateNtlm | NtlmNegotiateFlags.NegotiateNtOnly | NtlmNegotiateFlags.NegotiateLmKey | NtlmNegotiateFlags.NegotiateUnicode); negotiate.Version = 0; negotiate.OemDomainName = new BufferRegion((ushort)encoding.GetByteCount(domain), 0, (ushort)sizeof(NtlmNegotiateMessage)); negotiate.OemWorkstationName = new BufferRegion((ushort)encoding.GetByteCount(workstation), 0, (ushort)(sizeof(NtlmNegotiateMessage) + negotiate.OemDomainName.Length)); // Next, allocate the buffer, store the fixed-length header, and store the variable-length strings. int messageLength = sizeof(NtlmNegotiateMessage) + negotiate.OemDomainName.Length + negotiate.OemWorkstationName.Length; byte[]! negotiateBuffer = new byte[messageLength]; ref NtlmNegotiateMessage negotiate_ref = ref negotiateBuffer[0]; negotiate_ref = negotiate; // copy the header encoding.GetBytes(domain, 0, domain.Length, negotiateBuffer, negotiate.OemDomainName.Offset); encoding.GetBytes(workstation, 0, workstation.Length, negotiateBuffer, negotiate.OemWorkstationName.Offset); return negotiateBuffer; } #region From my DES port static byte[]! Convert7ByteKeyTo8ByteKey(byte[]! input) { // we pack the input into the HIGH bits of a 64-bit unsigned int ulong x = ((ulong)input[0] << 0x38) | ((ulong)input[1] << 0x30) | ((ulong)input[2] << 0x28) | ((ulong)input[3] << 0x20) | ((ulong)input[4] << 0x18) | ((ulong)input[5] << 0x10) | ((ulong)input[6] << 0x08); ulong a = x; byte[]! result = new byte[8]; for (int i = 0; i < 8; i++) { // the LOWEST bit is the parity bit // right now, we always set the parity bit to 0 byte b = (byte)((a >> 56) & 0xfe); b = FixParity0(b); // this isn't actually necessary result[i] = b; a <<= 7; } return result; } // bit 0 is the parity bit! static byte FixParity0(byte b) { byte temp = b; int parity = 0; for (int i = 1; i < 8; i++) { parity ^= temp & 2; temp >>= 1; } if (parity == 0) return (byte)(b | 1); else return (byte)(b & 0xfe); } #endregion /// // // // This method computes part of the NTLM hash function. It operates on part of the password, 7 characters // at a time, and computes a hash of them. The hash is then stored in the 'output' buffer at the offset // 'outputindex'. The hash is always 8 bytes long, so the 'output' buffer needs to have a length of at // least outputindex + 8. // // // This hash function is known to be cryptographically weak. // // // This method only works on passwords that contain only 7-bit ASCII character. // Lower-case characters are forced to upper-case. // // // // The cleartext password of the user. // The method will read up to 7 characters of the password, starting at 'passwordindex'. // // // The index of the first character within 'password' to read. // This value may legally be greater than or equal to the length of 'password'. // The method will read the first 7 characters, beginning at this index, and if there // are fewer than 7 valid characters (including no characters), the method will pad // with zeroes. // // // The output buffer in which to store the 8-byte hash of the portion of the password. // The length of this buffer must be at least 'outputindex' + 8. // //The position within the 'output' buffer to store the hash. // static void ComputeLmResponseHalf(string! password, int passwordindex, byte[]! output, int outputindex) requires passwordindex >= 0; // requires passwordindex <= password.Length; <-- This is *NOT* a precondition! requires outputindex >= 0; requires outputindex + 8 <= output.Length; { byte[]! desKey7 = new byte[7]; for (int i = 0; i < 7; i++) { if (i + passwordindex < password.Length) { char c = password[passwordindex + i]; if (c >= 0x80) throw new ArgumentException("The password provided cannot be encoded. NTLM/LM v1 does not support non-ASCII characters."); desKey7[i] = (byte)Char.ToUpper(c); } else desKey7[i] = 0; } byte[]! desKey8 = Convert7ByteKeyTo8ByteKey(desKey7); byte[]! cleartext = new byte[8]; const string cleartext_string = "KGS!@#$%"; for (int i = 0; i < 8; i++) cleartext[i] = (byte)cleartext_string[i]; byte[]! cipher = new byte[8]; Des.Encrypt(desKey8, cleartext, cipher); for (int i = 0; i < 8; i++) output[i + outputindex] = cipher[i]; } /// // // This method computes the NTLM v1 One-Way Function. // //Contains the 8-byte challenge (nonce) generated by the server. // // Contains the 21-byte intermediate password hash. // This buffer must be exactly 21 bytes long. // // // The computed One-Way Function. This is the value that is sent to the NTLM authenticator. // // public static byte[]! ComputeOwf(byte[]! challenge, byte[]! hash) requires challenge.Length >= 8; requires hash.Length == 21; ensures result.Length == 24; { if (hash.Length != 21) throw new Exception("Hash is wrong length"); #if NOISY DebugLine("ComputeOwf: challenge=" + Util.ByteArrayToStringHex(challenge) + " hash=" + Util.ByteArrayToStringHex(hash)); DebugLine(" challenge: " + Util.ByteArrayToStringBitsLe(challenge)); #endif byte[] response = new byte[24]; for (int r = 0; r < 3; r++) { #if NOISY DebugLine(" r = " + r); #endif // Next, we encrypt the server's challenge three times, using the DES algorithm. // The result is the LM response. byte[] thirdKey7 = new byte[7]; Array.Copy(hash, r * 7, thirdKey7, 0, 7); byte[] thirdKey8 = Convert7ByteKeyTo8ByteKey(thirdKey7); #if NOISY DebugLine(" key7 = " + Util.ByteArrayToStringHex(thirdKey7)); //DebugLine(" key7 = " + Util.ByteArrayToStringBitsLe(thirdKey7)); DebugLine(" key7 = " + Util.ByteArrayToStringBitsBe(thirdKey7)); DebugLine(" key8 = " + Util.ByteArrayToStringHex(thirdKey8)); //DebugLine(" key8 = " + Util.ByteArrayToStringBitsLe(thirdKey8)); DebugLine(" key8 = " + Util.ByteArrayToStringBitsBe(thirdKey8)); #endif byte[] cipher = new byte[8]; Des.Encrypt(thirdKey8, challenge, cipher); Array.Copy(cipher, 0, response, r * 8, 8); #if NOISY DebugLine(" cipher = " + Util.ByteArrayToStringBitsBe(cipher)); DebugLine(" cipher = " + Util.ByteArrayToStringHex(cipher)); #endif } #if NOISY DebugLine(" response: " + Util.ByteArrayToStringHex(response)); #endif return response; } /// // // // This method computes the NTLM v1 "LAN Manager" authentication response. // This response is known to be very weak, cryptographically. // // // The 'challenge' parameter contains the 8-byte challenge nonce, which was // generated by the NTLMSSP running in the context of the server application. // The 'password' parameter contains the clear-text user password. // // // This method does not have any side-effects. // // //The 8-byte challenge (nonce) generated by the server. //The cleartext password of the authenticating user. // // The return value is a newly-allocated buffer containing the encoded NTLM // response hash, which proves that the client is in possession of the password // for an identified account. This buffer is always 24 bytes in length. // // public static byte[]! ComputeLmResponse(byte[]! challenge, string! password) requires challenge.Length == 8; ensures result.Length == 24; { byte[]! hash = new byte[21]; ComputeLmResponseHalf(password, 0, hash, 0); ComputeLmResponseHalf(password, 7, hash, 8); for (int i = 16; i < 21; i++) hash[i] = 0; byte[]! response = ComputeOwf(challenge, hash); #if NOISY DebugLine("LM response: " + Util.ByteArrayToStringHex(response)); #endif return response; } /// // // // This method computes the NTLM v1 "NT" authentication response. // This response is known to be weak, cryptographically, but is better than // the NTLM v1 "LAN Manager" response. // // // This method does not have any side-effects. // // // // // Contains the 8-byte challenge nonce, which was generated by the NTLMSSP running // in the context of the server application. // //Contains the clear-text user password. // // A buffer containing the encoded NTLM response hash, which proves that the client // is in possession of the password for an identified account. This buffer is always // 24 bytes in length. // // public static byte[]! ComputeNtResponse(byte[]! challenge, string! password) requires challenge.Length == 8; ensures result.Length == 24; { byte[]! password_bytes = (!)Encoding.Unicode.GetBytes(password); #if true // THIS CAUSES BARTOK TO THROW AN UNHANDLED EXCEPTION! byte[]! digest = (!)(MD4Context.GetDigest(password_bytes).ToArray()); #else // compiler shuts up, but obviously the code doesn't work IgnoreConsume(password_bytes); byte[]! digest = new byte[0]; #endif #if NOISY DebugLine("Computing NTLM v1 response:"); DebugLine(" challenge: " + Util.ByteArrayToStringHex(challenge)); DebugLine(" password: " + password); DebugLine(" password bytes: " + Util.ByteArrayToStringHex(password_bytes)); DebugLine(" MD4(pw bytes): " + Util.ByteArrayToStringHex(digest)); #endif // The hash is the MD4 digest, padded with zeroes to a length of 21. // assert digest.Length == 16; byte[]! hash = new byte[21]; ArrayCopy(digest, 0, hash, 0, 16); ArrayClear(hash, 16, 5); byte[]! response = ComputeOwf(challenge, hash); #if NOISY DebugLine("NTLM v1 response: " + Util.ByteArrayToStringHex(response)); #endif return response; } static void ArrayCopy(byte[]! src, int srcoffset, byte[]! dst, int dstoffset, int length) { for (int i = 0; i < length; i++) dst[dstoffset + i] = src[srcoffset + i]; } static void ArrayClear(byte[]! dst, int offset, int length) { for (int i = 0; i < length; i++) dst[offset + i] = 0; } public const int ChallengeLength = 8; public const int ResponseLength = 24; /// // // This method generates an NTLMSSP "Response" message, given valid client credentials and // an NTLMSSP "Challenge" message. // // // // Contains the encoded NTLMSSP "Response" message, which was generated by the remote // NTLM-enabled application. // //The domain name of the authenticating user. //The username of the authenticating user. //The cleartext password of the authenticating user. // // A buffer containing the encoded NTLMSSP "Response" message. This buffer should be // sent to the remote application, using whatever transport is appropriate. // // public static byte[]! GetResponse( byte[]! challenge_buffer, string! domain, string! username, string! workstation, string! password) { if (challenge_buffer.Length < sizeof(NtlmChallengeMessage)) throw new Exception("The buffer supplied contains too little data to contain a valid NTLM challenge message."); ref NtlmChallengeMessage challenge_msg = ref challenge_buffer[0]; if (challenge_msg.Header.Signature != NtlmConstants.MessageSignature64Le) throw new Exception("The NTLM message has an invalid signature."); if (challenge_msg.Header.MessageType != (uint)NtlmMessageType.Challenge) throw new Exception("The NTLM message provided is not a Challenge message."); byte[]! challenge_bytes = NtlmUtil.GetSubArray(challenge_buffer, 0x18, NtlmConstants.ChallengeLength); // known good here #if NOISY string TargetName = NtlmUtil.GetCountedStringAt(challenge_buffer, challenge_msg.TargetName.Offset); DebugLine(" TargetName: " + TargetName); DebugLine(" Challenge: " + Util.ByteArrayToStringHex(challenge_bytes)); #endif // // First, compute the ancient, horrible, insecure LM response. // byte[]! Lm_response = ComputeLmResponse(challenge_bytes, password); byte[]! Nt_response = ComputeNtResponse(challenge_bytes, password); // assert Lm_response.Length == 24; // assert Nt_response.Length == 24; // // Next, build the Authenticate message. The message has a fixed-length header, described // by NtlmAuthenticateMessage, followed by the string bodies. // System.Text.Encoding encoding = System.Text.Encoding.Unicode; byte[]! domainBytes = (!)encoding.GetBytes(domain); byte[]! usernameBytes = (!)encoding.GetBytes(username); byte[]! workstationBytes = (!)encoding.GetBytes(workstation); int responseBufferLength = sizeof(NtlmResponseMessage) + Lm_response.Length + Nt_response.Length + domainBytes.Length + usernameBytes.Length + workstationBytes.Length; // known bad // Now we know the size of the entire response message. Allocate it. byte[]! responseBuffer = new byte[responseBufferLength]; ref NtlmResponseMessage response = ref responseBuffer[0]; response.Header.Signature = NtlmConstants.MessageSignature64Le; response.Header.MessageType = (uint)NtlmMessageType.Response; byte[]![]! strings = { Lm_response, Nt_response, domainBytes, usernameBytes, workstationBytes }; // Scan through the variable-length strings again and store the string headers. // Also copy the string body into place. int write_pos = sizeof(NtlmResponseMessage); for (int i = 0; i < strings.Length; i++) { byte[]! stringbytes = strings[i]; ref BufferRegion region = ref responseBuffer[sizeof(NtlmMessageHeader) + i * sizeof(BufferRegion)]; region = new BufferRegion((ushort)stringbytes.Length, (ushort)stringbytes.Length, (ushort)write_pos); Array.Copy(stringbytes, 0, responseBuffer, write_pos, stringbytes.Length); write_pos += stringbytes.Length; } return responseBuffer; } #if NOISY static void DebugLine(string msg) { DebugStub.WriteLine("NTLM: " + msg); } #endif static string! PadTruncate(string! s, int length) { if (s.Length == length) return s; if (s.Length > length) return s.Substring(0, length); return s.PadRight((Char)0); } } /*static*/ sealed class ByteOrder { private ByteOrder() {} public static ushort UInt16LeToHost(ushort value) { return value; } public static uint UInt32LeToHost(uint value) { return value; } } }