KDS KDF L0

This commit is contained in:
Michael Grafnetter 2023-09-25 23:52:05 +02:00
parent b387697c20
commit 13385b8716
12 changed files with 520 additions and 11 deletions

View File

@ -26,6 +26,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
@ -33,6 +34,7 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<UseVSHostingProcess>false</UseVSHostingProcess>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<Reference Include="CBOR, Version=4.5.2.0, Culture=neutral, PublicKeyToken=9cd62db60ea5554c, processorArchitecture=MSIL">
@ -76,6 +78,7 @@
<Compile Include="KerberosCredentialTester.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Cryptography\SecureStringExtensionsTester.cs" />
<Compile Include="ProtectionKeyIdentifierTester.cs" />
<Compile Include="SearchableDeviceKeyTester.cs" />
<Compile Include="SecurityIdentifierExtensionsTester.cs" />
<Compile Include="KeyCredentialTester.cs" />

View File

@ -52,5 +52,21 @@ namespace DSInternals.Common.Test
KdsRootKey.ParseSecretAgreementParameters(new byte[0] { });
throw new AssertInconclusiveException();
}
[TestMethod]
public void ComputeL0Key_Vector1()
{
byte[] l0Key = KdsRootKey.ComputeL0Key(
Guid.Parse("7dc95c96-fa85-183a-dff5-f70696bf0b11"),
"814ad2f3928ff96d3650487967392feab3924f3d0dff8629d46a723640101cff8ca2cbd6aba40805cf03b380803b27837d80663eb4d18fd4cec414ebb2271fe2".HexToBinary(),
"SP800_108_CTR_HMAC",
"00000000010000000e000000000000005300480041003500310032000000".HexToBinary(),
361
);
Assert.AreEqual(
"76d7341bbf6f85f439a14d3f68c6de31a83d2c55b1371c9c122f5b6f0eccff282973da43349da2b21a0a89b050b49e9ace951323f27638ccbfce8b6a0ead782b",
l0Key.ToHex());
}
}
}

View File

@ -0,0 +1,55 @@
using DSInternals.Common.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace DSInternals.Common.Test
{
[TestClass]
public class ProtectionKeyIdentifierTester
{
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ProtectionKeyIdentifier_Null()
{
var obj = new ProtectionKeyIdentifier(null);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ProtectionKeyIdentifier_Empty()
{
var obj = new ProtectionKeyIdentifier(new byte[0]);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ProtectionKeyIdentifier_Truncated1()
{
// Only contains version
byte[] blob = "01000000".HexToBinary();
var parsed = new ProtectionKeyIdentifier(blob);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ProtectionKeyIdentifier_Truncated2()
{
// The last byte has been trimmed
byte[] blob = "010000004b44534b02000000690100001a00000018000000965cc97d85fa3a18dff5f70696bf0b1100000000180000001800000063006f006e0074006f0073006f002e0063006f006d00000063006f006e0074006f0073006f002e0063006f006d0000".HexToBinary();
var parsed = new ProtectionKeyIdentifier(blob);
}
[TestMethod]
public void ProtectionKeyIdentifier_Parse()
{
byte[] blob = "010000004b44534b02000000690100001a00000018000000965cc97d85fa3a18dff5f70696bf0b1100000000180000001800000063006f006e0074006f0073006f002e0063006f006d00000063006f006e0074006f0073006f002e0063006f006d000000".HexToBinary();
var parsed = new ProtectionKeyIdentifier(blob);
Assert.AreEqual("contoso.com", parsed.DomainName);
Assert.AreEqual("contoso.com", parsed.ForestName);
Assert.AreEqual("7dc95c96-fa85-183a-dff5-f70696bf0b11", parsed.RootKeyId.ToString());
Assert.AreEqual(361, parsed.L0KeyId);
Assert.AreEqual(26, parsed.L1KeyId);
Assert.AreEqual(24, parsed.L2KeyId);
}
}
}

View File

@ -67,6 +67,7 @@
<Compile Include="Cryptography\Pbkdf2.cs" />
<Compile Include="Cryptography\PrivateKeyEncryptionType.cs" />
<Compile Include="Data\DNWithBinary.cs" />
<Compile Include="Data\DPAPI\ProtectionKeyIdentifier.cs" />
<Compile Include="Data\DPAPI\DPAPIObject.cs" />
<Compile Include="Data\DPAPI\KdsRootKey.cs" />
<Compile Include="Data\DPAPI\RoamedCredential.cs" />
@ -89,6 +90,7 @@
<Compile Include="Data\Hello\KeyMaterialFido.cs" />
<Compile Include="Data\Hello\VolumeType.cs" />
<Compile Include="Data\Hello\KeyUsage.cs" />
<Compile Include="Data\Principals\GroupManagedServiceAccount.cs" />
<Compile Include="Data\Principals\SupportedEncryptionTypes.cs" />
<Compile Include="Extensions\ByteArrayExtensions.cs" />
<Compile Include="Cryptography\Crc32.cs" />
@ -130,6 +132,7 @@
<Compile Include="Extensions\NTAccountExtensions.cs" />
<Compile Include="Extensions\RSAExtensions.cs" />
<Compile Include="Interop\CryptoBuffer.cs" />
<Compile Include="Interop\Enums\GroupKeyLevel.cs" />
<Compile Include="Interop\Enums\NetCancelOptions.cs" />
<Compile Include="Interop\Enums\NetConnectOptions.cs" />
<Compile Include="Interop\Enums\NetResourceDisplayType.cs" />
@ -145,6 +148,7 @@
<Compile Include="Interop\SafeOemStringPointer.cs" />
<Compile Include="Interop\Enums\Win32ErrorCode.cs" />
<Compile Include="Interop\Enums\NtStatus.cs" />
<Compile Include="Interop\SafeSidKeyProviderHandle.cs" />
<Compile Include="Interop\SafeUnicodeSecureStringPointer.cs" />
<Compile Include="Interop\OemString.cs" />
<Compile Include="Interop\SecureUnicodeString.cs" />
@ -183,4 +187,4 @@
<Folder Include="Data\FIDO2\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
</Project>

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using DSInternals.Common.Interop;
namespace DSInternals.Common.Data
{
@ -10,11 +11,16 @@ namespace DSInternals.Common.Data
/// </summary>
public class KdsRootKey
{
private const int L0KeyIteration = 1;
private const int L1KeyIteration = 32;
private const int L2KeyIteration = 32;
private int? version;
private DateTime ?creationTime;
private DateTime ?effectiveTime;
private byte[] privateKey;
private string kdfAlgorithmName;
private byte[] rawKdfParameters;
private string secretAgreementAlgorithmName;
private byte[] secretAgreementAlgorithmParam;
private int? privateKeyLength;
@ -53,9 +59,7 @@ namespace DSInternals.Common.Data
dsObject.ReadAttribute(CommonDirectoryAttributes.KdsKdfAlgorithm, out this.kdfAlgorithmName);
// KDF algorithm parameters (only 1 in current implementation)
byte[] rawKdfParams;
dsObject.ReadAttribute(CommonDirectoryAttributes.KdsKdfParameters, out rawKdfParams);
this.KdfParameters = ParseKdfParameters(rawKdfParams);
dsObject.ReadAttribute(CommonDirectoryAttributes.KdsKdfParameters, out this.rawKdfParameters);
// Secret agreement algorithm
dsObject.ReadAttribute(CommonDirectoryAttributes.KdsSecretAgreementAlgorithm, out this.secretAgreementAlgorithmName);
@ -148,8 +152,10 @@ namespace DSInternals.Common.Data
/// </summary>
public Dictionary<uint, string> KdfParameters
{
get;
private set;
get
{
return ParseKdfParameters(this.rawKdfParameters);
}
}
/// <summary>
@ -196,6 +202,53 @@ namespace DSInternals.Common.Data
}
}
public byte[] ComputeL0Key(int l0KeyId)
{
return ComputeL0Key(
this.KeyId,
this.KeyValue,
this.KdfAlgorithm,
this.rawKdfParameters,
l0KeyId
);
}
public static byte[] ComputeL0Key(
Guid kdsRootKeyId,
byte[] kdsRootKey,
string kdfAlgorithm,
byte[] kdfParameters,
int l0KeyId)
{
var result = NativeMethods.GenerateKDFContext(
kdsRootKeyId,
l0KeyId,
-1,
-1,
GroupKeyLevel.L0,
out byte[] kdfContext,
out int counterOffset
);
Validator.AssertSuccess(result);
result = NativeMethods.GenerateDerivedKey(
kdfAlgorithm,
kdfParameters,
kdsRootKey,
kdfContext,
null,
null,
L0KeyIteration,
out byte[] l0Key,
out string invalidAtribute
);
Validator.AssertSuccess(result);
return l0Key;
}
public static Dictionary<uint,string> ParseKdfParameters(byte[] blob)
{
if(blob == null || blob.Length == 0)
@ -259,4 +312,4 @@ namespace DSInternals.Common.Data
}
}
}
}
}

View File

@ -0,0 +1,121 @@
namespace DSInternals.Common.Data
{
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
/// <summary>
/// The Protection Key Identifier data structure is used to store metadata about keys used to cryptographically wrap DPAPI-NG encryption keys and to derive managed passwords.
/// </summary>
/// <see>https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/98a575da-ca48-4afd-ba79-f77a8bed4e4e</see>
public class ProtectionKeyIdentifier
{
private const string KdsKeyMagic = "KDSK";
private const int ExpectedVersion = 1;
private const int StructureHeaderLength = 9 * sizeof(int) + 16;
public int L0KeyId
{
get;
private set;
}
public int L1KeyId
{
get;
private set;
}
public int L2KeyId
{
get;
private set;
}
public Guid RootKeyId
{
get;
private set;
}
public string DomainName
{
get;
private set;
}
public string ForestName
{
get;
private set;
}
public ProtectionKeyIdentifier(byte[] blob)
{
Validator.AssertMinLength(blob, StructureHeaderLength, nameof(blob));
using (Stream stream = new MemoryStream(blob))
{
using (BinaryReader reader = new BinaryReader(stream))
{
// Version must be 0x00000001
int version = reader.ReadInt32();
Validator.AssertEquals(ExpectedVersion, version, nameof(version));
// Magic must be 0x4B53444B
byte[] binaryMagic = reader.ReadBytes(sizeof(int));
string magic = ASCIIEncoding.ASCII.GetString(binaryMagic);
Validator.AssertEquals(KdsKeyMagic, magic, nameof(magic));
// Flags must be 0x00000000
int flags = reader.ReadInt32();
// An L0 index
this.L0KeyId = reader.ReadInt32();
// An L1 index
this.L1KeyId = reader.ReadInt32();
// An L2 index
this.L2KeyId = reader.ReadInt32();
// A root key identifier
byte[] binaryRootKeyId = reader.ReadBytes(Marshal.SizeOf(typeof(Guid)));
this.RootKeyId = new Guid(binaryRootKeyId);
// Variable data lengths
int additionalInfoLength = reader.ReadInt32();
int domainNameLength = reader.ReadInt32();
int forestNameLength = reader.ReadInt32();
// Validate variable data length
int expectedLength = StructureHeaderLength + additionalInfoLength + domainNameLength + forestNameLength;
Validator.AssertMinLength(blob, expectedLength, nameof(blob));
if (additionalInfoLength > 0)
{
// Additional info used in key derivation
byte[] additionalInfo = reader.ReadBytes(additionalInfoLength);
}
if(domainNameLength > 0)
{
// DNS-style name of the Active Directory domain in which this identifier was created.
byte[] binaryDomainName = reader.ReadBytes(domainNameLength);
// Trim \0
this.DomainName = Encoding.Unicode.GetString(binaryDomainName, 0, binaryDomainName.Length - sizeof(char));
}
if(forestNameLength > 0)
{
// DNS-style name of the Active Directory forest in which this identifier was created.
byte[] binaryForestName = reader.ReadBytes(forestNameLength);
// Trim \0
this.ForestName = Encoding.Unicode.GetString(binaryForestName, 0, binaryForestName.Length - sizeof(char));
}
}
}
}
}
}

View File

@ -0,0 +1,54 @@
using DSInternals.Common.Cryptography;
namespace DSInternals.Common.Data
{
/// <summary>
/// Group Managed Service Account.
/// </summary>
/// <see>https://learn.microsoft.com/en-us/windows/win32/adschema/c-msds-groupmanagedserviceaccount</see>
public class GroupManagedServiceAccount : DSAccount
{
private const int DefaultPasswordValidityInterval = 30;
public ProtectionKeyIdentifier ManagedPasswordId
{
get;
private set;
}
public ProtectionKeyIdentifier ManagedPasswordPreviousId
{
get;
private set;
}
public int ManagedPasswordInterval
{
get;
private set;
}
public GroupManagedServiceAccount(DirectoryObject dsObject, string netBIOSDomainName, DirectorySecretDecryptor pek) : base(dsObject, netBIOSDomainName, pek)
{
// TODO: Check that this object is a gMSA
// Read and parse msDS-ManagedPasswordId
dsObject.ReadAttribute(CommonDirectoryAttributes.ManagedPasswordId, out byte[] rawManagedPasswordId);
if(rawManagedPasswordId != null )
{
this.ManagedPasswordId = new ProtectionKeyIdentifier(rawManagedPasswordId);
}
// Read and parse msDS-ManagedPasswordPreviousId
dsObject.ReadAttribute(CommonDirectoryAttributes.ManagedPasswordPreviousId, out byte[] rawManagedPasswordPreviousId);
if(rawManagedPasswordPreviousId != null)
{
this.ManagedPasswordPreviousId = new ProtectionKeyIdentifier(rawManagedPasswordPreviousId);
}
// Read msDS-ManagedPasswordInterval
dsObject.ReadAttribute(CommonDirectoryAttributes.ManagedPasswordInterval, out int? managedPasswordInterval);
this.ManagedPasswordInterval = managedPasswordInterval ?? DefaultPasswordValidityInterval;
}
}
}

View File

@ -63,6 +63,9 @@
public const int LMHashHistoryId = 589984;
public const int LMHashId = 589879;
public const string LockoutTime = "lockoutTime";
public const string ManagedPasswordId = "msDS-ManagedPasswordId";
public const string ManagedPasswordPreviousId = "msDS-ManagedPasswordPreviousId";
public const string ManagedPasswordInterval = "msDS-ManagedPasswordInterval";
public const string MasterNamingContexts = "msDS-hasMasterNCs";
public const string Member = "member";
public const string Name = "name";

View File

@ -0,0 +1,19 @@
namespace DSInternals.Common.Interop
{
/// <see>https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gkdi/4cac87a3-521e-4918-a272-240f8fabed39</see>
internal enum GroupKeyLevel : uint
{
/// <summary>
/// L0 key
/// </summary>
L0 = 0,
/// <summary>
/// L1 key
/// </summary>
L1 = 1,
/// <summary>
/// L2 key
/// </summary>
L2 = 2
}
}

View File

@ -15,12 +15,14 @@ namespace DSInternals.Common.Interop
internal const int LMHashNumBytes = NTHashNumBits / 8;
internal const int LMPasswordMaxChars = 14;
internal const int NTPasswordMaxChars = 128;
internal const int KdsRootKeySize = 64;
private const int MaxRegistryKeyClassSize = 256;
private const string Advapi = "advapi32.dll";
private const string CryptDll = "cryptdll.Dll";
private const string Ntdll = "ntdll.dll";
private const string Mpr = "mpr.dll";
private const string KdsCli = "KdsCli.dll";
private const string LMOwfInternalName = "SystemFunction006";
private const string NTOwfInternalName = "SystemFunction007";
private const string LMOwfEncryptInternalName = "SystemFunction024";
@ -48,7 +50,7 @@ namespace DSInternals.Common.Interop
/// <see>https://github.com/wine-mirror/wine/blob/master/dlls/advapi32/crypt_md4.c</see>
[DllImport(Advapi, SetLastError = true, EntryPoint = NTOwfInternalName, CharSet = CharSet.Unicode)]
private static extern NtStatus RtlCalculateNtOwfPassword([In] ref SecureUnicodeString password, [MarshalAs(UnmanagedType.LPArray, SizeConst = NTHashNumBytes), In, Out] byte[] hash);
/// <summary>
/// Takes the passed NtPassword and performs a one-way-function on it.
/// Uses the RSA MD4 function
@ -161,7 +163,7 @@ namespace DSInternals.Common.Interop
// Wrap to get rid of the unnecessary pointer to int
return RtlEncryptLmOwfPwdWithIndex(lmOwfPassword, ref index, encryptedLmOwfPassword);
}
/// <summary>
/// Faster arbitrary length data encryption function (using RC4)
/// </summary>
@ -281,7 +283,7 @@ namespace DSInternals.Common.Interop
{
uint securityDescriptorSize;
bool result = ConvertStringSecurityDescriptorToSecurityDescriptor(stringSecurityDescriptor, stringSDRevision, out securityDescriptor, out securityDescriptorSize);
if(result)
if (result)
{
return Win32ErrorCode.Success;
}
@ -290,5 +292,130 @@ namespace DSInternals.Common.Interop
return (Win32ErrorCode)Marshal.GetLastWin32Error();
}
}
/// <param name="rootKeyId">Root key identifier of the requested key. It can be set to NULL.</param>
/// <param name="l0KeyId">L0 index of the requested group key. It MUST be a signed 32-bit integer greater than or equal to -1.</param>
/// <param name="l1KeyId">L1 index of the requested group key. It MUST be a signed 32-bit integer between -1 and 31 (inclusive).</param>
/// <param name="l2KeyId">L2 index of the requested group key. It MUST be a 32-bit integer between -1 and 31 (inclusive).</param>
/// <param name="level">Group key level.</param>
/// <returns>If the function succeeds, the return value is NO_ERROR.</returns>
internal static Win32ErrorCode GenerateKDFContext(
Guid rootKeyId,
int l0KeyId,
int l1KeyId,
int l2KeyId,
GroupKeyLevel level,
out byte[] context,
out int counterOffset)
{
var result = GenerateKDFContext(
rootKeyId,
l0KeyId,
l1KeyId,
l2KeyId,
level,
out SafeSidKeyProviderHandle contextHandle,
out int contextLength,
out counterOffset
);
try
{
context = contextHandle.ToArray(contextLength);
}
finally
{
contextHandle.Close();
}
return result;
}
[DllImport(KdsCli, SetLastError = true)]
private static extern Win32ErrorCode GenerateKDFContext(
Guid rootKeyId,
int l0KeyId,
int l1KeyId,
int l2KeyId,
GroupKeyLevel level,
out SafeSidKeyProviderHandle context,
out int contextLength,
out int counterOffset);
internal static Win32ErrorCode GenerateDerivedKey(
string kdfAlgorithmName,
byte[] kdfParameters,
byte[] secret,
byte[] context,
int? counterOffset,
byte[] label,
int iteration,
out byte[] derivedKey,
out string invalidAttribute)
{
int kdfParametersLength = kdfParameters?.Length ?? 0;
int secretLength = secret?.Length ?? 0;
int contextLength = context?.Length ?? 0;
int labelLength = label?.Length ?? 0;
byte[] derivedKeyBuffer = new byte[KdsRootKeySize];
StringBuilder invalidAttributeBuffer = new StringBuilder(byte.MaxValue);
// Deal with the optional int parameter
int counterOffsetValue = counterOffset.GetValueOrDefault();
var counterOffsetHandle = GCHandle.Alloc(counterOffsetValue);
try
{
Win32ErrorCode result = GenerateDerivedKey(
kdfAlgorithmName,
kdfParameters,
kdfParametersLength,
secret,
secretLength,
context,
contextLength,
(counterOffset.HasValue ? (IntPtr) counterOffsetHandle : IntPtr.Zero),
label,
labelLength,
iteration,
derivedKeyBuffer,
KdsRootKeySize,
ref invalidAttributeBuffer
);
derivedKey = derivedKeyBuffer;
invalidAttribute = invalidAttributeBuffer.ToString();
return result;
}
finally
{
counterOffsetHandle.Free();
}
}
[DllImport(KdsCli, CharSet = CharSet.Unicode, SetLastError = true)]
private static extern Win32ErrorCode GenerateDerivedKey(
string kdfAlgorithmName,
byte[] kdfParameters,
int kdfParametersLength,
byte[] secret,
int secretLength,
byte[] context,
int contextLength,
IntPtr counterOffset,
byte[] label,
int labelLength,
int iteration,
[MarshalAs(UnmanagedType.LPArray)] byte[] key,
int keyLength,
ref StringBuilder invalidAttribute);
/// <summary>
/// Frees memory allocated for a credentials structure by the GenerateKDFContext and GenerateDerivedKey functions.
/// </summary>
/// <param name="memory">Memory to be freed.</param>
[DllImport(KdsCli)]
internal static extern void SIDKeyProvFree([In] IntPtr memory);
}
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;
using System.Security;
namespace DSInternals.Common.Interop
{
/// <summary>
/// Represents a wrapper class for SID Key Provider object handles.
/// </summary>
[SecurityCritical]
internal class SafeSidKeyProviderHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private SafeSidKeyProviderHandle() : base(true)
{
}
public SafeSidKeyProviderHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHandle)
{
this.SetHandle(preexistingHandle);
}
public byte[] ToArray(int size)
{
if(this.IsInvalid)
{
return null;
}
byte[] binaryData = new byte[size];
Marshal.Copy(this.handle, binaryData, 0, size);
return binaryData;
}
[SecurityCritical]
protected override bool ReleaseHandle()
{
NativeMethods.SIDKeyProvFree(this.handle);
// Presume that the memory release has been successful.
return true;
}
}
}

View File

@ -81,6 +81,15 @@ namespace DSInternals.Common
}
}
public static void AssertEquals(int expectedValue, int actualValue, string paramName)
{
if (expectedValue != actualValue)
{
string message = String.Format(Resources.UnexpectedValueMessage, actualValue, expectedValue);
throw new ArgumentException(message, paramName);
}
}
public static void AssertEquals(char expectedValue, char actualValue, string paramName)
{
if (expectedValue.CompareTo(actualValue) != 0)