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.
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.
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.
| Field | Value |
|---|---|
| File Type | ELF 64-bit LSB executable, x86-64 |
| Size | 8.0 MB (8,388,608 bytes) |
| Linking | STATICALLY LINKED no shared library dependencies, runs on any x86-64 Linux |
| Symbols | STRIPPED all debug symbols removed |
| Packing | NOT PACKED entropy ~6.0 bits/byte, strings plaintext-readable |
| Version | XMRig 6.21.3 |
| Compile Date | April 23, 2024 |
| Architecture | x86-64 (analyzed via qemu-x86_64-static on ARM64) |
| Section | Size | Purpose |
|---|---|---|
| .text | ~5.8 MB | Executable code: RandomX, KawPow, CryptoNight algorithms |
| .rodata | ~900 KB | Read-only data: string literals, SSL cert data, config templates |
| .data | ~500 KB | Initialized data |
| .bss | runtime | Uninitialized, RandomX dataset buffers allocated here at runtime |
| Capability | String Evidence | Purpose |
|---|---|---|
| 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 |
Executed via qemu-x86_64-static on Ubuntu 22.04 ARM64. Traced with strace -f -e trace=network,clone,openat.
Captured with tcpdump -i any -w xmrig_traffic.pcap during a 15-second live run targeting pool.supportxmr.com:443.
pool.supportxmr.com → CNAME pool-nyc.supportxmr.com192.168.64.2:60450 → 104.243.43.115:443 [SYN]192.168.64.2:60450 → 104.243.43.115:443 [PSH] length=480104.243.43.115:443 → VM [FIN] · XMRig log: "read error: end of file"192.168.64.2:45036 → 104.243.33.118:443 [SYN] → same sequenceA 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.
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.
| Type | Value |
|---|---|
| SHA-256 | 72ac2877c9e4cd7d70673c0643eb16805977a9b8d55b6b2e5a6491db565cee1f |
| MD5 | 7429d24207b100f6c164bf4703b5941e |
| ELF Build-ID | 989c8e3124a392451d99d52d4ffe7c9e75b887f2 |
| Type | Value | Source | Context |
|---|---|---|---|
| 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 |
| Type | Indicator | Significance |
|---|---|---|
| File access | /sys/fs/cgroup/cgroup.controllers | Container/Kubernetes detection at startup |
| File access | /proc/self/cpuset | CPU isolation detection (container CPU limits) |
| File access | /dev/cpu/*/msr | MSR register access for CPU optimization |
| Syscall | clone(CLONE_VM|CLONE_THREAD) | Worker thread creation before any I/O |
| Network | TCP/443 to non-HTTPS endpoints | Stratum-over-TLS evasion pattern |
| Process | Sustained CPU >90% on all cores | RandomX mining at max thread count |
stratum+ in TLS SNI or TCP payload on port 443. Stratum-over-TLS is the primary evasion, so port/IP blocking alone is insufficient.*.xmrig.com, *.supportxmr.com, *.moneroocean.stream at DNS firewall. Kills pool connectivity without needing to identify the binary./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.)resources.limits.cpu) and Docker (--cpus). Even if XMRig deploys successfully, CPU caps prevent effective mining, an economic disincentive for attackers.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.
| Sample | Coin | Algorithm | Source |
|---|---|---|---|
| xmr-stak-rx 1.0.5 | Monero | RandomX | github.com/fireice-uk/xmr-stak (official release) |
| cgminer | Bitcoin | SHA-256d | Ubuntu 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.
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.
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.
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.
§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.
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.
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.
| Field | Value |
|---|---|
| File Type | Mach-O universal binary, x86_64 and i386 |
| Size | 38 KB |
| Min OS | macOS 10.6 (Snow Leopard) |
| Bundle Name | FlashUpdate.app |
| Lure | Fake Adobe Flash updater |
| Capability | Evidence |
|---|---|
| Process execution | NSTask, fork, execvp |
| File operations | createDirectoryAtPath, writeToFile, removeItemAtPath |
| Timestamp manipulation | fileCreationDate, fileModificationDate, chmod |
| Self-location | mainBundle, bundleWithPath, executablePath |
| Payload execution | launch, wait |
LSBackgroundOnly=true, no Dock iconmainBundle → executablePath finds its own path on diskcreateDirectoryAtPath + writeToFile writes secondary payloadNSTask.launch() → wait runs dropped payload, blocks until doneremoveItemAtPath + chmod deletes dropper, sets persistence permissionsFilename translates from Vietnamese as "Detailed content," a classic OceanLotus spear-phish lure disguised as a document relevant to Vietnamese political topics.
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.
CC_MD5 generates bot ID from hardware fingerprintdeflate (zlib) compresses stolen dataCCCrypt (AES-128) encrypts compressed data with key from __DATAsend() over raw TCP, bypasses NSURLSession, avoids network loggingread → CCCrypt decrypt → inflate → dlopen/dlsym loads new modules dynamically| Variant | Type | Value |
|---|---|---|
| A · FlashUpdate.app | SHA-256 | 12f941f43b5aba416cbccabf71bce2488a7e642b90a3a1cb0e4c75525abb2888 |
| A | MD5 | 9831a7bfcf595351206a2ea5679fa65e |
| A | Bundle ID | com.apple.Application FAKE |
| B · Noi dung chi tiet | SHA-256 | 07154b7a45937f2f5a2cda5b701504b179d0304fc653edb2d0672f54796c35f7 |
| B | MD5 | d802aa9938e87dc33cf2c7a07e920b0b |
| B | AES key (in __DATA) | 65ed295dca5f6ea6d2e300f4792fcb55 |
| Type | Indicator |
|---|---|
| File access | IOPlatformSerialNumber read at startup |
| Network | Raw TCP socket to encrypted C2 (no HTTP/HTTPS headers) |
| File | ~/Library/LaunchAgents/com.apple.<random>.plist |
| Process | Background-only, no Dock icon, no menu bar |
| Syscall | dlopen on unsigned .dylib from ~/Library |
| Crypto | CCCrypt called before any network connection |
LSBackgroundOnly=true plus LSUIElement=true in unsigned or ad-hoc-signed .app bundles. Legitimate background apps are almost always properly signed.IOPlatformSerialNumber queries from non-Apple, non-MDM processes within 5 seconds of launch. Hardware fingerprinting is rare in legitimate third-party apps.dlopen loading unsigned .dylib files from ~/Library/Application Support, a dynamic plugin loading pattern used to add capabilities post-infection.~/Library/LaunchAgents/ for new plists with com.apple.* bundle IDs that are not Apple-signed, a common OceanLotus persistence pattern.CCCrypt before establishing any network connection, since this precedes C2 config decryption in this family.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.
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.
| Capability | Variant 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 |
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.
Sourced from theZoo public malware repository (OSX.Lazarus). The archive contains three files consistent with a trojanized macOS installer package.
| File | Type | Size |
|---|---|---|
| Installer package | xar archive (.pkg) | 13 MB |
| Payload executable | Mach-O 64-bit x86_64, PIE | 40 KB |
| Bundled resource | zlib compressed data | 20 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).
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.
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.
| Capability | Mangled Symbol | Demangled |
|---|---|---|
| Process enumeration | 14GetProcessListv | GetProcessList() |
| Exfiltration | 6uploadERK7QStringRK10QByteArray | upload(QString const&, QByteArray const&) |
| Payload obfuscation | i8XorCryptR10QByteArray | XorCrypt(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.
| Type | Value |
|---|---|
| Installer SHA-256 | ae4be6343ba403a264c0f0e5ccff169648dc854f0a71d6509f38b018ce325042 |
| Payload SHA-256 | 54c6107c09f591a11e5e347acad5b47c70ff5d5641a01647854643e007177dab |
| Payload MD5 | b63e8d4277b190e2e3f5236f07f89eee |
| C2 DOMAIN | wb-bot.org |
| C2 ENDPOINT | https://www.wb-bot.org/certpkg.php |
| Behavioral | HTTP POST, filename="temp.gif" but Content-Type: application/octet-stream (mismatch) |
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.
Analyzing two independently attributed nation-state actors targeting the same platform surfaces both convergent and divergent tradecraft.
| Dimension | OceanLotus (Vietnam) | Lazarus (North Korea) |
|---|---|---|
| Motive | Political / espionage | Financial (crypto theft) |
| Delivery | Spear-phished document | Trojanized .pkg installer |
| UI framework | Native Cocoa/Objective-C | Cross-platform Qt |
| C2 transport | Raw TCP, custom protocol | HTTP POST, mimics web traffic |
| C2 config | AES-128 encrypted, decrypted only in memory | Single hardcoded HTTPS URL, unencrypted |
| Data-in-transit obfuscation | AES-128 (CommonCrypto) | XOR, plus fake-image disguise |
| Fingerprinting | IOPlatformSerialNumber via IOKit | DiskArbitration + IOKit |
| Extensibility | dlopen/dlsym runtime plugins | Not 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.
| Phase | Tools Used |
|---|---|
| Sample acquisition | theZoo GitHub repo, unzip -P infected |
| File identification | file (Linux correctly parses Mach-O headers) |
| Hash generation | sha256sum, md5sum |
| String extraction | strings |
| Binary structure | llvm-objdump --macho --all-headers |
| Dylib imports | llvm-objdump --macho -p |
| Hex inspection | xxd |
| Installer/xar parsing | Python3 struct + zlib (manual xar header parse) |
| Encoded strings | Python3 (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.
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.
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.
Public analysis established the payload operated in three stages:
description field matches a specific value, and uses it as an AES-256 decryption key. Wrong environment = garbage output, not malicious-looking code.bitcore-wallet-client/lib/credentials.jsWalks 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.
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.
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.
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.
| Type | Value | Context |
|---|---|---|
| C2 DOMAIN | copayapi.host | Typosquat of legitimate Copay API |
| C2 IP | 111.90.151.134 | Hardcoded fallback |
| RSA KEY | b30e416c38497b6a3f2d93106bd8b252fe232f5b11f1342b681aa4397aa64391 | 2048-bit, SHA-256 of DER |
| Payload SHA-256 | d85a3e5172832dadfb0773d52be02236205146f5c260661fcccaab659021b0ac | As preserved |
| Compromised package | flatmap-stream | Removed from npm 2018-11-26 |
| Compromised dependent | event-stream v3.3.6 | Added the malicious dependency |
crypto, http, and reads the host's own package.json fields at runtime, none of which are needed for stream manipulation.SomeClass.prototype.someMethod while preserving and calling through to the original, a common way to intercept sensitive data without breaking legitimate functionality.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.
| Phase | Tools / Sources Used |
|---|---|
| Sample acquisition | Public GitHub gist (preserved payload), npm registry API, GitHub code search |
| Timeline reconstruction | npm's blog post, Snyk's post-mortem, academic incident paper |
| Static analysis | Direct source reading (JavaScript, no compilation needed) |
| C2 indicator decoding | Python3 (bytes.fromhex) |
| RSA key fingerprinting | OpenSSL (rsa -pubin -text, DER + SHA-256) |
| Hashing | sha256sum, 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.