Sign in

Indicators

/usr/bin/ssh on macOS 26.5 — Apple-signed, shipped on every Mac — produces six indicator records:

rule_id                                               evidence     matched
communication.protocol_decoder_with_shell_exec        RawString    <?xml version + system
communication.protocol_decoder_with_unbounded_memcpy  RawString    <?xml version + memcpy
communication.socket_fd_redirected_to_shell           Import       socket + connect + dup + system
discovery.sysinfo_uname                               Import       uname
discovery.sysinfo_gethostname                         Import       gethostname
discovery.files_walk_stat                             Import       stat

Every row is true. nm -u /usr/bin/ssh lists _socket, _connect, _dup, _dup2, _system, _execve and _memcpy; <?xml version="1.0" encoding="UTF-8"?> occurs six times — per arch slice, once in __TEXT,__info_plist and twice in the code-signature blob — and both string rows anchor on the __info_plist copy at 0x1000a0796. The third row is the reverse-shell primitive set, and ssh has it because ssh is a remote-shell program. An indicator says what a binary contains and what that pattern is for. It says nothing about whether the binary is hostile — and the layer that does draw that conclusion gets ssh wrong: three unique Communication rules aggregate to +30 and land Apple's own ssh at Suspicious.

195 rules scan one binary's strings, CFStrings, Objective-C selectors, imports, entitlements and non-executable section bytes, and emit one flat, unscored record per match across eight populated categories.

SectionAnswers
The indicator recordRule id, summary, category, evidence kind, matched text, optional address, ATT&CK ids, function hits — and no severity or score field
Category weights and tier floorsThe downstream verdict layer scores 10–25 points per unique rule id, caps each category at 45, floors the tier on any family or targeting hit — and does not abstain on Apple-signed binaries
Rule categoriesPersistence, Collection, Communication, Host interaction, Data manipulation, Anti-analysis, Malware family, Targeting — what each fires on, and the blind spot that follows
Match scope and rule gatesEverything matches at file scope, which is where the false positives come from; corroborator, count and length gates are what hold; the 923-rule catalog runs alongside and is never scored
Evidence kindsNine kinds — six from the baseline rules, three catalog-only — and the kind decides whether the hit carries an address
ATT&CK technique mappingSub-technique ids resolved against a pinned vendored snapshot; the 195 baseline rules reference 68 distinct techniques
Malware-family fingerprints28 plaintext string rules across 25 families: kinship, not attribution
LimitsNear-zero on Windows, no DGA detection, no decryption or unpacking, no scoring

The indicator record

One match produces one flat record, and that record has nowhere to put a verdict.

FieldCarries
Rule idStable identifier for the rule that matched
SummaryPlain-language statement of what the rule checks for
CategoryOne of eleven; eight are populated by the 195 baseline rules
EvidenceDiscriminant naming which surface the match came from
Matched textThe needle, the joined needle list for a composite, or the algorithm name for a byte-pattern hit (aes, curve25519)
AddressThe match's address, where one exists — a virtual address, except on PE section-byte hits, which report a file offset
ATT&CKZero or more MITRE technique ids
Function hitsThe functions whose code references that address

There is no severity field and no score field.

A 49,840-byte Mach-O dylib named libswiftos.dylib, shipped inside EvilEgg.zip and first seen 26 April 2026, produces exactly one record:

rule_id       targeting.bundle_id_apple_domain
category      Targeting
evidence      Entitlement
matched_text  com.apple.dt.runtime.swiftos
matched_va    null
att_ck        ["T1036.005"]   Masquerading: Match Legitimate Name or Location
function_hits []

The code-signature identifier claims Apple's namespace. The signature is genuine and chains to the Apple root, but the leaf certificate reads Developer ID Application: Andrej Sevostopol (49LJX6DH22), and 49LJX6DH22 is not on the Apple team-id allowlist — the same allowlist the signing-consistency surface uses, so both surfaces cannot disagree about what "Apple-signed" means. There is no code address to point at, because the claim lives in the signature blob rather than in an instruction, so matched_va is null and function attribution is empty. That single row is the whole output for that file.

Category weights and tier floors

Weighting is downstream, and its denominator is distinct rule ids, never matches. A 20,172,924-byte unpacked Mach-O labelled IPStorm produces 30 hits: 8 Communication, 20 Data-manipulation, 1 Host-interaction, 1 Malware-family. The verdict layer's roll-up over that hit stream is two rows:

+40  malware.indicators_communication    4 unique Communication hits: cgi.helper_with_shell_exec,
                                         communication.protocol_decoder_with_shell_exec,
                                         communication.socket_fd_redirected_to_shell,
                                         recon.external_ip_lookup
+25  malware.indicators_malware_family   1 unique MalwareFamily hits: family.ipstorm.stub

Eight Communication hits became four scoring units. Five separate recon.external_ip_lookup strings — ident.me, ifconfig.io, icanhazip.com, myexternalip.com, checkip.amazonaws.com, at five different addresses — count once.

CategoryPoints per unique ruleCap (points)Tier floor
Communication1045
Collection1045
Anti-analysis1545
Malware family2545Malicious
Targeting2545LikelyMalicious
Persistence0 — no ledger row; feeds the co-occurrence rules
Host interaction0 — no ledger row; feeds the co-occurrence rules
Data manipulation0 — no ledger row; suppresses the decryptor-loop detector
Load code0 — no rules to fire

The cap holds however many rules fire in a category. The two floors are applied outside the sum: any Malware-family hit forces the verdict to at least Malicious, any Targeting hit to at least LikelyMalicious. Tier boundaries are 15, 35 and 70 points, so the 10-point Communication step is coarse: two unique rules (+20) clear Benign, four (+40) reach LikelyMalicious, and the 45-point cap sits one rule above that line — ssh's three and IPStorm's four fall either side of that line.

The verdict layer's platform-signed abstention does not cover this roll-up, which is why ssh scores at all. A Mach-O whose CMS chain is anchored to an Apple root short-circuits 51 of the malware engine's detector modules and the entire co-occurrence layer to an empty result, and /usr/bin/ssh satisfies it exactly — signer Software Signing, Apple Inc., anchored. The roll-up checks only whether an indicator surface exists at all, so three Communication rule ids still become +30 on a first-party Apple binary. /usr/bin/curl comes off worse: two indicator records, 43 points, LikelyMalicious — only 10 of those points from this surface, the other 33 from a likely-packed high-entropy code section (+18) and a __TEXT,__const measuring 7.994 bits per byte (+15).

Persistence, Host-interaction and Data-manipulation score zero individually because they fire on curl, jq and ripgrep. A zero category produces no ledger row at all rather than a zero-point one.

Persistence and Host-interaction re-enter through five co-occurrence rules, each needing at least two predicates from distinct tactic families. Data manipulation feeds none of them. A crypto hit naming aes, chacha20, salsa20 or des instead short-circuits the decryptor-loop detector to an empty result, on the reasoning that a binary shipping a real cipher's tables runs that cipher's rounds rather than a hand-rolled string deobfuscator. Seven of IPStorm's twenty crypto hits name one of those four, so they do not contribute nothing — they switch a detector off.

Co-occurrence rulePredicates requiredPointsTier floor
Autonomous botIngress transfer + persistence + network: raw socket, listener or outbound IP literal45LikelyMalicious
Staged implantDiscovery + ingress transfer + persistence45LikelyMalicious
Persistent evasive network processPersistence + defense evasion + raw socket or outbound IP literal45LikelyMalicious
IoT/DDoS botCredential-gated telnet scan + ingress transfer + raw socket18
Discovery with persistenceDiscovery + persistence18

Rows are evaluated top to bottom and the first match wins, so at most one ever fires. The whole layer also goes inert when two or more high-confidence library matches name one of six benign tool brands — curl, wget, openssh, systemd, cron, ripgrep. The bottom two rows float free of the tier floors because a persistence-less telnet scanner and a weak-credential auditor are structurally the same binary. The discovery predicate carries its own scar: a lone uname or gethostname import, present in essentially every portable C binary, satisfied discovery-plus-persistence, so strong discovery now demands an OS-inventory path literal, a file-enumeration shape, or two distinct discovery symbols.

None of that weighting is part of the indicator surface; it lives in Malware, and the same hit stream is consumed unweighted by anything that wants a stable feature vector instead of a verdict.

Rule categories

Every category's blind spot follows directly from what it matches on.

CategoryFires onBlind spot
Persistence36 literal paths and env names: /Library/LaunchAgents/, /System/Library/LaunchDaemons/, /etc/systemd/system/, /var/spool/cron/, /etc/ld.so.preload, /.config/autostart/, DYLD_INSERT_LIBRARIES, LD_PRELOAD=, six shell-profile dotfilesThe path must exist as one contiguous string. A plist path assembled at run time from a directory constant and a filename constant matches nothing
CollectionKeychain (login.keychain-db, SecKeychainItemCopyContent), browser stores (Google/Chrome/Local State, Firefox/Profiles, Safari/History.db), NoteStore.sqlite, Messages/chat.db, NSPasteboard, CGWindowListCreateImage, kAudioUnitSubType_VoiceProcessingIO, and three co-occurrence rules — SecItemCopyMatching with Safe Storage, and two osascript credential-prompt shapesNames the target, not the read. A clipboard manager and a stealer both produce collection.clipboard_pasteboard
Communication24 regexes over the string pool — IPv4-with-port, .onion, dynamic-DNS providers, punycode hostnames, Telegram-bot and Discord-webhook templates, pastebin and raw-GitHub, ngrok, Cloudflare Workers, DNS-over-HTTPS, Mirai and Mozi markers, stratum+tcp://, ICMP tunnel markers — plus 7 co-occurrence shapesAn IP literal is a string, not a connection. Nothing proves the address is ever passed to connect
Host interaction56 rules: shell invocations (/bin/sh -c, /usr/bin/osascript -e), post-compromise utilities (base64 -d, chmod +x, xattr -d com.apple.quarantine, SetFile -a V, spctl --master-disable), reverse-shell tokens (nc -e, /dev/tcp/, mkfifo), setuid escalation (chmod 4755, chmod u+s), anti-forensics (HISTFILE=/dev/null, /var/log/wtmp), BusyBox ingress, eleven system-inventory probes, three file-enumeration shapes, one prctl kernel-thread masquerade, and three browser-engine identity composites (Chromium, V8, WebKit) that name a runtime rather than a behaviourThe largest and the least discriminating category. It scores zero on its own for exactly this reason
Data manipulation17 byte patterns scanned over the non-executable sections: AES forward and inverse S-boxes, the Rcon table, the AES T-tables, MD5 / SHA-1 / SHA-256 / SHA-512 init words, the SHA-256 round constants, the ChaCha20 and Salsa20 expand 32-byte k sigma, the Curve25519 base point, both secp256k1 generator coordinates, DES S-boxes S1 and S2Only S1 and S2 of DES's eight boxes; no Blowfish, no RC4. Executable bytes are excluded — the whole __TEXT segment on Mach-O, .text on PE, any executable-mapped section on ELF — because instruction streams contain arbitrary four-byte runs that trip short S-box heads constantly. On Mach-O the exclusion is by segment, not by section, and it is not free: /usr/bin/ssh carries the Curve25519 base point in __TEXT,__const and reports no crypto hit for it. On PE the exclusion is by section name, so code in UPX1 or any renamed section is scanned in full. Mach-O leaks the other way: a zero-fill section (__bss, __common) records file offset 0, so the scan window becomes the image header and __TEXT — ssh's 1,750-byte __DATA,__bss and 5,504-byte __DATA,__common are both read that way, and a hit there is labelled with a section it did not come from
Anti-analysis13 rules for artifacts with no purpose but environment detection: the 12-byte CPUID hypervisor brands, VirtualBox and VMware guest device paths, Windows VM registry keys, DMI and sysfs hypervisor probes, VM MAC OUI prefixes, Wine exports, container sentinels, sandbox monitor DLLs and pipes, ThreadHideFromDebugger / PTRACE_TRACEME / TracerPidBare VMware, VirtualBox, QEMU, IsDebuggerPresent and /proc/1/cgroup are excluded as too dual-use, so a check written against a bare product name is missed by design
Malware family28 string fingerprints across 25 familiesString kinship, not attribution. Every needle is plaintext, so an encrypted or packed build of the same family carries none of them
TargetingOne rule: a com.apple.* code-signature identifier on a binary that is neither Apple-platform-signed (a nonzero CodeDirectory platform byte) nor signed by a team id on Apple's allowlistOnly Apple impersonation. A binary impersonating a non-Apple brand produces nothing here
Load codeEmpty tableDynamic-load facts come from three other surfaces: resolved, computed and unresolved call sites with full address provenance, on the dynamic-load surface; the Findings CWE-94 detectors, which anchor per call site; and the Capabilities catalog's 33 load-code rules

Two further categories — Impact (wipers, shadow-copy deletion, defense disabling) and Exploitation (kernel gadget resolution, driver loading, heap spraying) — exist in the taxonomy but are populated only by the capability catalog, and thinly: 16 Impact rules and 4 Exploitation rules out of that catalog's 923. A consumer can rely on the eleven-element enum regardless of which surface produced a hit.

Match scope and rule gates

Every baseline rule matches at file scope: a needle is present or absent in the whole binary, and a co-occurrence rule asks whether two needles are both present somewhere, not whether they are near each other. Composite hits carry the joined needle list as their matched text and the address of whichever needle was seen first, so socket + connect + dup + system is not an assertion that those four appear in one function. Most of this surface's errors follow from that.

What that costs, measured on three Apple binaries. The ls and curl rows are those binaries' complete indicator output — one record and two; the ssh row is one of the six at the top:

/bin/ls        discovery.files_recursive_walk      Import     fts_open
/usr/bin/curl  cgi.input_with_unsafe_strings       RawString  CONTENT_LENGTH + strcpy
               generic.curl_retry                  RawString  curl + --retry + --retry-delay
/usr/bin/ssh   communication.socket_fd_redirected_to_shell   Import  socket + connect + dup + system

curl implements --retry, so its help text names the flag; it handles CGI environment variables and links strcpy. The rule that flags a CGI handler reading attacker input with unsafe string primitives is describing curl's own documentation. ssh genuinely imports bind, listen and accept for port forwarding, which is what promotes its plist XML preamble to a protocol-decoder marker.

The rules that survive contact with benign software are the ones with an explicit second gate.

GateRuleSecond conditionWhat forced the gate
Corroboratorc2.http_data_postA C2-shaped host was already matchedEvery REST client embeds Content-Type: application/x-www-form-urlencoded
Corroboratorc2.dns_covert_channelTXT-record parsing, a DNS-over-HTTPS endpoint, or a base32 subdomain alphabetA raw libresolv import (res_query, ns_parserr, dn_expand) on its own yields an unscored provenance fact; the gate is what keeps dig and nslookup inert
CorroboratorThe <?xml version preamble, demoted to a weak markerA server-side socket import — bind, listen, accept, accept4, the connectionless recvfrom / recvmsg / recvmmsg, or the Winsock WSAAccept / WSARecv / WSARecvFrom — so a UDP receiver with no listening socket satisfies itApple's Weather app tripped it with zero listening sockets and all traffic outbound to weather.apple.com
Countevasion.analysis_tool_scan2 distinct names from a 25-name roster, three-character word-boundary names like gdb and ida includedNaming one debugger or packet sniffer proves nothing
Countevasion.security_product_scan3 distinct names from a 26-name roster, every name at least four charactersA stricter bar because inventory tooling legitimately lists one or two AV products
Length anchorThe six shell-profile dotfile needlesUnanchored only inside observations of 128 bytes or less; longer than that they must be preceded by / or ~ripgrep's file-type glob table lists .zshrc and *.bashrc as type names inside one large merged blob, and matched as persistence without it

Both rosters were pruned by hand. The four-character bar excludes short ambiguous AV tokens like avp, and egui — a widely used Rust GUI crate — was left off rather than false-positive across an ecosystem. The analysis-tool roster dropped only r2, a common CPU register name, with radare2 standing in for it.

Rules with no such gate are exactly where the noise is. persistence.users_shared is the literal /Users/Shared/. crypto.curve25519_basepoint is the byte 0x09 followed by 31 zero bytes, a shape any zero-padded small constant reproduces: on the IPStorm sample it fired 13 times, at 13 addresses, four in __DATA,__noptrdata and nine in __DATA,__data.

Function-scoped and basic-block-scoped matching exists, but not here. The baseline has nothing but file scope; the Capabilities catalog's 923 rules spread across four scopes:

Catalog rule scopeRules
Function630
Basic block144
Whole file144
Non-binary container — OLE document, .lnk shortcut, browser extension, installer5

The two run together and the baseline wins on rule-id collision — a catalog hit whose id already fired at baseline is dropped, so the same behaviour is never counted twice.

None of the catalog's 923 rules is scored: catalog hits land in a separate enrichment surface with no verdict exposure, and only the baseline surface is weighted. The one re-entry path is a curated promotion table of 35 capability ids, each carrying the strength band of the hand-written detector it replaced, unchanged since 20 July 2026.

Evidence kinds

The evidence discriminant names which surface produced the matched text, and it decides whether the hit has an address at all. Nine kinds exist; six can be produced by the baseline rules, three only by the capability catalog.

EvidenceProduced bySourceAddress
Raw stringBaselineC-string poolThe string's VA, and the functions that reference it via the reverse data-xref index
CFStringBaselineDecoded CFString bodyThe struct's VA, same attribution
SelectorBaselineObjective-C selector nameNone — selector VAs are not threaded through the method records, so every selector hit is unanchored
ImportBaselineImport symbol, unioned with the direct-syscall surface so a stripped static ELF still matches. Also stamped on exported symbols — the record has no separate export kind, so a reader cannot tell the two apartNone. Import-evidence hits carry no address and no function attribution at all
EntitlementBaselineCode-signature identifier or entitlement keyNone — the claim is metadata, not code
Section bytesBaselineByte-pattern match in a named non-executable sectionSection base plus match offset — an absolute VA on Mach-O and ELF; on PE the recorded base is the section's file offset, so every PE byte hit reports a file offset in an address field
Recovered stringCatalogPlaintext produced by a family-specific static decryptorNone — the decryptor's output has no location in the file
Stack stringCatalogText reassembled from a run of constant stores into a function's frameThe constructing function's entry, because no data address exists
Go import pathCatalogA module the linker recorded, or a package compiled into the imageA provenance claim, not a byte location

Stamping any of the last three as a raw string would over-claim. A recovered string existed on disk only encrypted, so a benign binary carrying the same words in cleartext can never produce one. A stack string was never a contiguous byte run anywhere in the file. A Go import path asserts provenance rather than "these characters occur somewhere".

ATT&CK technique mapping

Rules declare canonical technique ids in sub-technique form, resolved against a vendored snapshot that is regenerated from a pinned MITRE CTI commit rather than fetched. The snapshot holds three tables.

TableSizeContents
macOS techniques430Techniques with macOS in their platform list
Enterprise techniques835Enterprise techniques across all platforms
Native-binary set712Enterprise techniques on a compiled platform, minus the pre-compromise tactics a binary cannot exhibit; the denominator for coverage accounting

430 is the size of the taxonomy, not a claim about coverage. The 195 baseline rules reference 68 distinct techniques between them; the capability catalog references 184. A technique row with no referencing rule is a lookup entry and nothing else.

Four of those 68 are not macOS techniques and resolve through the full enterprise table.

TechniqueName
T1003.008/etc/passwd and /etc/shadow
T1543.002Systemd Service
T1547.013XDG Autostart Entries
T1583.001Domains

T1583.001 sits in the resource-development tactic, which the twelve-bucket per-tactic vector deliberately omits along with reconnaissance, so c2.idn_punycode_hostname — the rule that declares it — contributes to a sentinel count rather than to a tactic column.

Declarations, in the form every rule uses:

RuleTechniques
persistence.launch_agentT1543.001 — Create or Modify System Process: Launch Agent
shell.curl_pipe_shT1105 + T1059.004 — Ingress Tool Transfer + Unix Shell
crypto.aes_sbox_forwardT1027 + T1573.001 — Obfuscated Files or Information + Symmetric Cryptography
collection.keychain_login_dbT1555.001 — Credentials from Password Stores: Keychain
targeting.bundle_id_apple_domainT1036.005 — Masquerading: Match Legitimate Name or Location

Malware-family fingerprints

Twenty-eight rules cover twenty-five families: eight curated from Patrick Wardle's Objective-See 2025 macOS malware roundup, twenty older stubs. Four of the eight roundup rules require several needles to co-occur; the other four are single needles.

FamilyNeedles requiredSource
DigitStealer8.8.8.8 + dig + TXT2025 roundup
NightPawcodesign --entitlements + --force2025 roundup
AMOS (first rule)An AppleScript with hidden answer prompt + login.keychain-db2025 roundup
AMOS (second rule)Exodus/exodus.wallet/ + Telegram Desktop/tdata/2025 roundup
ChillyHellThree separate one-predicate rules: com.apple.qtop.plist, Utils::RunCommand, Utils::KillProcess2025 roundup
Phexiacom.user.gfskjsnghdjsvuxj.plist2025 roundup
Shlayerflashupdate + curl + shasumLegacy stub
OSAMinerminergate + xmrLegacy stub
IPStormstorm_ + p2pLegacy stub
CloudMensis, XCSSET, CrescentCore, Silver Sparrow, Pirrit, Dacls, KeRanger, EvilQuest, WindTail, FruitFly, MacRansom, OceanLotus, Komplex, Snake, XAgent, Lazarus, WireLurkerA single string eachLegacy stubs (17)

family.ipstorm.stub is the weakest rule in the table, and it is the one that fired in the ledger above — two short tokens, present anywhere in a 20 MB binary, worth 25 points and a hard floor to Malicious.

A family hit means this binary shares strings with samples attributed to family X. It does not mean the binary is family X. Most AMOS builds ship encrypted, so the plaintext rules only reach the unpacked variants; attributing the packed bulk is a similarity problem, and the corpus-relative nearest-neighbour classifier that does it lives in Malware rather than here, because a claim about a binary's neighbours is not a fact about its bytes.

Limits

Windows. The rule table is shaped around Unix paths, shell tokens and Mach-O signing, and most Windows samples produce nothing at all. Measured over every Windows sample in the malware training corpus — 344 .exe and 32 .dll of its 1,729 samples, of which 375 completed and one 4.5 MB executable did not finish inside a five-minute analysis budget:

Records producedSamples
0259
149
232
3–918
10–6215
599 and 6012 — 25 MB executables, 577 of each being crypto.curve25519_basepoint, the ungated zero-run rule above

116 of the 375 produced any record at all. The tail is not Windows coverage, it is one rule misfiring at scale: crypto.curve25519_basepoint accounts for 1,560 of the 1,789 records emitted across the whole PE corpus, and crypto constants for 1,664 of them. Dropping the two 25 MB outliers barely moves the ratio: crypto constants are 406 of the remaining 589 records. Windows coverage on this surface is carried almost entirely by the capability catalog.

No decryption or unpacking. The two narrow exceptions — family-specific static decryptors and stack-string reconstruction — belong to the catalog and surface under their own evidence kinds, so neither can pass as a cleartext match. Unpacking happens upstream in the pipeline; Malware covers how a verdict follows the unpacked payload rather than the stub.

No DGA detection. The hostname rules name specific abused providers — duckdns, no-ip, hopto, zapto, ddns, dynu, ngrok, workers.dev, pastebin.com/raw/, raw.githubusercontent.com, api.telegram.org/bot, Discord webhooks — plus .onion and Punycode labels. There is no suspicious-TLD rule and no generated-domain scoring: a freshly registered algorithmic .xyz hostname matches nothing here.

No embedded YARA engine, no live ATT&CK fetch, no rule feed. Baseline rules are in-tree constants and the catalog is TOML compiled in at build time; both are diffable in git and versioned with the engine, and updates ship with engine releases. One runtime rule directory does exist — an operator-supplied override layered over the embedded catalog at startup, aborting the boot if it fails to parse — but it feeds the CWE catalog behind Findings, not this surface.

No scoring, severity or model. The surface emits features. Weighting, tier floors and verdicts are downstream and separable.