10 KiB
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
# 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.
eframeis 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.sysinforeports which is live asoverlay 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
sysinforeports them aspanic: <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 animatedspook, 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.
lockinputusesBlockInput, which requires an elevated process. An unelevated implant reports the refusal in its own log rather than pretending to have locked anything.sysinforeports the elevation state so this is predictable up front.0holds untilstopall, andsysinforeports an active hold asinput lock: active for …s.- No interactive desktop — a service, a scheduled task, or session 0 — means
every window command fails.
sysinforeportsdesktop: falsein that case. fontsizewritesHKCU\Control Panel\Desktop\LogPixelsand broadcastsWM_SETTINGCHANGE, so it takes effect for new windows only.setwallpaper/getwallpapergo throughSystemParametersInfoWwithSPI_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.
msgboxis the one exception and uses a realMessageBoxW, so it gets the genuine system styling.
Design notes
- One GUI thread, many viewports — under eframe. winit permits one
EventLoopper 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_MSinwebui/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.
MessageBoxWis modal, a key press grab has to hold,lockinputhas to be undone after a delay or astopall. All of them run throughgui::detach, so alockinput— timed or continuous — does not hold up the check-in loop. stopallis immediate. It bumps an epoch, releases any input hold, sets cancel tokens; overlays check a deadline on their own thread, andPlaySoundWis ended by re-issuing it with a null sound, which is the only way to stop it.stopalldoes not undo everything. Wallpaper, title, taskbar, font scale and volume are deliberately left changed — that is the joke.getwallpaperandgettitleexist so the original state can be read back afterwards. The one thing it must undo is input: a continuouslockinputhas no other way off, so unblocking it is part of the same cleanup.- Win32 is declared by hand, not through the
windowscrate. The declarations are plainextern "system", which keeps the dependency list and the binary small and links identically under the MSVC and GNU targets.