
Executive Summary
SLEEPWALKER is a passive Windows backdoor that never calls home. It opens no obvious listening port, contains no second-stage payload, and carries no domain, IP address or URL anywhere inside it. The file is a 64-bit DLL that impersonates Microsoft’s dpapi.dll, carries a forged ESET Management Agent version resource, and is built to be side-loaded into ERAAgent.exe. It checks only the host process name — not its signature or path — and stays completely inert unless that name matches. Once loaded, it puts every network interface into promiscuous mode and waits, indefinitely, for a single crafted packet to cross the wire.
What makes the sample worth a full write-up is what that packet carries. Not a readable command, and not a configuration blob, but a short program written in a command language the backdoor’s author designed from scratch. Recovering the AES-256 key embedded in the binary is not enough to read one of these programs: decryption yields a stream of opcodes in a format that exists nowhere except inside this one file, and the instruction set has to be reverse engineered on its own before any of it means anything. Those 23 instructions cover scheduling, six transports — including SMB named pipes with supplied credentials and VMware’s guest-to-host VMCI channel — staged file delivery with SHA-256 verification, LZMA decompression, and in-memory shellcode execution. The single instruction actually stored in the analyzed file is five bytes long and says only: watch every interface, forever, for the trigger. Everything else is capability waiting for an operator to use it.
Background
The author opens with an origin story that will be familiar to anyone who hunts samples for a living: losing VirusTotal Intelligence access at the start of the year turned out to be unexpectedly productive. Unable to hunt for new material, they stopped adding to the TODO pile and finally worked through the previous year’s backlog — which produced a detailed examination of BeheMOF and, along the way, this sample. It did not look especially noteworthy at first. Looking under the hood revealed a distinctive design: a passive backdoor that opens no obvious listening port, carries no payload inside itself, and sits in memory doing nothing whatsoever until one specifically crafted packet reaches the machine. Hence the name.
The author is careful not to oversell it. From a reverse-engineering perspective the design is genuinely interesting, but the implementation has several weaknesses and does not represent top-tier malware engineering. It may well be an early version, with newer and better builds in existence somewhere. That caveat is worth keeping in mind through the sections that follow, several of which describe outright bugs.

The configuration built into the file decrypts — with AES-256-CCM and a verified authentication tag — to that single bootstrap command. The backdoor carries a compact bytecode interpreter with 23 instructions covering scheduling, staged payload delivery with SHA-256 verification and in-memory shellcode execution. Its network capabilities span TCP, UDP, ICMP, SMB named pipes with lateral movement using supplied credentials, VMware’s internal VMCI channel between a guest and its host, and raw-socket promiscuous sniffing. A second trigger channel can carry commands inside DNS queries. All cryptography comes from a statically linked copy of mbedTLS rather than anything loaded at runtime.
To enable unauthenticated named-pipe access, SLEEPWALKER actively weakens its host: it enables anonymous SMB access and creates named pipes granting permissions to Everyone and Anonymous Logon. The combination — nothing to block until the operator sends one packet, and that packet able to arrive inside traffic that looks entirely ordinary, including a crafted DNS query — is what makes it hard to catch from the network side. A passive implant triggered this way, using multiple covert transports including VMCI and deployed through side-loading into a trusted ESET management component, most likely belongs to a targeted operation with other components that were never recovered. The author found no code resembling anything they had seen before and does not attribute the sample to any actor.
Key points
- The file is unsigned, copies ESET’s file information and is loaded through DLL side-loading.
- It checks only the host process name and activates when that name is
ERAAgent.exe, the Windows executable for ESET Management Agent. - It does not contact any server on its own. It waits for one specific encrypted network packet before doing anything.
- Once triggered, it runs programs written in a small custom command language, supporting scheduling, several network methods, staged file delivery and running code directly in memory.
- The file itself contains no ready-made malicious payload. Everything beyond the single starting instruction has to arrive later, over the network.
- It changes local Windows settings so that unauthenticated network connections can reach it.
File Characteristics
The sample is an unsigned 64-bit DLL for the Windows GUI subsystem, 59,904 bytes, with a compilation timestamp of 2024-06-10 09:18:27 UTC:
SHA-256: d347170752a28e2b8c4b8b9f3cab2e3a6541ba11682c94498d26eb9002779d60
SHA-1: 2ec8aa9661a33bccc002150ce1ed02d90c3986ff
MD5: 2318327b29bb1c0e2d2b5f0211fc7fac
Imphash: 4e2dbfa7e3efd4cca2f3662797df9735
The disguise is completed by a version resource copied wholesale from ESET’s real Management Agent:
| Field | Value |
|---|---|
| CompanyName | ESET |
| ProductName | ESET Management Agent |
| FileDescription | ESET Management Agent Module |
| InternalName | ERAAgent |
| OriginalFilename | dpapi.dll |
| File / Product version | 11.2.2076.0 |
| LegalCopyright | Copyright (c) ESET, spol. s r.o. 1992-2024. |
The file exports the same name and the same seven functions as the genuine dpapi.dll: CryptProtectDataNoUI, CryptProtectMemory, CryptResetMachineCredentials, CryptUnprotectDataNoUI, CryptUnprotectMemory, CryptUpdateProtectedState and iCryptIdentifyProtection. Each is a small stub that jumps through a pointer table, and that table starts out empty. The first call to any of the seven triggers a shared resolver that tries to LoadLibraryW a file named dpapisvc.dll, locate the real function inside it, and write the address into the table so the call can be forwarded.
That name belongs to nothing. No dpapisvc.dll ships with Windows. The nearest real name is dpapisrv.dll, an unrelated file exporting only two LSA extension functions — nothing like the seven being sought. A plain reference to dpapi.dll would not have worked either, because the malicious file already occupies that name inside the host process, so a bare LoadLibraryW would simply return a handle to itself rather than reaching the genuine library. Some other name or path was clearly needed, but the one actually chosen matches nothing on a real system.
When the load fails, the resolver terminates the entire host process rather than failing that single call. Whether this ever happens in practice depends on whether anything actually calls one of those seven functions, which this file alone cannot show. The author raises a plausible explanation: a fuller version of the attack may drop a renamed copy of the real dpapi.dll under this name alongside it, since a file in the application’s own folder is found before Windows ever checks System32 — the same search order the backdoor already relies on to get loaded at all. No such copy accompanied this sample, and nothing here confirms one exists.
That same first call also quietly re-runs the backdoor’s own startup check, giving it a second chance to wake up if something interfered with the first.
Initialization and Startup Sequence
Before anything else, the DLL checks the name of the process that loaded it. If that process is not ERAAgent.exe, the DLL stays inert — so it will not run inside a debugger, a sandbox or any other program unless that program happens to carry the exact name. This is a crude but effective anti-analysis measure: it costs nothing and defeats casual detonation. Once the check passes, a short sequence brings the backdoor to life:
- It starts a new background thread, separate from ESET’s own code, so the agent process is not blocked while it runs.
- It reserves a 128 KB block of memory to be used later for assembling programs that arrive in several pieces.
- It decrypts the one instruction stored inside the file.
- It prepares Windows networking and hands the decrypted instruction to its own internal interpreter.
That interpreter is not a one-shot. When a trigger later delivers a follow-up program over any of the transports described below, the exact same interpreter function runs it. This is why the full command language — scheduling, staged file delivery, in-memory execution — is available from the very first trigger onward, with no separate second-stage component needed.
As a fallback, the same check and sequence run again on the first call to any of the seven exported data-protection functions, before that call is forwarded. Two separate paths therefore reach identical startup code:
- Path 1: the DLL loads into
ERAAgent.exe(DllMain). - Path 2: the first call to any of the seven forwarded DPAPI exports.
Both paths independently check the host process name and then run the same startup sequence: start a background thread, reserve the 128 KB buffer, decrypt the bootstrap instruction, start the interpreter. Nothing checks whether the other path already ran — which is the root of the duplicate-worker problem the author notes.
The ERAAgent.exe string used for the check is not stored as readable text. It is rebuilt from a handful of numbers at runtime, the same trick applied to three function names the file never lists among its normal imports: VirtualProtect for running shellcode, SetSecurityDescriptorDacl for the permissive pipe permissions described later, and CryptGenRandom for its random pauses.
On DLL_PROCESS_DETACH, the DLL sets a process-wide stop flag polled by its interpreter, sleep, scheduling and listener loops. This requests that they exit, but does not guarantee a clean dynamic unload. The thread helper closes each worker handle immediately after CreateThread, and the detach path does not wait for workers to finish, so a worker can still be executing when the DLL is unmapped. During process termination Windows has already killed the other threads, so this risk applies mainly to dynamic unloads — another sign of an implementation that is clever in design and rough in execution.
No Autonomous Beaconing or Fixed Servers
Most backdoors reach out to a server shortly after starting so they can receive commands. SLEEPWALKER does not. After confirming its host process name, the embedded bootstrap makes no outbound connection at all, and there are no domains, IP addresses or URLs built into the file.
An important scoping note from the author: this describes SLEEPWALKER’s own startup behavior, not all network activity from its host process. The legitimate ESET Management Agent still checks in with ESET PROTECT on its configured interval, so ERAAgent.exe may continue producing entirely legitimate traffic while the backdoor sits dormant inside it — useful cover, and a trap for anyone triaging by process-level netflow.
Instead of connecting out, it places the network card into promiscuous mode so it can see every packet crossing the interface, not just packets addressed to the host. It then checks every packet for a specific pattern: a calculated checksum, an encoded length value and a block of encrypted data. Only a packet matching that pattern exactly is decrypted and treated as a command — the classic magic-packet trigger. The check runs in this order:
| Step | Check | If it fails |
|---|---|---|
| 1 | Packet is at least 48 bytes long | Ignored |
| 2 | XOR the packet’s last two 16-bit values together, then XOR the result with 0xAAAA, to get a candidate length | N/A |
| 3 | Candidate length falls inside a valid range | Ignored |
| 4 | The byte pair at position (packet length minus candidate length) equals the sum, not the XOR, of the same two trailing values | Ignored |
| 5 | The block the candidate length points to passes its own CRC-32 check | Ignored |
| 6 | Decrypt with AES-256-CCM and treat the result as a command | N/A |
A failure at any step drops the packet with no response whatsoever. Only a packet clearing every step in order is treated as a command, which means a scanner probing for the backdoor gets nothing back to fingerprint.
All of this runs against the raw contents of a packet, before Windows has sorted out whether it is TCP, UDP or anything else. Because the check happens at that level, the trigger can travel inside almost any kind of IP traffic rather than one specific protocol.
The backdoor watches at most eight interfaces at once, skipping loopback and any self-assigned address a machine uses when it cannot reach a network. After a successful trigger it waits at least three seconds before accepting another — primarily so it does not act on the same packet twice, rather than to throttle repeated attempts.
Because it never sends anything on its own and opens no obvious listening port by default, tooling that watches for connections to known-bad domains or unusual outbound traffic sees nothing. The only moment it becomes visible is when the operator sends the trigger. The author draws the defensive conclusion explicitly: the absence of outbound connections to known-bad infrastructure does not rule out infection. A machine can be fully compromised by this backdoor while producing nothing at all for a network monitor to flag.
The configuration inside the file contains exactly one instruction: listen on every interface, with no time limit, for a matching packet. Every other action arrives later, over the network, already encrypted.
Command Authentication and Encryption
Before the individual pieces, here is the shape of the whole pipeline a command travels through, from arrival to execution:
Trigger packet or DNS query
-> Framing and checksum check
-> AES-256-CCM decrypt
-> Bytecode interpreter
-> Command handler
Every command is encrypted with AES-256-CCM, which both hides the content and proves it was not altered after creation. Commands arriving through most channels share a layout: a 12-byte nonce that changes each time, a 16-byte authentication tag, then the ciphertext. The DNS-borne trigger uses a shortened version of the same layout, since a DNS name offers far less room.
On top of that, the raw trigger packet carries its own separate CRC-32 checksum. This has nothing to do with the encryption; it exists so the backdoor can reject a non-matching packet before spending any effort attempting decryption — a cheap first filter that keeps the promiscuous listener from burning CPU on ordinary traffic.
The encryption key is stored directly inside the DLL. The author recovered it during analysis, along with the nonce used specifically for the embedded configuration:
AES-256 key: 0x746531ff378dbb4bb51d2aa2b1d38d905350a959583186baf4c690f5f316b3ae
Config nonce: 0x3a6d357fb9bc51eacc8b8509
With that key and nonce the 2,048-byte encrypted configuration decrypts cleanly and its authentication tag verifies, confirming both are correct. Randomness for the jittered pauses comes from Windows’ own CryptGenRandom, resolved by name at runtime rather than imported normally.
The table below summarizes what SLEEPWALKER encrypts or encodes, how each type of content is protected, and whether it enters, leaves or stays within the backdoor:
| Content | Direction | Encoding or encryption | Explanation |
|---|---|---|---|
| Task programs delivered through the raw trigger, TCP, UDP, named pipes or VMCI | Into SLEEPWALKER | AES-256-CCM | The command bytecode is encrypted and authenticated before interpretation. The nonce and authentication tag remain visible by design. |
| Task programs carried in DNS labels | Into SLEEPWALKER (the DNS query itself may enter or leave the host) | Base32 over AES-256-CCM | Base32 makes the encrypted envelope suitable for DNS labels. Decoding Base32 reveals the AES envelope, not the plaintext command. |
Task programs loaded from a file by RUN_FILE_SCRIPT | Local | AES-256-CCM | The file contains an encrypted task envelope that is decrypted before interpretation. |
Nested programs used by CRON_SCHEDULE | Internal | AES-256-CCM, then XOR in memory | The program arrives inside the encrypted task, then remains XOR-obfuscated between scheduled executions. |
Data transmitted by TCP_SEND, UDP_SEND, ICMP_SEND or PIPE_SEND | Out of SLEEPWALKER | No automatic encryption | The instruction arrives encrypted, but the data it tells SLEEPWALKER to send is transmitted as supplied by the operator. |
| Network headers, trigger framing, CRC checksums, DNS markers, AES nonce and authentication tag | Accompanies task delivery | Visible metadata | These fields allow transport, recognition or validation. They do not expose the plaintext command bytecode. |
Bytecode Format
Once decrypted, a command is neither text nor a document. It is a short sequence of raw bytes that only makes sense read in a specific order.
This is what puts the design a step beyond most backdoors. The simplest ones send commands as plain text and numbers, which anyone reading the file or watching traffic can follow directly and which detection tooling matches on easily. More advanced ones encrypt that same plain text, but the protection evaporates the moment someone recovers the key. This one encrypts its commands and then places a second barrier behind the first. Decrypting with the recovered key does not yield a readable command or a settings list — it yields a stream of opcodes in a format that exists nowhere but inside this one file, and it stays unreadable until that format has been worked out independently. The key shows how to read the bytes; only reverse engineering the command language shows what they mean.
Every instruction begins with a single byte identifying which of the 23 kinds it is. What follows depends entirely on that first byte. A fixed-size number, such as a wait time, is written using a set number of bytes, most significant first. A piece of text or a block of data, which can be any length, is written as a small count of how many bytes follow, then the bytes themselves, so a reader always knows exactly where that field ends.
Take the one instruction actually found stored inside the analyzed file. In full, it is five bytes:
87 01 2A 00 00
Read left to right: 87 is the opcode identifying the instruction that watches the network for a hidden trigger. 01 is a length count saying the next field is one byte long. 2A is that byte — the character code for an asterisk, meaning every interface. 00 00 is a two-byte number, most significant byte first, specifying how many seconds to keep watching, where zero means no limit. Broken down, the five bytes form a small tree:
Command = SNIFF_MAGIC_PACKET (0x87)
├── interface_filter
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
└── deadline_seconds = 0 (0x0000)
Five bytes fully describe the instruction “watch every interface forever” — and that is the entire useful content of the file’s built-in configuration. Blocks of raw data such as network payloads or shellcode are written exactly like text: a count followed by that many bytes. Only the assigned meaning differs.
The length count itself is written compactly, so small numbers take one byte and larger ones take more. Each byte holds seven bits of the actual value plus one bit saying whether another byte follows. A length of 1, like the single *, fits in the one byte 01. A length of 200 does not fit in seven bits and needs two bytes: C8 01.
Some instructions carry more than numbers or text. A handful carry an entire second program as one of their fields, and the same reading process applies to that inner program when its turn comes. The scheduled instruction is the clearest example. In full, it is 22 bytes:
0E 00 00 00 00 00 00 00 01 00 00 02 00 FF FF FF FE 3E 03 9D FD C1
The single opcode byte is followed by four fixed-size numbers marking which minutes, hours, days and weekdays the schedule matches. After those, 03 is a length count saying the inner program that follows is 3 bytes long, and 9D FD C1 is that block. Broken down, the pieces form a tree with a smaller tree inside it:
Command = CRON_SCHEDULE (0x0E)
├── minute_bitmask = minute 0 (0x0000000000000001)
├── hour_bitmask = hour 9 (0x00000200)
├── day_of_month_bitmask = any day (0xFFFFFFFE)
├── weekday_bitmask = Monday to Friday (0x3E)
└── xor_masked_script
├── length = 03 (3 bytes follow)
└── data = 9D FD C1
└── XORed with the recovered key 0x90FDFD02, this becomes:
Command = SLEEP_RANDOM_SECONDS (0x0D)
└── modulus_seconds = 60 (0x003C)
Those three encrypted bytes only make sense once XORed with a short repeating key. Undone, they become a complete second instruction: wait a random number of seconds, up to 60. This is what it means for one instruction to contain another — the outer instruction is fully described by its own bytes, and one of its fields is a smaller program in disguise, read the same way when its turn comes.
The decryption is deliberately temporary. The code XORs the nested-program buffer with the key, hands the plaintext to the interpreter, then applies the same XOR again to restore the encrypted bytes. Decrypt, run, re-encrypt. The nested program stays XOR-protected while waiting between scheduled runs and is readable only during execution — a small but genuinely thoughtful touch against memory scanning. This inner XOR layer is unique to CRON_SCHEDULE; the scheduled instruction itself still arrives inside the ordinary AES-256-CCM envelope.
Command Language Reference
Everything the backdoor does after the initial trigger is controlled by the instructions just described. There are 23 in total, and several carry an inner program the way the scheduled instruction does. That nesting is what lets a short list of instruction types combine into a wide range of behaviors: a schedule can contain a network listener, which can contain a routine that waits for a file to be assembled and verified before it is allowed to run, and so on.
The table below lists all 23 instructions, grouped by purpose, with a plain-English description, parameters and each parameter’s wire type. string and blob are both length-prefixed and represent text and raw bytes respectively. u8, u16, u32 and u64 are fixed-width big-endian integers of 1, 2, 4 and 8 bytes carrying no length prefix. lzma_properties is a fixed 5-byte structure, also unprefixed.
| Instruction | What it does | Parameters |
|---|---|---|
| Basic control | ||
EXIT | Sets the process-wide stop flag rather than ending one program. Every loop in the file checks that flag, so this halts all running programs and the packet listener with them. | None |
SPAWN_THREAD_SCRIPT | Starts a second, smaller program running at the same time as the current one, in its own thread, so the first program can keep going. | Nested program to run (blob) |
| Timing and scheduling | ||
SLEEP_SECONDS | Pauses for a fixed number of seconds before moving on to the next instruction. | Duration, in seconds (u16) |
SLEEP_RANDOM_SECONDS | Pauses for a random number of seconds up to a chosen limit, adding jitter so repeated actions are not perfectly predictable. | Upper limit, in seconds (u16) |
CRON_SCHEDULE | Checks the current minute, hour, day of month and weekday against four stored patterns and runs an inner program whenever all four match. | Minute mask (u64), hour mask (u32), day of month mask (u32), weekday mask (u8), nested program, encrypted (blob) |
REPEAT_N | Runs a smaller program a fixed number of times in a row. | Repeat count (u16), nested program (blob) |
LOOP_FOREVER | Runs a smaller program over and over, without a limit, until the backdoor is told to stop entirely. | Nested program (blob) |
| Sending data | ||
TCP_SEND | Opens a TCP connection to a chosen address and port, sends a block of data and does not wait for a reply. The remote host can also be a VMware VMCI target instead of a normal network address. | Local address (string), local port (string), remote host (string), remote port (string), data (blob), deadline (u16) |
UDP_SEND | Sends a single block of data over UDP to a chosen address and port, without waiting for a reply. The remote host can also be a VMware VMCI target instead of a normal network address. | Local address (string), local port (string), remote host (string), remote port (string), data (blob), deadline (u16) |
ICMP_SEND | Hides a block of data inside a ping request and sends it to a target address. | Source address (string), remote host (string), data (blob), deadline (u16) |
PIPE_SEND | Writes a block of data to a Windows named pipe on a chosen computer, optionally logging in with a username and password first. | Server name (string), pipe name (string), username (string), password (string), data (blob), deadline (u16) |
| Inbound task reception | ||
TCP_CONNECT_RECV | Connects out to a chosen address and port and waits to receive a follow-up program. The infected machine reaches out, rather than waiting to be reached. The remote host can also be a VMware VMCI target instead of a normal network address. | Local address (string), local port (string), remote host (string), remote port (string), deadline (u16) |
TCP_LISTEN_RECV | Opens a TCP port, waits for one connection and receives a follow-up program from whoever connects. The bind address can also be a VMware VMCI target instead of a normal network address. | Bind address (string), bind port (string), deadline (u16) |
UDP_BIND_RECV | Opens a UDP port and waits for a single incoming block of data, treated as a follow-up program. The bind address can also be a VMware VMCI target instead of a normal network address. | Bind address (string), bind port (string), deadline (u16) |
PIPE_CLIENT_RECV | Connects to a named pipe on a chosen computer and waits to receive a follow-up program, optionally using a username and password. | Server name (string), pipe name (string), username (string), password (string), deadline (u16) |
PIPE_SERVER_RECV | Creates a local named pipe, waits for a connection and receives a follow-up program from whoever connects. | Pipe name (string), unused field (string), deadline (u16) |
| Building and running programs | ||
STAGE_WRITE | Copies a piece of a larger program into a shared 128 KB work area in memory, at a chosen position, so a program can be assembled a little at a time. | Offset (u32), chunk of data (blob) |
STAGE_VERIFY_EXEC | Compares a SHA-256 fingerprint of the pieces collected so far against one supplied with the instruction and only runs the assembled program on an exact match. | Length (u32), SHA-256 fingerprint (blob) |
DECOMPRESS_RUN | Expands a program that was compressed before being sent back to its original size, then runs the result. | Unpacked size (u32), compression settings (lzma_properties), compressed data (blob) |
RUN_SHELLCODE | Runs a block of raw machine code directly in memory, switching that memory from writable to executable right before calling it. | Machine code (blob) |
RUN_FILE_SCRIPT | Reads a file already saved on the local disk, decrypts it the same way as any other command and runs the result. | File path (string) |
| Trigger detection | ||
SNIFF_MAGIC_PACKET | Watches one or all network interfaces for the hidden trigger packet described earlier, for a chosen length of time or with no limit at all. This is the instruction actually stored in the analyzed file. | Interface (string), deadline (u16) |
SNIFF_MAGIC_PACKET_DNS | Does everything the instruction above does and also watches for the DNS-based trigger described further down. Not the instruction found in the analyzed file. | Interface (string), deadline (u16) |
Every instruction breaks down the same way the earlier walkthroughs did. The sections below follow the table’s grouping, each naming the instruction and its opcode byte, showing the bytes of a working example, describing what that example demonstrates, then showing the tree those bytes decode into.
Basic control
EXIT (0x06) — Example: 06. Shut the backdoor down.
Command = EXIT (0x06)
A single byte and nothing else; there is no operand to decode. It sets the same shared flag used by the DLL’s unload path. Every sleep, repeat, schedule and packet listener polls that flag, so an EXIT anywhere stops all of them rather than only the program in which it appears.
SPAWN_THREAD_SCRIPT (0x0B) — Example: 0B 05 87 01 2A 00 00. Run a background copy of the trigger listener while other work continues.
Command = SPAWN_THREAD_SCRIPT (0x0B)
└── script
├── length = 05 (5 bytes follow)
└── data = 87 01 2A 00 00
└── nested program:
Command = SNIFF_MAGIC_PACKET (0x87)
├── interface_filter
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
└── deadline_seconds = 0 (0x0000)
Timing and scheduling
SLEEP_SECONDS (0x0C) — Example: 0C 00 3C. Wait 60 seconds, then continue.
Command = SLEEP_SECONDS (0x0C)
└── duration_seconds = 60 (0x003C)
SLEEP_RANDOM_SECONDS (0x0D) — Example: 0D 01 2C. Wait somewhere between 0 and 299 seconds, then continue.
Command = SLEEP_RANDOM_SECONDS (0x0D)
└── modulus_seconds = 300 (0x012C)
CRON_SCHEDULE (0x0E) — Example: 0E 00 00 00 00 00 00 00 01 00 00 02 00 FF FF FF FE 3E 03 9D FD C1. Run every weekday at 09:00, then pause for a random interval.
Command = CRON_SCHEDULE (0x0E)
├── minute_bitmask = minute 0 (0x0000000000000001)
├── hour_bitmask = hour 9 (0x00000200)
├── day_of_month_bitmask = any day (0xFFFFFFFE)
├── weekday_bitmask = Monday to Friday (0x3E)
└── xor_masked_script
├── length = 03 (3 bytes follow)
└── data = 9D FD C1
└── XORed with the recovered key 0x90FDFD02, this becomes:
Command = SLEEP_RANDOM_SECONDS (0x0D)
└── modulus_seconds = 60 (0x003C)
The scheduled instruction was covered in full detail earlier, including the length byte and the re-encryption step after it runs. The tree is repeated here so it lines up with its row in the table.
REPEAT_N (0x0F) — Example: 0F 00 03 03 0C 00 0A. Run a 10-second pause three times in a row.
Command = REPEAT_N (0x0F)
├── repeat_count = 3 (0x0003)
└── script
├── length = 03 (3 bytes follow)
└── data = 0C 00 0A
└── nested program:
Command = SLEEP_SECONDS (0x0C)
└── duration_seconds = 10 (0x000A)
LOOP_FOREVER (0x10) — Example: 10 03 0C 00 3C. Repeat a 60-second pause without end.
Command = LOOP_FOREVER (0x10)
└── script
├── length = 03 (3 bytes follow)
└── data = 0C 00 3C
└── nested program:
Command = SLEEP_SECONDS (0x0C)
└── duration_seconds = 60 (0x003C)
Sending data
TCP_SEND (0x29) — Example: 29 01 2A 01 2A 0C 31 39 32 2E 31 36 38 2E 31 2E 31 30 03 34 34 33 03 69 64 0A 00 00. Send a short line of text to 192.168.1.10 on port 443.
Command = TCP_SEND (0x29)
├── local_bind_address
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
├── local_bind_port
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
├── remote_host
│ ├── length = 0C (12 bytes follow)
│ └── data = "192.168.1.10" (31 39 32 2E 31 36 38 2E 31 2E 31 30)
├── remote_port
│ ├── length = 03 (3 bytes follow)
│ └── data = "443" (34 34 33)
├── payload
│ ├── length = 03 (3 bytes follow)
│ └── data = "id\n" (69 64 0A)
└── deadline_seconds = 0 (0x0000)
The two "*" fields are the wildcard seen earlier: no specific local address or port is requested, so the operating system picks one automatically.
UDP_SEND (0x2A) — Example: 2A 01 2A 01 2A 08 31 30 2E 30 2E 30 2E 39 02 35 33 04 70 69 6E 67 00 00. Send the word “ping” to 10.0.0.9 on port 53.
Command = UDP_SEND (0x2A)
├── local_bind_address
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
├── local_bind_port
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
├── remote_host
│ ├── length = 08 (8 bytes follow)
│ └── data = "10.0.0.9" (31 30 2E 30 2E 30 2E 39)
├── remote_port
│ ├── length = 02 (2 bytes follow)
│ └── data = "53" (35 33)
├── payload
│ ├── length = 04 (4 bytes follow)
│ └── data = "ping" (70 69 6E 67)
└── deadline_seconds = 0 (0x0000)
ICMP_SEND (0x2B) — Example: 2B 01 2A 07 38 2E 38 2E 38 2E 38 04 CA FE BA BE 00 00. Send four bytes of data disguised as a ping to 8.8.8.8.
Command = ICMP_SEND (0x2B)
├── source_address
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
├── remote_host
│ ├── length = 07 (7 bytes follow)
│ └── data = "8.8.8.8" (38 2E 38 2E 38 2E 38)
├── payload
│ ├── length = 04 (4 bytes follow)
│ └── data = CA FE BA BE
└── deadline_seconds = 0 (0x0000)
PIPE_SEND (0x2C) — Example: 2C 04 44 43 30 31 07 73 70 6F 6F 6C 73 73 08 43 4F 52 50 5C 73 76 63 05 50 40 73 73 31 06 62 65 61 63 6F 6E 00 00. Write the word “beacon” to the spoolss pipe on a server named DC01, logging in as CORP\svc first.
Command = PIPE_SEND (0x2C)
├── server_name
│ ├── length = 04 (4 bytes follow)
│ └── data = "DC01" (44 43 30 31)
├── pipe_name
│ ├── length = 07 (7 bytes follow)
│ └── data = "spoolss" (73 70 6F 6F 6C 73 73)
├── username
│ ├── length = 08 (8 bytes follow)
│ └── data = "CORP\svc" (43 4F 52 50 5C 73 76 63)
├── password
│ ├── length = 05 (5 bytes follow)
│ └── data = "P@ss1" (50 40 73 73 31)
├── payload
│ ├── length = 06 (6 bytes follow)
│ └── data = "beacon" (62 65 61 63 6F 6E)
└── deadline_seconds = 0 (0x0000)
Inbound task reception
TCP_CONNECT_RECV (0x6F) — Example: 6F 01 2A 01 2A 04 76 6D 3A 32 04 39 30 30 30 00 3C. Connect out through VMware’s VMCI channel to context ID 2, the conventional host endpoint, on port 9000 instead of using a normal network address.
Command = TCP_CONNECT_RECV (0x6F)
├── local_bind_address
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
├── local_bind_port
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
├── remote_host
│ ├── length = 04 (4 bytes follow)
│ └── data = "vm:2" (76 6D 3A 32)
├── remote_port
│ ├── length = 04 (4 bytes follow)
│ └── data = "9000" (39 30 30 30)
└── deadline_seconds = 60 (0x003C)
The host field here is not an IP address. The vm: prefix selects VMware’s VMCI channel, and the decimal value after it is parsed as the destination context ID (svm_cid). The separate port string becomes the VMCI port (svm_port). In this example, CID 2 denotes the VMware host, not a virtual machine numbered 2.
TCP_LISTEN_RECV (0x70) — Example: 70 07 30 2E 30 2E 30 2E 30 04 38 34 34 33 00 00. Listen on port 8443 on any local address.
Command = TCP_LISTEN_RECV (0x70)
├── bind_address
│ ├── length = 07 (7 bytes follow)
│ └── data = "0.0.0.0" (30 2E 30 2E 30 2E 30)
├── bind_port
│ ├── length = 04 (4 bytes follow)
│ └── data = "8443" (38 34 34 33)
└── deadline_seconds = 0 (0x0000)
UDP_BIND_RECV (0x73) — Example: 73 07 30 2E 30 2E 30 2E 30 04 35 33 35 33 00 00. Listen on port 5353 on any local address.
Command = UDP_BIND_RECV (0x73)
├── bind_address
│ ├── length = 07 (7 bytes follow)
│ └── data = "0.0.0.0" (30 2E 30 2E 30 2E 30)
├── bind_port
│ ├── length = 04 (4 bytes follow)
│ └── data = "5353" (35 33 35 33)
└── deadline_seconds = 0 (0x0000)
PIPE_CLIENT_RECV (0x7D) — Example: 7D 04 57 4B 53 37 04 6D 6F 6A 6F 00 00 00 1E. Connect to a pipe named mojo on a workstation called WKS7, using the current login.
Command = PIPE_CLIENT_RECV (0x7D)
├── server_name
│ ├── length = 04 (4 bytes follow)
│ └── data = "WKS7" (57 4B 53 37)
├── pipe_name
│ ├── length = 04 (4 bytes follow)
│ └── data = "mojo" (6D 6F 6A 6F)
├── username
│ ├── length = 00 (0 bytes follow)
│ └── data = "" (0 bytes)
├── password
│ ├── length = 00 (0 bytes follow)
│ └── data = "" (0 bytes)
└── deadline_seconds = 30 (0x001E)
The empty username and password fields are still present on the wire as zero-length strings rather than being omitted. That is what connecting with the currently logged-in account looks like.
PIPE_SERVER_RECV (0x7E) — Example: 7E 09 6D 6F 6A 6F 5F 70 69 70 65 00 00 00. Wait for a connection on a locally created pipe named mojo_pipe.
Command = PIPE_SERVER_RECV (0x7E)
├── pipe_name
│ ├── length = 09 (9 bytes follow)
│ └── data = "mojo_pipe" (6D 6F 6A 6F 5F 70 69 70 65)
├── reserved (unused)
│ ├── length = 00 (0 bytes follow)
│ └── data = "" (0 bytes)
└── deadline_seconds = 0 (0x0000)
Building and running programs
STAGE_WRITE (0x32) — Example: 32 00 00 00 00 06 65 04 48 31 C0 C3. Write six bytes to the very start of the work area. On its own this instruction does nothing else: it only fills the buffer, and something else has to check and run the contents afterward. The six bytes chosen here are a complete instruction in their own right — the RUN_SHELLCODE example shown further down.
Command = STAGE_WRITE (0x32)
├── buffer_offset = 0 (0x00000000)
└── chunk_data
├── length = 06 (6 bytes follow)
└── data = 65 04 48 31 C0 C3
The offset travels with the instruction, so chunks need not arrive in order and can fill the work area in any pattern. Before copying, the code checks the offset against the size of that area, then checks offset and chunk length together in a way that also catches the numeric wraparound a careless check would miss — one of the few places where the implementation is noticeably careful. Nothing is verified or executed at this point, and the area keeps whatever it already held anywhere the new chunk does not cover.
STAGE_VERIFY_EXEC (0x33) — Example: 33 00 00 00 06 20 A0 A0 D4 5F 4B C3 12 59 D6 89 57 96 65 95 54 1F 60 24 C3 D5 F1 BB 36 81 C0 A2 7E 2C DE D5 68 C1. Confirm six previously written bytes match their expected fingerprint, then run them. The fingerprint is a SHA-256 hash — the same kind of check used to confirm a downloaded file was not corrupted in transit — and a single byte out of place is enough for the instruction to refuse to run anything.
Command = STAGE_VERIFY_EXEC (0x33)
├── verified_length = 6 (0x00000006)
└── expected_sha256
├── length = 20 (32 bytes follow)
└── data = A0 A0 D4 5F ... DE D5 68 C1
This pairs with STAGE_WRITE because both act on the same buffer: the fingerprint here is the SHA-256 of exactly the six bytes that write placed there, so the check passes. A match hands the buffer contents back to the interpreter rather than to the processor, so a staged program is bytecode and can be any instruction the language offers. Staging a RUN_SHELLCODE instruction, as here, is how staged bytes end up as running machine code. RUN_SHELLCODE on its own needs no staging.
DECOMPRESS_RUN (0x1F) — Example: 1F 00 00 08 00 5D 00 00 10 00 04 00 11 22 33. Expand a compressed block back to its original size before running it. Everything it needs travels with it: the claimed size of the output, the five settings bytes the decompressor requires, and the compressed data itself.
Command = DECOMPRESS_RUN (0x1F)
├── unpacked_size = 2048 (0x00000800)
├── lzma_properties = lc=3, lp=0, pb=2, 1 MiB dictionary (5D 00 00 10 00)
└── compressed_data
├── length = 04 (4 bytes follow)
└── data = illustrative only, not a full compressed stream (00 11 22 33)
This instruction is self-contained and has nothing to do with the shared work area the two staging instructions use. The compressed bytes are its own third field, so a complete program arrives in one message instead of being assembled from several. The output goes into a fresh block of heap memory sized by the claimed unpacked size rather than by anything measured from the data itself — a detail worth flagging, since it means the sender dictates the allocation. What comes out is handed to the interpreter, not the processor, so a decompressed program is bytecode like any other and still needs a RUN_SHELLCODE inside it to reach native code. Staging and compression solve different problems: one splits up a program too large for a single message, the other packs it into one.
RUN_SHELLCODE (0x65) — Example: 65 04 48 31 C0 C3. Run a very short block of test machine code. Memory is initially writable, the code is copied in, and VirtualProtect then changes it to executable before the call. VirtualProtect is resolved by name at runtime rather than appearing in the file’s normal imports.
Command = RUN_SHELLCODE (0x65)
└── shellcode
├── length = 04 (4 bytes follow)
└── data = xor rax, rax ; ret (48 31 C0 C3)
This is the only instruction in the language that hands bytes to the processor rather than back to the interpreter. The two-step permission change prevents the block from being writable and executable simultaneously, which is the safer sequence and also the one least likely to trip a W^X-based detection. The call happens on the current thread, so the interpreter waits until the code returns, and the block is released the moment it does, leaving nothing behind unless the code itself arranged otherwise.
RUN_FILE_SCRIPT (0x66) — Example: 66 14 43 3A 5C 50 72 6F 67 72 61 6D 44 61 74 61 5C 64 2E 64 61 74. Load and run a program stored in a file under C:\ProgramData.
Command = RUN_FILE_SCRIPT (0x66)
└── file_path
├── length = 14 (20 bytes follow)
└── data = "C:\ProgramData\d.dat" (43 3A 5C 50 72 6F 67 72 61 6D 44 61 74 61 5C 64 2E 64 61 74)
The entire file is read into memory and passed through the same decryption the network channels use, with the same embedded key and envelope. A file on disk is not a different kind of payload, only a different delivery route: it holds an ordinary encrypted task program and reaches the interpreter through the same code path as a trigger packet’s contents. Nothing limits how large the file may be before it is read, and it is left in place afterward rather than deleted. Crucially, nothing in the command language puts that file there. No instruction writes to disk, and every handle the backdoor opens asks for a file that already exists, so it cannot create one. From inside the language, only a RUN_SHELLCODE payload can create it with native code. Anything else has to come from elsewhere in the intrusion.
Trigger detection
SNIFF_MAGIC_PACKET (0x87) — Example: 87 01 2A 00 00. The instruction actually stored in the analyzed file: watch every interface, forever, for the raw trigger packet only. The DNS-based trigger is not active under this opcode.
Command = SNIFF_MAGIC_PACKET (0x87)
├── interface_filter
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
└── deadline_seconds = 0 (0x0000)
SNIFF_MAGIC_PACKET_DNS (0x88) — Example: 88 01 2A 00 00. The same instruction as above, watching every interface forever, but with the DNS-based trigger also active. This is the opcode that switches the DNS carrier on, and it is not the opcode stored in the analyzed file.
Command = SNIFF_MAGIC_PACKET_DNS (0x88)
├── interface_filter
│ ├── length = 01 (1 byte follows)
│ └── data = "*" (0x2A)
└── deadline_seconds = 0 (0x0000)
Taken together, this language lets an operator describe a wide range of behavior from a short list of building blocks. Despite that range, the single instruction actually stored and encrypted inside the analyzed file was short: listen on every network interface, with no time limit, for the trigger packet. Everything else — scheduling, staged file delivery, running code in memory — exists only as capability the language provides. The programs an operator might actually send still have to arrive later, over the network.
Alternative Trigger Channels and Transports
Five of the networking instructions share an unusual extra capability. TCP_SEND, UDP_SEND, TCP_CONNECT_RECV, TCP_LISTEN_RECV and UDP_BIND_RECV all check whether the address they were given starts with vm:, and if so they use VMware’s internal guest-to-host channel — VMCI — instead of a normal network address. If the infected machine is a VMware guest, this lets commands pass between guest and host, or between two guests on the same host, without that traffic ever appearing on a regular network, because the communication travels through the virtualization layer rather than a network adapter. A packet capture between machines would contain none of it. To find the correct address family value for this channel, the backdoor opens the device object \\.\VMCI and asks it directly, exactly as VMware’s own VMCI Sockets API does.
There is also a second way to deliver a trigger, hidden inside ordinary-looking DNS lookups — though it is not what the analyzed file uses. A separate opcode, one value higher than the instruction stored in the file, enables the DNS trigger alongside the raw one. Activating it would require either a different build with that opcode embedded, or a follow-up task delivered through another route after the deployed listener had already been reached. The backdoor treats certain DNS queries as commands by encoding the command with a text-safe scheme, similar to how email attachments are sometimes encoded, and splitting it across the parts of a domain name. This lets a command travel through networks that only allow DNS traffic out — which many networks do even when most other outbound traffic is restricted.
Before any of that, the packet has to look like a DNS question: UDP or TCP to port 53, carrying a standard query header asking exactly one question and claiming no answer, authority or additional records. Nothing else in the header is examined, including the transaction ID and the record type. One detail makes UDP the practical carrier: a DNS query sent over TCP is prefixed with a two-byte length field, and this code never skips it, so a standards-compliant TCP query arrives two bytes out of step and fails to parse. That is a bug, not a design choice, and it is a good example of the roughness the author flagged at the outset.
Each DNS label used this way — one dot-separated part of a domain name — has its own small format, separate from the length-prefixed fields used everywhere else. A label is built from three parts: one marker character, a run of Base32-encoded text in the middle, and a second marker character. The two markers are not fixed letters. Between them they carry a single checksum byte covering the middle text, which is what lets the backdoor distinguish a genuine label from an ordinary one. Any label failing that checksum is silently skipped — which matters, because a real query usually has more than one label (the example and com parts of example.com), and only the specific label carrying the trigger needs to pass.
The label checksum is a CRC-8 using polynomial 0x31, run from a starting value of zero through a 256-entry lookup table, covering the middle characters only and not the markers. The resulting byte is split in half: the top four bits become the first marker and the bottom four the last, each added to the letter g. Four bits hold sixteen values, so both markers always land between g and v, and checking that range is the first thing the backdoor does. A label whose first or last character sits outside it is dropped before any checksum is computed, which is why ordinary labels cost almost nothing to reject.
To demonstrate this end to end, the author built and verified a trigger of their own — not something captured from real traffic — encoding the same SLEEP_SECONDS(60) instruction used earlier. Encrypted with the DNS channel’s own framing (7-byte nonce, 4-byte tag, then ciphertext, using the same embedded AES-256 key as every other channel), the instruction comes to 14 bytes:
81 5C 22 62 CC B7 09 31 24 6F D3 5F 34 4D
Base32 encoding those 14 bytes with the backdoor’s lowercase alphabet gives a 23-character string. Its CRC-8 works out to 0x65, so the markers are the letters standing for 6 and 5 — m and l. Wrapping those around the middle turns it into a single valid label:
mqfoceywmw4etcjdp2nptitil
Placed in an otherwise ordinary-looking domain name, the full query becomes:
mqfoceywmw4etcjdp2nptitil.example.com
Reading it back the way the backdoor would: m and l both sit between g and v, so they are treated as markers. Subtracting g from each gives 6 and 5, which recombine into 0x65. Recomputing the CRC-8 over the 23 characters between them produces that same 0x65, so the label is genuine. The example and com labels that follow are rejected on the range test alone — because e and c both come before g, the backdoor skips them without special handling. Base32 decoding the 23-character middle section gives back the exact 14 bytes shown above:
[label] mqfoceywmw4etcjdp2nptitil
├── marker (first) = "m"
├── payload (base32, 23 chars) = qfoceywmw4etcjdp2nptiti
└── marker (last) = "l"
└── decodes to 14 bytes: 81 5C 22 62 CC B7 09 31 24 6F D3 5F 34 4D
├── nonce = 81 5C 22 62 CC B7 09 (7 bytes)
├── tag = 31 24 6F D3 (4 bytes)
└── ciphertext = 5F 34 4D (3 bytes)
└── decrypted with AES-256-CCM and the embedded AES-256 key:
Command = SLEEP_SECONDS (0x0C)
└── duration_seconds = 60 (0x003C)
Everything after the decode is identical to any other channel. Joining the decoded labels back together produces the AES-256-CCM envelope, and from there it is decrypted and handed to the interpreter just as a trigger packet’s contents would be. DNS adds only a preceding encoding layer: the envelope arrives split across one or more labels rather than in one piece.
Taken together, the networking instructions use six underlying transports. A shared factory installs the appropriate send, receive, bind and listen functions for the selected transport, so each networking opcode uses its chosen channel consistently. None of these transports has a hard-coded address, domain or URL — every target is supplied at runtime inside the task program.
| Transport | Mechanism | Notes |
|---|---|---|
| TCP | socket / connect / listen / accept | Client and server. Host and port are resolved with getaddrinfo. |
| UDP | sendto / recvfrom | One-shot send and bind-and-receive. |
| ICMP | IcmpSendEcho | Data is smuggled inside ping echo-request payloads. |
| SMB named pipe | CreateNamedPipeW / CreateFileW on \\host\pipe\name | Can mount the remote share with supplied credentials first, for lateral movement. |
| VMware VMCI | Address family resolved through \\.\VMCI | A covert guest-to-host or guest-to-guest channel that never touches a physical network adapter. |
| Raw / promiscuous | Raw socket with promiscuous mode enabled | How the hidden trigger packet described earlier is received. |
Network Reachability and Attacker Positioning
Two questions are worth separating: how an operator delivers the first command to an idle backdoor, and how far a task’s transport can reach once a task is running. They have different answers:
| Channel | Internet | Firewall / NAT | Internal network | Target host |
|---|---|---|---|---|
| Raw trigger (first command) | Blocked | Blocked | Reaches | Reaches |
| DNS trigger (implemented, not active in this sample) | Reaches | Reaches | Reaches | Reaches |
| VMCI (guest/host channel) | Not applicable | Not applicable | Not applicable | Reaches only within the same VMware host or VMCI fabric |
| Outbound-initiated transports (after trigger) | Reaches | Reaches | Reaches | Reaches |
| Inbound-facing transports (after trigger) | Blocked | Blocked | Reaches | Reaches |
“Blocked” here means a perimeter firewall or NAT gateway ordinarily stops it, not that it is impossible under every network configuration.
Delivering the first command depends on an ordinary packet actually reaching the interface the backdoor is watching. A perimeter firewall or NAT gateway commonly blocks unsolicited raw traffic from the open internet, so reaching the raw trigger in practice means the operator already has a path onto that network — either by being on it, or by pivoting from another machine that is. The usual exceptions apply: a host with a public IP address, a NAT or port-forwarding rule aimed at it, or a host running a public-facing DNS service can all be reached directly.
There is a less obvious exception worth dwelling on. Each interface is captured using Windows’ SIO_RCVALL option, set to receive everything crossing it rather than only packets addressed to the local host. On an ordinary endpoint this makes little difference. On a machine that routes or forwards traffic for others — a gateway, a VPN server, a host bridging two segments — traffic addressed to a completely different machine still crosses the watched interface and could carry the trigger. A machine used this way does not need to be the operator’s actual destination at all, which considerably widens the set of hosts from which a trigger can be delivered.
The DNS-based trigger exists in the binary as a workaround for the more restrictive case, but it is not what the analyzed sample runs. The bootstrap selects the plain listener; the DNS-aware listener uses a separate opcode an operator would have to select by shipping a different build or sending a follow-up task through another route. Where it is used, DNS is one of the few kinds of traffic a network almost always allows out and one of the least closely inspected, making it the channel best suited to crossing a boundary that would stop the raw trigger outright. It does not remove the need for a packet to reach the interface, only the need for the operator to be close enough for a plain raw packet to get there. Such a trigger could also arrive with no inbound delivery at all if something on the machine is induced to make an outbound DNS lookup carrying it — the same listener would see that query on its way out.
Once a task is running, its reach depends on the transport it selects, and most transports need far less access than the initial trigger. TCP_SEND, UDP_SEND, TCP_CONNECT_RECV and ICMP_SEND all have the infected machine connect or send outward to an address the task supplies — the same direction as any ordinary outbound connection — so they typically still work from behind a NAT gateway or firewall that would have blocked the initial trigger. Only TCP_LISTEN_RECV and UDP_BIND_RECV go the other way, waiting for something to connect or send to the infected machine, carrying the same inbound-reachability requirement as the trigger. An SMB named pipe is ordinary Windows networking, reachable across a local network the same way any file share is. VMCI requires the operator endpoint and target to run as two guests, or as guest and host, on the same physical VMware machine — a narrower and qualitatively different kind of closeness than sharing a network.
The deployment fits this picture. Riding inside ERAAgent.exe means the realistic target is a managed 64-bit Windows endpoint or server with ESET Management Agent installed — the kind of machine normally placed behind a firewall and NAT rather than exposed to the internet. That is exactly the setting in which the raw trigger alone would struggle to reach the target, which makes it notable that the analyzed sample relies on it anyway, without the DNS workaround switched on. So the first command favors an operator with an existing position on or next to the target’s network, or one of the narrower exceptions above, over a stranger on the open internet. Once that command lands, though, most of what a task can do reaches outward rather than requiring anything to reach in, so the operator does not need to keep that position for everything that follows.
Host Configuration Changes
To enable unauthenticated named-pipe access, the backdoor changes two security settings on the infected machine:
- It sets
EveryoneIncludesAnonymous, causing permissions granted to Everyone to apply to anonymous access tokens. - It adds its pipe name to
NullSessionPipes, allowing that named pipe to be reached without a username or password.
It also creates its named pipes with permission rules allowing Everyone and Anonymous Logon to connect. Together, these changes let unauthenticated callers reach the backdoor’s named-pipe channel wherever the surrounding network permits it.
The code attempts to undo these changes later, but its bookkeeping does not reliably preserve the original configuration. Specifically, it records whether adding the NullSessionPipes entry succeeded, not whether the entry already existed — so cleanup can remove an entry that was present before the backdoor ever ran. For defenders this cuts both ways: it is another implementation flaw, but it also means a post-incident registry state may not match the pre-incident one even after the implant tidies up.
None of this involves privilege escalation. There is no code anywhere in the file attempting to bypass UAC or gain higher permissions than it starts with. Changing those two registry keys already requires local administrator rights, so the backdoor simply relies on the security context of its host process rather than obtaining those rights itself. And as long as the malicious file stays in the same folder as ERAAgent.exe, it is loaded again whenever the ESET Management Agent service starts. The side-loading is the persistence mechanism — there is no other.
A Note on AI Usage
The author includes a methodology section that is worth reproducing in substance, because it says something about how this kind of analysis is now done. SLEEPWALKER was one of several samples used to compare frontier AI models on reverse engineering of Windows PE malware. The initial analysis was performed manually, both to build a basic understanding and to preserve the hands-on challenge that makes malware analysis enjoyable; AI then assisted with the detailed analysis and verification presented in the post.
The models tested were Claude Opus 5 and GPT-5.6-Sol, with Opus 4.8 and Sonnet 5 used when safety restrictions prevented Opus 5 from continuing. Kimi K3 was planned but access had not arrived. Fable was excluded because, in the author’s testing, its security filters blocked even general questions whose answers might have dual-use applications. The test set combined several previously undisclosed samples from the backlog with a few publicly described samples whose binaries had never been released, such as STRAITBIZARRE (SBZ).
On SLEEPWALKER specifically, the models produced broadly similar results with usually small differences. Claude performed better on some parts, GPT on others, and neither family was consistently ahead. One notable exception was SNIFF_MAGIC_PACKET_DNS: on three separate attempts Claude described opcode 0x87 and opcode 0x88 as functionally identical, while GPT identified the important difference on its first attempt — that 0x87 enables only the raw-packet trigger while 0x88 also enables the DNS-based one.
The author’s overall experience with GPT was better than expected, particularly as they had not previously used it for malware analysis, and its weekly usage allowance proved easier to work with than Claude’s hourly limit during long reverse-engineering sessions. None of the GPT runs were interrupted by safety refusals despite the author not being enrolled in the Trusted Access for Cyber program. By contrast, they eventually encountered a refusal in every malware-analysis run with Claude Opus or Sonnet — sometimes early, sometimes only after substantial progress — despite acceptance into the Cyber Verification Program. These interruptions made longer investigations difficult to complete in a continuous workflow.
The author’s own position on the trade-off: malware analysis can of course be misused, and a newly discovered technique or vulnerability could theoretically be repurposed, but this is an unlikely outcome when the work is done by a responsible analyst. Their suggestion is that Anthropic should apply stricter admission checks to applicants for programs such as the Cyber Verification Program and, in return, give approved researchers fewer restrictions when conducting legitimate reverse engineering — addressing abuse concerns without repeatedly interrupting legitimate research.
The broader conclusion is measured. AI is a powerful accelerant for malware analysis: dissections that once took hours, days, weeks or months can be completed in a fraction of the time. It does not remove the need for technical expertise or careful verification — every result still has to be checked against the code and the available evidence — but doing so is usually much faster than performing every step by hand. The same applies to the write-up. For many researchers the dissection is the enjoyable part, and turning findings into a clear, readable document can feel like the documentation phase at the end of a long software project. AI can help organize notes, shape structure and draft prose, but publication still requires substantial proofreading, technical verification and correction. It does not remove the work; it shortens the path from completed analysis to readable report.
What Remains Unknown
The analysis rests on a single binary, with no related incident records or network captures. The author is unusually explicit about the resulting gaps, which is worth reproducing in full because it defines what the sample can and cannot support:
- Sample origin and victim: there is no collection context tying the file to a confirmed intrusion, so no victim, industry, country or affected organization can be identified. The requirement to run inside
ERAAgent.exepoints to a 64-bit Windows endpoint or server with ESET Management Agent installed, but does not reveal whether the actual host was a workstation, server, gateway, VPN system or VMware guest — nor does it prove the sample was ever successfully deployed. - Initial access and delivery: DLL side-loading explains how SLEEPWALKER executes and persists once placed beside
ERAAgent.exe. It does not explain how an operator first entered the environment, obtained the required administrator access, or wrote the malicious DLL into that protected application directory. No dropper, installer, exploit or initial-access technique is present in the file. - Companion components and operator tooling: the backdoor cannot install itself, and its command language provides no general way to create the files it expects to find. The unresolved
dpapisvc.dllforwarding dependency may indicate another component places a renamed genuine DLL beside it, but no such file accompanied the sample. The trigger generator, bytecode task builder, delivery mechanism and any later payloads must also exist outside this binary. A wider toolset is therefore possible, but the sample cannot show whether those pieces belong to a reusable framework or were assembled for one operation. - Commands actually received: the only encrypted task stored in the sample starts the raw-packet listener. The remaining instructions describe capabilities, not observed attacker behavior. Without captured trigger traffic, memory from an infected host or the local files referenced by later tasks, there is no way to know which commands were sent, which payloads ran, what data was collected, or whether lateral movement occurred.
- Channels actually used: DNS triggering, VMCI, ICMP, named pipes and the other transports are implemented, but their presence does not prove an operator used them. DNS support in particular is not enabled by the embedded bootstrap, and VMCI support does not by itself prove the intended or actual victim was a VMware guest.
- Infrastructure and operator position: there are no hard-coded servers, domains, addresses or operator identifiers. The raw trigger favors someone already able to put a packet onto or through the target network, but the code cannot say whether that access came from another compromised host, an insider position, a routed system, a public-facing interface or some other path.
- Attribution, campaign and spread: nothing in the file identifies its developer or operator. The author found no related code supporting attribution to a known group, and one sample cannot establish when or how widely SLEEPWALKER was deployed, whether variants exist, or whether it belongs to a continuing campaign.
The binary supports the assessment of a targeted and technically capable operation, but the victim, operator, delivery chain and real post-compromise activity remain unconfirmed.
The author invites contact from anyone who believes they have been targeted by SLEEPWALKER or has encountered a related sample, and has built a toolkit to help decode its bytecode, examine encrypted and network artifacts, summarize behavior and indicators, and safely reproduce its receiving pipeline without executing commands or transmitting traffic. A mitigation guide with a post-detection remediation script also exists.


Even where original evidence cannot be shared, the author notes that sanitized technical details would help fill in the missing picture — particularly how the malware was delivered, what other files or tools accompanied it, which commands and payloads were observed, what infrastructure and transports were used, and whether any TTPs connect the operator or wider toolset to other activity.
Conclusion of the Original Analysis
SLEEPWALKER is a passive backdoor that does not beacon on its own, carries no embedded second-stage payload, and is designed to run through DLL side-loading into ERAAgent.exe, activating only when the host process carries that name. The binary implements scheduling, six transports, staged delivery with SHA-256 verification and in-memory execution. What arrives later is the bytecode that selects and combines those capabilities.
Taken as a whole, the approach is consistent with a targeted, well-resourced operation rather than an opportunistic one: a passive implant woken by a single crafted packet, several covert transports including a rarely seen VMware channel, and deployment through side-loading into a trusted ESET management component. The design favors an operator who can already get a packet onto the target’s network, since the trigger has to reach a watched interface. The DNS-based trigger is the one feature that would loosen that requirement — and it is implemented but not switched on, since the bootstrap in this sample listens for the raw trigger alone. The author could not attribute the sample to a specific group, having seen no similar code before and having no information about the attack chain.
At the time of publication no earlier public reporting of this backdoor existed and detection coverage for the file remained low. That lack of exposure means SLEEPWALKER could still be in use and may still be under development, with later or modified builds not yet identified.
File Download
The original article makes the sample available for download at sleepwalker.zip, archived with the password sleepwalker_infected. It is live malware — handle it only in an isolated analysis environment.
Indicators of Compromise
- SHA-256:
d347170752a28e2b8c4b8b9f3cab2e3a6541ba11682c94498d26eb9002779d60 - An unexpected
dpapi.dllbesideERAAgent.exe - An unexpected
dpapisvc.dllin the same directory EveryoneIncludesAnonymousset to 1- An unexpected entry in
NullSessionPipes
The registry values require comparison with a known-good baseline and are not proof of SLEEPWALKER on their own.
Appendix
The appendix provides two detection tools built from the findings above. The author frames them as starting points rather than finished products. Both were checked against the analyzed sample directly: every hash, byte pattern and string in the YARA rule was confirmed in the actual file, and the scanner was run against synthetic test data and a copy of the real sample.
YARA detection rule
import "pe"
rule sleepwalker_backdoor
{
meta:
author = "Dominik Reichel"
description = "Detects the SLEEPWALKER passive backdoor."
sha256 = "d347170752a28e2b8c4b8b9f3cab2e3a6541ba11682c94498d26eb9002779d60"
date = "2026-08-11"
reference = "https://r136a1.dev/2026/08/24/sleepwalker-a-passive-backdoor-with-its-own-command-language/"
strings:
// Static AES-256 key used for every authenticated task envelope
$aes_key = { 74 65 31 FF 37 8D BB 4B B5 1D 2A A2 B1 D3 8D 90
53 50 A9 59 58 31 86 BA F4 C6 90 F5 F3 16 B3 AE }
// 12-byte nonce for the embedded-bootstrap task envelope
$config_nonce = { 3A 6D 35 7F B9 BC 51 EA CC 8B 85 09 }
// Trigger-packet validation logic: length check, then XOR the packet's
// last two 16-bit values together and XOR again with 0xAAAA to get a
// candidate length, checked against a minimum of 0x1C (28). This is
// the backdoor's own protocol code, not a masquerade string or the
// per-build task key, so it holds regardless of which system DLL a
// variant imitates or which vendor name it forges. It is still
// compiled code, so a rebuild with a different compiler or different
// optimization settings could change register choice and break the
// match. The 4-byte jump offset is wildcarded since it shifts if
// unrelated code elsewhere in the file changes size.
$magic_packet_algo = {
49 83 FC 30 // cmp r12, 0x30
0F 82 ?? ?? ?? ?? // jb ...
47 0F B7 44 25 FC // movzx r8d, word [r13+r12-4]
47 0F B7 4C 25 FE // movzx r9d, word [r13+r12-2]
B8 AA AA 00 00 // mov eax, 0xAAAA
41 0F B7 C8 // movzx ecx, r8w
66 41 33 C9 // xor cx, r9w
66 33 C8 // xor cx, ax
66 83 F9 1C // cmp cx, 0x1C
}
// Non-existent DPAPI service DLL
$dpapi_svc = "dpapisvc.dll" wide
condition:
uint16(0) == 0x5A4D and
uint32(uint32(0x3C)) == 0x00004550 and
(
any of ($aes_key, $config_nonce, $magic_packet_algo)
or (
pe.version_info["OriginalFilename"] contains "dpapi.dll" and
(
pe.version_info["FileDescription"] contains "ESET Management Agent Module" or
$dpapi_svc
) and
pe.exports("CryptProtectDataNoUI") and
pe.exports("CryptProtectMemory") and
pe.exports("CryptResetMachineCredentials") and
pe.exports("CryptUnprotectDataNoUI") and
pe.exports("CryptUnprotectMemory") and
pe.exports("CryptUpdateProtectedState") and
pe.exports("iCryptIdentifyProtection")
)
)
}
PowerShell detection script
The script reads and reports but never writes, so it is safe to run across an estate before deciding on a response. It was checked against the analyzed sample directly: the SHA-256 hash was confirmed against the real file, and the scan logic was run against synthetic test data and a copy of the sample.
It covers the host-side indicators from the companion guide: a dpapi.dll next to ERAAgent.exe, its SHA-256 hash, a dpapisvc.dll alongside it, and the two registry values. The optional -IncludeMetadata switch also collects the candidate’s Authenticode status and version-resource claims. An optional -Path sweep searches any folder or file share for exact hash matches, filtering on the sample’s exact 59,904-byte size before hashing to keep large scans efficient.
The script reports the contents of NullSessionPipes without attributing individual entries to SLEEPWALKER. Any nonempty list is classified as RegistryReviewRequired and produces exit code 1, so an analyst can compare it against a known-good baseline. Registry access errors are suppressed by this compact scanner, so a clean result means no readable indicators were found in the scanned scope — it does not prove every registry value was successfully queried. Exit codes make it usable in a scheduled sweep: 0 for nothing found, 1 for an anomaly or registry configuration requiring review, and 2 for a confirmed hash match.
<#
.SYNOPSIS
Scans a Windows host for the SLEEPWALKER backdoor (masquerading as dpapi.dll,
side-loaded beside ESET's ERAAgent.exe).
.DESCRIPTION
Read-only. Checks for a dpapi.dll beside ERAAgent.exe, the known SHA-256,
dpapisvc.dll and the two registry values changed by the backdoor. NullSessionPipes
entries are reported for comparison with the host's baseline, not attributed
automatically to SLEEPWALKER.
.PARAMETER SearchRoot
Directories to search for ERAAgent.exe. Defaults to both Program Files locations.
.PARAMETER Path
Extra directories to sweep for the exact sample by size and SHA-256.
.PARAMETER IncludeMetadata
Collect Authenticode status and version-resource claims for candidate DLLs.
.PARAMETER AsJson
Emit one JSON object instead of formatted text, for collection at scale.
.NOTES
Exit codes: 0 nothing found, 1 anomaly or registry review required,
2 confirmed hash match. A confirmed match requires incident response.
Paths skipped due to access-denied/IO errors during the sweep are reported
(InaccessiblePathCount, or the "could not be scanned" line / -Verbose in
text mode) but do not change the exit code -- an incomplete scan is not
itself evidence of compromise, so check that count separately.
#>
[CmdletBinding()]
param(
[string[]] $SearchRoot,
[string[]] $Path,
[switch] $IncludeMetadata,
[switch] $AsJson
)
$ErrorActionPreference = 'Stop'
if (-not $SearchRoot) {
$programFilesX86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)')
$SearchRoot = @($env:ProgramFiles, $programFilesX86) | Where-Object { $_ }
}
$KnownBadSha256 = 'D347170752A28E2B8C4B8B9F3CAB2E3A6541BA11682C94498D26EB9002779D60'
$KnownBadSize = 59904
$CompanionDllName = 'dpapisvc.dll'
$LsaKeyPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa'
$LsaValueName = 'EveryoneIncludesAnonymous'
$LanmanParamsKeyPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters'
$NullSessionValueName = 'NullSessionPipes'
# Populated via -ErrorVariable +script:InaccessiblePaths in the sweeps below, so an
# access-denied subtree is reported instead of silently making the scan look clean.
$InaccessiblePaths = @()
function Find-SideLoadedDpapiDll {
param(
[string[]] $Roots,
[switch] $IncludeMetadata
)
Write-Verbose "Searching for ERAAgent.exe under: $($Roots -join ', ')"
$agents = foreach ($root in $Roots) {
if (Test-Path -LiteralPath $root) {
Get-ChildItem -LiteralPath $root -Filter 'ERAAgent.exe' -Recurse -File -ErrorAction SilentlyContinue -ErrorVariable +script:InaccessiblePaths
} else {
Write-Warning "Skipping '$root': not found."
}
}
if (-not $agents) {
[PSCustomObject]@{
Status = 'NoAgentFound'
Message = 'No ERAAgent.exe found under the given search roots. If ESET is installed elsewhere, pass -SearchRoot.'
}
return
}
foreach ($agent in $agents) {
$candidate = Join-Path $agent.DirectoryName 'dpapi.dll'
$companionPresent = Test-Path -LiteralPath (Join-Path $agent.DirectoryName $CompanionDllName)
if (-not (Test-Path -LiteralPath $candidate)) {
[PSCustomObject]@{
Status = if ($companionPresent) { 'AnomalousPresence' } else { 'Clean' }
AgentPath = $agent.FullName
DllPath = $candidate
CompanionDllFound = $companionPresent
Message = if ($companionPresent) {
"No dpapi.dll here, but a $CompanionDllName is present. No genuine Windows component uses that name, so review it."
} else {
'No dpapi.dll sitting beside this ERAAgent.exe. A legitimate install has no reason to carry one here.'
}
}
continue
}
$sha256 = $null
$hashError = $null
try {
$sha256 = (Get-FileHash -LiteralPath $candidate -Algorithm SHA256).Hash
} catch {
$hashError = $_.Exception.Message
}
$signatureStatus = $null
$claims = $null
if ($IncludeMetadata) {
try {
$signatureStatus = (Get-AuthenticodeSignature -LiteralPath $candidate).Status.ToString()
} catch {
$signatureStatus = 'Unavailable'
}
try {
$versionInfo = (Get-Item -LiteralPath $candidate).VersionInfo
$claims = "$($versionInfo.CompanyName) / $($versionInfo.ProductName)"
} catch {
$claims = 'Unavailable'
}
}
$isKnownBad = $null -ne $sha256 -and $sha256 -eq $KnownBadSha256
[PSCustomObject]@{
Status = if ($isKnownBad) { 'ConfirmedMatch' } else { 'AnomalousPresence' }
AgentPath = $agent.FullName
DllPath = $candidate
CompanionDllFound = $companionPresent
Sha256 = $sha256
HashError = $hashError
SignatureStatus = $signatureStatus
Claims = $claims
Message = if ($hashError) {
'A dpapi.dll exists next to ERAAgent.exe but could not be hashed. Review it manually.'
} elseif ($isKnownBad) {
'SHA-256 matches the known SLEEPWALKER sample exactly.'
} else {
'A dpapi.dll exists next to ERAAgent.exe but its hash does not match the known sample. Its location remains anomalous and warrants manual review as a possible variant.'
}
}
}
}
function Find-SampleByHash {
param([string[]] $Roots)
foreach ($root in $Roots) {
if (-not (Test-Path -LiteralPath $root)) {
Write-Warning "Skipping '$root': not found."
continue
}
Write-Verbose "Sweeping $root for files of exactly $KnownBadSize bytes."
Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue -ErrorVariable +script:InaccessiblePaths |
Where-Object { $_.Length -eq $KnownBadSize } |
ForEach-Object {
$file = $_
try {
$sha256 = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash
if ($sha256 -eq $KnownBadSha256) {
[PSCustomObject]@{
Status = 'ConfirmedMatch'
DllPath = $file.FullName
Sha256 = $sha256
Message = 'Contents match the known SLEEPWALKER sample, under a different name or location.'
}
}
} catch {
Write-Warning "Could not hash '$($file.FullName)': $($_.Exception.Message)"
}
}
}
}
$findings = @(Find-SideLoadedDpapiDll -Roots $SearchRoot -IncludeMetadata:$IncludeMetadata)
if ($Path) {
$findings += @(Find-SampleByHash -Roots $Path)
}
$lsaValue = (Get-ItemProperty -LiteralPath $LsaKeyPath -Name $LsaValueName -ErrorAction SilentlyContinue).$LsaValueName
$nullSessionEntries = (Get-ItemProperty -LiteralPath $LanmanParamsKeyPath -Name $NullSessionValueName -ErrorAction SilentlyContinue).$NullSessionValueName
$registryState = [PSCustomObject]@{
EveryoneIncludesAnonymous = $lsaValue
IsAnonymousShareAccessOn = ($lsaValue -eq 1)
NullSessionPipes = $nullSessionEntries
NullSessionPipeCount = @($nullSessionEntries | Where-Object { $_ }).Count
}
$confirmedCount = @($findings | Where-Object { $_.Status -eq 'ConfirmedMatch' }).Count
$anomalousCount = @($findings | Where-Object { $_.Status -eq 'AnomalousPresence' }).Count
$registryRequiresReview = $registryState.IsAnonymousShareAccessOn -or $registryState.NullSessionPipeCount -gt 0
$overallStatus = if ($confirmedCount -gt 0) {
'ConfirmedMatch'
} elseif ($anomalousCount -gt 0) {
'AnomalousFile'
} elseif ($registryRequiresReview) {
'RegistryReviewRequired'
} else {
'Clean'
}
$scanTimestamp = Get-Date
$inaccessiblePathMessages = @($InaccessiblePaths | ForEach-Object { $_.Exception.Message })
if ($AsJson) {
[PSCustomObject]@{
ScannedAtUtc = $scanTimestamp.ToUniversalTime().ToString('o')
ComputerName = $env:COMPUTERNAME
Findings = $findings
RegistryState = $registryState
RegistryRequiresReview = $registryRequiresReview
InaccessiblePathCount = $inaccessiblePathMessages.Count
InaccessiblePaths = $inaccessiblePathMessages
OverallStatus = $overallStatus
ConfirmedCount = $confirmedCount
AnomalousCount = $anomalousCount
} | ConvertTo-Json -Depth 5
} else {
Write-Host "`nSLEEPWALKER scan - $env:COMPUTERNAME - $($scanTimestamp.ToString('u'))" -ForegroundColor Cyan
Write-Host "`n== dpapi.dll beside ERAAgent.exe ==" -ForegroundColor Cyan
foreach ($finding in $findings) {
$color = switch ($finding.Status) {
'ConfirmedMatch' { 'Red' }
'AnomalousPresence' { 'Yellow' }
'NoAgentFound' { 'Gray' }
default { 'Green' }
}
Write-Host "[$($finding.Status)] $($finding.Message)" -ForegroundColor $color
}
Write-Host "`n$(($findings | Format-List | Out-String).Trim())"
Write-Host "`n== Registry state (read-only) ==" -ForegroundColor Cyan
Write-Host "`n$(($registryState | Format-List | Out-String).Trim())"
if ($registryState.IsAnonymousShareAccessOn) {
Write-Host 'EveryoneIncludesAnonymous is 1. The backdoor sets this so anonymous callers can reach its named pipe.' -ForegroundColor Yellow
}
if ($registryState.NullSessionPipeCount -gt 0) {
Write-Host 'NullSessionPipes currently contains:' -ForegroundColor Yellow
$registryState.NullSessionPipes | ForEach-Object { Write-Host " - $_" }
Write-Host 'This value is not modified automatically. Compare these entries against a known-good baseline and remove only entries confirmed as unauthorized.' -ForegroundColor Yellow
}
if ($inaccessiblePathMessages.Count -gt 0) {
Write-Host "`n$($inaccessiblePathMessages.Count) path(s) could not be scanned (access denied or I/O error) - coverage may be incomplete. Re-run elevated for full coverage, or with -Verbose to see which paths." -ForegroundColor Yellow
$inaccessiblePathMessages | ForEach-Object { Write-Verbose $_ }
}
Write-Host "`n== Result ==" -ForegroundColor Cyan
if ($confirmedCount -gt 0) {
Write-Host "$confirmedCount confirmed hash match(es). Treat this machine as compromised and rebuild it." -ForegroundColor Red
} elseif ($anomalousCount -gt 0) {
Write-Host "$anomalousCount anomalous finding(s) with no exact hash match. Review manually as a possible variant." -ForegroundColor Yellow
} elseif ($registryRequiresReview) {
Write-Host 'No SLEEPWALKER file indicator was found, but the registry configuration requires review.' -ForegroundColor Yellow
} else {
Write-Host 'No readable SLEEPWALKER file indicator or registry setting requiring review was found in the scanned scope.' -ForegroundColor Green
}
Write-Host 'This script changed nothing. It reports on local files and configuration only and cannot tell you which commands the backdoor may already have received and carried out.' -ForegroundColor Gray
}
if ($confirmedCount -gt 0) {
exit 2
} elseif ($anomalousCount -gt 0 -or $registryRequiresReview) {
exit 1
} else {
exit 0
}
Key Takeaways
- A custom bytecode language is a second encryption layer. Recovering the AES-256 key gets an analyst to a stream of opcodes, not to a readable command. The instruction set exists nowhere but inside the binary, so the command language has to be reverse engineered on its own before any captured traffic means anything.
- Passive triggering defeats the entire outbound-detection model. No beacon, no domain, no IP, no URL, and no listening port by default. Netflow analysis, DNS reputation and C2 blocklists all see nothing. The absence of suspicious outbound traffic is not evidence of a clean host.
- The host-process name check is the whole anti-analysis strategy. The DLL stays inert unless loaded into a process called
ERAAgent.exe, which costs nothing to implement and defeats casual sandbox detonation outright. - Six transports, none of them hard-coded. TCP, UDP, ICMP, SMB named pipes with supplied credentials, raw sniffing and VMware VMCI — with every target address supplied at runtime inside the task program, so the binary yields no infrastructure to pivot from.
- VMCI traffic never touches the network. Guest-to-host and guest-to-guest communication through the virtualization layer produces nothing for a packet capture between machines to record, making it a genuinely covert channel on VMware estates.
- The implant weakens the host to reach itself. Setting
EveryoneIncludesAnonymousand adding toNullSessionPipesopens unauthenticated named-pipe access — and its faulty cleanup bookkeeping can remove aNullSessionPipesentry that legitimately predated the infection. - Interesting design, rough implementation. Duplicate startup paths with no coordination, a DNS parser that cannot handle standards-compliant TCP queries, a forwarding dependency on a DLL that exists nowhere, and unsafe unload handling all suggest an early build — and imply better versions may exist.
Defensive Recommendations
- Hunt on the file-system indicators, not the network ones. Sweep for a
dpapi.dllordpapisvc.dllsitting besideERAAgent.exe. The author’s PowerShell scanner above does this read-only and is safe to run across an estate; the 59,904-byte size filter keeps a wide sweep efficient. - Baseline and monitor the two registry values.
EveryoneIncludesAnonymousset to 1 and any unexpectedNullSessionPipesentry both warrant investigation — but only against a known-good baseline, since neither is proof on its own and the implant’s cleanup can alter pre-existing entries. - Treat application directories as integrity-monitored. Side-loading is this implant’s only persistence mechanism. File-integrity monitoring on the folders of trusted management agents catches the entire technique at the point of deployment, before any trigger is ever sent.
- Watch for promiscuous mode on endpoints. A managed workstation or server enabling
SIO_RCVALLon its interfaces has essentially no legitimate reason to do so. This is one of the few behavioral signals the implant cannot avoid emitting while dormant. - Extend the hunt to forwarding hosts. Because the listener sees everything crossing the interface, gateways, VPN concentrators and segment-bridging hosts can receive a trigger addressed to an entirely different machine. Prioritize them in scoping rather than treating them as ordinary endpoints.
- Do not exempt VMware guests from network monitoring assumptions. VMCI traffic bypasses the virtual network entirely, so guest-to-host and guest-to-guest channels need host-level visibility — a network sensor between VMs will never see it.
- Deploy the YARA rule at scale. Run it across file shares, software distribution points and backup images, not just live endpoints, since the sample cannot install itself and may have been staged before deployment.
- Assume other components exist. The backdoor cannot write its own files, cannot install itself, and expects a
dpapisvc.dllthat never accompanied it. Any confirmed detection should trigger a full intrusion investigation rather than single-file remediation.
Conclusion
What makes SLEEPWALKER worth studying is not any single technique but the way its choices compound. Passive triggering removes the network indicators defenders usually rely on; the host-process name check removes the sandbox behavior they would fall back to; the custom bytecode removes the readable configuration that key recovery would normally yield; and VMCI removes the packet capture entirely on virtualized estates. Each layer individually is known art. Stacked together, and delivered by side-loading into a trusted security agent, they produce an implant that can sit on a network for a long time producing almost nothing to find. That the implementation is visibly rough in places — duplicate startup paths, a DNS parser that breaks on compliant TCP queries, a forwarding dependency on a file that does not exist — is the most uncomfortable detail in the write-up, because it suggests this is an early build of something whose later versions have not yet been identified.
Original text: “SLEEPWALKER: A Passive Backdoor With Its Own Command Language” — author not clearly listed (site: R136a1, X: @TheEnergyStory), August 24, 2026.
![[QuickNote] SolidPDFCreator – Mustang Panda Stage-1 Backdoor (Target India)](https://core-jmp.org/wp-content/uploads/2026/07/image-7-1024x919.png)

