diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ebcbf24 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: + push: + pull_request: + +jobs: + build-windows: + runs-on: windows-latest + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup msvc + uses: ilammy/msvc-dev-cmd@v1 + + - name: build x64 release + shell: powershell + run: | + if (Test-Path "library.sln") { + msbuild "library.sln" /m /p:Configuration=Release /p:Platform=x64 + } elseif (Test-Path "*.sln") { + $sln = Get-ChildItem -Filter *.sln | Select-Object -First 1 + msbuild $sln.FullName /m /p:Configuration=Release /p:Platform=x64 + } else { + Write-Error "no solution (.sln) found to build" + } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..23110ef --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased +- Network hardening (client-side only): HTTPS-only transport, redirects disabled, and optional host allowlist + public key pinning to reduce the risk of traffic redirection or man-in-the-middle interception. +- Integrity checks: `.text` integrity and page-protection checks, plus non-executable page checks for `.data` and `.rdata` to help detect tampering (transparent, no stealth behavior). + +## Notes +- These protections are defensive and transparent; they do not alter the backend or API and are intended to reduce common redirection and tampering risks. diff --git a/README.md b/README.md index eb59e5f..aea9940 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,121 @@ x86 : 4- Lib Configuration -> Preprocessor definiton for CURL -> CURL_STATICLIB +## **Using The Library** +This section covers a minimal, working integration with the headers in this repo. + +1. Add the library headers and sources to your project (or build the `.lib` from this repo). +2. Include `auth.hpp` in your project file. +3. Initialize the API once at startup, then call login/license/upgrade as needed. +4. Keep your build settings on C++17 and link with the same libraries as this repo. + +Minimal example: +```cpp +#include "auth.hpp" + +using namespace KeyAuth; + +std::string name = "your_app_name"; +std::string ownerid = "your_owner_id"; +std::string version = "1.0"; +std::string url = "https://keyauth.win/api/1.3/"; +std::string path = ""; // optional + +api KeyAuthApp(name, ownerid, version, url, path); + +int main() { + KeyAuthApp.init(); + if (!KeyAuthApp.response.success) { + return 1; + } + KeyAuthApp.license("your_license_key"); + if (!KeyAuthApp.response.success) { + return 1; + } + return 0; +} +``` + +Notes: +1. If you are using the KeyAuth examples, keep their integrity/session checks intact. +2. Use the same `CURL_STATICLIB` define as shown above when statically linking. +3. Rebuild the library after pulling updates to keep everything in sync. + +## **Security Features (Built-In)** +The library ships with security checks enabled by default. You do not need to manually call anything beyond `init()` and a normal login/license call. + +What runs automatically: +1. **Integrity checks** (prologue snapshots, function region validation, `.text` hashing, page protections). +2. **Module checks** (core module signature verification + RWX section detection). +3. **Hosts-file checks** for API host tampering. +4. **Timing anomaly checks** to detect time tamper. +5. **Session heartbeat** after successful login/license/upgrade/web login. + +## **Security Overview** +This SDK includes lightweight, client-side defenses that raise the cost of common bypass techniques while keeping normal integrations simple. + +What it protects against: +1. **Inline patching/NOPs**: prologue snapshots and detour heuristics catch modified function entry points. +2. **Code tamper**: `.text` hashing and page‑protection checks detect modified code pages. +3. **API redirection**: hosts‑file checks flag local DNS overrides of the API host. +4. **Time spoofing**: timing anomaly checks reduce abuse of expired keys by system clock changes. +5. **Tampered system DLLs**: core module signature checks reject patched or unsigned system libraries. + +Benefits: +1. **Fail‑closed behavior**: when a check fails, requests are blocked before the API call. +2. **Low integration cost**: no additional calls are required beyond `init()` and a normal login/license flow. +3. **Reduced false positives**: checks are limited to core modules and conservative tamper signals. + +Design notes: +1. These are **client‑side** protections. They complement — not replace — server‑side session validation. +2. If you modify or strip checks, you reduce protection. Keep the SDK updated to inherit fixes. +3. Optional hardening ideas are listed below for advanced users who accept higher false‑positive risk. + +How to keep security enabled: +1. Always call `KeyAuthApp.init()` once before any other API call. +2. Do not remove the built-in checks or tamper with the library internals. +3. Keep your application linked against the updated library after pulling changes. + +How to verify it is running: +1. Use the library normally — the checks are automatic. +2. If a check fails, the library will fail closed with an error message. + +## **Optional Hardening Ideas (Not Enabled)** +These are intentionally **not** enabled in the library to avoid false positives, but you can add them if your app needs them. + +1. **PE header erase**: wipe PE header pages after load to make casual dumping harder. This is not a check; it simply reduces dump quality. +2. **Module allowlists**: require a strict set of loaded modules; this breaks overlays and many legitimate plugins. +3. **System module path checks**: enforce System32/SysWOW64-only paths; can fail on custom Windows installs. +4. **Hypervisor detection**: block VMs; useful for niche threat models but unfriendly to legit users. +5. **IAT validation**: detect import-table hooks for any imported API; can false-positive in some environments. + +## **Security Troubleshooting** +If you see security failures, common causes include: +1. **Modified system DLLs**: non‑Microsoft versions or patched DLLs will be rejected. +2. **Time tampering**: manual clock changes or large time skew can trigger timing checks. +3. **Patched binaries**: inline hooks/NOP patches or modified `.text` will fail integrity checks. + +## **Changelog (Overhaul Summary)** +This list summarizes all changes made in the overhaul: +1. **Integrity checks**: prologue snapshots, function region validation, detour detection, `.text` slice hashing, page protections. +2. **Module trust**: Microsoft signature verification for core DLLs, RWX section detection. +3. **Timing checks**: timing anomaly detection to catch clock tamper. +4. **Import checks**: import address validation. +5. **Network hardening**: hosts‑file override detection for API host. +6. **Session hardening**: session heartbeat after successful login/license/upgrade/web login. +7. **DLL search order**: hardened DLL lookup and removed current‑dir hijacking. +8. **String exposure**: request data zeroized after use; sensitive parameters wiped via `ScopeWipe`. +9. **Debug logging**: minimized request/URL logging to reduce in‑memory exposure. +10. **Parsing hardening**: safer JSON parsing and substring handling to avoid crashes. +11. **Curl safety**: fixed cleanup issues; enforced static libcurl linkage. +12. **Module path APIs**: removed hardcoded System32 paths (uses `GetSystemDirectoryW`). +13. **Example/docs**: added usage section, security feature docs, and troubleshooting guidance. + +Helpful references: +- https://github.com/KeyAuth/KeyAuth-CPP-Example +- https://keyauth.cc/app/ +- https://keyauth.cc/app/?page=forms + ## **What is KeyAuth?** KeyAuth is a powerful cloud-based authentication system designed to protect your software from piracy and unauthorized access. With KeyAuth, you can implement secure licensing, user management, and subscription systems in minutes. Client SDKs available for [C#](https://github.com/KeyAuth/KeyAuth-CSHARP-Example), [C++](https://github.com/KeyAuth/KeyAuth-CPP-Example), [Python](https://github.com/KeyAuth/KeyAuth-Python-Example), [Java](https://github.com/KeyAuth-Archive/KeyAuth-JAVA-api), [JavaScript](https://github.com/mazkdevf/KeyAuth-JS-Example), [VB.NET](https://github.com/KeyAuth/KeyAuth-VB-Example), [PHP](https://github.com/KeyAuth/KeyAuth-PHP-Example), [Rust](https://github.com/KeyAuth/KeyAuth-Rust-Example), [Go](https://github.com/mazkdevf/KeyAuth-Go-Example), [Lua](https://github.com/mazkdevf/KeyAuth-Lua-Examples), [Ruby](https://github.com/mazkdevf/KeyAuth-Ruby-Example), and [Perl](https://github.com/mazkdevf/KeyAuth-Perl-Example). KeyAuth has several unique features such as memory streaming, webhook function where you can send requests to API without leaking the API, discord webhook notifications, ban the user securely through the application at your discretion. Feel free to join https://t.me/keyauth if you have questions or suggestions. @@ -46,3 +161,17 @@ of the licensor in the software. Any use of the licensor’s trademarks is subje to applicable law. Thank you for your compliance, we work hard on the development of KeyAuth and do not appreciate our copyright being infringed. + +## Live ban monitor (threaded) + +Optional background check that polls every 45 seconds. Always stop it before exiting. + +```cpp +KeyAuthApp.start_ban_monitor(45, false, [] { + std::cout << "Blacklisted, exiting..." << std::endl; + exit(0); +}); + +// later, before exit +KeyAuthApp.stop_ban_monitor(); +``` diff --git a/auth.cpp b/auth.cpp index 90fd644..07bb1cc 100644 --- a/auth.cpp +++ b/auth.cpp @@ -34,11 +34,25 @@ #pragma comment(lib, "rpcrt4.lib") #pragma comment(lib, "httpapi.lib") +#pragma comment(lib, "psapi.lib") +#pragma comment(lib, "wintrust.lib") #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include #include @@ -72,6 +86,32 @@ void checkFiles(); void checkRegistry(); void error(std::string message); std::string generate_random_number(); +std::string curl_escape(CURL* curl, const std::string& input); +auto check_section_integrity( const char *section_name, bool fix ) -> bool; +void integrity_check(); +std::string extract_host(const std::string& url); +bool hosts_override_present(const std::string& host); +bool module_has_rwx_section(HMODULE mod); +bool core_modules_signed(); +static std::wstring get_system_dir(); +static std::wstring get_syswow_dir(); +void snapshot_prologues(); +bool prologues_ok(); +bool func_region_ok(const void* addr); +bool timing_anomaly_detected(); +void start_heartbeat(KeyAuth::api* instance); +void heartbeat_thread(KeyAuth::api* instance); +void snapshot_text_hashes(); +bool text_hashes_ok(); +bool detour_suspect(const uint8_t* p); +bool import_addresses_ok(); +void snapshot_text_page_protections(); +bool text_page_protections_ok(); +void snapshot_data_page_protections(); +bool data_page_protections_ok(); + +inline void secure_zero(std::string& value) noexcept; +inline void securewipe(std::string& value) noexcept; std::string seed; void cleanUpSeedData(const std::string& seed); std::string signature; @@ -80,10 +120,63 @@ bool initialized; std::string API_PUBLIC_KEY = "5586b4bc69c7a4b487e4563a4cd96afd39140f919bd31cea7d1c6a1e8439422b"; bool KeyAuth::api::debug = false; std::atomic LoggedIn(false); +std::atomic last_integrity_check{ 0 }; +std::atomic integrity_fail_streak{ 0 }; +std::atomic last_module_check{ 0 }; +std::atomic last_periodic_check{ 0 }; +std::atomic prologues_ready{ false }; +std::atomic heartbeat_started{ false }; +std::array pro_verify{}; +std::array pro_checkinit{}; +std::array pro_error{}; +std::array pro_integrity{}; +std::array pro_section{}; +std::atomic text_hashes_ready{ false }; +struct TextHash { size_t offset; size_t len; uint32_t hash; }; +std::vector text_hashes; +std::atomic text_prot_ready{ false }; +std::vector> text_protections; +std::atomic data_prot_ready{ false }; +std::vector> data_protections; +std::atomic heavy_fail_streak{ 0 }; + +static inline void secure_zero(std::string& value) noexcept +{ + if (!value.empty()) { + SecureZeroMemory(value.data(), value.size()); + value.clear(); + value.shrink_to_fit(); + } +} + +static inline void securewipe(std::string& value) noexcept +{ + secure_zero(value); +} + +struct ScopeWipe final { + std::string* value; + explicit ScopeWipe(std::string& v) noexcept : value(&v) {} + ~ScopeWipe() noexcept { securewipe(*value); } +}; void KeyAuth::api::init() { + // harden dll search order to reduce current-dir hijacks + SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32 | LOAD_LIBRARY_SEARCH_USER_DIRS); + SetDllDirectoryW(L""); + { + wchar_t exe_path[MAX_PATH] = {}; + GetModuleFileNameW(nullptr, exe_path, MAX_PATH); + std::wstring exe_dir = exe_path; + const auto last_slash = exe_dir.find_last_of(L"\\/"); + if (last_slash != std::wstring::npos) { + exe_dir = exe_dir.substr(0, last_slash); + AddDllDirectory(exe_dir.c_str()); + } + } std::thread(runChecks).detach(); + snapshot_prologues(); seed = generate_random_number(); std::atexit([]() { cleanUpSeedData(seed); }); CreateThread(0, 0, (LPTHREAD_START_ROUTINE)modify, 0, 0, 0); @@ -100,8 +193,12 @@ void KeyAuth::api::init() XorStr("type=init") + XorStr("&ver=") + version + XorStr("&hash=") + hash + - XorStr("&name=") + curl_easy_escape(curl, name.c_str(), 0) + + XorStr("&name=") + curl_escape(curl, name) + XorStr("&ownerid=") + ownerid; + if (curl) { + curl_easy_cleanup(curl); // avoid leak from escape helper. -nigel + curl = nullptr; + } // to ensure people removed secret from main.cpp (some people will forget to) if (path.find("https") != std::string::npos) { @@ -142,7 +239,7 @@ void KeyAuth::api::init() data += XorStr("&token=").c_str() + token; data += XorStr("&thash=").c_str() + path; } - curl_easy_cleanup(curl); + // curl was only used for escape above auto response = req(data, url); @@ -228,23 +325,27 @@ size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata) size_t totalSize = size * nitems; std::string header(buffer, totalSize); - - // Convert to lowercase for comparison - std::string lowercase = header; - std::transform(lowercase.begin(), lowercase.end(), lowercase.begin(), ::tolower); - - // Signature - if (lowercase.find("x-signature-ed25519: ") == 0) { - signature = header.substr(header.find(": ") + 2); - signature.erase(signature.find_last_not_of("\r\n") + 1); - //std::cout << "[DEBUG] Captured signature header: " << signature << std::endl; + if (header.empty()) + return totalSize; + // trim CRLF + while (!header.empty() && (header.back() == '\r' || header.back() == '\n')) { + header.pop_back(); + } + const auto colon = header.find(':'); + if (colon == std::string::npos) + return totalSize; + std::string key = header.substr(0, colon); + std::string value = header.substr(colon + 1); + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.erase(value.begin()); } + std::transform(key.begin(), key.end(), key.begin(), ::tolower); - // Timestamp - if (lowercase.find("x-signature-timestamp: ") == 0) { - signatureTimestamp = header.substr(header.find(": ") + 2); - signatureTimestamp.erase(signatureTimestamp.find_last_not_of("\r\n") + 1); - //std::cout << "[DEBUG] Captured timestamp header: " << signatureTimestamp << std::endl; + if (key == "x-signature-ed25519") { + signature = value; + } + if (key == "x-signature-timestamp") { + signatureTimestamp = value; } return totalSize; @@ -254,6 +355,9 @@ size_t header_callback(char* buffer, size_t size, size_t nitems, void* userdata) void KeyAuth::api::login(std::string username, std::string password, std::string code) { checkInit(); + ScopeWipe wipe_user(username); + ScopeWipe wipe_pass(password); + ScopeWipe wipe_code(code); std::string hwid = utils::get_hwid(); auto data = @@ -310,6 +414,7 @@ void KeyAuth::api::login(std::string username, std::string password, std::string LI_FN(GlobalAddAtomA)(ownerid.c_str()); LoggedIn.store(true); + start_heartbeat(this); } else { LI_FN(exit)(12); @@ -327,6 +432,7 @@ void KeyAuth::api::login(std::string username, std::string password, std::string void KeyAuth::api::chatget(std::string channel) { checkInit(); + ScopeWipe wipe_channel(channel); auto data = XorStr("type=chatget") + @@ -343,6 +449,8 @@ void KeyAuth::api::chatget(std::string channel) bool KeyAuth::api::chatsend(std::string message, std::string channel) { checkInit(); + ScopeWipe wipe_message(message); + ScopeWipe wipe_channel(channel); auto data = XorStr("type=chatsend") + @@ -355,12 +463,13 @@ bool KeyAuth::api::chatsend(std::string message, std::string channel) auto response = req(data, url); auto json = response_decoder.parse(response); load_response_data(json); - return json[("success")]; + return json[XorStr("success")]; } void KeyAuth::api::changeUsername(std::string newusername) { checkInit(); + ScopeWipe wipe_user(newusername); auto data = XorStr("type=changeUsername") + @@ -417,7 +526,7 @@ KeyAuth::api::Tfa& KeyAuth::api::enable2fa(std::string code) if (json.contains("2fa")) { - api::response.success = json["success"]; + api::response.success = json[XorStr("success")]; api::tfa.secret = json["2fa"]["secret_code"]; api::tfa.link = json["2fa"]["QRCode"]; } @@ -641,9 +750,9 @@ void KeyAuth::api::web_login() HTTP_SET_NULL_ID(&requestId); int bufferSize = 4096; int requestSize = sizeof(HTTP_REQUEST) + bufferSize; - BYTE* buffer = new BYTE[requestSize]; - PHTTP_REQUEST pRequest = (PHTTP_REQUEST)buffer; - RtlZeroMemory(buffer, requestSize); + auto buffer = std::make_unique(requestSize); + PHTTP_REQUEST pRequest = (PHTTP_REQUEST)buffer.get(); + RtlZeroMemory(buffer.get(), requestSize); ULONG bytesReturned; result = HttpReceiveHttpRequest( requestQueueHandle, @@ -706,7 +815,6 @@ void KeyAuth::api::web_login() NULL ); - delete[]buffer; continue; } @@ -759,6 +867,7 @@ void KeyAuth::api::web_login() LI_FN(GlobalAddAtomA)(ownerid.c_str()); LoggedIn.store(true); + start_heartbeat(this); } else { LI_FN(exit)(12); @@ -818,8 +927,6 @@ void KeyAuth::api::web_login() going = false; } - delete[]buffer; - if (!success) LI_FN(exit)(0); } @@ -836,6 +943,7 @@ void KeyAuth::api::web_login() void KeyAuth::api::button(std::string button) { checkInit(); + ScopeWipe wipe_button(button); // from https://perpetualprogrammers.wordpress.com/2016/05/22/the-http-server-api/ @@ -881,9 +989,9 @@ void KeyAuth::api::button(std::string button) HTTP_SET_NULL_ID(&requestId); int bufferSize = 4096; int requestSize = sizeof(HTTP_REQUEST) + bufferSize; - BYTE* buffer = new BYTE[requestSize]; - PHTTP_REQUEST pRequest = (PHTTP_REQUEST)buffer; - RtlZeroMemory(buffer, requestSize); + auto buffer = std::make_unique(requestSize); + PHTTP_REQUEST pRequest = (PHTTP_REQUEST)buffer.get(); + RtlZeroMemory(buffer.get(), requestSize); ULONG bytesReturned; result = HttpReceiveHttpRequest( requestQueueHandle, @@ -946,12 +1054,15 @@ void KeyAuth::api::button(std::string button) NULL ); - delete[]buffer; } } void KeyAuth::api::regstr(std::string username, std::string password, std::string key, std::string email) { checkInit(); + ScopeWipe wipe_user(username); + ScopeWipe wipe_pass(password); + ScopeWipe wipe_key(key); + ScopeWipe wipe_email(email); std::string hwid = utils::get_hwid(); auto data = @@ -1025,6 +1136,8 @@ void KeyAuth::api::regstr(std::string username, std::string password, std::strin void KeyAuth::api::upgrade(std::string username, std::string key) { checkInit(); + ScopeWipe wipe_user(username); + ScopeWipe wipe_key(key); auto data = XorStr("type=upgrade") + @@ -1081,6 +1194,8 @@ std::string generate_random_number() { void KeyAuth::api::license(std::string key, std::string code) { checkInit(); + ScopeWipe wipe_key(key); + ScopeWipe wipe_code(code); std::string hwid = utils::get_hwid(); auto data = @@ -1150,6 +1265,8 @@ void KeyAuth::api::license(std::string key, std::string code) { void KeyAuth::api::setvar(std::string var, std::string vardata) { checkInit(); + ScopeWipe wipe_var(var); + ScopeWipe wipe_data(vardata); auto data = XorStr("type=setvar") + @@ -1165,6 +1282,7 @@ void KeyAuth::api::setvar(std::string var, std::string vardata) { std::string KeyAuth::api::getvar(std::string var) { checkInit(); + ScopeWipe wipe_var(var); auto data = XorStr("type=getvar") + @@ -1205,6 +1323,7 @@ std::string KeyAuth::api::getvar(std::string var) { void KeyAuth::api::ban(std::string reason) { checkInit(); + ScopeWipe wipe_reason(reason); auto data = XorStr("type=ban") + @@ -1272,7 +1391,7 @@ bool KeyAuth::api::checkblack() { size_t resultCode = hasher(json[(XorStr("code"))]); if (!json[(XorStr("success"))] || (json[(XorStr("success"))] && (resultCode == expectedHash))) { - return json[("success")]; + return json[XorStr("success")]; } LI_FN(exit)(9); } @@ -1367,6 +1486,7 @@ std::string KeyAuth::api::var(std::string varid) { void KeyAuth::api::log(std::string message) { checkInit(); + ScopeWipe wipe_message(message); char acUserName[100]; DWORD nUserName = sizeof(acUserName); @@ -1386,6 +1506,7 @@ void KeyAuth::api::log(std::string message) { std::vector KeyAuth::api::download(std::string fileid) { checkInit(); + ScopeWipe wipe_fileid(fileid); auto to_uc_vector = [](std::string value) { return std::vector(value.data(), value.data() + value.length() ); @@ -1397,7 +1518,7 @@ std::vector KeyAuth::api::download(std::string fileid) { XorStr("&fileid=") + fileid + XorStr("&sessionid=") + sessionid + XorStr("&name=") + name + - XorStr("&ownerid=").c_str() + ownerid; + XorStr("&ownerid=") + ownerid; auto response = req(data, url); auto json = response_decoder.parse(response); @@ -1416,13 +1537,17 @@ std::vector KeyAuth::api::download(std::string fileid) { std::string KeyAuth::api::webhook(std::string id, std::string params, std::string body, std::string contenttype) { checkInit(); + ScopeWipe wipe_id(id); + ScopeWipe wipe_params(params); + ScopeWipe wipe_body(body); + ScopeWipe wipe_type(contenttype); CURL *curl = curl_easy_init(); auto data = XorStr("type=webhook") + XorStr("&webid=") + id + - XorStr("¶ms=") + curl_easy_escape(curl, params.c_str(), 0) + - XorStr("&body=") + curl_easy_escape(curl, body.c_str(), 0) + + XorStr("¶ms=") + curl_escape(curl, params) + + XorStr("&body=") + curl_escape(curl, body) + XorStr("&conttype=") + contenttype + XorStr("&sessionid=") + sessionid + XorStr("&name=") + name + @@ -1555,6 +1680,8 @@ void KeyAuth::api::fetchstats() void KeyAuth::api::forgot(std::string username, std::string email) { checkInit(); + ScopeWipe wipe_user(username); + ScopeWipe wipe_email(email); auto data = XorStr("type=forgot") + @@ -1599,18 +1726,168 @@ void KeyAuth::api::logout() { load_response_data(json); } +void KeyAuth::api::start_ban_monitor(int interval_seconds, bool check_session, std::function on_ban) +{ + if (ban_monitor_running_) { + return; + } + + if (interval_seconds < 1) { + interval_seconds = 1; + } + + ban_monitor_detected_ = false; + ban_monitor_running_ = true; + ban_monitor_thread_ = std::thread([this, interval_seconds, check_session, on_ban]() { + while (ban_monitor_running_) { + if (check_session) { + this->check(false); + } + + if (this->checkblack()) { + ban_monitor_detected_ = true; + ban_monitor_running_ = false; + if (on_ban) { + on_ban(); + } + return; + } + + std::this_thread::sleep_for(std::chrono::seconds(interval_seconds)); + } + }); +} + +void KeyAuth::api::stop_ban_monitor() +{ + ban_monitor_running_ = false; + if (ban_monitor_thread_.joinable()) { + ban_monitor_thread_.join(); + } +} + +bool KeyAuth::api::ban_monitor_running() const +{ + return ban_monitor_running_.load(); +} + +bool KeyAuth::api::ban_monitor_detected() const +{ + return ban_monitor_detected_.load(); +} + +std::string KeyAuth::api::expiry_remaining(const std::string& expiry) +{ + if (expiry.empty()) + return "unknown"; + long long exp = 0; + try { + exp = std::stoll(expiry); + } + catch (...) { + return "unknown"; + } + const long long now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + long long diff = exp - now; + if (diff <= 0) + return "expired"; + + const long long days = diff / 86400; + const long long weeks = days / 7; + const long long months = days / 30; + const long long hours = (diff % 86400) / 3600; + const long long minutes = (diff % 3600) / 60; + + std::time_t tt = static_cast(exp); + std::tm tm{}; +#ifdef _WIN32 + localtime_s(&tm, &tt); +#else + tm = *std::localtime(&tt); +#endif + char buf[32] = {}; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + + std::ostringstream out; + out << days << "d " << hours << "h " << minutes << "m remaining" + << " (expires " << buf << ", ~" << weeks << "w / " << months << "mo)"; + return out.str(); +} + +void KeyAuth::api::init_fail_delay() +{ + Sleep(kInitFailSleepMs); +} + +void KeyAuth::api::bad_input_delay() +{ + Sleep(kBadInputSleepMs); +} + +void KeyAuth::api::close_delay() +{ + Sleep(kCloseSleepMs); +} + +bool KeyAuth::api::lockout_active(const lockout_state& state) +{ + return std::chrono::steady_clock::now() < state.locked_until; +} + +int KeyAuth::api::lockout_remaining_ms(const lockout_state& state) +{ + if (!lockout_active(state)) + return 0; + const auto now = std::chrono::steady_clock::now(); + const auto remaining = std::chrono::duration_cast(state.locked_until - now).count(); + return remaining > 0 ? static_cast(remaining) : 0; +} + +void KeyAuth::api::record_login_fail(lockout_state& state, int max_attempts, int lock_seconds) +{ + if (max_attempts < 1) + max_attempts = 1; + if (lock_seconds < 1) + lock_seconds = 1; + state.fails += 1; + if (state.fails >= max_attempts) { + state.fails = 0; + state.locked_until = std::chrono::steady_clock::now() + std::chrono::seconds(lock_seconds); + } +} + +void KeyAuth::api::reset_lockout(lockout_state& state) +{ + state.fails = 0; + state.locked_until = std::chrono::steady_clock::time_point{}; +} + int VerifyPayload(std::string signature, std::string timestamp, std::string body) { - long long unix_timestamp = std::stoll(timestamp); + if (!prologues_ok()) { + error(XorStr("function prologue check failed, possible inline hook detected.")); + } + integrity_check(); + long long unix_timestamp = 0; + try { + unix_timestamp = std::stoll(timestamp); + } + catch (...) { + std::cerr << "[ERROR] Invalid timestamp format\n"; + MessageBoxA(0, "Signature verification failed (invalid timestamp)", "KeyAuth", MB_ICONERROR); + exit(2); + } auto current_time = std::chrono::system_clock::now(); long long current_unix_time = std::chrono::duration_cast( current_time.time_since_epoch()).count(); - if (current_unix_time - unix_timestamp > 20) { - std::cerr << "[ERROR] Timestamp too old (diff = " - << (current_unix_time - unix_timestamp) << "s)\n"; - MessageBoxA(0, "Signature verification failed (timestamp too old)", "KeyAuth", MB_ICONERROR); + const long long diff = std::llabs(current_unix_time - unix_timestamp); + if (diff > 120) { + std::cerr << "[ERROR] Timestamp too skewed (diff = " + << diff << "s)\n"; + MessageBoxA(0, "Signature verification failed (timestamp skew)", "KeyAuth", MB_ICONERROR); exit(3); } @@ -1678,21 +1955,753 @@ std::string get_str_between_two_str(const std::string& s, const std::string& start_delim, const std::string& stop_delim) { - unsigned first_delim_pos = s.find(start_delim); - unsigned end_pos_of_first_delim = first_delim_pos + start_delim.length(); - unsigned last_delim_pos = s.find(stop_delim); + const auto first_delim_pos = s.find(start_delim); + if (first_delim_pos == std::string::npos) + return {}; + const auto end_pos_of_first_delim = first_delim_pos + start_delim.length(); + const auto last_delim_pos = s.find(stop_delim, end_pos_of_first_delim); + if (last_delim_pos == std::string::npos || last_delim_pos < end_pos_of_first_delim) + return {}; return s.substr(end_pos_of_first_delim, last_delim_pos - end_pos_of_first_delim); } +std::string curl_escape(CURL* curl, const std::string& input) +{ + if (!curl) + return input; + char* escaped = curl_easy_escape(curl, input.c_str(), 0); + if (!escaped) + return {}; + std::string out(escaped); + curl_free(escaped); + return out; +} + +std::string extract_host(const std::string& url) +{ + std::string host = url; + const auto scheme_pos = host.find("://"); + if (scheme_pos != std::string::npos) + host = host.substr(scheme_pos + 3); + const auto slash_pos = host.find('/'); + if (slash_pos != std::string::npos) + host = host.substr(0, slash_pos); + const auto colon_pos = host.find(':'); + if (colon_pos != std::string::npos) + host = host.substr(0, colon_pos); + return host; +} + +static bool is_ip_literal(const std::string& host) +{ + sockaddr_in sa4{}; + sockaddr_in6 sa6{}; + return inet_pton(AF_INET, host.c_str(), &sa4.sin_addr) == 1 || + inet_pton(AF_INET6, host.c_str(), &sa6.sin6_addr) == 1; +} + +static bool is_private_or_loopback_ipv4(uint32_t addr_net_order) +{ + const uint32_t a = ntohl(addr_net_order); + const uint8_t b1 = static_cast(a >> 24); + const uint8_t b2 = static_cast((a >> 16) & 0xFF); + if (b1 == 10) return true; + if (b1 == 127) return true; + if (b1 == 0) return true; + if (b1 == 169 && b2 == 254) return true; + if (b1 == 192 && b2 == 168) return true; + if (b1 == 172) { + const uint8_t b3 = static_cast((a >> 8) & 0xFF); + if (b3 >= 16 && b3 <= 31) return true; + } + return false; +} + +static bool is_loopback_ipv6(const in6_addr& addr) +{ + static const in6_addr loopback = IN6ADDR_LOOPBACK_INIT; + return std::memcmp(&addr, &loopback, sizeof(loopback)) == 0; +} + +static bool host_is_keyauth(const std::string& host_lower) +{ + if (host_lower == "keyauth.win" || host_lower == "keyauth.cc" || host_lower == "api-worker.keyauth.win") + return true; + const std::string suffix = ".keyauth.win"; + if (host_lower.size() > suffix.size() && + host_lower.compare(host_lower.size() - suffix.size(), suffix.size(), suffix) == 0) + return true; + return false; +} + +static bool is_https_url(const std::string& url) +{ + const std::string prefix = "https://"; + if (url.size() < prefix.size()) + return false; + for (size_t i = 0; i < prefix.size(); ++i) { + const char c = static_cast(std::tolower(static_cast(url[i]))); + if (c != prefix[i]) + return false; + } + return true; +} + +static bool proxy_env_set() +{ + const char* keys[] = { "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy" }; + for (const char* k : keys) { + const char* v = std::getenv(k); + if (v && *v) + return true; + } + return false; +} + +static bool winhttp_proxy_set() +{ + WINHTTP_PROXY_INFO info{}; + if (!WinHttpGetDefaultProxyConfiguration(&info)) + return false; + bool set = false; + if (info.lpszProxy && *info.lpszProxy) + set = true; + if (info.lpszProxyBypass && *info.lpszProxyBypass) + set = true; + if (info.lpszProxy) GlobalFree(info.lpszProxy); + if (info.lpszProxyBypass) GlobalFree(info.lpszProxyBypass); + return set; +} + +static bool host_resolves_private_only(const std::string& host, bool& has_public) +{ + has_public = false; + addrinfo hints{}; + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; + addrinfo* res = nullptr; + if (getaddrinfo(host.c_str(), nullptr, &hints, &res) != 0) + return false; + bool any = false; + bool all_private = true; + for (addrinfo* p = res; p; p = p->ai_next) { + if (!p->ai_addr) + continue; + any = true; + if (p->ai_family == AF_INET) { + const auto* sa = reinterpret_cast(p->ai_addr); + if (!is_private_or_loopback_ipv4(sa->sin_addr.s_addr)) { + all_private = false; + has_public = true; + } + } else if (p->ai_family == AF_INET6) { + const auto* sa = reinterpret_cast(p->ai_addr); + if (!is_loopback_ipv6(sa->sin6_addr)) { + all_private = false; + has_public = true; + } + } + } + freeaddrinfo(res); + if (!any) + return false; + return all_private; +} + +bool hosts_override_present(const std::string& host) +{ + if (host.empty()) + return false; + std::string host_lower = host; + std::transform(host_lower.begin(), host_lower.end(), host_lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + const char* sysroot = std::getenv("SystemRoot"); + std::string hosts_path = sysroot ? std::string(sysroot) : "C:\\Windows"; + hosts_path += "\\System32\\drivers\\etc\\hosts"; + std::ifstream file(hosts_path); + if (!file.good()) + return false; + std::string line; + while (std::getline(file, line)) { + auto hash_pos = line.find('#'); + if (hash_pos != std::string::npos) + line = line.substr(0, hash_pos); + std::transform(line.begin(), line.end(), line.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (line.find(host_lower) == std::string::npos) + continue; + // basic whole-word check + if (line.find(" " + host_lower) != std::string::npos || line.find("\t" + host_lower) != std::string::npos) + return true; + } + return false; +} + +static std::wstring to_lower_ws(std::wstring value) +{ + std::transform(value.begin(), value.end(), value.begin(), + [](wchar_t c) { return static_cast(towlower(c)); }); + return value; +} + +static bool path_has_any(const std::wstring& p, const std::initializer_list& needles) +{ + for (const auto& n : needles) { + if (p.find(n) != std::wstring::npos) + return true; + } + return false; +} + +bool module_has_rwx_section(HMODULE mod) +{ + if (!mod) + return false; + auto base = reinterpret_cast(mod); + auto dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) + return false; + auto nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) + return false; + auto section = IMAGE_FIRST_SECTION(nt); + for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) { + const auto ch = section->Characteristics; + if ((ch & IMAGE_SCN_MEM_EXECUTE) && (ch & IMAGE_SCN_MEM_WRITE)) + return true; + } + return false; +} + +static bool verify_signature(const std::wstring& path) +{ + WINTRUST_FILE_INFO file_info{}; + file_info.cbStruct = sizeof(file_info); + file_info.pcwszFilePath = path.c_str(); + + WINTRUST_DATA trust_data{}; + trust_data.cbStruct = sizeof(trust_data); + trust_data.dwUIChoice = WTD_UI_NONE; + trust_data.fdwRevocationChecks = WTD_REVOKE_NONE; + trust_data.dwUnionChoice = WTD_CHOICE_FILE; + trust_data.pFile = &file_info; + trust_data.dwStateAction = WTD_STATEACTION_IGNORE; + + GUID policy = WINTRUST_ACTION_GENERIC_VERIFY_V2; + LONG status = WinVerifyTrust(nullptr, &policy, &trust_data); + return status == ERROR_SUCCESS; +} + +bool core_modules_signed() +{ + // verify core dll signatures and reject rwx sections -nigel + const wchar_t* kModules[] = { L"ntdll.dll", L"kernel32.dll", L"kernelbase.dll", L"user32.dll" }; + for (const auto* name : kModules) { + HMODULE mod = GetModuleHandleW(name); + if (!mod) + return false; + wchar_t path[MAX_PATH] = {}; + if (!GetModuleFileNameW(mod, path, MAX_PATH)) + return false; + if (!verify_signature(path)) + return false; + if (module_has_rwx_section(mod)) + return false; + } + return true; +} + +static bool reg_key_exists(HKEY root, const wchar_t* path) +{ + HKEY h = nullptr; + const LONG res = RegOpenKeyExW(root, path, 0, KEY_READ, &h); + if (res == ERROR_SUCCESS) { + RegCloseKey(h); + return true; + } + return false; +} + +static bool file_exists(const std::wstring& path) +{ + const DWORD attr = GetFileAttributesW(path.c_str()); + return (attr != INVALID_FILE_ATTRIBUTES) && !(attr & FILE_ATTRIBUTE_DIRECTORY); +} + +static std::wstring get_system_dir() +{ + wchar_t buf[MAX_PATH] = {}; + if (GetSystemDirectoryW(buf, MAX_PATH) == 0) + return L""; + return std::wstring(buf); +} + +static std::wstring get_syswow_dir() +{ + wchar_t buf[MAX_PATH] = {}; + if (GetSystemWow64DirectoryW(buf, MAX_PATH) == 0) + return L""; + return std::wstring(buf); +} + + +void snapshot_prologues() +{ + if (prologues_ready.load()) + return; + const auto verify_ptr = reinterpret_cast(reinterpret_cast(&VerifyPayload)); + const auto check_ptr = reinterpret_cast(reinterpret_cast(&checkInit)); + const auto error_ptr = reinterpret_cast(reinterpret_cast(&error)); + const auto integ_ptr = reinterpret_cast(reinterpret_cast(&integrity_check)); + const auto section_ptr = reinterpret_cast(reinterpret_cast(&check_section_integrity)); + std::memcpy(pro_verify.data(), verify_ptr, pro_verify.size()); + std::memcpy(pro_checkinit.data(), check_ptr, pro_checkinit.size()); + std::memcpy(pro_error.data(), error_ptr, pro_error.size()); + std::memcpy(pro_integrity.data(), integ_ptr, pro_integrity.size()); + std::memcpy(pro_section.data(), section_ptr, pro_section.size()); + prologues_ready.store(true); + snapshot_text_hashes(); + snapshot_text_page_protections(); + snapshot_data_page_protections(); +} + +bool prologues_ok() +{ + if (!prologues_ready.load()) + return true; + const auto verify_ptr = reinterpret_cast(reinterpret_cast(&VerifyPayload)); + const auto check_ptr = reinterpret_cast(reinterpret_cast(&checkInit)); + const auto error_ptr = reinterpret_cast(reinterpret_cast(&error)); + const auto integ_ptr = reinterpret_cast(reinterpret_cast(&integrity_check)); + const auto section_ptr = reinterpret_cast(reinterpret_cast(&check_section_integrity)); + return std::memcmp(pro_verify.data(), verify_ptr, pro_verify.size()) == 0 && + std::memcmp(pro_checkinit.data(), check_ptr, pro_checkinit.size()) == 0 && + std::memcmp(pro_error.data(), error_ptr, pro_error.size()) == 0 && + std::memcmp(pro_integrity.data(), integ_ptr, pro_integrity.size()) == 0 && + std::memcmp(pro_section.data(), section_ptr, pro_section.size()) == 0; +} + +bool func_region_ok(const void* addr) +{ + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(addr, &mbi, sizeof(mbi)) == 0) + return false; + if (mbi.Type != MEM_IMAGE) + return false; + const DWORD prot = mbi.Protect; + const bool exec = (prot & PAGE_EXECUTE) || (prot & PAGE_EXECUTE_READ) || (prot & PAGE_EXECUTE_READWRITE) || (prot & PAGE_EXECUTE_WRITECOPY); + const bool write = (prot & PAGE_READWRITE) || (prot & PAGE_EXECUTE_READWRITE) || (prot & PAGE_WRITECOPY) || (prot & PAGE_EXECUTE_WRITECOPY); + if (!exec || write) + return false; + return true; +} + +bool timing_anomaly_detected() +{ + const auto wall_now = std::chrono::system_clock::now(); + const auto steady_now = std::chrono::steady_clock::now(); + static auto wall_last = wall_now; + static auto steady_last = steady_now; + static ULONGLONG tick_last = GetTickCount64(); + static long long wall_last_sec = std::chrono::duration_cast( + wall_now.time_since_epoch()).count(); + const auto wall_delta = std::chrono::duration_cast(wall_now - wall_last).count(); + const auto steady_delta = std::chrono::duration_cast(steady_now - steady_last).count(); + wall_last = wall_now; + steady_last = steady_now; + const ULONGLONG tick_now = GetTickCount64(); + const long long tick_delta = static_cast((tick_now - tick_last) / 1000ULL); + tick_last = tick_now; + const long long wall_now_sec = std::chrono::duration_cast( + wall_now.time_since_epoch()).count(); + const long long wall_tick_delta = wall_now_sec - wall_last_sec; + wall_last_sec = wall_now_sec; + if (wall_delta < -60 || wall_delta > 300) + return true; + if (std::llabs(wall_delta - steady_delta) > 120) + return true; + if (std::llabs(wall_tick_delta - tick_delta) > 120) + return true; + return false; +} + +static bool get_text_section_info(std::uintptr_t& base, size_t& size) +{ + const auto hmodule = GetModuleHandle(nullptr); + if (!hmodule) return false; + const auto base_0 = reinterpret_cast(hmodule); + const auto dos = reinterpret_cast(base_0); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; + const auto nt = reinterpret_cast(base_0 + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + auto section = IMAGE_FIRST_SECTION(nt); + for (auto i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) { + if (std::memcmp(section->Name, ".text", 5) == 0) { + base = base_0 + section->VirtualAddress; + size = section->Misc.VirtualSize; + return true; + } + } + return false; +} + +static bool get_data_section_info(std::uintptr_t& base, size_t& size) +{ + const auto hmodule = GetModuleHandle(nullptr); + if (!hmodule) return false; + const auto base_0 = reinterpret_cast(hmodule); + const auto dos = reinterpret_cast(base_0); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; + const auto nt = reinterpret_cast(base_0 + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + auto section = IMAGE_FIRST_SECTION(nt); + for (auto i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) { + if (std::memcmp(section->Name, ".data", 5) == 0) { + base = base_0 + section->VirtualAddress; + size = section->Misc.VirtualSize; + return true; + } + } + return false; +} + +static bool get_rdata_section_info(std::uintptr_t& base, size_t& size) +{ + const auto hmodule = GetModuleHandle(nullptr); + if (!hmodule) return false; + const auto base_0 = reinterpret_cast(hmodule); + const auto dos = reinterpret_cast(base_0); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; + const auto nt = reinterpret_cast(base_0 + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + auto section = IMAGE_FIRST_SECTION(nt); + for (auto i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) { + if (std::memcmp(section->Name, ".rdata", 6) == 0) { + base = base_0 + section->VirtualAddress; + size = section->Misc.VirtualSize; + return true; + } + } + return false; +} + +static uint32_t fnv1a(const uint8_t* data, size_t len) +{ + uint32_t hash = 2166136261u; + for (size_t i = 0; i < len; ++i) { + hash ^= data[i]; + hash *= 16777619u; + } + return hash; +} + +void snapshot_text_hashes() +{ + if (text_hashes_ready.load()) + return; + std::uintptr_t base = 0; + size_t size = 0; + if (!get_text_section_info(base, size) || size < 256) + return; + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution dist(0, size - 64); + text_hashes.clear(); + for (int i = 0; i < 8; ++i) { + const size_t offset = dist(gen); + const uint8_t* ptr = reinterpret_cast(base + offset); + text_hashes.push_back({ offset, 64, fnv1a(ptr, 64) }); + } + text_hashes_ready.store(true); +} + +bool text_hashes_ok() +{ + if (!text_hashes_ready.load()) + return true; + std::uintptr_t base = 0; + size_t size = 0; + if (!get_text_section_info(base, size)) + return true; + for (const auto& h : text_hashes) { + if (h.offset + h.len > size) + return false; + const uint8_t* ptr = reinterpret_cast(base + h.offset); + if (fnv1a(ptr, h.len) != h.hash) + return false; + } + return true; +} + +void snapshot_text_page_protections() +{ + if (text_prot_ready.load()) + return; + std::uintptr_t base = 0; + size_t size = 0; + if (!get_text_section_info(base, size)) + return; + text_protections.clear(); + const size_t page = 0x1000; + for (size_t off = 0; off < size; off += page) { + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(reinterpret_cast(base + off), &mbi, sizeof(mbi)) == 0) + continue; + text_protections.emplace_back(reinterpret_cast(mbi.BaseAddress), mbi.Protect); + } + text_prot_ready.store(true); +} + +void snapshot_data_page_protections() +{ + if (data_prot_ready.load()) + return; + data_protections.clear(); + const size_t page = 0x1000; + + std::uintptr_t base = 0; + size_t size = 0; + if (get_data_section_info(base, size)) { + for (size_t off = 0; off < size; off += page) { + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(reinterpret_cast(base + off), &mbi, sizeof(mbi)) == 0) + continue; + data_protections.emplace_back(reinterpret_cast(mbi.BaseAddress), mbi.Protect); + } + } + + base = 0; + size = 0; + if (get_rdata_section_info(base, size)) { + for (size_t off = 0; off < size; off += page) { + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(reinterpret_cast(base + off), &mbi, sizeof(mbi)) == 0) + continue; + data_protections.emplace_back(reinterpret_cast(mbi.BaseAddress), mbi.Protect); + } + } + + data_prot_ready.store(true); +} + +bool text_page_protections_ok() +{ + if (!text_prot_ready.load()) + return true; + for (const auto& entry : text_protections) { + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(reinterpret_cast(entry.first), &mbi, sizeof(mbi)) == 0) + return false; + const DWORD prot = mbi.Protect; + if (prot != entry.second) + return false; + const bool exec = (prot & PAGE_EXECUTE) || (prot & PAGE_EXECUTE_READ) || (prot & PAGE_EXECUTE_READWRITE) || (prot & PAGE_EXECUTE_WRITECOPY); + const bool write = (prot & PAGE_READWRITE) || (prot & PAGE_EXECUTE_READWRITE) || (prot & PAGE_WRITECOPY) || (prot & PAGE_EXECUTE_WRITECOPY); + if (!exec || write) + return false; + } + return true; +} + +bool data_page_protections_ok() +{ + if (!data_prot_ready.load()) + return true; + for (const auto& entry : data_protections) { + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(reinterpret_cast(entry.first), &mbi, sizeof(mbi)) == 0) + return false; + const DWORD prot = mbi.Protect; + if (prot != entry.second) + return false; + const bool exec = (prot & PAGE_EXECUTE) || (prot & PAGE_EXECUTE_READ) || (prot & PAGE_EXECUTE_READWRITE) || (prot & PAGE_EXECUTE_WRITECOPY); + if (exec) + return false; + } + return true; +} + + +bool detour_suspect(const uint8_t* p) +{ + if (!p) + return true; + // jmp rel32 / call rel32 / jmp rel8 + if (p[0] == 0xE9 || p[0] == 0xE8 || p[0] == 0xEB) + return true; + // jmp/call [rip+imm32] + if (p[0] == 0xFF && (p[1] == 0x25 || p[1] == 0x15)) + return true; + // mov rax, imm64; jmp rax + if (p[0] == 0x48 && p[1] == 0xB8 && p[10] == 0xFF && p[11] == 0xE0) + return true; + return false; +} + +static bool addr_in_module(const void* addr, const wchar_t* module_name) +{ + HMODULE mod = module_name ? GetModuleHandleW(module_name) : GetModuleHandle(nullptr); + if (!mod) + return false; + MODULEINFO mi{}; + if (!GetModuleInformation(GetCurrentProcess(), mod, &mi, sizeof(mi))) + return false; + const auto base = reinterpret_cast(mi.lpBaseOfDll); + const auto end = base + mi.SizeOfImage; + return addr >= base && addr < end; +} + +bool import_addresses_ok() +{ + // wintrust functions should resolve inside wintrust.dll when loaded + if (GetModuleHandleW(L"wintrust.dll")) { + if (!addr_in_module(reinterpret_cast(&WinVerifyTrust), L"wintrust.dll")) + return false; + } + // VirtualQuery should be inside kernelbase/kernel32 when loaded + if (GetModuleHandleW(L"kernelbase.dll") || GetModuleHandleW(L"kernel32.dll")) { + if (!addr_in_module(reinterpret_cast(&VirtualQuery), L"kernelbase.dll") && + !addr_in_module(reinterpret_cast(&VirtualQuery), L"kernel32.dll")) + return false; + } + // curl functions must live in main module (static) + if (!addr_in_module(reinterpret_cast(&curl_easy_perform), nullptr)) + return false; + return true; +} + +static bool iat_get_import_address(HMODULE module, const char* import_name, void*& out_addr, bool& found) +{ + if (!module) + return true; + auto base = reinterpret_cast(module); + auto dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) + return true; + auto nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) + return true; + const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if (!dir.VirtualAddress) + return true; + auto desc = reinterpret_cast(base + dir.VirtualAddress); + for (; desc->Name; ++desc) { + const char* dll = reinterpret_cast(base + desc->Name); + if (_stricmp(dll, "KERNEL32.DLL") != 0 && _stricmp(dll, "KERNELBASE.DLL") != 0) + continue; + auto thunk = reinterpret_cast(base + desc->FirstThunk); + auto orig = desc->OriginalFirstThunk + ? reinterpret_cast(base + desc->OriginalFirstThunk) + : thunk; + for (; orig->u1.AddressOfData; ++orig, ++thunk) { + if (orig->u1.Ordinal & IMAGE_ORDINAL_FLAG) + continue; + auto import = reinterpret_cast(base + orig->u1.AddressOfData); + if (strcmp(reinterpret_cast(import->Name), import_name) == 0) { + found = true; + out_addr = reinterpret_cast(thunk->u1.Function); + return true; + } + } + } + return true; +} + +void heartbeat_thread(KeyAuth::api* instance) +{ + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution sleep_seconds(45, 90); + while (true) { + Sleep(static_cast(sleep_seconds(gen) * 1000)); + if (!LoggedIn.load()) + continue; + instance->check(false); + if (!instance->response.success) { + error(XorStr("session check failed.")); + } + } +} + +void start_heartbeat(KeyAuth::api* instance) +{ + if (heartbeat_started.exchange(true)) + return; + std::thread(heartbeat_thread, instance).detach(); +} + void KeyAuth::api::setDebug(bool value) { KeyAuth::api::debug = value; } -std::string KeyAuth::api::req(const std::string& data, const std::string& url) { +std::string KeyAuth::api::req(std::string data, const std::string& url) { signature.clear(); signatureTimestamp.clear(); + // gate requests on integrity checks to reduce bypasses -nigel + integrity_check(); + // usage: keep this in req() so every api call is protected -nigel + if (!prologues_ok()) { + error(XorStr("function prologue check failed, possible inline hook detected.")); + } + if (!func_region_ok(reinterpret_cast(&VerifyPayload)) || + !func_region_ok(reinterpret_cast(&checkInit)) || + !func_region_ok(reinterpret_cast(&error)) || + !func_region_ok(reinterpret_cast(&integrity_check)) || + !func_region_ok(reinterpret_cast(&check_section_integrity))) { + error(XorStr("function region check failed, possible hook detected.")); + } + if (!is_https_url(url)) { + error(XorStr("API URL must use HTTPS.")); + } + std::string host = extract_host(url); + ScopeWipe host_wipe(host); + // block hosts-file redirects for api host -nigel + if (hosts_override_present(host)) { + error(XorStr("Hosts file override detected for API host.")); + } + // block loopback/private redirects for keyauth domains -nigel + { + std::string host_lower = host; + ScopeWipe host_lower_wipe(host_lower); + std::transform(host_lower.begin(), host_lower.end(), host_lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (!allowed_hosts.empty()) { + bool allowed = false; + for (const auto& entry : allowed_hosts) { + std::string entry_lower = entry; + std::transform(entry_lower.begin(), entry_lower.end(), entry_lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (entry_lower.rfind("*.", 0) == 0) { + auto suffix = entry_lower.substr(1); + if (host_lower.size() >= suffix.size() && + host_lower.compare(host_lower.size() - suffix.size(), suffix.size(), suffix) == 0) { + allowed = true; + break; + } + } else if (host_lower == entry_lower) { + allowed = true; + break; + } + } + if (!allowed) { + error(XorStr("API host is not in allowed host list.")); + } + } + if (host_is_keyauth(host_lower)) { + if (is_ip_literal(host_lower)) { + error(XorStr("API host must not be an IP literal.")); + } + if (proxy_env_set() || winhttp_proxy_set()) { + error(XorStr("Proxy settings detected for API host.")); + } + bool has_public = false; + if (host_resolves_private_only(host_lower, has_public) && !has_public) { + error(XorStr("API host resolves to private or loopback.")); + } + } + } CURL* curl = curl_easy_init(); if (!curl) { @@ -1701,11 +2710,16 @@ std::string KeyAuth::api::req(const std::string& data, const std::string& url) { std::string to_return; std::string headers; + struct curl_slist* req_headers = nullptr; // Set CURL options curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 0L); + curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); + curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS); curl_easy_setopt(curl, CURLOPT_CERTINFO, 1L); curl_easy_setopt(curl, CURLOPT_NOPROXY, XorStr("keyauth.win").c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); @@ -1713,21 +2727,42 @@ std::string KeyAuth::api::req(const std::string& data, const std::string& url) { curl_easy_setopt(curl, CURLOPT_WRITEDATA, &to_return); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_headers); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "KeyAuth"); + + if (!pinned_public_keys.empty()) { +#ifdef CURLOPT_PINNEDPUBLICKEY + if (pinned_public_keys.size() > 1) { + error(XorStr("Multiple pinned public keys not supported.")); + } + curl_easy_setopt(curl, CURLOPT_PINNEDPUBLICKEY, pinned_public_keys.at(0).c_str()); +#else + error(XorStr("Pinned public key not supported by this libcurl build.")); +#endif + } // Perform the request CURLcode code = curl_easy_perform(curl); if (code != CURLE_OK) { std::string errorMsg = "CURL Error: " + std::string(curl_easy_strerror(code)); - curl_easy_cleanup(curl); + if (req_headers) curl_slist_free_all(req_headers); + curl_easy_cleanup(curl); error(errorMsg); } - debugInfo(data, url, to_return, "Sig: " + signature + "\nTimestamp:" + signatureTimestamp); - curl_easy_cleanup(curl); + if (KeyAuth::api::debug) { + debugInfo("n/a", "n/a", to_return, "n/a"); + } + if (req_headers) curl_slist_free_all(req_headers); + curl_easy_cleanup(curl); + secure_zero(data); return to_return; } void error(std::string message) { + for (char& c : message) { + if (c == '&' || c == '|' || c == '\"') c = ' '; // minimize cmd injection surface. -nigel + } system((XorStr("start cmd /C \"color b && title Error && echo ").c_str() + message + XorStr(" && timeout /t 5\"")).c_str()); LI_FN(__fastfail)(0); } @@ -1970,7 +3005,10 @@ void KeyAuth::api::debugInfo(std::string data, std::string url, std::string resp std::istream_iterator()); for (auto const& value : results) { - datas[value.substr(0, value.find('='))] = value.substr(value.find('=') + 1); + const auto pos = value.find('='); + if (pos == std::string::npos) + continue; + datas[value.substr(0, pos)] = value.substr(pos + 1); } RedactField(datas, "sessionid"); @@ -2059,7 +3097,83 @@ void checkInit() { if (!initialized) { error(XorStr("You need to run the KeyAuthApp.init(); function before any other KeyAuth functions")); } + // usage: call init() once at startup; checks run automatically after that -nigel + const auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + const auto last_mod = last_module_check.load(); + if (now - last_mod > 60) { + last_module_check.store(now); + // core module trust check to detect tampered system dlls -nigel + if (!core_modules_signed()) { + error(XorStr("module path check failed, possible side-load detected.")); + } + } + const auto last_periodic = last_periodic_check.load(); + if (now - last_periodic > 30) { + last_periodic_check.store(now); + // detect basic clock tampering to block expired key reuse -nigel + if (timing_anomaly_detected()) { + error(XorStr("timing anomaly detected, possible time tamper.")); + } + // periodic integrity sweep across code regions -nigel + const bool heavy_ok = + text_hashes_ok() && + text_page_protections_ok() && + data_page_protections_ok() && + import_addresses_ok() && + !detour_suspect(reinterpret_cast(&VerifyPayload)) && + !detour_suspect(reinterpret_cast(&checkInit)) && + !detour_suspect(reinterpret_cast(&error)) && + prologues_ok() && + func_region_ok(reinterpret_cast(&VerifyPayload)) && + func_region_ok(reinterpret_cast(&checkInit)) && + func_region_ok(reinterpret_cast(&error)) && + func_region_ok(reinterpret_cast(&integrity_check)) && + func_region_ok(reinterpret_cast(&check_section_integrity)); + + if (!heavy_ok) { + const int streak = heavy_fail_streak.fetch_add(1) + 1; + if (streak >= 2) { + error(XorStr("security checks failed, possible tamper detected.")); + } + } else { + heavy_fail_streak.store(0); + } +periodic_done: + if (check_section_integrity(XorStr(".text").c_str(), false)) { + const int streak = integrity_fail_streak.fetch_add(1) + 1; + if (streak >= 2) { + error(XorStr("check_section_integrity() failed, don't tamper with the program.")); + } + } else { + integrity_fail_streak.store(0); + } + } + if (!prologues_ok()) { + error(XorStr("function prologue check failed, possible inline hook detected.")); + } + if (!func_region_ok(reinterpret_cast(&VerifyPayload)) || + !func_region_ok(reinterpret_cast(&checkInit)) || + !func_region_ok(reinterpret_cast(&error)) || + !func_region_ok(reinterpret_cast(&integrity_check)) || + !func_region_ok(reinterpret_cast(&check_section_integrity))) { + error(XorStr("function region check failed, possible hook detected.")); + } + integrity_check(); } + +void integrity_check() { + const auto now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + const auto last = last_integrity_check.load(); + if (now - last > 30) { + last_integrity_check.store(now); + if (check_section_integrity(XorStr(".text").c_str(), false)) { + error(XorStr("check_section_integrity() failed, don't tamper with the program.")); + } + } +} + // code submitted in pull request from https://github.com/BINM7MD BOOL bDataCompare(const BYTE* pData, const BYTE* bMask, const char* szMask) { diff --git a/auth.hpp b/auth.hpp index f229460..6e40730 100644 --- a/auth.hpp +++ b/auth.hpp @@ -1,6 +1,10 @@ #include #include #include +#include +#include +#include +#include #define CURL_STATICLIB @@ -47,6 +51,26 @@ namespace KeyAuth { void fetchstats(); void forgot(std::string username, std::string email); void logout(); + void start_ban_monitor(int interval_seconds = 45, bool check_session = false, std::function on_ban = {}); + void stop_ban_monitor(); + bool ban_monitor_running() const; + bool ban_monitor_detected() const; + static std::string expiry_remaining(const std::string& expiry); + static constexpr const char* kSavePath = "test.json"; + static constexpr int kInitFailSleepMs = 1500; + static constexpr int kBadInputSleepMs = 3000; + static constexpr int kCloseSleepMs = 5000; + struct lockout_state { + int fails = 0; + std::chrono::steady_clock::time_point locked_until{}; + }; + static void init_fail_delay(); + static void bad_input_delay(); + static void close_delay(); + static bool lockout_active(const lockout_state& state); + static int lockout_remaining_ms(const lockout_state& state); + static void record_login_fail(lockout_state& state, int max_attempts = 3, int lock_seconds = 30); + static void reset_lockout(lockout_state& state); class subscriptions_class { public: @@ -104,31 +128,55 @@ namespace KeyAuth { appdata app_data; responsedata response; Tfa tfa; + + // Optional network hardening controls (do not require backend changes). + void set_allowed_hosts(const std::vector& hosts) { allowed_hosts = hosts; } + void add_allowed_host(const std::string& host) { allowed_hosts.push_back(host); } + void clear_allowed_hosts() { allowed_hosts.clear(); } + + void set_pinned_public_keys(const std::vector& pins) { pinned_public_keys = pins; } + void add_pinned_public_key(const std::string& pin) { pinned_public_keys.push_back(pin); } + void clear_pinned_public_keys() { pinned_public_keys.clear(); } private: std::string sessionid, enckey; - static std::string req(const std::string& data, const std::string& url); + std::vector allowed_hosts; + std::vector pinned_public_keys; + std::string req(std::string data, const std::string& url); static void debugInfo(std::string data, std::string url, std::string response, std::string headers); static void setDebug(bool value); void load_user_data(nlohmann::json data) { - api::user_data.username = data[XorStr("username")]; - api::user_data.ip = data[XorStr("ip")]; - if (data[XorStr("hwid")].is_null()) { + const std::string key_username = XorStr("username"); + const std::string key_ip = XorStr("ip"); + const std::string key_hwid = XorStr("hwid"); + const std::string key_created = XorStr("createdate"); + const std::string key_lastlogin = XorStr("lastlogin"); + const std::string key_subs = XorStr("subscriptions"); + const std::string key_sub_name = XorStr("subscription"); + const std::string key_sub_expiry = XorStr("expiry"); + api::user_data.username = data.value(key_username, ""); + api::user_data.ip = data.value(key_ip, ""); + if (!data.contains(key_hwid) || data[key_hwid].is_null()) { api::user_data.hwid = XorStr("none"); } else { - api::user_data.hwid = data[XorStr("hwid")]; + api::user_data.hwid = data[key_hwid]; } - api::user_data.createdate = data[XorStr("createdate")]; - api::user_data.lastlogin = data[XorStr("lastlogin")]; - - for (int i = 0; i < data[XorStr("subscriptions")].size(); i++) { // Prompto#7895 & stars#2297 was here - subscriptions_class subscriptions; - subscriptions.name = data[XorStr("subscriptions")][i][XorStr("subscription")]; - subscriptions.expiry = data[XorStr("subscriptions")][i][XorStr("expiry")]; - api::user_data.subscriptions.emplace_back(subscriptions); + api::user_data.createdate = data.value(key_created, ""); + api::user_data.lastlogin = data.value(key_lastlogin, ""); + + api::user_data.subscriptions.clear(); + if (data.contains(key_subs) && data[key_subs].is_array()) { + for (const auto& sub : data[key_subs]) { + subscriptions_class subscriptions; + if (sub.contains(key_sub_name)) + subscriptions.name = sub.value(key_sub_name, ""); + if (sub.contains(key_sub_expiry)) + subscriptions.expiry = sub.value(key_sub_expiry, ""); + api::user_data.subscriptions.emplace_back(subscriptions); + } } } @@ -150,14 +198,21 @@ namespace KeyAuth { } void load_channel_data(nlohmann::json data) { - api::response.success = data["success"]; // intentional. Possibly trick a reverse engineer into thinking this string is for login function + api::response.success = data[XorStr("success")]; // intentional. Possibly trick a reverse engineer into thinking this string is for login function api::response.message = data["message"]; api::response.channeldata.clear(); //If you do not delete the data before pushing it, the data will be repeated. github.com/TTakaTit - for (const auto sub : data["messages"]) { - - std::string authoroutput = sub[XorStr("author")]; - std::string messageoutput = sub["message"]; - int timestamp = sub[XorStr("timestamp")]; std::string timestampoutput = std::to_string(timestamp); + if (!data.contains("messages") || !data["messages"].is_array()) { + return; // avoid invalid server payload crash. -nigel + } + const std::string key_author = XorStr("author"); + const std::string key_timestamp = XorStr("timestamp"); + for (const auto& sub : data["messages"]) { + if (!sub.is_object()) + continue; + std::string authoroutput = sub.value(key_author, ""); + std::string messageoutput = sub.value("message", ""); + const int timestamp = sub.value(key_timestamp, 0); + std::string timestampoutput = std::to_string(timestamp); authoroutput.erase(remove(authoroutput.begin(), authoroutput.end(), '"'), authoroutput.end()); messageoutput.erase(remove(messageoutput.begin(), messageoutput.end(), '"'), messageoutput.end()); timestampoutput.erase(remove(timestampoutput.begin(), timestampoutput.end(), '"'), timestampoutput.end()); @@ -166,6 +221,10 @@ namespace KeyAuth { } } + std::atomic ban_monitor_running_{ false }; + std::atomic ban_monitor_detected_{ false }; + std::thread ban_monitor_thread_; + nlohmann::json response_decoder; }; diff --git a/killEmulator.hpp b/killEmulator.hpp index 50c2ddf..d1cdcd3 100644 --- a/killEmulator.hpp +++ b/killEmulator.hpp @@ -1,3 +1,4 @@ +#pragma once #include #include #include @@ -20,7 +21,7 @@ namespace protection MODULEINFO info; // place holder for the information // use this function in order to get the module information. - bool result = GetModuleInformation(reinterpret_cast(-1), + bool result = GetModuleInformation(GetCurrentProcess(), module, &info, sizeof(info)); if (result) { diff --git a/library.vcxproj b/library.vcxproj index 884ee9e..17661dc 100644 --- a/library.vcxproj +++ b/library.vcxproj @@ -160,7 +160,7 @@ MachineX64 - libsodium.lib;libcurl.lib%(AdditionalDependencies) + libsodium.lib;libcurl.lib;winhttp.lib;%(AdditionalDependencies) .\curl;.\libsodium;%(AdditionalLibraryDirectories) /NODEFAULTLIB:libcurl.lib %(AdditionalOptions) diff --git a/utils.cpp b/utils.cpp index 0b09151..221e5cc 100644 --- a/utils.cpp +++ b/utils.cpp @@ -12,9 +12,11 @@ std::string utils::get_hwid() { } std::time_t utils::string_to_timet(std::string timestamp) { - auto cv = strtol(timestamp.c_str(), NULL, 10); - - return (time_t)cv; + char* end = nullptr; + auto cv = strtol(timestamp.c_str(), &end, 10); + if (end == timestamp.c_str()) + return 0; + return static_cast(cv); } std::tm utils::timet_to_tm(time_t timestamp) { diff --git a/utils.hpp b/utils.hpp index a2e0343..c0cbf91 100644 --- a/utils.hpp +++ b/utils.hpp @@ -1,10 +1,12 @@ #pragma once #include +#include #include +#include namespace utils { std::string get_hwid(); std::time_t string_to_timet(std::string timestamp); std::tm timet_to_tm(time_t timestamp); -} \ No newline at end of file +}