Is a “Mac OS X GateKeeper bypass” what it says?

From time to time, there are claims that ‘Gatekeeper’ in macOS has been bypassed. Back in February, Filippo Cavallarin reported what he considered was a new bypass mechanism to Apple. On 24 May, as Apple had failed to make any change in macOS 10.14.5 update, after 90 days he has released full details of what he found.

Here I’m going to look at two specific aspects of his description: the role of the quarantine flag, which is the trigger for ‘Gatekeeper’ to check downloaded apps, and what is actually happening (or not) in signature checking in this situation. My conclusion is that, although this can bypass ‘Gatekeeper’, it doesn’t quite work as assumed in Cavallarin’s report, and is essentially an architectural feature. This is not to try to dismiss his observations, but to put them into the broader context of what ‘Gatekeeper’ really does.

Quarantine flags

For an app to undergo ‘Gatekeeper’ checks, the standard requirement is for there to be a ‘quarantine flag’ attached as an extended attribute (xattr) to the app’s bundle. I have described quarantine flag operation in detail here for apps.

It isn’t hard for malicious software to exploit this: removing the quarantine xattr com.apple.quarantine doesn’t require elevated privileges, and is an action performed automatically whenever you update an app in place using the Sparkle mechanism, for instance. There are even well-known ways of downloading apps from malicious sites without setting the quarantine xattr in the first place, using a tool like curl.

Because the quarantine mechanism relies on the xattr to function, any file system which doesn’t support xattrs will effectively strip the flags, and disable the mechanism. The best-known example of a file system which can do this happens to be NFS (in general). So there’s a good chance that any app obtained from an NFS share won’t trigger ‘Gatekeeper’ in any case. That has been a known limitation of ‘Gatekeeper’ since its introduction, but doesn’t appear to have been used as a bypass to date.

There’s another important issue too: quarantine flags are only checked when an app is launched (or a file is opened) from the Finder or via another route such as NSWorkspace which calls LaunchServices (which is the key). Executable code which is launched from Terminal, shell scripts, or within other code doesn’t undergo any checks for the presence of quarantine flags. The slight twist to this is that the Finder normally blocks users from running scripts which have their quarantine flag set.

What happens when you run a ‘new’ app

When an app with a quarantine flag set is run from the Finder, a series of actions occurs, conveniently lumped under the term ‘Gatekeeper checks’. In Mojave these currently include:

  • app translocation to a temporary folder;
  • a signature check called by AMFI, which can result in the app being crashed immediately in the event of certain errors;
  • a malware scan by XProtect;
  • display of a confirmation dialog to the user;
  • if checks are passed and the user consents, the app is run;
  • if the app is run, the quarantine flag is cleared on all executable components within that app, but not removed from them.

macOS Mojave (at least) has a fallback mechanism to handle some situations similar to those described by Cavallarin: Apple Mobile File Integrity (AMFI). When an app is run from the Finder, via LaunchServices, a check is made to determine whether that app is known to have been run from that specific path before. When it hasn’t been, a signature check should normally be called by AMFI, but the other actions listed above aren’t performed. If the app’s signature is found to be damaged, or its integrity check fails, then that error results in the app being crashed and its launch is terminated.

This can be observed if you copy an app which has been run many times before (and has cleared quarantine) and double-click to run it from a new folder. The sequence of checks is reflected in the following abbreviated log excerpt, taken from a signed, hardened and notarized app in 10.14.5 (times in decimal seconds):
0.358156 Finder AppKit sendAction:
0.391615 LaunchServices LaunchApplication: appToLaunch={ "ApplicationType"="Foreground", "CFBundleExecutablePath"="/Volumes/External1/Documents/0newDownloads/MyHotApps/Signet.app/Contents/MacOS/Signet" […]
0.399168 amfid Security SecTrustEvaluateIfNecessary
0.400503 trustd asynchronously fetching CRL (http://crl.apple.com/root.crl) for client (amfid[123]/0#-1 LF=0)
0.400527 trustd cert[2]: AnchorTrusted =(leaf)[force]> 0
0.416269 CommCenter #I handleLSNotitifcation_sync: Application launched: <private>

The first entry marks the last of four records of the double-click, following which LaunchServices starts preparing to launch the app. Because this is from a previously unknown path, AMFI then calls trustd to perform the signature check. As that is passed without error, app launch is allowed to proceed, and is finally notified.

There’s a problem trying to perform the same sequence with unsigned apps, though: with no certificate to check, the app has to be taken on trust. This is discovered by an earlier trustd check called by lsd (the LaunchServices daemon), which reports the absence of a signature as an error -67062:
5.395531 Finder AppKit sendAction:
5.403534 lsd Security MacOS error: -67062
5.409659 Error LaunchServices Failed to register <private> trusted: NSOSStatusErrorDomain/-67060
5.413717 LaunchServices LaunchApplication: appToLaunch={ "ApplicationType"="Foreground", "CFBundleExecutablePath"="/Volumes/External1/Documents/0newDownloads/MyHotApps/Lightweight-IDE 1.0a2.app/Contents/MacOS/Lightweight-IDE" […]
5.419947 taskgated Security MacOS error: -67062
5.420045 CommCenter #I handleLSNotitifcation_sync: Application launched: <private>

An earlier security evaluation for lsd reveals that the app is unsigned (error -67062), so AMFI is unable to evaluate its integrity or signature, and launch has to proceed in the face of that knowledge.

These checks are in any case only performed when an app is run via LaunchServices, i.e. the Finder. So a user shouldn’t be able to run an app with a broken signature from a new location using the Finder, but they can run an app with no signature at all, and any malicious script or process can execute code from an app with a broken signature without any signature checks being performed, unless it’s kind enough to ask for them.

Where’s the vulnerability?

The “GateKeeper bypass” reported by Filippo Cavallarin appears to be a combination of features which have been present since Apple first introduced ‘Gatekeeper’ in 2007, and are a consequence of protection mechanisms which are reliant throughout on opting in. It’s actually more straightforward for malware to avoid the attachment of quarantine flags altogether by using curl (for example), or simply by stripping them before running unsigned code.

Ultimately, though, users must remember that these mechanisms only come into play as a result of user actions in the Finder. One way in which Cavallarin’s elaborate NFS automount ploy could be made more dangerous would be to persuade the user to run an unsigned malicious app without a quarantine flag attached, as that currently doesn’t undergo any meaningful security check which could alert or protect the user.

In a week’s time, we will learn whether the new security features in macOS 10.15 finally start to enforce longstanding features such as code signing, and if they do, how they will still allow users to run their own scripts. It’s that limitation which will otherwise keep ‘Gatekeeper’ opt-in, and protection from which malware will always be able to opt out.

Lightly amended to clarify the role of LaunchServices in ensuring the checking of quarantine flags. Thanks to Jeff Johnson @lapcatsoftware for pointing that out.