From fdd7f5071d0733bd5b025a512a837da8a558dc57 Mon Sep 17 00:00:00 2001 From: rxwx <2202542+rxwx@users.noreply.github.com> Date: Wed, 4 Jan 2023 18:46:42 +0000 Subject: [PATCH 1/5] Adds support for local (non-domain-joined) machines. Supports specifying the password in plaintext or SHA1 with the /password flag and /local flag. --- README.md | 1 + SharpDPAPI/Commands/Masterkeys.cs | 7 +++-- SharpDPAPI/Domain/Info.cs | 4 +-- SharpDPAPI/lib/Dpapi.cs | 46 ++++++++++++++----------------- SharpDPAPI/lib/Triage.cs | 5 ++-- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 8be22a4..977fab9 100755 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ SharpDPAPI is licensed under the BSD 3-Clause license. /pvk:key.pvk - use a DPAPI domain private key file to first decrypt reachable user masterkeys /password:X - first decrypt the current user's masterkeys using a plaintext password or NTLM hash (works remotely) /server:SERVER - triage a remote server, assuming admin access + /local - machine is non domain-joined. Keys use SHA1 instead of NTLM. Password may be supplied in plaintext or as a SHA1 hash Arguments for the credentials|vaults|rdg|keepass|triage|blob|ps commands: diff --git a/SharpDPAPI/Commands/Masterkeys.cs b/SharpDPAPI/Commands/Masterkeys.cs index aee4f58..f548fbd 100755 --- a/SharpDPAPI/Commands/Masterkeys.cs +++ b/SharpDPAPI/Commands/Masterkeys.cs @@ -55,12 +55,13 @@ public void Execute(Dictionary arguments) { if (!arguments.ContainsKey("/sid")) { - Console.WriteLine("[X] When using /password:X with /target:X, a /sid:X (domain user SID) is required!"); + Console.WriteLine("[X] When using /password:X with /target:X, a /sid:X (user SID) is required!"); return; } else { Console.WriteLine("[*] Triaging masterkey target: {0}\r\n", arguments["/target"]); - mappings = Triage.TriageUserMasterKeys(null, true, "", password, arguments["/target"], arguments["/sid"]); + mappings = Triage.TriageUserMasterKeys(null, true, "", password, arguments["/target"], + arguments["/sid"], false, arguments.ContainsKey("/local")); } } else @@ -79,7 +80,7 @@ public void Execute(Dictionary arguments) { if (!arguments.ContainsKey("/sid")) { - Console.WriteLine("[X] When dumping hashes with /target:X, a /sid:X (domain user SID) is required!"); + Console.WriteLine("[X] When dumping hashes with /target:X, a /sid:X (user SID) is required!"); return; } else diff --git a/SharpDPAPI/Domain/Info.cs b/SharpDPAPI/Domain/Info.cs index f11fc48..2633fec 100755 --- a/SharpDPAPI/Domain/Info.cs +++ b/SharpDPAPI/Domain/Info.cs @@ -45,7 +45,7 @@ public static void ShowUsage() /target:FILE/folder - triage a specific masterkey, or a folder full of masterkeys (otherwise triage local masterkeys) /pvk:BASE64... - use a base64'ed DPAPI domain private key file to first decrypt reachable user masterkeys /pvk:key.pvk - use a DPAPI domain private key file to first decrypt reachable user masterkeys - /password:X - first decrypt the current user's masterkeys using a plaintext password (works remotely) + /password:X - first decrypt the current user's masterkeys using a plaintext password or hash (works remotely) /server:SERVER - triage a remote server, assuming admin access @@ -53,7 +53,7 @@ public static void ShowUsage() Decryption: /unprotect - force use of CryptUnprotectData() for 'ps', 'rdg', or 'blob' commands - /password:X - first decrypt the current user's masterkeys using a plaintext password. Works with any function, as well as remotely. + /password:X - first decrypt the current user's masterkeys using a plaintext password or hash. Works with any function, as well as remotely. GUID1:SHA1 ... - use a one or more GUID:SHA1 masterkeys for decryption /mkfile:FILE - use a file of one or more GUID:SHA1 masterkeys for decryption /pvk:BASE64... - use a base64'ed DPAPI domain private key file to first decrypt reachable user masterkeys diff --git a/SharpDPAPI/lib/Dpapi.cs b/SharpDPAPI/lib/Dpapi.cs index 576b02d..ba5fc57 100755 --- a/SharpDPAPI/lib/Dpapi.cs +++ b/SharpDPAPI/lib/Dpapi.cs @@ -1747,44 +1747,38 @@ public static byte[] CalculateKeys(string password, string directory, bool domai utf16sid.CopyTo(utf16sidfinal, 0); utf16sidfinal[utf16sidfinal.Length - 2] = 0x00; - byte[] sha1bytes_password; - byte[] hmacbytes; + byte[] derived = null; + byte[] finalKey = null; if (!domain) { - //Calculate SHA1 from user password - using (var sha1 = new SHA1Managed()) + if (Regex.IsMatch(password, "^[a-f0-9]{40}$", RegexOptions.IgnoreCase)) { - sha1bytes_password = sha1.ComputeHash(utf16pass); + // Pass-the-hash (skip initial SHA1 phase) + // Note: SHA1 hash must be in format: SHA1(UTF16LE(password)) + // e.g. from mimikatz' sekurlsa::msv + derived = Helpers.ConvertHexStringToByteArray(password); } - var combined = Helpers.Combine(sha1bytes_password, utf16sidfinal); - using (var hmac = new HMACSHA1(sha1bytes_password)) + else { - hmacbytes = hmac.ComputeHash(utf16sidfinal); + //Calculate SHA1 from user password + using (var sha1 = new SHA1Managed()) + { + derived = sha1.ComputeHash(utf16pass); + } } - return hmacbytes; } else { //Calculate NTLM from user password. Kerberos's RC4_HMAC key is the NTLM hash //Skip NTLM hashing if the password is in NTLM format - string rc4Hash = Regex.IsMatch(password, "^[a-f0-9]{32}$", RegexOptions.IgnoreCase) ? password : + string rc4Hash = Regex.IsMatch(password, "^[a-f0-9]{32}$", RegexOptions.IgnoreCase) ? password : Crypto.KerberosPasswordHash(Interop.KERB_ETYPE.rc4_hmac, password); var ntlm = Helpers.ConvertHexStringToByteArray(rc4Hash); - var combinedNTLM = Helpers.Combine(ntlm, utf16sidfinal); - byte[] ntlmhmacbytes; - //Calculate SHA1 of NTLM from user password - using (var hmac = new HMACSHA1(ntlm)) - { - ntlmhmacbytes = hmac.ComputeHash(utf16sidfinal); - } - byte[] tmpbytes1; - byte[] tmpbytes2; - byte[] tmpkey3bytes; using (var hMACSHA256 = new HMACSHA256()) { @@ -1795,15 +1789,15 @@ public static byte[] CalculateKeys(string password, string directory, bool domai using (var hMACSHA256 = new HMACSHA256()) { var deriveBytes = new Pbkdf2(hMACSHA256, tmpbytes1, utf16sid, 1); - tmpbytes2 = deriveBytes.GetBytes(16, "sha256"); + derived = deriveBytes.GetBytes(16, "sha256"); } + } - using (var hmac = new HMACSHA1(tmpbytes2)) - { - tmpkey3bytes = hmac.ComputeHash(utf16sidfinal); - } - return tmpkey3bytes; + using (var hmac = new HMACSHA1(derived)) + { + finalKey = hmac.ComputeHash(utf16sidfinal); } + return finalKey; } public static KeyValuePair DecryptMasterKey(byte[] masterKeyBytes, byte[] backupKeyBytes) diff --git a/SharpDPAPI/lib/Triage.cs b/SharpDPAPI/lib/Triage.cs index 56f9511..b969cac 100755 --- a/SharpDPAPI/lib/Triage.cs +++ b/SharpDPAPI/lib/Triage.cs @@ -11,10 +11,9 @@ namespace SharpDPAPI public class Triage { public static Dictionary TriageUserMasterKeys(byte[] backupKeyBytes, bool show = false, string computerName = "", - string password = "", string target = "", string userSID = "", bool dumpHash = false) + string password = "", string target = "", string userSID = "", bool dumpHash = false, bool local = false) { // triage all *user* masterkeys we can find, decrypting if the backupkey is supplied - var mappings = new Dictionary(); var preferred = new Dictionary(); var canAccess = false; @@ -90,7 +89,7 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy } else if (!String.IsNullOrEmpty(password)) { - byte[] hmacBytes = Dpapi.CalculateKeys(password, "", true, userSID); + byte[] hmacBytes = Dpapi.CalculateKeys(password, "", !local, userSID); plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); } else if (dumpHash) From 21006fcefdc5d37b1975531ac658175b887ba085 Mon Sep 17 00:00:00 2001 From: rxwx <2202542+rxwx@users.noreply.github.com> Date: Thu, 5 Jan 2023 11:57:24 +0000 Subject: [PATCH 2/5] indicate when we are using a SHA1/NTLM hash instead of a password --- SharpDPAPI/Commands/Blob.cs | 7 ++++++- SharpDPAPI/Commands/Credentials.cs | 7 ++++++- SharpDPAPI/Commands/Masterkeys.cs | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/SharpDPAPI/Commands/Blob.cs b/SharpDPAPI/Commands/Blob.cs index a1ccdac..c0c0335 100755 --- a/SharpDPAPI/Commands/Blob.cs +++ b/SharpDPAPI/Commands/Blob.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Text.RegularExpressions; namespace SharpDPAPI.Commands { @@ -63,7 +64,11 @@ public void Execute(Dictionary arguments) else if (arguments.ContainsKey("/password")) { string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); + + Console.WriteLine("[*] Will decrypt user masterkeys with {0}: {1}\r\n", + Regex.IsMatch(password, @"^([a-f0-9]{32}|[a-f0-9]{40})$", RegexOptions.IgnoreCase) + ? "hash" : "password", password); + if (arguments.ContainsKey("/server")) { masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); diff --git a/SharpDPAPI/Commands/Credentials.cs b/SharpDPAPI/Commands/Credentials.cs index a83c7c6..9ad4549 100755 --- a/SharpDPAPI/Commands/Credentials.cs +++ b/SharpDPAPI/Commands/Credentials.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.RegularExpressions; namespace SharpDPAPI.Commands { @@ -44,7 +45,11 @@ public void Execute(Dictionary arguments) else if (arguments.ContainsKey("/password")) { string password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); + + Console.WriteLine("[*] Will decrypt user masterkeys with {0}: {1}\r\n", + Regex.IsMatch(password, @"^([a-f0-9]{32}|[a-f0-9]{40})$", RegexOptions.IgnoreCase) + ? "hash" : "password", password); + if (arguments.ContainsKey("/server")) { masterkeys = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); diff --git a/SharpDPAPI/Commands/Masterkeys.cs b/SharpDPAPI/Commands/Masterkeys.cs index f548fbd..fdcff32 100755 --- a/SharpDPAPI/Commands/Masterkeys.cs +++ b/SharpDPAPI/Commands/Masterkeys.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.RegularExpressions; namespace SharpDPAPI.Commands { @@ -46,7 +47,11 @@ public void Execute(Dictionary arguments) else if (arguments.ContainsKey("/password")) { password = arguments["/password"]; - Console.WriteLine("[*] Will decrypt user masterkeys with password: {0}\r\n", password); + + Console.WriteLine("[*] Will decrypt user masterkeys with {0}: {1}\r\n", + Regex.IsMatch(password, @"^([a-f0-9]{32}|[a-f0-9]{40})$", RegexOptions.IgnoreCase) + ? "hash" : "password", password); + if (arguments.ContainsKey("/server")) { mappings = Triage.TriageUserMasterKeys(null, true, arguments["/server"], password); From 5ff8cb4be793587aea8fb5f4dda210f834a55b68 Mon Sep 17 00:00:00 2001 From: rxwx <2202542+rxwx@users.noreply.github.com> Date: Thu, 5 Jan 2023 12:22:34 +0000 Subject: [PATCH 3/5] Add /local flag check when triaging a folder of masterkeys specified with the /target: flag. This is needed since the directory might not contain a BK file if it was lifted from another system. --- SharpDPAPI/lib/Triage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharpDPAPI/lib/Triage.cs b/SharpDPAPI/lib/Triage.cs index b969cac..afb54a6 100755 --- a/SharpDPAPI/lib/Triage.cs +++ b/SharpDPAPI/lib/Triage.cs @@ -58,7 +58,7 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy } else if (!String.IsNullOrEmpty(password) && !String.IsNullOrEmpty(userSID)) { - byte[] hmacBytes = Dpapi.CalculateKeys(password, "", true, userSID); + byte[] hmacBytes = Dpapi.CalculateKeys(password, "", !local, userSID); plaintextMasterKey = Dpapi.DecryptMasterKeyWithSha(masterKeyBytes, hmacBytes); } else if (dumpHash) From 475f2b2a4fe592e29842b184b8900d83ef978b14 Mon Sep 17 00:00:00 2001 From: "richard.warren" Date: Fri, 31 May 2024 11:03:51 +0100 Subject: [PATCH 4/5] Adding support for DPAPI *protection* with a specified masterkey --- SharpChrome/SharpChrome.csproj | 4 +- SharpChrome/app.config | 2 +- SharpDPAPI/Commands/Protect.cs | 81 +++++++++ SharpDPAPI/Domain/CommandCollection.cs | 1 + SharpDPAPI/Domain/Info.cs | 10 +- SharpDPAPI/SharpDPAPI.csproj | 3 +- SharpDPAPI/app.config | 2 +- SharpDPAPI/lib/Crypto.cs | 163 +++++++++++++++++- SharpDPAPI/lib/Dpapi.cs | 228 +++++++++++++++++++++---- SharpDPAPI/lib/Helpers.cs | 7 + SharpDPAPI/lib/Triage.cs | 2 +- 11 files changed, 461 insertions(+), 42 deletions(-) create mode 100644 SharpDPAPI/Commands/Protect.cs diff --git a/SharpChrome/SharpChrome.csproj b/SharpChrome/SharpChrome.csproj index 70692df..db14ecf 100755 --- a/SharpChrome/SharpChrome.csproj +++ b/SharpChrome/SharpChrome.csproj @@ -9,7 +9,7 @@ Properties SharpChrome SharpChrome - v3.5 + v4.8 512 @@ -23,6 +23,7 @@ prompt 4 true + false AnyCPU @@ -33,6 +34,7 @@ prompt 0 true + false diff --git a/SharpChrome/app.config b/SharpChrome/app.config index 2fa6e95..3e0e37c 100644 --- a/SharpChrome/app.config +++ b/SharpChrome/app.config @@ -1,3 +1,3 @@ - + diff --git a/SharpDPAPI/Commands/Protect.cs b/SharpDPAPI/Commands/Protect.cs new file mode 100644 index 0000000..f45034b --- /dev/null +++ b/SharpDPAPI/Commands/Protect.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using static SharpDPAPI.Crypto; + +namespace SharpDPAPI.Commands +{ + public class Protect : ICommand + { + public static string CommandName => "protect"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Encrypt DPAPI blob"); + + if (!arguments.ContainsKey("/mkfile")) + { + Console.WriteLine("[!] Error: Provide a master key file using /mkfile:"); + return; + } + + if (!arguments.ContainsKey("/password")) + { + Console.WriteLine("[!] Error: Provide a password"); + return; + } + + if (!arguments.ContainsKey("/input")) + { + Console.WriteLine("[!] Error: provide an input file path or base64 using /input:"); + return; + } + + if (!arguments.ContainsKey("/output")) + { + Console.WriteLine("[!] Error: provide an output file path using /output:"); + return; + } + + byte[] plainBytes; + string inputFile = arguments["/input"].Trim('"').Trim('\''); + string outputFile = arguments["/output"].Trim('"').Trim('\''); + string masterKeyFile = arguments["/mkfile"].Trim('"').Trim('\''); + string password = arguments["/password"]; + + if (File.Exists(inputFile)) + { + plainBytes = File.ReadAllBytes(inputFile); + } + else + { + plainBytes = Convert.FromBase64String(inputFile); + } + + Console.WriteLine("[*] Using masterkey: {0}", masterKeyFile); + + string userSID = Dpapi.ExtractSidFromPath(masterKeyFile); + + var masterkeys = Triage.TriageUserMasterKeys( + null, password: password, target: masterKeyFile, + local: true, userSID: userSID + ); + + if (masterkeys.Count != 1) + { + Console.WriteLine("[!] Failed to decrypt masterkey. Wrong password?"); + return; + } + + var masterKey = masterkeys.First(); + byte[] enc = Dpapi.CreateDPAPIBlob(plainBytes, Helpers.StringToByteArray(masterKey.Value), + EncryptionAlgorithm.CALG_AES_256, + HashAlgorithm.CALG_SHA_512, + new Guid(masterKey.Key)); + + File.WriteAllBytes(outputFile, enc); + Console.WriteLine("[+] Done! Wrote {0} bytes to: {1}", enc.Length, outputFile); + } + } +} \ No newline at end of file diff --git a/SharpDPAPI/Domain/CommandCollection.cs b/SharpDPAPI/Domain/CommandCollection.cs index 8f1bd41..2c9f67f 100755 --- a/SharpDPAPI/Domain/CommandCollection.cs +++ b/SharpDPAPI/Domain/CommandCollection.cs @@ -33,6 +33,7 @@ public CommandCollection() _availableCommands.Add(Certificate.CommandName, () => new Certificate()); _availableCommands.Add(Search.CommandName, () => new Search()); _availableCommands.Add(SCCM.CommandName, () => new SCCM()); + _availableCommands.Add(Protect.CommandName, () => new Protect()); } public bool ExecuteCommand(string commandName, Dictionary arguments) diff --git a/SharpDPAPI/Domain/Info.cs b/SharpDPAPI/Domain/Info.cs index 2633fec..4d0f0d2 100755 --- a/SharpDPAPI/Domain/Info.cs +++ b/SharpDPAPI/Domain/Info.cs @@ -73,7 +73,15 @@ public static void ShowUsage() /machine - use the local machine store for certificate triage /mkfile | /target - for /machine triage /pvk | /mkfile | /password | /server | /target - for user triage - + + +Encryption: + Arguments for the 'protect' command: + /input - the input file or base64 encoded string you want to protect using DPAPI + /output - the output file the encrypted blob will be written to + /mkfile - the path to the masterkey file to use for encryption + /password - the password, or password hash (SHA1 or NTLM) to decrypt the masterkey file + Note: in most cases, just use *triage* if you're targeting user DPAPI secrets and *machinetriage* if you're going after SYSTEM DPAPI secrets. These functions wrap all the other applicable functions that can be automatically run. diff --git a/SharpDPAPI/SharpDPAPI.csproj b/SharpDPAPI/SharpDPAPI.csproj index 05b1ba5..ffbe41d 100755 --- a/SharpDPAPI/SharpDPAPI.csproj +++ b/SharpDPAPI/SharpDPAPI.csproj @@ -9,7 +9,7 @@ Properties SharpDPAPI SharpDPAPI - v3.5 + v4.8 512 publish\ true @@ -72,6 +72,7 @@ + diff --git a/SharpDPAPI/app.config b/SharpDPAPI/app.config index cf7e7ab..2c0f559 100755 --- a/SharpDPAPI/app.config +++ b/SharpDPAPI/app.config @@ -1,3 +1,3 @@ - + diff --git a/SharpDPAPI/lib/Crypto.cs b/SharpDPAPI/lib/Crypto.cs index 25079c0..ce18d1e 100755 --- a/SharpDPAPI/lib/Crypto.cs +++ b/SharpDPAPI/lib/Crypto.cs @@ -3,11 +3,35 @@ using System.Runtime.InteropServices; using System.ComponentModel; using System.IO; +using System.Collections.Generic; namespace SharpDPAPI { public class Crypto { + public enum EncryptionAlgorithm + { + CALG_3DES = 26115, + CALG_AES_256 = 26128 + } + + public enum HashAlgorithm + { + CALG_SHA1 = 32772, + CALG_SHA_256 = 32780, + CALG_SHA_512 = 32782 + } + + public static byte[] GetRandomBytes(int length) + { + using (var rng = new RNGCryptoServiceProvider()) + { + var randomBytes = new byte[length]; + rng.GetBytes(randomBytes); + return randomBytes; + } + } + public static string KerberosPasswordHash(Interop.KERB_ETYPE etype, string password, string salt = "", int count = 4096) { // use the internal KERB_ECRYPT HashPassword() function to calculate a password hash of a given etype @@ -38,15 +62,70 @@ public static string KerberosPasswordHash(Interop.KERB_ETYPE etype, string passw return BitConverter.ToString(output).Replace("-", ""); } + public static byte[] EncryptBlob(byte[] plaintext, byte[] key, + EncryptionAlgorithm algCrypt, PaddingMode padding = PaddingMode.Zeros) + { + // encrypts a DPAPI blob using 3DES or AES + + switch (algCrypt) + { + case EncryptionAlgorithm.CALG_3DES: + { + // takes a byte array of plaintext bytes and a key array, encrypt the blob with 3DES + var desCryptoProvider = new TripleDESCryptoServiceProvider(); + + var ivBytes = new byte[8]; + + desCryptoProvider.Key = key; + desCryptoProvider.IV = ivBytes; + desCryptoProvider.Mode = CipherMode.CBC; + desCryptoProvider.Padding = padding; + try + { + var ciphertextBytes = desCryptoProvider.CreateEncryptor() + .TransformFinalBlock(plaintext, 0, plaintext.Length); + return ciphertextBytes; + } + catch (Exception e) + { + Console.WriteLine("[x] An exception occured: {0}", e); + } + + return new byte[0]; + } + + case EncryptionAlgorithm.CALG_AES_256: + { + // takes a byte array of plaintext bytes and a key array, encrypt the blob with AES256 + var aesCryptoProvider = new AesManaged(); + + var ivBytes = new byte[16]; + + aesCryptoProvider.Key = key; + aesCryptoProvider.IV = ivBytes; + aesCryptoProvider.Mode = CipherMode.CBC; + aesCryptoProvider.Padding = padding; + + var ciphertextBytes = aesCryptoProvider.CreateEncryptor() + .TransformFinalBlock(plaintext, 0, plaintext.Length); + + return ciphertextBytes; + } + + default: + throw new Exception($"Could not encrypt blob. Unsupported algorithm: {algCrypt}"); + } + } + public static byte[] DecryptBlob(byte[] ciphertext, byte[] key, int algCrypt, PaddingMode padding = PaddingMode.Zeros) { // decrypts a DPAPI blob using 3DES or AES // reference: https://docs.microsoft.com/en-us/windows/desktop/seccrypto/alg-id - switch (algCrypt) + switch ((EncryptionAlgorithm)algCrypt) { - case 26115: // 26115 == CALG_3DES + case EncryptionAlgorithm.CALG_3DES: { // takes a byte array of ciphertext bytes and a key array, decrypt the blob with 3DES var desCryptoProvider = new TripleDESCryptoServiceProvider(); @@ -71,7 +150,7 @@ public static byte[] DecryptBlob(byte[] ciphertext, byte[] key, int algCrypt, Pa return new byte[0]; } - case 26128: // 26128 == CALG_AES_256 + case EncryptionAlgorithm.CALG_AES_256: { // takes a byte array of ciphertext bytes and a key array, decrypt the blob with AES256 var aesCryptoProvider = new AesManaged(); @@ -93,6 +172,75 @@ public static byte[] DecryptBlob(byte[] ciphertext, byte[] key, int algCrypt, Pa throw new Exception($"Could not decrypt blob. Unsupported algorithm: {algCrypt}"); } } + /* +def CryptSessionKeyWin7(masterkey, nonce, hashAlgo, entropy=None, strongPassword=None): + """Computes the decryption key for XP DPAPI blob, given the masterkey and optional information. + + This implementation relies on an RFC compliant HMAC implementation + This algorithm is also used when checking the HMAC for integrity after decryption + + :param masterkey: decrypted masterkey (should be 64 bytes long) + :param nonce: this is the nonce contained in the blob or the HMAC in the blob (integrity check) + :param entropy: this is the optional entropy from CryptProtectData() API + :param strongPassword: optional password used for decryption or the blob itself (integrity check) + :returns: decryption key + :rtype : str + """ + if len(masterkey) > 20: + masterkey = hashlib.sha1(masterkey).digest() + + digest = M2Crypto.EVP.HMAC(masterkey, hashAlgo.name) + digest.update(nonce) + if entropy is not None: + digest.update(entropy) + if strongPassword is not None: + digest.update(strongPassword) + return digest.final() + */ + + public static byte[] DeriveKey(byte[] key, byte[] nonce, int hashAlgorithm, byte[] blob, byte[] entropy = null) + { + HMAC hmac; + switch (hashAlgorithm) + { + case (int)HashAlgorithm.CALG_SHA1: + hmac = new HMACSHA1(key); + break; + case (int)HashAlgorithm.CALG_SHA_256: + hmac = new HMACSHA256(key); + break; + case (int)HashAlgorithm.CALG_SHA_512: + hmac = new HMACSHA512(key); + break; + default: + throw new Exception($"Unsupported hash algorithm: {hashAlgorithm}"); + } + + var keyMaterial = new List(); + keyMaterial.AddRange(nonce); + if (entropy != null) + { + keyMaterial.AddRange(entropy); + } + keyMaterial.AddRange(blob); + + return hmac.ComputeHash(keyMaterial.ToArray()); + } + + /* + def CryptDeriveKey(h, cipherAlgo, hashAlgo): + """Internal use. Mimics the corresponding native Microsoft function""" + if len(h) > hashAlgo.blockSize: + h = hashlib.new(hashAlgo.name, h).digest() + if len(h) >= cipherAlgo.keyLength: + return h + h += "\x00" * hashAlgo.blockSize + ipad = "".join(chr(ord(h[i]) ^ 0x36) for i in range(hashAlgo.blockSize)) + opad = "".join(chr(ord(h[i]) ^ 0x5c) for i in range(hashAlgo.blockSize)) + k = hashlib.new(hashAlgo.name, ipad).digest() + hashlib.new(hashAlgo.name, opad).digest() + k = cipherAlgo.do_fixup_key(k) + return k + */ public static byte[] DeriveKey(byte[] keyBytes, byte[] saltBytes, int algHash, byte[] entropy = null) { @@ -101,10 +249,12 @@ public static byte[] DeriveKey(byte[] keyBytes, byte[] saltBytes, int algHash, b //Console.WriteLine("[*] key : {0}", BitConverter.ToString(keyBytes).Replace("-", "")); //Console.WriteLine("[*] saltBytes : {0}", BitConverter.ToString(saltBytes).Replace("-", "")); //Console.WriteLine("[*] entropy : {0}", BitConverter.ToString(entropy).Replace("-", "")); - //Console.WriteLine("[*] algHash : {0}", algHash); + //Console.WriteLine("[*] algHash : {0}", (HashAlgorithm)algHash); - if (algHash == 32782) + if (algHash == (int)HashAlgorithm.CALG_SHA_512) { + // TODO: pretty sure this is wrong. It only calculates the session key but doesn't do derivation + // calculate the session key -> HMAC(salt) where the sha1(masterkey) is the key // 32782 == CALG_SHA_512 @@ -117,7 +267,7 @@ public static byte[] DeriveKey(byte[] keyBytes, byte[] saltBytes, int algHash, b { return HMACSha512(keyBytes, saltBytes); } - } else if (algHash == 32772) + } else if (algHash == (int)HashAlgorithm.CALG_SHA1) { // 32772 == CALG_SHA1 @@ -156,6 +306,7 @@ public static byte[] DeriveKey(byte[] keyBytes, byte[] saltBytes, int algHash, b } else { + Console.WriteLine("[!] Unsupported Hash Algorithm"); return new byte[0]; } } diff --git a/SharpDPAPI/lib/Dpapi.cs b/SharpDPAPI/lib/Dpapi.cs index ba5fc57..3ca108e 100755 --- a/SharpDPAPI/lib/Dpapi.cs +++ b/SharpDPAPI/lib/Dpapi.cs @@ -10,6 +10,8 @@ using System.Security.Principal; using System.Text; using System.Text.RegularExpressions; +using static SharpDPAPI.Crypto; +using HashAlgorithm = SharpDPAPI.Crypto.HashAlgorithm; namespace SharpDPAPI { @@ -851,6 +853,174 @@ public static Dictionary PVKTriage(Dictionary ar return masterkeys; } + public static byte[] CreateDPAPIBlob(byte[] plaintext, byte[] masterKey, EncryptionAlgorithm algCrypt, + HashAlgorithm algHash, Guid masterKeyGuid, byte[] entropy = null, string description = "") + { + var descBytes = string.IsNullOrEmpty(description) ? + new byte[2] : Encoding.Unicode.GetBytes(description); + + var saltBytes = GetRandomBytes(32); // Default salt length (TODO: check) + var hmacKeyLen = 0; // Default HMAC key length (TODO: check) + var hmac2KeyLen = 32; // Default HMAC2 key length (TODO: check) + + var hmacKey = GetRandomBytes(hmacKeyLen); + var hmac2Key = GetRandomBytes(hmac2KeyLen); + + var algHashLen = 0; + var algCryptLen = 0; + var signLen = 0; + + switch (algCrypt) + { + case EncryptionAlgorithm.CALG_3DES: + algCryptLen = 192; + break; + case EncryptionAlgorithm.CALG_AES_256: + algCryptLen = 256; + break; + } + + switch (algHash) + { + case HashAlgorithm.CALG_SHA1: + algHashLen = 160; + signLen = 20; + break; + case HashAlgorithm.CALG_SHA_256: + algHashLen = 256; + signLen = 32; + break; + case HashAlgorithm.CALG_SHA_512: + algHashLen = 512; + signLen = 64; + break; + } + + // Deriving key + var derivedKeyBytes = Crypto.DeriveKey(masterKey, saltBytes, (int)algHash, entropy); + var finalKeyBytes = new byte[algCryptLen / 8]; + Array.Copy(derivedKeyBytes, finalKeyBytes, algCryptLen / 8); + + // Encrypting data + var encData = EncryptBlob(plaintext, finalKeyBytes, algCrypt, PaddingMode.PKCS7); + + var blobLength = 0; + blobLength += 4; // dwVersion + blobLength += 16; // guidProvider + blobLength += 4; // dwMasterKeyVersion + blobLength += 16; // guidMasterKey + blobLength += 4; // dwFlags + blobLength += 4; // dwDescriptionLen + blobLength += descBytes.Length; // szDescription + blobLength += 4; // algCrypt + blobLength += 4; // dwAlgCryptLen + blobLength += 4; // dwSaltLen + blobLength += saltBytes.Length; // pbSalt + blobLength += 4; // dwHmacKeyLen + blobLength += hmacKey.Length; // pbHmackKey + blobLength += 4; // algHash + blobLength += 4; // dwAlgHashLen + blobLength += 4; // dwHmac2KeyLen + blobLength += hmac2Key.Length; // pbHmack2Key + blobLength += 4; // dwDataLen + blobLength += encData.Length; // pbData + blobLength += 4; // dwSignLen + blobLength += signLen; // pbSign + + var blobBytes = new byte[blobLength]; + var offset = 0; + + // Setting version + var version = 1; + Array.Copy(BitConverter.GetBytes(version), 0, blobBytes, offset, 4); + offset += 4; + + // Provider GUID (df9d8cd0-1501-11d1-8c7a-00c04fc297eb) + var providerGuid = new Guid("df9d8cd0-1501-11d1-8c7a-00c04fc297eb"); + Array.Copy(providerGuid.ToByteArray(), 0, blobBytes, offset, 16); + offset += 16; + + // Master key version + var masterKeyVersion = 1; + Array.Copy(BitConverter.GetBytes(masterKeyVersion), 0, blobBytes, offset, 4); + offset += 4; + + // Master key GUID + Array.Copy(masterKeyGuid.ToByteArray(), 0, blobBytes, offset, 16); + offset += 16; + + // Flags + var flags = 0; + Array.Copy(BitConverter.GetBytes(flags), 0, blobBytes, offset, 4); + offset += 4; + + // Description length + Array.Copy(BitConverter.GetBytes(descBytes.Length), 0, blobBytes, offset, 4); + offset += 4; + + // Description + Array.Copy(descBytes, 0, blobBytes, offset, descBytes.Length); + offset += descBytes.Length; + + // Algorithm ID + Array.Copy(BitConverter.GetBytes((int)algCrypt), 0, blobBytes, offset, 4); + offset += 4; + + // Algorithm key length + Array.Copy(BitConverter.GetBytes(algCryptLen), 0, blobBytes, offset, 4); + offset += 4; + + // Salt length + Array.Copy(BitConverter.GetBytes(saltBytes.Length), 0, blobBytes, offset, 4); + offset += 4; + + // Salt + Array.Copy(saltBytes, 0, blobBytes, offset, saltBytes.Length); + offset += saltBytes.Length; + + // Copying HMAC key length + Array.Copy(BitConverter.GetBytes(hmacKeyLen), 0, blobBytes, offset, 4); + offset += 4; + + // HMAC key + Array.Copy(hmacKey, 0, blobBytes, offset, hmacKeyLen); + offset += hmacKeyLen; + + // Hash algorithm ID + Array.Copy(BitConverter.GetBytes((int)algHash), 0, blobBytes, offset, 4); + offset += 4; + + // Hash length + Array.Copy(BitConverter.GetBytes(algHashLen), 0, blobBytes, offset, 4); + offset += 4; + + // HMAC2 key length + Array.Copy(BitConverter.GetBytes(hmac2KeyLen), 0, blobBytes, offset, 4); + offset += 4; + + // HMAC2 key + Array.Copy(hmac2Key, 0, blobBytes, offset, hmac2KeyLen); + offset += hmac2KeyLen; + + // Data length + Array.Copy(BitConverter.GetBytes(encData.Length), 0, blobBytes, offset, 4); + offset += 4; + + // Encrypted data + Array.Copy(encData, 0, blobBytes, offset, encData.Length); + offset += encData.Length; + + // Sign (HMAC) over the entire blob except for the sign field + var signBlob = Helpers.SubArray(blobBytes, 20, offset - 20); + var sign = Crypto.DeriveKey(masterKey, hmac2Key, (int)algHash, signBlob, entropy); + + // Copying sign length and sign + Array.Copy(BitConverter.GetBytes(signLen), 0, blobBytes, offset, 4); + offset += 4; + Array.Copy(sign, 0, blobBytes, offset, signLen); + + return blobBytes; + } public static byte[] DescribeDPAPIBlob(byte[] blobBytes, Dictionary MasterKeys, string blobType = "credential", bool unprotect = false, byte[] entropy = null) { @@ -904,6 +1074,7 @@ public static byte[] DescribeDPAPIBlob(byte[] blobBytes, Dictionary FormatHash(byte[] masterKeyBytes, string sid, int context = 3) { if (string.IsNullOrEmpty(sid) || masterKeyBytes == null) diff --git a/SharpDPAPI/lib/Helpers.cs b/SharpDPAPI/lib/Helpers.cs index bb1f274..c52ae38 100755 --- a/SharpDPAPI/lib/Helpers.cs +++ b/SharpDPAPI/lib/Helpers.cs @@ -13,6 +13,13 @@ namespace SharpDPAPI { public static class Helpers { + public static byte[] SubArray(byte[] data, int index, int length) + { + byte[] result = new byte[length]; + Array.Copy(data, index, result, 0, length); + return result; + } + public static void EncodeLength(BinaryWriter stream, int length) { if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative"); diff --git a/SharpDPAPI/lib/Triage.cs b/SharpDPAPI/lib/Triage.cs index afb54a6..49a68ad 100755 --- a/SharpDPAPI/lib/Triage.cs +++ b/SharpDPAPI/lib/Triage.cs @@ -21,7 +21,7 @@ public static Dictionary TriageUserMasterKeys(byte[] backupKeyBy if (!String.IsNullOrEmpty(target)) { // if we're targeting specific masterkey files - if (((backupKeyBytes == null) || (backupKeyBytes.Length == 0)) && String.IsNullOrEmpty(userSID)) + if (((backupKeyBytes == null) || (backupKeyBytes.Length == 0)) && String.IsNullOrEmpty(password)) { // currently only backupkey is supported Console.WriteLine("[X] The masterkey '/target:X' option currently requires '/pvk:BASE64...' or '/password:X'"); From 1ffffc76a351fe266c8eec1a5c2668741246e537 Mon Sep 17 00:00:00 2001 From: "richard.warren" Date: Mon, 16 Sep 2024 17:16:03 +0100 Subject: [PATCH 5/5] Add CRYPTPROTECT_LOCAL_MACHINE flag --- SharpDPAPI/Commands/Protect.cs | 60 +++++++++++++++++++++++++++------- SharpDPAPI/Domain/Info.cs | 2 ++ SharpDPAPI/lib/Dpapi.cs | 6 ++-- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/SharpDPAPI/Commands/Protect.cs b/SharpDPAPI/Commands/Protect.cs index f45034b..d7f4ee9 100644 --- a/SharpDPAPI/Commands/Protect.cs +++ b/SharpDPAPI/Commands/Protect.cs @@ -39,10 +39,20 @@ public void Execute(Dictionary arguments) } byte[] plainBytes; + byte[] entropy = null; + string inputFile = arguments["/input"].Trim('"').Trim('\''); string outputFile = arguments["/output"].Trim('"').Trim('\''); + string sid = arguments.ContainsKey("/sid") ? arguments["/sid"] : string.Empty; string masterKeyFile = arguments["/mkfile"].Trim('"').Trim('\''); string password = arguments["/password"]; + bool isLocalMachine = arguments.ContainsKey("/local"); + string description = arguments.ContainsKey("/description") ? arguments["/description"] : string.Empty; + + if (arguments.ContainsKey("/entropy")) + { + entropy = Helpers.StringToByteArray(arguments["/entropy"]); + } if (File.Exists(inputFile)) { @@ -54,28 +64,56 @@ public void Execute(Dictionary arguments) } Console.WriteLine("[*] Using masterkey: {0}", masterKeyFile); + + string userSID = string.Empty; + Dictionary keyDict; + KeyValuePair keyPair = default; - string userSID = Dpapi.ExtractSidFromPath(masterKeyFile); - - var masterkeys = Triage.TriageUserMasterKeys( - null, password: password, target: masterKeyFile, - local: true, userSID: userSID - ); - - if (masterkeys.Count != 1) + byte[] masterKeyBytes = null; + Guid masterKeyGuid = default; + + try + { + if (!isLocalMachine) + { + userSID = string.IsNullOrEmpty(sid) ? sid : Dpapi.ExtractSidFromPath(masterKeyFile); + keyDict = Triage.TriageUserMasterKeys(null, password: password, target: masterKeyFile, local: true, userSID: userSID); + if (keyDict.Count == 1) + { + keyPair = keyDict.First(); + masterKeyBytes = Helpers.StringToByteArray(keyPair.Value); + masterKeyGuid = new Guid(keyPair.Key); + } + } + else + { + keyPair = Dpapi.DecryptMasterKeyWithSha(File.ReadAllBytes(masterKeyFile), Helpers.StringToByteArray(password)); + masterKeyBytes = Helpers.StringToByteArray(keyPair.Value); + masterKeyGuid = new Guid(keyPair.Key); + } + } + catch + { + } + + if (masterKeyBytes == null || masterKeyGuid == null) { Console.WriteLine("[!] Failed to decrypt masterkey. Wrong password?"); return; } - var masterKey = masterkeys.First(); - byte[] enc = Dpapi.CreateDPAPIBlob(plainBytes, Helpers.StringToByteArray(masterKey.Value), + byte[] enc = Dpapi.CreateDPAPIBlob(plainBytes, masterKeyBytes, EncryptionAlgorithm.CALG_AES_256, HashAlgorithm.CALG_SHA_512, - new Guid(masterKey.Key)); + masterKeyGuid, + isLocalMachine: isLocalMachine, + entropy: entropy, + description: description + ); File.WriteAllBytes(outputFile, enc); Console.WriteLine("[+] Done! Wrote {0} bytes to: {1}", enc.Length, outputFile); + Console.WriteLine("[*] {0}", Convert.ToBase64String(enc)); } } } \ No newline at end of file diff --git a/SharpDPAPI/Domain/Info.cs b/SharpDPAPI/Domain/Info.cs index 4d0f0d2..9a69767 100755 --- a/SharpDPAPI/Domain/Info.cs +++ b/SharpDPAPI/Domain/Info.cs @@ -81,6 +81,8 @@ public static void ShowUsage() /output - the output file the encrypted blob will be written to /mkfile - the path to the masterkey file to use for encryption /password - the password, or password hash (SHA1 or NTLM) to decrypt the masterkey file + /local - encrypt the data with the local machine context instead of the user (requires SYSTEM DPAPI key) + /sid - provide the SID to use for decrypting the masterkey (by default this is guessed from the path) Note: in most cases, just use *triage* if you're targeting user DPAPI secrets and *machinetriage* if you're going after SYSTEM DPAPI secrets. diff --git a/SharpDPAPI/lib/Dpapi.cs b/SharpDPAPI/lib/Dpapi.cs index 3ca108e..932b012 100755 --- a/SharpDPAPI/lib/Dpapi.cs +++ b/SharpDPAPI/lib/Dpapi.cs @@ -854,11 +854,14 @@ public static Dictionary PVKTriage(Dictionary ar } public static byte[] CreateDPAPIBlob(byte[] plaintext, byte[] masterKey, EncryptionAlgorithm algCrypt, - HashAlgorithm algHash, Guid masterKeyGuid, byte[] entropy = null, string description = "") + HashAlgorithm algHash, Guid masterKeyGuid, byte[] entropy = null, string description = "", bool isLocalMachine = false) { var descBytes = string.IsNullOrEmpty(description) ? new byte[2] : Encoding.Unicode.GetBytes(description); + var flags = 0; + flags |= isLocalMachine ? 0x4 : 0; // CRYPTPROTECT_LOCAL_MACHINE + var saltBytes = GetRandomBytes(32); // Default salt length (TODO: check) var hmacKeyLen = 0; // Default HMAC key length (TODO: check) var hmac2KeyLen = 32; // Default HMAC2 key length (TODO: check) @@ -950,7 +953,6 @@ public static byte[] CreateDPAPIBlob(byte[] plaintext, byte[] masterKey, Encrypt offset += 16; // Flags - var flags = 0; Array.Copy(BitConverter.GetBytes(flags), 0, blobBytes, offset, 4); offset += 4;