singrdk/base/Libraries/Ntlm/NtlmSupplicant.sg

566 lines
21 KiB
Plaintext

////////////////////////////////////////////////////////////////////////////////
//
// 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
{
/**
<summary>
<para>
Implements the NTLM authentication protocol.
</para>
<para>
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.
</para>
</summary>
*/
public /*static*/ sealed class NtlmSupplicant
{
private NtlmSupplicant() {}
/**
<summary>
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.
</summary>
<remarks>
<para>
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.
</para>
<para>
This method does not have any side-effects.
</para>
</remarks>
<param name="flags">
Enables certain NTLM options. See the NtlmNegotiateFlags enumerated type for more info.
</param>
<param name="domain">
The domain name of the client computer. If the caller does not want
to provide this information, pass an empty string.
</param>
<param name="workstation">
The name of the client computer. If the caller does not want to provide this information,
then pass an empty string.
</param>
<returns>
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.
</returns>
*/
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
/**
<summary>
<para>
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.
</para>
<para>
This hash function is known to be cryptographically weak.
</para>
<para>
This method only works on passwords that contain only 7-bit ASCII character.
Lower-case characters are forced to upper-case.
</para>
</summary>
<param name="password">
The cleartext password of the user.
The method will read up to 7 characters of the password, starting at 'passwordindex'.
</param>
<param name="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.
</param>
<param name="output">
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.
</param>
<param name="outputindex">The position within the 'output' buffer to store the hash.</param>
*/
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];
}
/**
<summary>
This method computes the NTLM v1 One-Way Function.
</summary>
<param name="challenge">Contains the 8-byte challenge (nonce) generated by the server.</param>
<param name="hash">
Contains the 21-byte intermediate password hash.
This buffer must be exactly 21 bytes long.
</param>
<returns>
The computed One-Way Function. This is the value that is sent to the NTLM authenticator.
</returns>
*/
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;
}
/**
<summary>
<para>
This method computes the NTLM v1 "LAN Manager" authentication response.
This response is known to be very weak, cryptographically.
</para>
<para>
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.
</para>
<para>
This method does not have any side-effects.
</para>
</summary>
<param name="challenge">The 8-byte challenge (nonce) generated by the server.</param>
<param name="password">The cleartext password of the authenticating user.</param>
<returns>
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.
</returns>
*/
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;
}
/**
<summary>
<para>
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.
</para>
<para>
This method does not have any side-effects.
</para>
</summary>
<param name="challenge">
Contains the 8-byte challenge nonce, which was generated by the NTLMSSP running
in the context of the server application.
</param>
<param name="password">Contains the clear-text user password.</param>
<returns>
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.
</returns>
*/
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;
/**
<summary>
This method generates an NTLMSSP "Response" message, given valid client credentials and
an NTLMSSP "Challenge" message.
</summary>
<param name="challenge_buffer">
Contains the encoded NTLMSSP "Response" message, which was generated by the remote
NTLM-enabled application.
</param>
<param name="domain">The domain name of the authenticating user.</param>
<param name="username">The username of the authenticating user.</param>
<param name="password">The cleartext password of the authenticating user.</param>
<returns>
A buffer containing the encoded NTLMSSP "Response" message. This buffer should be
sent to the remote application, using whatever transport is appropriate.
</returns>
*/
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;
}
}
}