Syspolicyd mac os что это
When run (on macOS 11 — 11.5), though unsigned and non-notarized it is allowed to execute and pops Calculator.app
Background
In macOS 12 beta 6 (and also then in macOS 11.6), Apple patched an intriguing bug as CVE-2021-30853 :
/>CVE-2021-30853 (credit: Gordon Long)
Discovered and reported by Gordon Long (@ethicalhax), Apple noted that via this flaw “a malicious application may bypass Gatekeeper checks”. Such bugs are often particularly impactful to everyday macOS users as they provide a means for adware & malware authors to sidestep macOS security mechanisms, …mechanisms that otherwise would thwart infection attempts!
As we’ll see, the bug patched as CVE-2021-30853 bypasses not just Gatekeeper, but also File Quarantine, and macOS’s recent notarization requirements.
For more details on the macOS security mechanisms File Quarantine, Gatekeeper, and notarization requirements, see my previous blog post, “All Your Macs Are Belong To Us.”
…this means a single double-click of what appears to be an innocuous file (like my “resume”), can lead to the full compromise of a macOS system:
Early this year, I published, “All Your Macs Are Belong To Us” that detailed CVE-2021-30657. This bug, discovered by Cedric Owens, also allowed “a malicious application [to] bypass Gatekeeper checks” (which I determined was due to flaw in Apple’s user-mode system policy daemon).
Though both CVE-2021-30657 and CVE-2021-30853 have the same impact, and upon initial triage seem closely related, a detailed analysis reveals that the bug we’re discussing today (CVE-2021-30853), is due to a completely separate flaw (found within the kernel).
Let’s now dive deeper in CVE-2021-30657 , discussing:
The components of simple PoC that trigger the flaw
How application launches are handled on macOS, specifically focusing on the code paths triggered by the PoC.
How such code paths later undermine logic in the Apple System Policy kernel extension which leads to a full File Quarantine, Gatekeeper, and Notarization bypass.
The PoC
The proof of concept that exploits CVE-2021-30657 is an unsigned, non-notarized application.
/>The unsigned, and non-notarized PoC
When downloaded from the Internet it is, as expected, quarantined:
Normally quarantined items should trigger File Quarantine, Gatekeeper, and notarization checks …and if the item is unsigned (and thus unnotarized), should be blocked:
/>An unsigned application, normally should be blocked!
It should be noted that to trigger exploitation, a user would have to be tricked (or coerced) into running the application. Though this may seem somewhat of a high bar to overcome, hackers have proven time and time again that macOS users can be, rather trivially, tricked into doing so:
/>common macOS infection vectors
…moreover as shown in the demo above, the application can masquerade as harmless PDF, perhaps sent via email, or other distribution channels.
As noted early, File Quarantine, Gatekeeper, or macOS’ notarization requirements were designed to specifically prevent any unsigned and non-notarized application from running, even when launched by the user. However due to the flaw exploited by CVE-2021-30657 these checks are not performed.
Taking a closer look at the PoC application reveals its main executable component appears to be is a script:
/>An 'interpreter-less' application, though unsigned & unnotarized is allowed to execute
The astute reader may have noticed that though the script started with the familiar #! (“Shebang”), it is missing an interpreter such as /bin/bash . However when launched, macOS seems to handle this without issue, and still executed the script.
Specifically, as shown below, in the output of a process monitor, when launched you can first see launchd exec’ing xpcproxy . This then executes /bin/sh , which in turn executes /bin/bash to execute the PoC (which has been translocated, as its from the Internet):
Though the process monitor output confirms that macOS will execute the “interpreter-less” script via bash , that fact that no interpreter was specified is ultimately what triggers a (rather nuanced) bug in the kernel. A bug, that allows the PoC to execute without being subjected to File Quarantine, Gatekeeper, nor notarization checks!
Application Launch (User-mode)
When our proof of concept is launched, though it is from the Internet (read: quarantined) and both unsigned and non-notarized, macOS allows it to launch! It is not blocked, nor are any alerts displayed. Why are File Quarantine, Gatekeeper, and macOS’s recent notarization requirements all fully bypassed?! …let’s find out!
To uncover the root cause of this flaw let’s start by exploring how processes (well, applications) are launched on macOS, focusing on the code paths triggered by this PoC.
On macOS, launching an application is a surprisingly complex process. In a 2016 talk at ShmooCon titled, “Gatekeeper Exposed; Come, See, Conquer”, I provided a detailed (although now somewhat dated) walk-through of these interactions:
/>Launching an application is a complicated ordeal
Since this talk, Apple has expanded (read: complicated) this process, adding XPC calls into its system policy daemon, syspolicyd , and its XProtect (anti-virus) agent, XprotectService .
As part of this complex application launching process, the system (is supposed to) ensure that any application downloaded from the Internet is blocked unless it is signed and notarized:
/>An unsigned application, normally should be blocked!
…and even when an application is both signed and notarized, an alert should still shown to the user, informing them that they are launching executable content from the (untrusted) Internet.
As the PoC is allowed to run uninhibited, clearly there was a flaw in macOS …somewhere within the logic that handles the launching of applications.
Unfortunately due to both the complexity and many component involved in this process, determining which component (daemon, framework, or even kernel extension) is responsible for this bug, is not a trivial exercise.
As noted above, the (what appeared to be) similar CVE-2021-30657 (detailed in my lengthy blog post, “All Your Macs Are Belong To Us”), was ultimately due to a flaw in Apple’s user-mode system policy daemon, syspolicyd . I was able to uncover the fact that syspolicyd was the problematic component for that bug, largely due to messages displayed in Apple’s logging subsystem:
/>logging message, from syspolicyd (for CVE-2021-30657)
If syspolicyd is not invoked (for whatever reason), the application will not be evaluated, and thus will simply be allowed to run!
Due to similarities with CVE-2021-30657 , specifically the fact that a script-based application could bypass of Gatekeeper (et. al.), it seemed reasonable first turn to the log message of syspolicyd .
Via the log command I instructed macOS to stream all syspolicyd ’s’ log messages. Then I launched the proof of concept application that triggers CVE-2021-30853 .
…but nothing was shown?!
This was especially strange considering that syspolicyd should always display some log messages, as it is largely the arbiter in determining if an application should be allowed to run. For example, we should at least see a message such as (as was the case for the PoC that exploited the previous bug, CVE-2021-30657 ):
syspolicyd: [com.apple.syspolicy.exec:default] Script evaluation: /Users/patrick/Downloads/PoC.app/Contents/MacOS/PoC, /bin/sh
After (triple) checking my log predicate (as well as also just streaming all log messages), it seemed only reasonable to assume that syspolicyd was not displaying any log messages as it was not being called upon to scan (analyze) the PoC application.
Of course this was initially just an assumption …and I really had no idea why this would be the case. However if true, it would explain both why no log message where shown, and more importantly, why the untrusted PoC was allowed. (Note that syspolicyd is responsible for evaluating launched applications, and blocking ones it deems untrusted …as well as alerting the user to this fact via an XPC call to CoreServicesUIAgent . If it is not invoked, the application will not be evaluated, and thus will simply be allowed to run!).
As I did not know why syspolicyd was not invoked (nor even who was responsible for invoking it) I decided to bite the bullet and start (near) the beginning of the complex application launch logic with the (conceptually) simple goal of figuring out why syspolicyd was not invoked to scan the proof of concept, thus allowing it to execute uninhibited.
On macOS, launchd is the daemon responsible to launching (all?) processes. Though Apple decided to close-source it, the source code of older versions, such as version 842.92.1 are still available (via opensource.apple.com). Perusing that code we can see it executes /usr/libexec/xpcproxy to handle the launch of applications, such as our PoC. This matches what we saw in the output from the process monitor, when we launched the PoC application:
The xpcproxy process then executes the PoC, as was also shown in the process monitor:
Let’s figure out exactly how xpcproxy it delegating continued execution to launch the application.
Hopping into a debugger we can attach to xpcproxy when it’s spawned by launchd (via the —waitfor command):
Once attached, we then place a breakpoint on the posix_spawnp API, as this is the API invoked by xpcproxy to spawn our PoC application:
Once this breakpoint is hit, we can print out the parameters of posix_spawnp to confirm that yes indeed that our (translocated) PoC.app is about to be spawned:
Once again the astute reader may be wondering why posix_spawnp versus the more familiar posix_spawn was being invoked by xpcproxy . And that is a excellent question, as it is directly relevant to the bug we’re tracking down.
The man page for posix_spawn/posix_spawnp explain the difference between the two noting:
“The posix_spawnp() function is identical to the posix_spawn() function if the file specified contains a slash character; otherwise, the file parameter is used to construct a pathname, with its path prefix being obtained by a search of the path specified in the environment by the «PATH variable’’.”
If we look at a stacktrace ( bt in the debugger), we find the code in xpcproxy responsible for the call to posix_spawnp :
Interestingly, based on a bit value at offset 0xb4 of some structure (found in the r15 register), xpcproxy will either invoke the more familiar posix_spawn API, or the posix_spawnp API.
Let’s examine the bit value at this offset ( r15+0xb4 ):
…as the bit at 0xd is set (to 0x1 ) the jb instruction is taken, and thus the instruction at 0x00000001050d00a9 is executed meaning the rbx register is set to posix_spawnp .
Continued analysis seemed to indicate that bit is known as the “inferred program” flag, set by launchd (and viewable via launchctl procinfo <pid> ). Interestingly on macOS 10.* (which is not vulnerable), this flag is not set and thus posix_spawnp is not called (which as we’ll see does not trigger the flaw):
So, as the (on macOS 11.*), the inferred program flag is set, posix_spawnp (instead of posix_spawn ) is invoked.
As source code for posix_spawnp is available, it’s easy to understand what exactly it does.
After constructing a path name (as described in its man page), it then invokes posix_spawn :
Continuing to peruse the source of the posix_spawnp function, we see it then examines the value returned by the call to posix_spawn . Interestingly (and very relevant to the bug we’re analyzing today), if an ENOEXEC was returned, meaning the application failed to execute, it will call posix_spawn again. This time however, the the process path is hard-coded to the value of _PATH_BSHELL (which is "/bin/sh" ), while the original item’s path is moved into the second argument:
…in other words macOS will (re)attempt to execute the failed item via the shell ( "/bin/sh" ):
/>posix_spawnp's error handling logic
In a debugger, we can confirm that our “interpreter-less” script-based application causes the primary call to posix_spawn to fail, and thus triggers the ENOEXEC logic. Thus the second call to posix_spawn will be executed, meaning the PoC is launched via the shell ( "/bin/sh" ):
That’s rather a lot of debug output, so let’s walk thru it. First we tell the debugger to wait and attach to xpcproxy . After launching the PoC application, an instance of xpcproxy is launched, which the debugger catches, and halts.
We then set a breakpoint on the posix_spawn API (that recall is invoked by the posix_spawnp API). Once hit, we print out the path of the program it’s about to spawn. As this is the 2nd argument to posix_spawn , it’s found in the RSI register. Unsurprisingly it’s the (translocated) path to the PoC application’s executable component PoC.app/Contents/MacOS/PoC .
If we allow the call to posix_spawn to complete (via the finish debugger command), we see that the called failed. Specifically the RAX register (which holds the return value from a function call), is set to 0x8 , which maps to ENOEXEC . In other words, macOS failed to executed the PoC application. (We’ll show exactly why soon, but essentially because no interpreter is specified on the first line of the PoC’s script).
/>an ENOEXEC error is triggered, causing /bin/sh to be invoked
Then, in the debugger output we see the breakpoint on posix_spawn is hit again. If we examine the arguments passed to this second call we see the path has been set to "/bin/sh" , and if we examine the arguments (pointed to by the 5th argument, the R8 register), we find that argv[0] has been set to the string "sh" , while argv[1] has been set to the PoC’s executable component.
This of course matches exactly what we saw in the output of the process monitor:
As the PoC’s interpreter-less script, after failing the first time, was then executed directly via /bin/sh it succeeds. But at what cost!?
Application Launch (Kernel-mode)
If we forget for a moment about File Quarantine, Gatekeeper, and notarization checks, everybody is happy. That is to say, macOS was “smart” enough to handle the case whereas a interpreter-less script was executed. So it’s feeling pretty swell.
Unfortunately this logic causes a subtle corner case, which in the context of File Quarantine, Gatekeeper, and Notarization checks, is massively problematic …as in all these checks are ultimately skipped!
But to understand why, we need to follow the posix_spawn call into the kernel to understand exactly why the first call failed (with ENOEXEC ) and more importantly the implications of this.
As portions of the macOS kernel (XNU) are still open-source, we can follow the posix_spawn call into the kernel fairly trivially.
Ultimately we end up at a call to the exec_activate_image function:
After setting up kernel debugging session, and setting a breakpoint on the exec_activate_image , we can examine a stack backtrace to confirm indeed it’s invoked by the kernel mode posix_spawn function:
The exec_activate_image function, found within the kern_exec.c file will iterate thru a hard-coded table of “image activators” name execsw . Such “activators” are simply functions that implement the loading logic for various (supported) file types such as Mach-O binaries and scripts:
It should be pointed out that activator is called in turn, until one “claims” the image. This means that even for a script, the Mach-O activator will be invoked (as it at index zero of the execsw array).
Let’s take peek at the activator functions, starting with the first one exec_mach_imgact , that handles Mach-O files:
In the context of this blog post and discussion, we simply note that it (first) checks if the item to be executed is a mach-O binary. In the case of our PoC (which recall is really a script-based application, not a mach-O binary), the exec_mach_imgact will simply bail out early, as it correctly determined the PoC’s script, is not a mach-O binary.
The second image activator, exec_fat_imgact will also bail with an error, as the PoC’s script, is also not a fat (universal) binary.
Finally we get to the image activator for scripts: exec_shell_imgact . Before we dive in recall two things
- The PoC’s script did not specify an interpreter, instead simply started with #!
- The (first) call to posix_spawn failed with ENOEXEC
Ok, now take a look at the exec_shell_imgact function:
As the comments note, the function is the “Image activator for interpreter scripts” and that “If the image begins with the characters “#!”, then it is an interpreter script.” The remaining comments discuss validation of the specified interpreter, and notes, “the [interpreter] line ends when we encounter a comment character (’#’) or newline”.
Looking at the source code, we can see this is the case. Indeed it first checks the script begins with #! (which the PoC’s script does). So far so good.
Then it attempts to find the script’s interpreter by parsing the rest of the first line. First, the IS_EOL macro is invoked on each remaining character on the first line. It is define as: IS_EOL(ch) ((ch == ‘#’) || (ch == ‘\n’)) . Thus, a script that starts with only #! (or !#\n ) will cause IS_EOL to return true, and as the comment note means “Did not find interpreter” …and ENOEXEC is returned.
Now we know exactly why the first call to posix_spawn fails on the PoC’s interpreter-less script ( #! ), and why ENOEXEC was returned first by exec_shell_imgact and then all the way back to user-mode (which recall then triggered the second call to posix_spawn to execute the script directly via /bin/sh ).
/>an ENOEXEC error is returned when no interpreter is specified
And if a valid script was specified? Well the exec_shell_imgact function, as shown above returns the value of -3 (which as the comments note, means, “Success: interpreter: relookup”).
Back in the exec_activate_image (the caller of the exec_shell_imgact ), we see that return value of all activators are checked, including -3 (for valid scripts):
This makes sense. The code in exec_activate_image wants to know if the image activator was successful. For example did exec_shell_imgact determine the script was valid? …and thus loaded and activated, so that (final) process execution can commence.
However (again assuming the image activation succeeded), before this (final) process execution commences, other image-specific code is executed.
For example for case -3, (returned by exec_shell_imgact for a valid script) we find the following:
Specifically values in the image’s image_params structure, here named imgp , are set, such as:
The latter, ip_scriptlabelp is set to the value of structure allocated via a call to mac_vnode_label_alloc , and then populated via a call to mac_vnode_label_copy .
In Apple’s sys/imgact.h header file, we find succinct descriptions for each:
The astute reader (which is likely you, if you made it this far), may have noticed that the code that sets these members of the image_params struct is wrapped in an #if CONFIG_MACF . This is notable since MACF is the technology leveraged by macOS to enforce many security checks! Thus in the context of allowing, or equally important disallowing (untrusted) scripts, this code seems particularly relevant.
Such readers may also be wondering what happens to these members of the image_params struct if the exec_shell_imgact function fails …for example when it encounters the PoC’s interpreter-less script? Well as the code that sets these structure members is never executed they remain unset.
We can confirm this by dumping the image_params structure for our PoC, after the exec_activate_image (and exec_shell_imgact function) has returned:
Note that in the debugger output, the ip_vdata member of the structure is set the script’s contents, while the ip_strings member to the (translocated) path. Most importantly, though ip_scriptlabelp and ip_scriptvp remain NULL, unset, as the exec_shell_imgact function returned ENOEXEC (since the PoC script didn’t specify an interpreter).
Note that for a normal script-based application (that specifies an interpreter):
the ip_startargv member of the image_params structure will be updated with the path to the interpreter (e.g. /bin/bash )
the ip_scriptlabelp and ip_scriptvp members will be set (i.e. will be non-NULL).
We can confirm this by executing a normal script-based application (whose script does specify a interpreter), and dumping it’s image_params structure:
Of course the question now is, “how are these structure members utilized”? Continued code analysis and (kernel) debugging reveals they are passed to the kauth_proc_label_update_execve API. This is shown below (from kern_exec.c ):
Debugging this kernel logic when a normal script-based application (that specifies an interpreter) is executed confirms this:
In the debugger output, note that the kauth_proc_label_update_execve is invoked with the values from imgp->ip_scriptvp (named scriptvp ) and imgp->ip_scriptlabelp (named scriptl ).
/>The kauth_proc_label_update_execve function will update a label, if specified
Once the kauth_proc_label_update_execve returns, we can (re)examine the proc structure and confirm that (for a script-based application that specifies an interpreter) the p_ucred member (of type ucred ) has been updated. If we then dump the updated p_ucred structure, we see that the value of its cr_label member has been set to a MAC label that contains a pointer to the normal script:
We’ll see shortly how code elsewhere in the kernel will inspect this cr_label (specifically the value index 0x3 ) in order to determine if the process is a script.
In the case of our problematic PoC, the system detects the interpreter is missing and thus leaves ip_scriptlabelp and ip_scriptvp NULL. This also means that the cr_label structure for that process object (representing the PoC) will not contain the path to the script.
Now this initially appears not to be an issue as we’re bailing out of the kernel anyways with a ENOEXEC , since the exec_shell_imgact correctly determined that the script was interpreter-less.
…but recall posix_spawnp doesn’t give up that easily and turns around and (re)invokes posix_spawn again, but this time not with a script, but rather /bin/sh , a trusted, Apple-signed platform Mach-O binary. Since sh is a mach-O binary exec_activate_image calls the mach-O activator, exec_mach_imgact . As sh is a valid mach-O binary, this function succeeds, meaning sh is successfully spawned. Of course any script-related members of its image_params structure remain unset (NULL), as sh is not a script:
…but it, sh is (about) to execute untrusted the PoC script!
/>. in summary
Application Policy Checks (Kernel-mode)
Recall that we’re really just attempting to figure why the PoC application is allowed to execute, though it is unsigned and unnotarized.
So far, we shown that a script-based application will be executed via the posix_spawnp API. This API will first attempt to directly execute the application’s script. If this fails (as was the case with the PoC’s interpreter-less script), the script instead will be executed directly via /bin/sh .
…however, this second attempt means that script-based members of the image_params structure such as ip_scriptlabelp and ip_scriptvp are not set.
Is this an issue? YES!! …let’s now show exactly why!
But first, also recall we previously conjectured that as syspolicyd was not displaying any log messages (related to evaluating the PoC), it perhaps was not being invoked at all …to scan (and analyze) the proof of concept! And if syspolicyd was not called upon to examine the PoC, it would be allowed to execute without being subject to File Quarantine, Gatekeeper, and notarization checks.
Figuring out who is responsible for invoking syspolicyd to evaluate applications (and confirming why for the PoC syspolicyd was not invoked) was not a trivial venture. However, various spelunking and googling ultimately gives us the answer.
In this section of the blog post we’ll explore macOS’s application policy check logic, largely focusing on the AppleSystemPolicy kernel extension. Specifically, we’ll show how this extension acts as the initial arbiter to evaluate processes (such as our PoC) and then (normally) delegates continued evaluation to the user-mode syspolicyd daemon.
/>an overview of interactions between the system, the AppleSystemPolicy kext, and syspolicyd
As noted in a previous blog post, the syspolicyd daemon will perform various policy checks and ultimately prevent the execution of untrusted applications, such as those that are unsigned or unnotarized.
But, what if the AppleSystemPolicy kext decides that the syspolicyd daemon does not need to be invoked? Well then, the process is allowed! And if this decision is made incorrectly, well then, you have a lovely File Quarantine, Gatekeeper, and notarization bypass.
The AppleSystemPolicy kext is briefly detailed in an excellent write up by the macOS security researcher (and #OBTS speaker!) Scott Knight. In a blog post titled “ syspolicyd Internals” Scott notes the following:
AppleSystemPolicy kext hooks various MACF operations such as mac_proc_notify_exec_complete .
AppleSystemPolicy kext is the “client” of the syspolicyd daemon.
As both of these are relevant in the context of our problematic PoC let’s dig in a bit more.
First, let’s examine the MACF operations that AppleSystemPolicy kext hooks. In a nutshell Mandatory Access Control Framework ( MACF ) is a private kernel framework that allows Apple kexts to hook a myriad of (MACF) operations. Such hook (or callbacks) will then be invoked automatically by the kernel proper, and thus afford the kext (who registered the hook) the opportunity to a take a action …for example, to examine and block untrusted processes!
In his blog post, Scott pointed out the AppleSystemPolicy kext hooks mac_proc_notify_exec_complete . This is also confirmed in an appendix for Jonathon Levin’s invaluable *OS Internal books, which notes:
“A new MACF Policy, AppleSystemPolicy (com.apple.SystemPolicy), is now in use in MacOS. The policy (identified as ‘ASP’) hooks mac_proc_notify_exec_complete (new in this version)…”
If we disassemble the AppleSystemPolicy kext, we can see that within its start method, it invokes an method, aptly named registerMACPolicy . This method will initialize a MACF policy structure, mac policy_conf (with the MACF hooks/callbacks it is interested in including mac_proc_notify_exec_complete ), the register via call to the mac_policy_register API:
As shown in the above decompilation, the kext’s hook for mac_proc_notify_exec_complete is named proc_notify_exec_complete , which ultimately calls AppleSystemPolicy::procNotifyExecComplete .
A quick triage of this rather involved method reveals strings such as “ASP: Security policy would not allow process” and method calls such as:
- AppleSystemPolicy::getDaemonPort
- ASPEvaluationManager::createEvaluation
- ASPEvaluationManager::addEvaluationRequest
- AppleSystemPolicy::blockRevokedProcess
- AppleSystemPolicy::evaluateScript
The former method name are related to AppleSystemPolicy kext communications and delegations to the user-mode syspolicyd daemon. Levin notes that “[AppleSystemPolicy] makes upcalls to /usr/libexec/syspolicyd over HOST_SYSPOLICYD_PORT (i.e. host special port #29). "
We can confirm this by looking at the disassembly of the getDaemonPort method, which reveals a call to the host_get_special_port port with 0x1D (29d):
And if we pop into user-mode and look at the launch daemon property list for syspolicyd ( /System/Library/LaunchDaemons/com.apple.security.syspolicy.plist ), we see that indeed it will create a Mach ( com.apple.security.AppleSystemPolicy.mig ) listener on port 29:
Back to the methods invoked from AppleSystemPolicy ’s procNotifyExecComplete method, the AppleSystemPolicy::evaluateScript one seem particularly relevant to our script-based PoC.
So, let’s set a kernel-mode breakpoint on this method and execute a “normal” script-based application (that is, one with a correctly specified interpreter):
…as expected the breakpoint is triggered:
…moreover, if we look at the stack backtrace (via the bt debugger command), we confirm it was called by MACF subsystem in response to the mac_proc_notify_exec_complete hook installed by the AppleSystemPolicy kext:
If we reverse the evaluateScript method, as expected we see if calls out to syspolicyd in order to perform the script evaluation in user-mode (note the log message, “Calling out for script evaluation”):
…and if the script-based application is untrusted (read: not notarized) once syspolicyd processes the evaluation request, it will soundly reject it:
/>untrusted script-based application, (normally) are blocked
And what about our interpreter-less PoC application? Well, if we execute it, AppleSystemPolicy ’s evaluateScript method is never called.
…meaning syspolicyd is never invoked to evaluate the PoC!
…at the time this was strange, as we assumed syspolicyd would be invoked to evaluate the application.
Now, the lack of log messages (from syspolicyd), all makes sense as it’s never even asked to evaluate the PoC! Rude!?
Of course the question is, why isn’t syspolicyd invoked to evaluate our non-notarized PoC application? The short answer is, AppleSystemPolicy doesn’t think it needs to! But let’s show exactly why.
Recall that AppleSystemPolicy ’s evaluateScript method is invoked by the procNotifyExecComplete (which is itself called for each process launch, thanks to the mac_proc_notify_exec_complete MACF hook that AppleSystemPolicy installed).
First take a look at the following image that contains an annotation implementation of this method, reconstructed from its disassembly:
/>The procNotifyExecComplete method
Turns out it’s simpler to understand the conditions upon which the evaluateScript method is invoked by debugging a normal script-based application, that specifies an interpreter (which will trigger the call to evaluateScript ).
First though, let’s look at the conditional code (within procNotifyExecComplete ) that must be fulfilled before evaluateScript is invoked:
We see various registers (e.g. r15 and r13 ) and that must not be NULL, as well as some local variable who’s value must be 0x0.
We’ll focus on the r13 register, for reasons that will become clearly shortly.
As noted, if the r13 register is NULL, the call to evaluateScript will be skipped.
First, we find that the r13 is initialized with the value of a local variable ( var_100 ):
…looking back in the disassembly, we find this local variable is initialized with a vnode of a path:
Note that if the call to vnode_for_path fails, the jne instruction will not be taken which triggers an error message to be logged …and error message with the following format:
Based on this, we can assume the vnode is being retrieved from the path of the script. We can confirm this, by looking at the disassembly just prior to the call to vnode_for_path
From this, we can see the code invokes a helper method ( ::cred ) which simply invokes the proc_ucred Apple API to retrieve a kauth_cred_t for a specified process (e.g. the process being evaluated).
The code then extracts something from offset 0x78 in the kauth_cred_t structure, and adds this other value (from offset 0xb64 ).
If we look at the kauth_cred_t , we find typedef ’d as a pointer to a ucred structure, which is defined in sys/ucred.h . And what’s at offset 0x78 ?? A pointer to label structure named cr_label that the comments specify is a “MAC label”
We can confirm this, by printing out the kauth_cred_t structure returned by the call to ASPProcessInfo::cred and then manually the value at offset 0x78 (and confirm they are the same):
BSD documentation, notes that this label structure “consists of a fixed-length array of unions, each holding a void * pointer and a long.”
The value (extracted from offset 0xb64 of rbx ) is an index, required to index into the correct l_perpolicy slot. During dynamic analysis, this value was also 0x3.
As this value extracted from this structure is then passed to the vnode_for_path API, we can assume it’s a path. But to what? Our script!
/>The label within the credential structure (normally) contains the path to the script
We can confirm this in the debugger by printing out the label structure (from the ucred structure returned by the call to ASPProcessInfo::cred() ) …specifically the value found in l_perpolicy[0x3]
…recall that in the exec_activate_image function during the process launch of a normal script-based application, the ip_scriptlabelp member would be set and appears to have been initialized with the path to the script (in l_perpolicy[0x3] ).
If we continue to debug, and stop to the call to the vnode_for_path API we can confirm this path (found in the rdi register), is passed to the API:
No real surprise, the code is simply looking the vnode of the path of the script that it’s about to be executed.
So the vnode_for_path API is being invoked to get the vnode of the script, which then recall is (eventually) moved into the r13 register.
And again recall that if the r13 register is not NULL, the evaluateScript function will be invoked! (which in turn calls out to syspolicyd to evaluate and block untrusted scripts).
Great! But what happens if the exec_activate_image function does not set the ip_scriptlabelp member? Well, this means the code in the procNotifyExecComplete that checks if this MAC label is NULL will trigger:
Specifically the ‘je’ (jump equal/zero) instruction at 0x0000000000007c9d will be taken, which explicitly NULLs out the local variable which normally holds the vnode of the script:
As this variable ( var_100 ) is what populates the r13 register, this means the r13 register will be NULL, which recall, means the evaluateScript is not called!
…which explain why our PoC application is never evaluated, and thus allowed to execute though it is unsigned and unnotarized.
/>For an interpreter-less script, the label remains unset
In Summary
Well, we covered a lot, so let’s end by succinctly summarizing the bug:
When a script-based application with no specified interpreter is launched, execution initially fails with ENOEXEC . This causes the script to be (re)executed via /bin/sh .
As /bin/sh is a Mach-O (not a script), no script labels are set. Thus the script is never evaluated (nor blocked) by syspolicyd .
The policy engine just sees /bin/sh (a trusted platform binary), and thus allows it to execute. Of course /bin/sh then executes the PoC script.
End result? Though the PoC’s script is unsigned and not notarized, it is allowed to execute! #gameover
Conclusions
The vast, vast, majority of macOS malware requires some user interaction (such as directly running the actual malicious code) in order to infect a macOS system. Unfortunately such macOS malware still abounds and everyday countless Mac users are infected.
Since 2007 (with the introduction of File Quarantine), Apple has sought to protect users from inadvertently infecting themselves if they are tricked into running such malicious code. This is a good thing as sure, users may be naive, but anybody can make a mistakes. Moreover such protections (specifically notarization requirements) may now even protect users from advanced supply-chain attacks …and more.
Unfortunately due to a subtle logic flaw spread across various components of the macOS kernel, such security mechanisms were proven fully and 100% moot, and as such we (well macOS), were basically back to square one.
In this blog post, we started with an unsigned, unnotarized, script-based proof of concept application that could trivially and reliably sidestep all of macOS’s relevant security mechanisms (File Quarantine, Gatekeeper, and Notarization Requirements) …even on a fully patched M1 macOS system. Armed with such a capability macOS malware authors could return to their proven methods of targeting and infecting macOS users. Yikes again!
The core of the blog post dug deep into both the application launch mechanism (in both user and kernel mode), as well as the system policy internals found within the AppleSystemPolicy kernel extension. This analysis revealed exactly why “interpreter-less” script-based applications were “ignored” by policy engine. And thus, essential security logic, such as alerting the user and blocking the untrusted application, was skipped.
Luckily Apple has now patched the flaw, originally in macOS 12 beta 6 and also then in macOS 11.6.
And even before that, if you were running BlockBlock with “notarization mode” enabled, you’d have been protected (even though BlockBlock had no a priori knowledge of this flaw):
/>BlockBlock, block blocking
Support Me:
Love these blog posts? or my free, open-source tools? You can support them via my Patreon page!
Catalina — syspolicyd high cpu usage: SoftwareUpdateNotificationManager crashes
Recently I’ve been noticing my ‘syspolicyd’ is taking up extremely high (90-100%) CPU usage at startup on macOS catalina 10.15.7. Lately, it takes half an hour or more and it keeps rescanning. This is new and extremely annoying behaviour for me, so I started to check ‘Console.app’. Here I get a crash report of ‘SoftwareUpdateNotificationManager’ (see pastebin below):

Process: SoftwareUpdateNotificationManager [497]Path: — Pastebin.com
In the past I know that I disable update notifications via the terminal commands:
»softwareupdate —ignore "macOS Catalina"
and
»
#!/bin/bash
defaults write com.apple.systempreferences AttentionPrefBundleIDs 0
killall Dock
"
(A picture from my console is added too). Do any of you know what can cause this issue and how to tackle this in the future?
Update: For now it seems to be fixed by doing this:
—Running the below terminal command:
"sudo softwareupdate —reset-ignored"
—SMC reset.
Please feel free to still comment, as I’d like to learn from your response (+ it can be interesting for others). Thanks.
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.

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.
or to evaluate if something will pass gatekeeper checks or not
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.
You can see the data associated with any extended attribute using following command
You can also add your own extended attributes:
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:
The format of the data is the following:
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
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:
- LSQuarantineEventIdentifier – Unique Id for the record
- LSQuarantineTimeStamp – The time (in seconds since 1/1/2001 12am)
- LSQuarantineAgentBundleIdentifier – the downloading application bundle name
- LSQuarantineAgentName – The application name
- LSQuarantineDataURLString – The URL the file was downloaded from
- LSQuarantineSenderName – The name of the person who sent you the email from which you downloaded the attachment
- LSQuarantineSenderAddress – The email of the person who sent you the email from which you downloaded the attachment
- 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
- LSQuarantineOriginTitle – No info, and seemed to be always empty.
- 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.”
- 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:
- Id:Canonical ID
- version: version of the Rule. Will always set to 1
- 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.
- requirement — code requirement — Stores the cdhash on main macho file.
- allow — If the gke exception is allowed. Always be 1(since then only the rule is added in this db)
- disabled — If following rule entry is disabled.
- expires — Expiration of rule. Julian date format. Will be always 5000000(June 7, 8977).
- label — An optional text labeling the rule. Mostly set to GKE or null.
- 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)
- 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
- ctime — rule creation time (Julian)
- mtime — time rule was last changed (Julian)
- 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.
- remarks — optional remarks string. Set to either (gke) or path of bundle/executable in case of unsigned code.
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
- 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 .
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.
OK nerds, the sequel to "Accepted CDHash" is here, discussing Apple’s whitelist of 3rd party code signature hashes: http://t.co/zWU0VlpHst
— daniel.free (@danielpunkass) October 6, 2014
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.
Forbidden Commands to Speed Up macOS
First, ask yourself, would you like to undo a decade of security protections painstakingly created by Apple, protecting your Mac from malware, spyware, and ransomware? What if these so-called protections prevented the normal and speedy usage of your Mac? See exhibits: [A, B, C, D, E, F]
Is that a yes? Speed and convenience over security any day! Let us march on boldly ! The steps listed below will give you a short description of each protection we disable, and the necessary command in Terminal.
Step-by-step Guide
Step 1: Disable GateKeeper. This is the part of macOS that deals with code signature validation. It checks if the app in question was signed by the creator, and then checks whether Apple has given the creator a thumbs-up. macOS 10.15 made this much more stringent, requiring Apple to give each app a thumbs-up.
Step 2: Disable Library Validation. This protection checks if an app’s libraries are signed by Apple or the creator. Until very recently, macOS apps could load code freely from foreign sources called code libraries. With macOS 10.15, apps are no longer allowed to load libraries that weren’t originally packaged with it, unless they explicitly allow it.
Step 3: Disable System Integrity Protection. You have to enter Recovery Mode (by holding Command+R while rebooting) in order to disable SIP. This mode lets us change boot data for the Mac. SIP prevents both malware and power-users alike from modifying the system files, core apps, and the kernel of macOS. It does this by only allowing apps and extensions signed by Apple to modify the system.
Step 4: Disable Apple Mobile File Integrity. AMFI is the macOS kernel module that enforces the code-signing validation from Step 1 and the library validation from Step 2. However, even after disabling the services above, AMFI is still checking the signatures of every app that is run, and will cause non-Apple apps to crash when they touch extra-sensitive areas of the system.
Step 5: Reboot & Enjoy Liberty. No explanation required.
Caveats:
- If GateKeeper is enabled while AMFI is disabled, some apps will hang while opening.
- If AMFI is disabled, prompts to allow apps access to the camera, microphone, accessibility etc. will not be shown. The tccplus utility, found here, alleviates this (there is a GUI script in the repo).
Further Reading:
Addendum:
Unfortunately, my article was flagged by some users after getting on the front page of Hacker News! I guess they just didn’t appreciate the humour? However, you can check out the discussion before it was taken down.