Improved WDigest and NT hash calculation

This commit is contained in:
Michael Grafnetter 2018-07-16 22:20:29 +02:00
parent b44d022c38
commit 1f1750a833
5 changed files with 105 additions and 53 deletions

View File

@ -1,6 +1,5 @@
using DSInternals.Common;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Security;
namespace DSInternals.Common.Cryptography.Test
@ -47,6 +46,14 @@ namespace DSInternals.Common.Cryptography.Test
string result = NTHash.ComputeHash(password).ToHex(true);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void NTHash_String_LongInput()
{
string password = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
string result = NTHash.ComputeHash(password).ToHex(true);
}
[TestMethod]
public void NTHash_SecureString_TestVector1()
{

View File

@ -1,9 +1,8 @@
namespace DSInternals.Common.Cryptography.Test
{
using DSInternals.Common;
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Security;
using System;
[TestClass]
public class WDigestHashTester
@ -16,7 +15,7 @@
}
[TestMethod]
public void WDigestHash_TestVector1()
public void WDigestHash_ASCIIInput()
{
var hashes = WDigestHash.ComputeHash(@"Pa$$w0rd".ToSecureString(), null, "Administrator", "ADATUM", "Adatum.com");
Assert.AreEqual(WDigestHash.HashCount, hashes.Length);
@ -50,5 +49,40 @@
Assert.AreEqual("9c0d49ddf1ac7cff9d1e6f96cc8a55d5", hashes[27].ToHex());
Assert.AreEqual("8da34a0b94604b06cd58f9bf69142383", hashes[28].ToHex());
}
[TestMethod]
public void WDigestHash_UnicodeInput()
{
var hashes = WDigestHash.ComputeHash(@"Příliš žluťoučký kůň úpěl ďábelské ódy. В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!".ToSecureString(), "April@adatum.com", "April", "ADATUM", "Adatum.com");
Assert.AreEqual(WDigestHash.HashCount, hashes.Length);
Assert.AreEqual("fad613ac9301e7ae6dcc537e2eef5021", hashes[0].ToHex());
Assert.AreEqual("f3b76b7db452d6be096942f9bcce74ea", hashes[1].ToHex());
Assert.AreEqual("919952d85318a3d5b67e9df5a78b7141", hashes[2].ToHex());
Assert.AreEqual("fad613ac9301e7ae6dcc537e2eef5021", hashes[3].ToHex());
Assert.AreEqual("35dd0ca1e7631567f2ae132e038ccd8f", hashes[4].ToHex());
Assert.AreEqual("f6d89b7867a2d3d1cbae038091bdb0cc", hashes[5].ToHex());
Assert.AreEqual("0bfedf219f856e2edcfec5231470c0b3", hashes[6].ToHex());
Assert.AreEqual("df33d4e6eb9af1406b25e0e16548a2da", hashes[7].ToHex());
Assert.AreEqual("5e2d43bdbb743c5d2601baafdd32a1af", hashes[8].ToHex());
Assert.AreEqual("5277f1cf72b11b2ff48721cf9d431893", hashes[9].ToHex());
Assert.AreEqual("9681f457b0767ed163b8dc8526469af6", hashes[10].ToHex());
Assert.AreEqual("3e9ce47f72f7e39661199f18ee8bc2aa", hashes[11].ToHex());
Assert.AreEqual("404243dbcec9a1d73d0b1cb49060c196", hashes[12].ToHex());
Assert.AreEqual("29d93dc099bd1b5c9f20bbeb042bb4ff", hashes[13].ToHex());
Assert.AreEqual("0ecdf90500b3189dc3082dd1bce0b5bc", hashes[14].ToHex());
Assert.AreEqual("4a4135a2ac1daa81fea605adc9188c67", hashes[15].ToHex());
Assert.AreEqual("0a1f7bd410a8340342045daf0cc78789", hashes[16].ToHex());
Assert.AreEqual("be97dc768a7683e839438b29e30b6f7b", hashes[17].ToHex());
Assert.AreEqual("7d6a36f69323172b25ef61a761f29d1b", hashes[18].ToHex());
Assert.AreEqual("ed2d15fcde27c04892014aaf46201ec8", hashes[19].ToHex());
Assert.AreEqual("51f43cb00d57226f530d52908a072c43", hashes[20].ToHex());
Assert.AreEqual("35329ba52771477b8665ef47b89bebbf", hashes[21].ToHex());
Assert.AreEqual("7515013a7da75b573467e521abb783e1", hashes[22].ToHex());
Assert.AreEqual("9bab42c45f29cda4f4dadad0a14cc8b6", hashes[23].ToHex());
Assert.AreEqual("26075b3e4791600800363160d217265b", hashes[24].ToHex());
Assert.AreEqual("bbba87b75b45d71cf7fd84d4e2a22344", hashes[25].ToHex());
Assert.AreEqual("18ca7497b2624e7fde7165854788dd49", hashes[26].ToHex());
Assert.AreEqual("0d9a52cbff395f8df9e285b01c2a78fb", hashes[27].ToHex());
Assert.AreEqual("c4280744f0ff1181b939b71b1ddc37f5", hashes[28].ToHex());
}
}
}

View File

@ -1,9 +1,8 @@
using DSInternals.Common;
using DSInternals.Common.Interop;
using System.Security;
namespace DSInternals.Common.Cryptography
namespace DSInternals.Common.Cryptography
{
using DSInternals.Common.Interop;
using System.Security;
// See http://msdn.microsoft.com/en-us/library/system.security.cryptography.hashalgorithm%28v=vs.110%29.aspx
public static class NTHash
{
@ -26,7 +25,8 @@ namespace DSInternals.Common.Cryptography
public static byte[] ComputeHash(SecureString password)
{
Validator.AssertNotNull(password, "password");
Validator.AssertMaxLength(password, MaxInputLength, "password");
byte[] hash;
using(SafeUnicodeSecureStringPointer passwordPtr = new SafeUnicodeSecureStringPointer(password))
{
@ -38,7 +38,8 @@ namespace DSInternals.Common.Cryptography
public static byte[] ComputeHash(string password)
{
Validator.AssertNotNull(password, "password");
Validator.AssertMaxLength(password, MaxInputLength, "password");
byte[] hash;
NtStatus result = NativeMethods.RtlCalculateNtOwfPassword(password, out hash);
Validator.AssertSuccess(result);

View File

@ -2,7 +2,6 @@
{
using DSInternals.Common;
using System;
using System.Collections.Generic;
using System.Security;
using System.Security.Cryptography;
using System.Text;
@ -23,12 +22,11 @@
/// </summary>
public const int HashCount = 29;
/// <summary>
/// This string is used instead of the realm name when calculating some of the hashes.
/// </summary>
private const string MagicRealm = "Digest";
// All strings are converted to ISO Latin I code page prior to the hashing
private const string StringEncodingName = "ISO-8859-1";
private static readonly Encoding StringEncoder = Encoding.GetEncoding(StringEncodingName);
/// <summary>
/// Calculates WDigest hashes of a password.
/// </summary>
@ -57,99 +55,99 @@
// Construct the pre-Windows 2000 logon name as netBiosDomainName\samAccountName
string logonName = String.Format(@"{0}\{1}", netBiosDomainName, samAccountName);
// List of the resulting 29 hashes
var result = new List<byte[]>(HashCount);
using (var md5 = new MD5Cng())
// Array of the resulting 29 hashes
byte[][] result = new byte[HashCount][];
using (var md5 = MD5.Create())
{
// Hash1: MD5(sAMAccountName, NETBIOSDomainName, password)
result.Add(md5.ComputeHash(GetBytes(samAccountName, netBiosDomainName, password)));
result[0] = md5.ComputeHash(GetBytes(samAccountName, netBiosDomainName, password));
// Hash2: MD5(LOWER(sAMAccountName), LOWER(NETBIOSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToLower(), netBiosDomainName.ToLower(), password)));
result[1] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), netBiosDomainName.ToLower(), password));
// Hash3: MD5(UPPER(sAMAccountName), UPPER(NETBIOSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToUpper(), netBiosDomainName.ToUpper(), password)));
result[2] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), netBiosDomainName.ToUpper(), password));
// Hash4: MD5(sAMAccountName, UPPER(NETBIOSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName, netBiosDomainName.ToUpper(), password)));
result[3] = md5.ComputeHash(GetBytes(samAccountName, netBiosDomainName.ToUpper(), password));
// Hash5: MD5(sAMAccountName, LOWER(NETBIOSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName, netBiosDomainName.ToLower(), password)));
result[4] = md5.ComputeHash(GetBytes(samAccountName, netBiosDomainName.ToLower(), password));
// Hash6: MD5(UPPER(sAMAccountName), LOWER(NETBIOSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToUpper(), netBiosDomainName.ToLower(), password)));
result[5] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), netBiosDomainName.ToLower(), password));
// Hash7: MD5(LOWER(sAMAccountName), UPPER(NETBIOSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToLower(), netBiosDomainName.ToUpper(), password)));
result[6] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), netBiosDomainName.ToUpper(), password));
// Hash8: MD5(sAMAccountName, DNSDomainName, password)
result.Add(md5.ComputeHash(GetBytes(samAccountName, dnsDomainName, password)));
result[7] = md5.ComputeHash(GetBytes(samAccountName, dnsDomainName, password));
// Hash9: MD5(LOWER(sAMAccountName), LOWER(DNSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToLower(), dnsDomainName.ToLower(), password)));
result[8] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), dnsDomainName.ToLower(), password));
// Hash10: MD5(UPPER(sAMAccountName), UPPER(DNSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToUpper(), dnsDomainName.ToUpper(), password)));
result[9] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), dnsDomainName.ToUpper(), password));
// Hash11: MD5(sAMAccountName, UPPER(DNSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName, dnsDomainName.ToUpper(), password)));
result[10] = md5.ComputeHash(GetBytes(samAccountName, dnsDomainName.ToUpper(), password));
// Hash12: MD5(sAMAccountName, LOWER(DNSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName, dnsDomainName.ToLower(), password)));
result[11] = md5.ComputeHash(GetBytes(samAccountName, dnsDomainName.ToLower(), password));
// Hash13: MD5(UPPER(sAMAccountName), LOWER(DNSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToUpper(), dnsDomainName.ToLower(), password)));
result[12] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), dnsDomainName.ToLower(), password));
// Hash14: MD5(LOWER(sAMAccountName), UPPER(DNSDomainName), password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToLower(), dnsDomainName.ToUpper(), password)));
result[13] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), dnsDomainName.ToUpper(), password));
// Hash15: MD5(userPrincipalName, password)
result.Add(md5.ComputeHash(GetBytes(userPrincipalName, String.Empty, password)));
result[14] = md5.ComputeHash(GetBytes(userPrincipalName, String.Empty, password));
// Hash16: MD5(LOWER(userPrincipalName), password)
result.Add(md5.ComputeHash(GetBytes(userPrincipalName.ToLower(), String.Empty, password)));
result[15] = md5.ComputeHash(GetBytes(userPrincipalName.ToLower(), String.Empty, password));
// Hash17: MD5(UPPER(userPrincipalName), password)
result.Add(md5.ComputeHash(GetBytes(userPrincipalName.ToUpper(), String.Empty, password)));
result[16] = md5.ComputeHash(GetBytes(userPrincipalName.ToUpper(), String.Empty, password));
// Hash18: MD5(NETBIOSDomainName\sAMAccountName, password)
result.Add(md5.ComputeHash(GetBytes(logonName, password)));
result[17] = md5.ComputeHash(GetBytes(logonName, password));
// Hash19: MD5(LOWER(NETBIOSDomainName\sAMAccountName), password)
result.Add(md5.ComputeHash(GetBytes(logonName.ToLower(), password)));
result[18] = md5.ComputeHash(GetBytes(logonName.ToLower(), password));
// Hash20: MD5(UPPER(NETBIOSDomainName\sAMAccountName), password)
result.Add(md5.ComputeHash(GetBytes(logonName.ToUpper(), password)));
result[19] = md5.ComputeHash(GetBytes(logonName.ToUpper(), password));
// Hash21: MD5(sAMAccountName, "Digest", password)
result.Add(md5.ComputeHash(GetBytes(samAccountName, MagicRealm, password)));
result[20] = md5.ComputeHash(GetBytes(samAccountName, MagicRealm, password));
// Hash22: MD5(LOWER(sAMAccountName), "Digest", password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToLower(), MagicRealm, password)));
result[21] = md5.ComputeHash(GetBytes(samAccountName.ToLower(), MagicRealm, password));
// Hash23: MD5(UPPER(sAMAccountName), "Digest", password)
result.Add(md5.ComputeHash(GetBytes(samAccountName.ToUpper(), MagicRealm, password)));
result[22] = md5.ComputeHash(GetBytes(samAccountName.ToUpper(), MagicRealm, password));
// Hash24: MD5(userPrincipalName, "Digest", password)
result.Add(md5.ComputeHash(GetBytes(userPrincipalName, MagicRealm, password)));
result[23] = md5.ComputeHash(GetBytes(userPrincipalName, MagicRealm, password));
// Hash25: MD5(LOWER(userPrincipalName), "Digest", password)
result.Add(md5.ComputeHash(GetBytes(userPrincipalName.ToLower(), MagicRealm, password)));
result[24] = md5.ComputeHash(GetBytes(userPrincipalName.ToLower(), MagicRealm, password));
// Hash26: MD5(UPPER(userPrincipalName), "Digest", password)
result.Add(md5.ComputeHash(GetBytes(userPrincipalName.ToUpper(), MagicRealm, password)));
result[25] = md5.ComputeHash(GetBytes(userPrincipalName.ToUpper(), MagicRealm, password));
// Hash27: MD5(NETBIOSDomainName\sAMAccountName, "Digest", password)
result.Add(md5.ComputeHash(GetBytes(logonName, MagicRealm, password)));
result[26] = md5.ComputeHash(GetBytes(logonName, MagicRealm, password));
// Hash28: MD5(LOWER(NETBIOSDomainName\sAMAccountName), "Digest", password)
result.Add(md5.ComputeHash(GetBytes(logonName.ToLower(), MagicRealm, password)));
result[27] = md5.ComputeHash(GetBytes(logonName.ToLower(), MagicRealm, password));
// Hash29: MD5(UPPER(NETBIOSDomainName\sAMAccountName), "Digest", password)
result.Add(md5.ComputeHash(GetBytes(logonName.ToUpper(), MagicRealm, password)));
result[28] = md5.ComputeHash(GetBytes(logonName.ToUpper(), MagicRealm, password));
}
return result.ToArray();
return result;
}
private static byte[] GetBytes(string userName, SecureString password)
@ -159,8 +157,9 @@
private static byte[] GetBytes(string userName, string realm, SecureString password)
{
var concatenatedString = String.Format("{0}:{1}:{2}", userName, realm, password.ToUnicodeString());
return StringEncoder.GetBytes(concatenatedString);
var concatenatedString = String.Join(":", userName, realm, password.ToUnicodeString());
// Although the documentation says that strings are converted to ISO Latin I code page prior to the hashing, the AD implementation actually uses UTF-8 instead.
return Encoding.UTF8.GetBytes(concatenatedString);
}
}
}

View File

@ -103,6 +103,7 @@ namespace DSInternals.Common
throw new ArgumentOutOfRangeException(paramName, value.Length, Resources.UnexpectedLengthMessage);
}
}
public static void AssertMaxLength(SecureString password, int maxLength, string paramName)
{
AssertNotNull(password, paramName);
@ -111,6 +112,16 @@ namespace DSInternals.Common
throw new ArgumentOutOfRangeException(paramName, password.Length, Resources.InputLongerThanMaxMessage);
}
}
public static void AssertMaxLength(string input, int maxLength, string paramName)
{
AssertNotNull(input, paramName);
if (input.Length > maxLength)
{
throw new ArgumentOutOfRangeException(paramName, input.Length, Resources.InputLongerThanMaxMessage);
}
}
public static void AssertMinLength(byte[] data, int minLength, string paramName)
{
AssertNotNull(data, paramName);