Martin's Blog

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:

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:

  1. The class name already exists, OrionImprovementBusinessLayer. The shape of the future backdoor’s home is already in place.
  2. 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.
  3. There is no Initialize, no Update, no JobEngine, no CryptoHelper; 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:

A caveat that matters for triage. Although both Mandiant and the decompilation classify 8890 as benign, 41 VirusTotal engines still flag it as trojan.sunburst. That is signature-matching on the OrionImprovementBusinessLayer class 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:

// 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:

  1. Read the PEB. hProcess is a handle to the running MsBuild.exe. The first ReadProcessMemory pulls 0x28 (40) bytes from the process’s Process Environment Block; the explicit NumberOfBytesRead == 40 check confirms the full read. The PEB is the root of a process’s user mode bookkeeping.
  2. Follow the pointer to ProcessParameters. A field in the PEB points to the RTL_USER_PROCESS_PARAMETERS structure; the second read pulls 0x80 (128) bytes of it (again length-checked). That structure holds, among other things, the process’s command line (pointer + length, captured here as v15/v14).
  3. 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.

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:

  1. if (!OrionImprovementBusinessLayer.IsAlive): a single-instance guard. IsAlive is backed by the named pipe 583da945-… (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.
  2. new Thread(OrionImprovementBusinessLayer.Initialize): the backdoor runs on its own background thread, entry point Initialize (the arming sequence dissected in Chapter 4). It does not block or alter the inventory routine’s normal work.
  3. 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.
  4. 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.PipeNetTcp endpoint option)
CoreBusinessLayerService.cs Benign product change (same endpoint option)
Properties/AssemblyInfo.cs Version bump 88909083; copyright year 20192020
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

↑ Revisiting the SolarWinds Compromise