Martin's Blog

6. Hands-on-Keyboard: The Second Stage

Chapter 5 ended at the funnel: up to roughly 18,000 potentially affected customers, the DNS control plane observed in passive DNS, and a much smaller publicly documented set receiving hands-on follow-on activity. SUNBURST was only ever the door. For selected targets, the operators came through it by hand, and that is where a supply-chain infection became a real intrusion.

This chapter has two layers, and the line between them matters:

  1. The loaders, as artifacts. TEARDROP and RAINDROP were examined as binaries; their job—to map Cobalt Strike BEACON into memory—is read line by line from the decompilation.
  2. What the operators did once inside. The hands-on tradecraft (credential theft, Golden SAML, mailbox export) is not established by those binaries; it is documented by vendor incident response (Mandiant, Microsoft, Volexity) and is included here, clearly marked as cited, not derived, because it is the layer where the espionage objective was actually met.

6.1 The kill chain, and where each piece lives

SUNBURST has no in-memory loader of its own. Its command set (Chapter 4) is all disk-based: WriteFile (stage bytes), SetRegistryValue (config/persistence), RunTaskProcess.Start (launch). So the chain hands off in stages:

SUNBURST  ── stages loader DLL (+ payload) to disk, installs a service ──▶
TEARDROP / RAINDROP  ── map & execute BEACON in memory ──▶
Cobalt Strike BEACON  (in memory)  ── operator C2, lateral movement, theft

Each link is a different tool with a different on-disk footprint. SUNBURST writes files and starts a service; the loader is the thing on disk that turns a blob into running BEACON; BEACON itself is the hands-on implant. The two loaders examined here solve the same problem, get BEACON running without dropping a recognizable BEACON PE, in two different ways.

6.2 TEARDROP: a service loader that reads a “JPEG”

TEARDROP (1817a5bf…, on-disk name NETSETUPSVC.DLL) is a Windows-service loader. It is not self-contained: it reads its payload from an external file masqueraded as a JPEG, and maps the recognizable BEACON PE entirely into memory so it never touches disk. It decompiles to a compact 79 functions (IDA) / 201 (Binary Ninja). The call order:

NetSetupServiceMain()                       // ServiceMain export, runs as the NETSETUPSVC service
  └ CreateThread(StartAddress)
      └ StartAddress(): AddVectoredExceptionHandler(Handler) → sub_21514B2960()   // the loader

Running as a service ServiceMain is the persistence and the privilege: a service runs at boot, unattended, with high privilege. The real work is in the loader, sub_21514B2960:

// TEARDROP, IDA Hex-Rays
FileA = CreateFileA("festive_computer.jpg", 0x80000000, 3u, 0, 3u, 0x80u, 0);    // (1) open the "JPEG"
ReadFile(FileA, &hdr, 0x40u, );                                                 // (2) read 0x40-byte header
if ( hdr[0] == 0xFF && hdr[1] == 0xD8 ) { strcpy(Buf2,"JFIF"); memcmp(&hdr[6],"JFIF",4); }  // (3) looks like a JPEG

v7 = v4 ^ 0x7FFFFFFF;                        // (4) XOR-decode the body → the real BEACON shellcode
// __write_memory_part_0():  VirtualProtect(…, 0x40 /*PAGE_EXECUTE_READWRITE*/) + memcpy   // (5)
v9();                                       // (6) call the decoded shellcode IN MEMORY

Line by line:

  1. CreateFileA("festive_computer.jpg", …): open a file that, on disk, looks like an image. The payload is staged separately (by SUNBURST); the loader just consumes it. (Vendor reporting documents a sibling variant reading gracious_truth.jpg; the file name is per-intrusion, the technique is constant.)
  2. ReadFile(… &hdr, 0x40u …): read the first 64 bytes.
  3. hdr[0]==0xFF && hdr[1]==0xD8"JFIF": check the JPEG magic (FF D8) and the JFIF tag. The file is a structurally valid-looking JPEG header, so a cursory look, or a file type check, sees an image. The real payload sits past the header.
  4. v4 ^ 0x7FFFFFFF: XOR-decode the body. Until this runs, the BEACON shellcode is just obfuscated bytes inside an “image”; there is no executable BEACON on disk to scan for.
  5. VirtualProtect(…, PAGE_EXECUTE_READWRITE) + memcpy: copy the decoded bytes into memory and make that memory executable (0x40 = PAGE_EXECUTE_READWRITE). This is the moment the data becomes code.
  6. v9(…): call into the now-executable region: Cobalt Strike BEACON runs, in memory.

A disk scanner therefore sees a service DLL and a harmless .jpg: nothing that resembles BEACON. The payload only becomes code after the VirtualProtect(PAGE_EXECUTE_READWRITE) flip. (The loader also references a SOFTWARE\Microsoft\CTF registry path, an alternate payload/config source across variants.) This exact decode-and-map routine is what FireEye’s APT_Dropper_*_TEARDROP YARA matches, keying on the byte runs 4A 46 49 46 ("JFIF") and 53 4F 46 54 57 41 52 45 ("SOFTWARE") plus the decode opcodes.

6.3 RAINDROP: the same goal, fully self-contained

RAINDROP (be9dbbec…, on-disk name 7z.dll) is a second BEACON loader, built from a modified 7-Zip so it can ride a legitimate executable as a side-loaded DLL. It was used for lateral movement, spreading BEACON to additional hosts. The key difference from TEARDROP: RAINDROP carries the BEACON payload embedded inside itself; there is no external “JPEG,” no separate payload file. It is the larger of the two (797 functions IDA / 848 BN). Entry is via DllMain:

// RAINDROP, IDA Hex-Rays: DllMain
CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)post_pgo_initialization, nullptr, 0, nullptr);

Loading the DLL (via the legitimate host binary it side-loads into) is enough to kick off a worker thread. That thread then assembles and decrypts the embedded payload (a telltale 31× memmove to reconstruct it in pieces), maps it executable, and runs it in the classic loader sequence:

VirtualAlloc ×5  →  copy/decrypt payload  →  VirtualProtect ×2 (PAGE_EXECUTE_READ)  →  CreateThread

Same destination as TEARDROP, BEACON executing in memory, reached without an external payload file at all. RAINDROP is thus the more “fileless” of the pair: only the loader DLL is on disk.

6.4 What “memory-only” does and does not mean

“Memory-only” is widely misread as “nothing on disk.” That is wrong, and the distinction is a detection point:

Stage On disk? In memory only?
SUNBURST DLL Yes (it is a signed Orion file) No
Loader (TEARDROP NETSETUPSVC.DLL / RAINDROP 7z.dll) Yes, always No
TEARDROP payload (festive_computer.jpg) Yes, as a disguised file No
Cobalt Strike BEACON PE No, never dropped Yes

“Memory-only” means the recognizable BEACON PE is never written to disk; it exists only after the loader decodes and maps it. It does not mean the attack leaves no disk artifacts: the loader DLL is always present, and TEARDROP additionally leaves the disguised payload. For a defender, that is the opening: you will not find BEACON on disk, but you can find an anomalous service DLL (NETSETUPSVC.DLL), a side-loaded 7z.dll, a stray *.jpg that isn’t an image or, at runtime, a private memory region freshly flipped to PAGE_EXECUTE_READWRITE inside a service process.

6.5 What the operators did once inside (cited, not derived from the loaders)

Provenance boundary. Everything in §6.2–6.4 is read from the loader binaries. Everything in this section is vendor-documented incident response reporting from Mandiant, Microsoft, and Volexity. It is not a capability of the loaders here, and it was not derived from any artifact. It is included because it is where the campaign’s espionage objective was actually achieved.

BEACON in memory was a means, not an end. Once the operators had hands-on access in an escalated victim, the reporting describes a deliberately low-malware, credential-centric style: “living off the land,” using legitimate credentials and rotating per-victim infrastructure (often hosted in the US to dodge geolocation heuristics) so the activity blended with normal admin work.

The signature technique was an attack on identity infrastructure, not endpoints:

Hunting pivots that follow from this (for retrospective log review, per the same reporting): historic DNS for avsvmcloud[.]com, treating CNAME responses as a far stronger targeting signal than A records (Chapter 5); unexplained outbound web traffic from Orion servers; New-MailboxExportRequest / Get-MailboxExportRequest and archives under OWA paths; anomalous SAML token issuance with no corresponding AD FS event; and suspicious process ancestry such as rundll32.exe or cmd.exe spawned by wmiprvse.exe or solarwinds.businesslayerhost.exe, the very process SUNBURST lived in (Chapter 4).

6.6 Why this layer is the real damage

The loaders are the interesting reverse engineering; the identity tradecraft is the actual harm. Stacking the chain: a poisoned update gave the actor a foothold in the Orion service process, which SolarWinds documented as running under LocalSystem by default (the Epilogue); SUNBURST could use that inherited context to drop a loader; the loader ran BEACON in memory; and BEACON’s operators pivoted into the cloud by forging identity itself, reading the email of exactly the people whose email an intelligence service wants. That is the espionage objective from Chapter 2, completed.

It also explains why, for the escalated victims, patching was never enough. If the actor forged your federation trust, rebuilding the Orion box does nothing; remediation meant rotating the AD FS token signing certificate, reissuing secrets, and re-establishing trust. The policy response in Chapter 10 had to grapple with that burden. The product’s documented privilege model increased the potential reach of that foothold, but the artifacts examined here do not prove the exact path or success of every hands-on intrusion; that distinction is the Epilogue’s subject.

Sources & evidence

↑ Revisiting the SolarWinds Compromise