Someone double-clicks what they think is Maccy, a clipboard manager, and gets a macOS password prompt: "Maccy wants to make changes." They type it in. Most infostealers would grab whatever you typed and run. This one validates it against PAM first -- the same Pluggable Authentication Modules stack macOS uses to actually authenticate you -- and only keeps it if it's real.
That's PamStealer (Jamf's name; ManageEngine tracks the same campaign as "Fake Maccy Stealer"). Two stages, and the interesting part for practitioners isn't that it exists -- it's how deliberately it routes around the process-based signals most macOS detection keys on. It's quieter than the commodity norm in the places that matter, and loud in a couple of places it can't avoid. Here's the chain, then how to hunt it.
Stage 1: the dropper mostly doesn't shell out
Delivery is the usual macOS pattern with a twist. A compiled AppleScript, Maccy.scpt, ships on a disk image from a lookalike domain (maccyapp[.]com) and opens in Script Editor. Inside is an obfuscated JXA (JavaScript for Automation) payload that does the download.
The twist: it doesn't call curl or zsh. It fetches and stages through NSURLSession and the Objective-C bridge -- native APIs, no shell subprocess for the network stage. That kills the process-spawn signal a lot of behavioral rules lean on.
It is not processless, though, and that's what the first wave of coverage got wrong. Before launch it ad-hoc signs the staged bundle:
codesign -fs - --deep /path/to/staged.app
That's a codesign process spawned by Script Editor against a bundle in Application Support -- a detection opportunity both teardowns flag. It also drops a .Maccy marker file and launches hidden, no window, no Dock.
The config is gated. The dropper derives a key from a host fingerprint -- CPU architecture, locale, keyboard layout, time zone -- and the encrypted config only unlocks on a matching machine. ManageEngine notes those checks double as a CIS geo-fence: it aborts on hosts that look Russian or post-Soviet. Names and config values rotate sample-to-sample while behavior holds constant, which reads as an automated builder.
Stage 2: Rust, a runtime-loaded framework, and a pbpaste loop
The payload is a stripped, arm64-native Mach-O in Rust -- uncommon in a stealer scene Jamf notes is dominated by Swift, Go, and Objective-C. Most strings decode at runtime, so static triage gets you the linked libraries and the ObjC selectors and not much else.
The evasion move worth internalizing: instead of linking Security.framework, it loads it at runtime. Its keychain-access capability never lands in the static import table. If your triage leans on the load commands, you won't see it:
otool -L /path/to/suspect.bin # Security.framework won't be listed
It bundles its own SQLite and reads browser credential, cookie, and wallet-extension DBs as files directly. Exfil is encrypted with ChaCha20-Poly1305, so the traffic resists inspection even though the .config it drops holds the C2 URL in cleartext.
Then it gets loud. Running as a fake Finder, Jamf's sample read the clipboard by spawning pbpaste on a loop -- every 10-30 seconds, for the whole run. A process posing as Finder, out of Application Support, shelling out to pbpaste every few seconds is about the loudest signature in the chain. Caveat: ManageEngine's variant did the same theft in-process via NSPasteboard with no subprocess, so the pbpaste noise is Jamf's sample, not guaranteed campaign-wide.
The password: PAM instead of dscl
The namesake behavior. It shows an NSAlert with a secure text field, styled like a system authorization prompt ("Maccy wants to make changes," account name pre-filled), and validates what you type through the PAM API (pam_start / pam_authenticate / pam_end).
Why it matters for detection: other commodity stealers confirm a captured password by shelling out. MacSync uses dscl. That's a spawned process you can catch. A PAM check runs in-process and spawns nothing -- one less chain in the tree.
Get it wrong, it re-prompts. Get it right, it shows a decoy -- "'Maccy' is damaged and can't be opened. You should move it to the Trash" -- so you trash the lure and assume a bad download while it's already persistent.
Persistence is via login items, modern and legacy (SMAppService and LSSharedFileList), and it deliberately avoids the LaunchAgents/LaunchDaemons where detection coverage is densest. It masquerades as Finder with the genuine icon and delays its Full Disk Access request -- made under the Finder disguise -- by up to 40 minutes so the prompt doesn't correlate with launch.
The Ethereum bit (Jamf only, purpose open)
Jamf decrypted the server config (avenger-config-v2) and found two public Ethereum JSON-RPC endpoints, then caught the fake-Finder process connecting to one. They didn't capture the calls, so the purpose stays open -- resilient dead-drop or wallet recon. ManageEngine doesn't mention it. Flagging it as single-source so you don't repeat it as settled.
Hunting it: the artifacts that survive
Quieter is not invisible. The native routing dodges shell-watching rules; it doesn't beat behavioral EDR or on-disk IOCs.
Dump the Background Task Management store and look for a Finder or Software Update login item running from a user path:
sudo sfltool dumpbtm
Check for a "Finder" that isn't Finder. The real one runs from /System/Library/CoreServices/Finder.app; anything else named Finder is suspect:
pgrep -lf -i finder # flag any path outside /System/Library/CoreServices/
Inspect the signature. The staged bundle carries an ad-hoc - identity, not a Developer ID:
codesign -dv --verbose=4 /path/to/suspect.app 2>&1 | grep -Ei 'Signature|Authority|TeamIdentifier'
# ad-hoc shows "Signature=adhoc", with no Authority and no TeamIdentifier
Behavioral rules for Endpoint Security / your EDR:
- Script Editor spawning
codesignagainst a bundle in~/Library/Application Support/ - A process named Finder, running outside
/System/Library/CoreServices/, spawningpbpasteor reading a Chromium credential DB - Login-item registration for a Finder or Software Update bundle in a user directory
On-disk / network IOCs from the two teardowns:
- Lure domain:
maccyapp[.]com - C2:
avenger-sync[.]live(Jamf),avengerflow[.]com(ManageEngine) -- rotated per sample - Bundle IDs:
com.apple.finder.core,com.apple.finder.monitor,com.apple.security.daemon - Caches:
~/Library/Caches/com.apple.finder.core/,~/Library/HTTPStorages/com.apple.finder.core/ - Markers:
.Maccy,.lock,.config(C2 URL in cleartext)
Full hashes and the complete IOC set are in the Jamf and ManageEngine writeups.
Sourcing, and one divergence to hold
The deep internals -- the validate-your-password-through-PAM workflow the name is built on, the avenger-config-v2 server response, the 40-minute FDA delay -- are Jamf's. ManageEngine independently analyzed the same campaign and corroborates the chain, but frames PAM differently: as libpam linkage the binary could use to intercept sudo and system-auth responses, not the validate-before-steal workflow. Both saw PAM; they emphasize different uses. If someone asks, the validate-before-steal account is Jamf's specifically.
The part that generalizes past this one sample: this is commodity malware (ManageEngine ties it to a malware-as-a-service pattern) that went quiet by going native -- PAM instead of dscl, a runtime-loaded framework instead of a linked one, login items instead of LaunchAgents. If your macOS detection assumes the malware will announce itself by spawning curl, this is the case for pointing it at framework loads, codesign lineage, and Finder-shaped processes doing things Finder doesn't do.
Top comments (2)
nice article Kerry
Thank you.