3. Compromising SolarWinds: The Skeleton and the Backdoor
The popular telling of SolarWinds collapses into a single sentence: “they hid a backdoor in an update.” The reality is more disciplined and, for a reverse-engineer, more interesting. The actor did not insert a backdoor and hope. They ran a rehearsal, shipping an empty backdoor class in a real, signed release months before the real one, purely to learn whether tampered code could ride the SolarWinds build pipeline unnoticed. Only once that succeeded did they build the machine, SUNSPOT, that welded the working backdoor into Orion at compile time, then quietly restored the clean source so nobody at SolarWinds would see a thing.
This chapter follows that sequence in order: the foothold (Act 0), the skeleton (Act 1), the injector (Act 2), the single line of injected code (the trigger), and the first weaponized release (Act 3). Three pieces of code carry the story, and each is annotated line by line.
3.1 Act 0: Initial access (the gap)
SolarWinds’ own investigation places the first unauthorized access on or about 4 September 2019. That date, and the vectors behind it, are the single biggest hole in the public record, and this book flags it rather than papering over it:
- The day-level date is secondary reporting of SolarWinds’ investigation timeline, not a fact derived from the artifacts examined for this book.
- The initial access vector was never conclusively published. SolarWinds said its investigators considered compromised credentials and/or third-party application access via a then-zero-day vulnerability the most likely paths, but the company did not publicly resolve the question. Theories of weak/leaked credentials and public application exploitation circulated; none was confirmed.
- Equally undocumented is how SUNSPOT reached the build server: the path from the initial foothold to code execution on the Orion build infrastructure.
So the story this chapter can tell with confidence starts once the actor already had access to the build environment. What they did with that access is recoverable in detail, because the output, the DLLs, is available for analysis. The entry is not, and nobody should pretend otherwise. The gap remains explicit in the appendix timeline.
3.2 Act 1: The skeleton: a dry run in a signed release (October 2019)
Before risking a working backdoor, the actor tested the delivery mechanism. On
10 October 2019, compile timestamp 2019-10-10 13:26:39, locally verified from the
assembly, SolarWinds shipped Orion build 2019.4.5200.8890. Its
SolarWinds.Orion.Core.BusinessLayer.dll (a25cadd4…, carried inside
2019.4.5220.20161-CoreInstaller.msi) contains a class with a name that would later become
infamous, OrionImprovementBusinessLayer, but at this point the class is an empty
eleven-line shell:
// OrionImprovementBusinessLayer.cs (decompiled from benign build 8890)
internal class OrionImprovementBusinessLayer
{
public static bool Is64BitOperatingSystem()
=> Environment.Is64BitOperatingSystem;
}
Read it closely, because the absence is the point:
- The class name already exists,
OrionImprovementBusinessLayer. The shape of the future backdoor’s home is already in place. - Its only method is a one-line wrapper around a stock .NET property
(
Environment.Is64BitOperatingSystem). It does nothing a backdoor would do: no thread, no timer, no network, no registry, no process checks. - There is no
Initialize, noUpdate, noJobEngine, noCryptoHelper; none of the machinery dissected in Chapter 4. This is a placeholder, not a payload.
This is the trial run. The actor’s question was narrow: can we slip an extra class into a signed SolarWinds release and have it ride the build-and-sign pipeline out to customers without anyone noticing? The answer was yes. Mandiant independently classifies this exact hash as Benign, “suspected attacker testing,” which is precisely how the decompiled evidence reads: a named, harmless stub, deliberately innocuous so that if anyone did look, there was nothing to find.
Two corroborating details:
- The timeline (secondary reporting). SolarWinds’ investigation, as relayed by secondary reporting, dates the trial code injection to ~12 September 2019 and its removal to ~4 November 2019, i.e. the placeholder lived in the build for roughly the period around this October release, then was pulled once it had served its purpose. These day-level dates were not independently verified here; the compile timestamp of the shipped DLL was.
- A second copy, re-signed a day later. A near-duplicate of the
8890DLL re-signed on 2019-10-11 exists asd3c6785e…, consistent with the actor also testing the signing step itself, not just the source injection.
A caveat that matters for triage. Although both Mandiant and the decompilation classify
8890as benign, 41 VirusTotal engines still flag it astrojan.sunburst. That is signature-matching on theOrionImprovementBusinessLayerclass name and assembly structure, not on any backdoor behaviour, because there is none in this build. A high detection count is not, by itself, evidence of malicious capability; the static analysis is the authority. The empty stub is the experimental control against which the weaponized builds are diffed (§3.5).
3.3 Act 2: SUNSPOT and the weaponized build pipeline (February 2020)
The trial proved the pipeline could be subverted. The next step was to do it for real, without
committing a backdoor to SolarWinds’ source repository, where a code review or a git history
might expose it. The actor’s solution was SUNSPOT: a build server implant that altered the
source in the compiler’s working tree, at build time, and then put it back.
SUNSPOT is upstream of every SUNBURST DLL examined for this book; the four Orion builds are
its output, not the malware itself. The injector was examined as a standalone companion sample
(c45c9bda…; on-disk name taskhostsvc.exe), with a PE TimeDateStamp of
2020-02-20 11:40:02 UTC read directly from the sample’s COFF header, matching CrowdStrike’s
analysis. It decompiles to 1,811 functions (IDA) / 2,102 (Binary Ninja). Its mechanism,
recovered from the binary and corroborated by CrowdStrike’s write-up:
- Persistence. It survives reboot via a boot scheduled task, and serializes itself with
a mutex (
{12d61a41-4b74-7610-a4d8-3028d2f56395}) and an innocuously-named log file (C:\Windows\Temp\vmware-vmdmp.log). - The watch loop. It runs a roughly one-second loop watching the process list for
MsBuild.exe, the MSBuild compiler. A build of Orion is its trigger. - Finding the source. When MSBuild is running, SUNSPOT reads the compiler’s own process memory to discover which solution is being built and where its source lives. This is the first annotated snippet, a textbook remote PEB walk, in IDA’s recovered pseudocode:
// SUNSPOT, IDA Hex-Rays: sub_140002840
if ( ReadProcessMemory(hProcess, lpBaseAddress, Buffer, 0x28u, &NumberOfBytesRead) // (1) PEB
&& NumberOfBytesRead == 40
&& ReadProcessMemory(hProcess, v12, v13, 0x80u, &NumberOfBytesRead) // (2) ProcessParameters
&& NumberOfBytesRead == 128
&& (v2 = j__malloc_base(v14 + 2LL)) != nullptr )
{
ReadProcessMemory(hProcess, v15, v2, v14, &NumberOfBytesRead); // (3) command line
}
// debug strings nearby in the binary: "[ERROR] Step16 fails", "[ERROR] Step14 fails with error %#x"
Walking it:
- Read the PEB.
hProcessis a handle to the runningMsBuild.exe. The firstReadProcessMemorypulls0x28(40) bytes from the process’s Process Environment Block; the explicitNumberOfBytesRead == 40check confirms the full read. The PEB is the root of a process’s user mode bookkeeping. - Follow the pointer to
ProcessParameters. A field in the PEB points to theRTL_USER_PROCESS_PARAMETERSstructure; the second read pulls0x80(128) bytes of it (again length-checked). That structure holds, among other things, the process’s command line (pointer + length, captured here asv15/v14). - Read the command line. With a buffer allocated (
malloc(v14 + 2)), the third read copies the command line out of MSBuild’s address space. From the command line SUNSPOT learns which Orion solution is compiling and the path to its source tree: the information it needs to find the file to swap.
The "[ERROR] StepNN fails" strings are SUNSPOT’s own internal stage labels (CrowdStrike’s
documented “StepNN” logic). The implant is carefully staged and checks each step’s success,
because a build failure would be exactly the kind of anomaly that gets a developer’s attention.
- The swap, then the restore. Having located the source, SUNSPOT replaces the legitimate
source file (
InventoryManager.cs, plus the backdoor class) with a SUNBURST-injected copy (stored inside SUNSPOT as AES-128-CBC-encrypted blobs), lets MSBuild consume it, then restores the clean source and hash-checks its own work so the build neither breaks nor looks altered afterward. The repository is never touched; only the bytes the compiler reads, for the few seconds it reads them, are different. - Before signing. All of this happens before the code signing step. That is why the resulting DLLs carry what vendors documented as a valid SolarWinds Authenticode signature: the tampering is invisible to signing because it precedes it. (Per this book’s provenance rule, the valid signature fact is vendor-documented, not verified in this analysis; no Authenticode verifier was available.)
That last point is the crux of the whole supply-chain attack. The signature was never forged or stolen; it was applied, legitimately, by SolarWinds’ own pipeline, to code the actor had already swapped in. Every downstream trust decision (the customer’s and the OS’s) was made correctly, against a signature that was genuinely valid, on a binary that was genuinely malicious.
3.4 The single injected change: the trigger
For all of SUNSPOT’s machinery, the change it actually injects into otherwise-legitimate Orion
code is astonishingly small. It is one block, added inside the legitimate
InventoryManager background inventory routine. Recovered from the compiled weaponized build:
// BackgroundInventory/InventoryManager.cs (the only edit to legitimate code)
try
{
if (!OrionImprovementBusinessLayer.IsAlive)
{
Thread thread = new Thread(OrionImprovementBusinessLayer.Initialize);
thread.IsBackground = true;
thread.Start();
}
}
catch (Exception)
{
}
Line by line, this is a study in blending in:
if (!OrionImprovementBusinessLayer.IsAlive): a single-instance guard.IsAliveis backed by the named pipe583da945-…(Chapter 4); if the backdoor is already running, do nothing. This keeps exactly one copy alive and is also why the implant doesn’t trip over itself across the multiple threads of the Orion service.new Thread(OrionImprovementBusinessLayer.Initialize): the backdoor runs on its own background thread, entry pointInitialize(the arming sequence dissected in Chapter 4). It does not block or alter the inventory routine’s normal work.thread.IsBackground = true: a background thread won’t keep the process alive on its own; the backdoor is a passenger on the Orion service’s lifetime, not an anchor.catch (Exception) { }: a silent catch-all. If anything in the backdoor’s startup throws, the host inventory routine carries on as if nothing happened. From the perspective of Orion’s own code, this block is invisible: it never errors, never logs, never changes the inventory result.
That is the entire bridge from “dormant code in a DLL” to “running backdoor.” Twelve lines,
wrapped in a swallow-everything try/catch, launched from inside a routine whose normal job is
to refresh a hardware/software inventory, about as boring a place to hide a launch as exists in
the codebase.
3.5 Act 3: SUNBURST ships in the first weaponized release (March 2020)
The first fully weaponized build, 2019.4.5200.9083 (32519b85…), was produced on
24 March 2020 (locally verified compile timestamp: 2020-03-24 08:52:34) and delivered not
as a fresh installer but as a patch: SolarWinds-Core-v2019.4.5220-Hotfix5.msp (installer
hash d0d626de…). This is the precise moment a clean 2019.4 install became backdoored.
That the MSP is the patch which converts a clean install into a compromised one is provable
from the package metadata, not merely asserted. The Hotfix-5 MSP and the 2019.4 base MSI carry
the same product code {079A74C5-95D0-446E-86F7-B8EAF0A29654} in their summary streams,
establishing the patch lineage: the MSP upgrades the benign 8890 DLL to the weaponized
9083 DLL on top of an existing 2019.4.5220.20161 install. SolarWinds states the trojanized
DLL reached customers around 26 March 2020 (secondary reporting). (A sibling build,
2020.2.100.12299 / abe22cf0…, compiled 2020-03-25, exists in the vendor hash set but is not
among the four builds examined for this book.)
How much actually changed. This is where the trial build earns its keep as a control. Both 2019.4 builds of the DLL were decompiled with ILSpy and compared file-by-file. Each decompiles to 187 source files, and only six differ:
| File | Nature of change |
|---|---|
OrionImprovementBusinessLayer.cs |
MALICIOUS: the 11-line stub (§3.2) → a 3,346-line SUNBURST backdoor |
BackgroundInventory/InventoryManager.cs |
MALICIOUS: the 12-line trigger (§3.4) injected to launch it |
CoreBusinessLayerPlugin.cs |
Benign product change (a WCF Net.Pipe → NetTcp endpoint option) |
CoreBusinessLayerService.cs |
Benign product change (same endpoint option) |
Properties/AssemblyInfo.cs |
Version bump 8890 → 9083; copyright year 2019 → 2020 |
SolarWinds.Orion.Core.BusinessLayer.csproj |
Build metadata |
Four of the six differences are ordinary product churn. Two are the entire attack. The same
empty class name from October, OrionImprovementBusinessLayer, has grown from eleven lines
into a 3,346-line implant (ConfigManager, ProcessTracker, Job/JobEngine,
HttpHelper, DnsHelper, CryptoHelper, ZipHelper, …), and a twelve-line launcher has been
slipped into a background inventory routine to start it. Everything else in 187 files is
unchanged.
That diff is the cleanest possible proof of the skeleton-then-backdoor pattern: the same filename, the same class, the same delivery pipeline: first proven empty and harmless, then filled with the real thing. The backdoor that grew into that 3,346-line class is the subject of the next chapter.
Sources & evidence
- Book reference: See the Appendix: Timeline, IOCs, and Artifact Map for the consolidated timeline, hashes, and evidence-to-section map.
- Decompiled artifacts (locally verified): the benign
8890and weaponized9083builds were diffed file-by-file (187 files, 6 differ). TheInventoryManager.cstrigger,OrionImprovementBusinessLayer.csgrowth, DLL hashes and compile timestamps, and the MSP↔MSI shared product code were read directly from the artifacts. - SUNSPOT: the
c45c9bda…sample supplies theReadProcessMemoryPEB walk insub_140002840, the"StepNN"strings, PE timestamp, mutex, log path, and encrypted source blobs. - Provenance flags: compile timestamps are locally verified; the day-level 2019 dates (4 Sep / 12 Sep / ~4 Nov 2019, ~26 Mar 2020) and the initial access vector are secondary reporting / unresolved; the valid Authenticode signature is vendor-documented, not verified here.
- Public reporting: CrowdStrike’s SUNSPOT analysis, SolarWinds’ investigation timeline, and Mandiant’s SUNBURST analysis.
- Forward reference: the backdoor class → Chapter 4.