Uncovering the security protections in MacOS - Gatekeeper

In this series, I will be uncovering the internals of MacOS security protections. In this part I will be focus on Gatekeeper.

13 min read
Uncovering the  security protections in MacOS - Gatekeeper

With increase in number of malware affecting MacOS in last decades, Apple has introduced multiple Application security protection features to safeguard users from malicious contents. Like everything else Apple does, they did good amount of advertisement of these security protections exclusively to their costumers.

But MacOS been a mostly closed source operating system, it's tough to know what and how they are protecting. And most important, is it worth to solely rely on?

To answer these questions, I decided to dig more into all types of default application security features present in MacOS.

Gatekeeper

Gatekeeper is the major security feature present in MacOS introduced in OS X Leopard back in 2012. Most of the gatekeeper implementation code resides in syspolicyd  which is located in /usr/libexec/ and few part in Quarantine.kext. syspolicyd also responsible for handling other macos components like kext loading and execmanager.

In a nutshell, gatekeeper's job is to verify and validate an application bundle or an executable based on different criteria before it start as a process, so that only trusted software runs on Mac. The major features that gatekeeper holds are the following:

  • Quarantine check
  • Whitelisting and Blacklisting policies validation
  • Code signing and developer id verification
  • Notarization check

A user can interact with gatekeeper using spctl command. For example, one can disable gatekeeper using the following command.

$sudo spctl --master-disable

or to evaluate if something will pass gatekeeper checks or not

$spctl -a -t exec -vvv Myapp.app

Quarantine Check

If you download an application from the internet and try to open it, you must going to see following dialog:

This is quarantine check in action. To know more about quarantine check, we first need to understand extended attributes:

Extended attributes:

Extended attributes are arbitrary metadata of a file, stored separately from the basic filesystem attributes. These attributes are introduced in unix systems and now available in almost all *nix operating systems. You can check all extended attributes set to a file using xattr command in MacOS.

xattr mypdf.pdf

You can see the data associated with any extended attribute using following command

xattr -p com.apple.myxattr mypdf.pdf

You can also add your own extended attributes:

xattr -w com.apple.myxattr "This is the data of my xattribute" mypdf.pdf

Quarantine attribute:

For gatekeeper, apple has introduced quarantine extended attribute named com.apple.quarantine  which get set on any file downloaded from internet. This attribute will be set by the agent application (like chrome or firefox) when you download any file using them.

If you check the data of quarantine attribute, you will see structure something like below:

$ xattr -p com.apple.quarantine mypdf.pdf
0081;6138fdb6;Firefox;78B4F60F-4838-431E-8A72-6C666B15E5A6

The format of the data is the following:

flag;date;agent_name;UUID;

Flag value 0081 states that the application/file is executing for the first time. This attribute is responsible for the warning that appears when you first try to open an application, saying that the application is downloaded from the internet.

Once you open a file with quarantine flag set, gatekeeper stores the quarantine related metadata associated with the file in Quarantine database present at ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2.

Understanding the quarantine database structure

Quarantine database com.apple.LaunchServices.QuarantineEventsV2 is present in User's home directory, hence can be modified easily. It's Launch Services's responsibility to manage this database. Once you open a file by double clicking it, if launch services find the file having quarantine attribute present, it adds the entry of that in Quarantine database. The database is SQLite db with only one table named LSQuarantineEvent.

There are multiple columns in the table with the following usage:

  1. LSQuarantineEventIdentifier – Unique Id for the record
  2. LSQuarantineTimeStamp – The time (in seconds since 1/1/2001 12am)
  3. LSQuarantineAgentBundleIdentifier – the downloading application bundle name
  4. LSQuarantineAgentName – The application name
  5. LSQuarantineDataURLString – The URL the file was downloaded from
  6. LSQuarantineSenderName – The name of the person who sent you the email from which you downloaded the attachment
  7. LSQuarantineSenderAddress – The email of the person who sent you the email from which you downloaded the attachment
  8. LSQuarantineTypeNumber – Enum identifying the type of application that downloaded the file. The value is one of the following:
    • kLSQuarantineTypeWebDownload=0 – if the URL scheme was http(s) the download will be set to this, otherwise to 1.
    • kLSQuarantineTypeOtherDownload=1 – anything that wasn’t identified.
    • kLSQuarantineTypeEmailAttachment=2 – supposedly an email attachement (I wasn’t able to reproduce this)
    • kLSQuarantineTypeInstantMessageAttachment=3 – supposedly a download from instant messaging app
    • kLSQuarantineTypeCalendarEventAttachment=4 – an ical file via email?
    • kLSQuarantineTypeOtherAttachment=5
  9. LSQuarantineOriginTitle – No info, and seemed to be always empty.
  10. LSQuarantineOriginURLString – “The URL of the resource originally hosting the quarantined item, from the user’s point of view. For web downloads, this property is the URL of the web page on which the user initiated the download. For attachments, this property is the URL of the resource to which the quarantined item was attached (e.g. the email message, calendar event, etc.). The origin URL may be a file URL for local resources, or a custom URL to which the quarantining application will respond when asked to open it. The quarantining application should respond by displaying the resource to the user. Note: The origin URL should not be set to the data URL, or the quarantining application may start downloading the file again if the user choses to view the origin URL while resolving a quarantine warning.”
  11. LSQuarantineOriginAlias – No info, and seemed to be always empty in my log.

The database contain good amount of user's activity that can be serious privacy issue if the computer was indeed exposed to any spyware/malware. It has already seen to be exfiltrate by Bundlore/Shlayer in the past.  Moreover, even if you disable gatekeeper or try to create exclusion for certain application, the database is still maintained by MacOS for certain applications. This is because MacOS manages another exclusion list which force application to use LSFileQuarantineEnabled. This  list is present at /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/Exceptions.plist.

Limitations of Quarantine Check

  • 1) Since Quarantine check rely on extended attribute and extended attribute stores in file system. There are certain file systems that don't support extended attribute like following locations:

–        USB flash drive
–        Optical disk
–        External hard drive
–        Network shared drives(smb and nfs)

Hence, on these file systems, quarantine check doesn't work if an application is executed from above fs or symlinked to these locations. There are  multiple CVE's associated with this, like CVE-2019-8656. Although in recent versions, Apple fixed this issue by blocking the execution of the application from a remote shared location.

  • 2) Another common issue with quarantine check is, the quarantine attribute need to be set by agent application. If the agent application doesn't set this attribute, then there is no way to find if the file is downloaded from internet. One such application that doesn't set the quarantine attribute is curl. Due to this most malware in MacOS use curl or wget to download their payload.
  • 3) You will also find that gatekeeper doesn't verify quarantine attribute presence for certain file extensions that can be used for code execution. A few of such extensions are .inetloc or .fileloc. There are certain vulnerabilities related to this in the past. One of them is https://ssd-disclosure.com/ssd-advisory-macos-finder-rce/
  • 4) One minor limitation of extended attribute is in copying file. By default, copying a file copies the extended attributes too, but if the extended attribute is corrupted than copyfile operation skip setting the attribute to new target file. If a malicious actor can find a way to send a corrupted quarantine attribute through any compressed format that keeps extended attributes. Extracting it can skip the setting of quarantine attribute to target files. Unluckily, this doesn't work with .dmg, but you can explore other methods.

Whitelisting Policy

Apple uses multiple databases for whitelisting trusted application based on certain checks for gatekeeper. In below section, we will be talking about those databases.

SystemPolicy Database

To understand the usage of SystemPolicy database, let's go through a scenario.

  • Download a macho executable from internet.
  • Save the quarantine attribute data for that file somewhere using xattr -p com.apple.quarantine myfile > attr.data
  • Execute the macho and allow the gatekeeper check.
  • You  will see that the quarantine attribute data has been modified.
  • Reset the quarantine attribute value to previous state using following command xattr -w com.apple.quarantine "`cat attr.data`" myfile.
  • Now execute the macho again.

You will notice that this time application executed without the gatekeeper alert. This has the same implication as the case where you right-click and open the application to bypass gatekeeper check for that specific application. Both of the cases register an exception for the executable/bundle for gatekeeper into SystemPolicy database present at /var/db/SystemPolicy.

SystemPolicy db is used to store the details about the executables which has gone through gatekeeper check for the first time and user has allowed the execution. In other words, it is a primary exception or whitelisted application list for gatekeeper. One can add/update entry in SystemPolicy db in multiple ways:

  • Allowing opening of application from UI alert.
  • Right click -> Open the application/executable.
  • Using spctl. spctl --add /Path/To/Application.app

Behind the scene, these entries call SecAssessmentCopyUpdate to update /var/db/SystemPolicy database.

SystemPolicy db format

SystemPolicy database has multiple tables. We will go through each one by one.

The primary table in the database is authority.

Apple describe each entries as a Rule. Each column has following meaning:

  1. Id:Canonical ID
  2. version: version of the Rule. Will always set to 1
  3. type: Operation type. These are operations the system policy can express opinions on. These can be one of these:
    • 1 - kSecAssessmentOperationTypeExecute - Set on Code execution from standalone macho
    • 2 - kSecAssessmentOperationTypeInstall - Sets on Bundle installation.
    • 3 - kSecAssessmentOperationTypeOpenDocument - Sets on opening of document(application spawned by another app) using launchservice.
  4. requirement - code requirement - Stores the cdhash on main macho file.
  5. allow - If the gke exception is allowed. Always be 1(since then only the rule is added in this db)
  6. disabled - If following rule entry is disabled.
  7. expires - Expiration of rule. Julian date format. Will be always 5000000(June 7, 8977).
  8. label - An optional text labeling the rule. Mostly set to GKE or null.
  9. filter_unsigned - Will contain hash of Info.plist file from the bundle. If bundle is not signed, this value is used to verify the bundle. Can have value in multiple format:
    • "R" + hash - used for sha1 hash of Info.plist
    • "I" + hash - used for md5 hash
    • "M" + hash - used for md5 hash
    • N - set for rule type 1(direct macho execution)
  10. Flags: A mask of flag bits passed to SecAssessment calls to influence their operation.Can have following values:
    • SecAssessmentDefaultFlags = 0, // default behavior
    • kSecAssessmentFlagDirect = 1 << 30, // in-process evaluation
    • kSecAssessmentFlagAsynchronous = 1 << 29, // request asynchronous operation
    • kSecAssessmentFlagIgnoreCache = 1 << 28, // do not search cache
    • kSecAssessmentFlagNoCache = 1 << 27, // do not populate cache
    • kSecAssessmentFlagEnforce = 1 << 26, // force on (disable bypass switches)
    • kSecAssessmentFlagAllowWeak = 1 << 25, // allow weak signatures
    • kSecAssessmentFlagIgnoreWhitelist = 1 << 24, // do not search weak signature whitelist
    • // 1 << 23 removed (was kSecAssessmentFlagDequarantine)
    • kSecAssessmentFlagIgnoreActiveAssessments = 1 << 22, // permit parallel re-assessment of the same target
    • kSecAssessmentFlagLowPriority = 1 << 21, // run the assessment in low priority
  11. ctime - rule creation time (Julian)
  12. mtime - time rule was last changed (Julian)
  13. user - user requesting this rule (NULL if unknown). Looks like mac developers are too lazy to set this since you will find the value NULL in all entries.
  14. remarks - optional remarks string. Set to either (gke) or path of bundle/executable in case of unsigned code.
Remark: If the main executable of a bundle is script rather than macho. There will be no update in SystemPolicy since cdhash will not present in that case.

Few other tables of SystemPolicy db:

active_authority - Subset of authority. Contains rules that are presently active(Disable = 0).

scan_authority - Subset of authority. Contains rules subject to priority scan: active_authority but including disabled rules.

bookmarkhints - A table to carry (potentially large-ish) filesystem data stored as a bookmark blob.

features - Features present/used in that database and if they can be upgraded.

object - The cache table lists previously determined outcomes for individual objects (by object hash). Entries come from full evaluations of authority records, or by explicitly inserting override rules that preempt the normal authority.

object_state - Some useful view of object.

Gatekeeper's bundle

Gatekeeper keeps two loadable bundles inside /var/db which is mostly used for predefined whitelisting purpose. The bundles are with following name gke.bundle and gkopaque.bundle. Let take a look at what they used for.

gke.bundle

First we will check gke.bundle. Since its loadable bundle, it only has two Resource file that are important. gke.auth and gk.db.

gke.auth is a plist file. In the words of Apple's developer, it contains weak signature whitelist. After looking in source code of policydb.cpp and policyengine.cpp I found the list is some kind of whitelist of application that are not signed but need to be trusted. It is used for successfully created temporary signature (kind of adhoc signing) some application to execute. Once you start an application, if syspolicyd found the application to be completely unsigned, it checks this plist to temporary set signature for the bundle if application is listed here. The plist has one key named authority which has different subkey and their value as dictionary. This is how a single key looks like


<key>00426538-36a2-42e6-8b2f-641385980298</key>
  <dict>
    <key>cdhash</key>                    <string>cf2488248efe976413a5ac489d60cc6b1ba00c5d</string>
    <key>path</key>
    <string>(gke)</string>
    <key>screen</key>                    <string>I4057d28ea08f639c4ec81778d327db4bf37d1cb6</string>
    <key>type</key>
    <string>2</string>
    <key>version</key>
    <integer>2</integer>
  </dict>

  • 00426538-36a2-42e6-8b2f-641385980298 - id for that entrie/rule.
  • cdhash - cdhash of the main macho file.
  • path - probably implies the method through which the whitelist will be applicable. always set to "(gke)".
  • screen - hash of Info.plist from bundle. Can have any of the following format.

"I" + sha-1 - Old method
"R" + sha-256 - New default
"N" - In case of standalone macho (no practicle entry)
"M" + sha-1 - In case of anything else (no practicle entry)

  • type - It's unclear if this field is similar to 'type' field in SystemPolicy.db. Mostly value set to 1 or 2 in case of screen is sha-1 hash and 3 in case screen is sha-256.
  • version - Rule version(optional). Value lies from 1 to 3.

There are more than 2000 entries in gke.auth. Most of the applications whitelisted here are Intel x86 unsigned bundles(any type like .app or .kext) from trusted vendors like vmware, eclipse etc. Surprisingly, while looking for those Info.plist hashes on VirusTotal I found some entries to be flagged as malicious by multiple vendors.

gk.db is another sqlite db present on gke.bundle. It contains one main table named timestamp_exceptions which contain cdhashes in a weird(decimal bytes with comma) format.

There is no information available about the usage of this db in macos's available source code. But I guess this is another type of whitelisting for standalone macho files.

gkopaque.bundle

It is another loadable bundle which contain only one important file, gkopaque.bundle/Contents/Resources/gkopaque.db. It has been said that this bundle is used for whitelisting application before gke.bundle since later one is added on quite recent macos version.

gkopaque.db is the SQLite db file present in gkopaque bundle. It uses for whitelisting application/executables before gke.bundle was introduced, but macos still uses it in some way. The database file has 3 tables:

Primary table name is "whitelist". whitelist contain 2 columns:

both column contain cdhashes of executable that needed to be whitelist. The usage of these column is mentioned in following funtion in opaquewhitelist.cpp.  


//
// Check if a code object is whitelisted
//
bool OpaqueWhitelist::contains(SecStaticCodeRef codeRef, SecAssessmentFeedback feedback, OSStatus reason)
{
	// make our own copy of the code object, so we can poke at it without disturbing the original
	SecPointer<SecStaticCode> code = new SecStaticCode(SecStaticCode::requiredStatic(codeRef)->diskRep());

	CFCopyRef<CFDataRef> current = code->cdHash();	// current cdhash
	CFDataRef opaque = NULL;	// holds computed opaque cdhash
	bool match = false; 	// holds final result

	if (!current)
		return false;	// unsigned

	// collect auxiliary information for trace
	CFRef<CFDictionaryRef> info;
	std::string team = "";
	CFStringRef cfVersion = NULL, cfShortVersion = NULL, cfExecutable = NULL;
	...
    ...

	// compute and attach opaque signature
	attachOpaque(code->handle(false), feedback);
	opaque = code->cdHash();

	// lookup current cdhash in whitelist
	if (opaque) {
		SQLite::Statement lookup(*this, "SELECT opaque FROM whitelist WHERE current=:current"
			" AND opaque != 'disable override'");
		lookup.bind(":current") = current.get();
		while (lookup.nextRow()) {
			CFRef<CFDataRef> expected = lookup[0].data();
			if (CFEqual(opaque, expected)) {
				match = true;	// actual opaque cdhash matches expected
				break;
			}
		}
	}
    ...
    ...

Code above gives an idea that the cdhash of the target executable is compared with an opaque list, and if the hashes match than the cdhash get whitelisted. It is still unclear why the MAC developers are using both the list and why there are multiple entries for one cdhash in the current list. Aside from that, it is mentioned by someone on a twitter thread that MacOS version 10.9.4 contain an agent that uploads these cdhashes to apple server. So, from there they have developed this whitelist.

Another table in this database is named conditions which contain only two entries. Both are to whitelist google chrome applications.

Conclusion

Although gatekeeper is a prevalent security feature for MacOS, but time to time there are multiple CVE's arises which shows the implementation requires lots of fixes to make it more robust. CVE's like CVE-2021-30657 and CVE-2021-30853 shown it's not complicated to find a way to bypass gatekeeper.

In the next part, we will be discussing xprotect and notarization. Stay tuned.