It’s a breezy autumn night and I’m doing some research on my Mac while lazily flipping through volume one of “*OS Internals” by Johnathan Levin, specifically the chapter on the Mach-O file format. All of a sudden, a thought buds in my mind: “What if I can translocate one code signature blob from another, entitlements blob and all?”
I put together a quick script in Python 3 using LIEF to translocate the code signature from Apple’s ps to Apple’s lldb. Failure. The process is killed by taskgated, the enforcer for anything to do with the task_for_pid() syscall. A few more days of toying around and I can’t get it to work. I move on to a different project and the macOS codesigning translocation “vulnerability” is left on my hard drive, to be revisited sometime later.
Several months later, as I was performing a source code audit of Apple’s Security.framework, the codesigning “vulnerability” pops into my head again. I figure, “What the hell, I’ll give it another shot.”
It worked. And it continues to work for a good 40-or so minutes, before the application suddenly starts dying when I try to execute it. This is the story of how I “broke” macOS’ codesigning system, top to bottom.
Don’t expect any more fancy prose.
Codesigning is an important part of the macOS security model, in combination with a myriad of other systems, such as TCC (Transparency, Control, and Consent), AMFI, SIP, and the mosaic of controls that is Security.framework. The framework for codesigning and that which enforces it resides mainly within the loader, dyld, and is scattered around Security.framework. alongside AppleMobileFileIntegrity.kext and amfid.
What you see in the above screenshot happened once, and only once, and has proven thus far to be impossible to replicate. I do know that my Mac was having trouble connecting to http://ocsp.apple.com, but that’s not the only variable involved. From trustd experiencing undefined behavior to taskgated (seemingly) ignoring its job, there has been yet to be an identifiable root cause that can pin down why we were able to sign a binary with restricted entitlements and have it execute. (A possible root cause may have been discovered, but that’s explained below.)
It is far easier, however, to break the codesigning system and sign your binary as an Apple binary. But let’s get this straight: even though the machine will be aware that the LC_CODE_SIGNATURE LoadCommand is tainted, it will still execute.
There are many other restrictions to this as well, including:
- macOS codesigning translocation cannot be used to bypass the hardened runtime. dyld enforces this, and will kill the process.
- macOS codesigning translocation cannot (normally) use any entitlements associated with task ports. taskgated will kill off the process.
- The operating system will recognize the code signature blob as tainted and therefore invalid, thus entitlements (should not) work.
- If deployed to a foreign computer via a web download, it is still subject to the normal xattrs applied during download (com.apple.quarantine), and thus the annoying prompt that almost everyone ignores.
- A binary with a LC_CODE_SIGNATURE LoadCommand with entitlements marked as tainted will be killed by taskgated, barring a few exceptions.
- macOS codesigning translocation cannot be performed on “fat” (Universal) Mach-O or AArch64 binaries due to Apple’s additions to the ISA and the library (LIEF) being used to perform this. You can, however, still perform it with dd and otool.
While writing this article, lightning has struck twice.
Obviously AMFI isn’t enforcing anything at this point.
There’s two caveats: a binary with
com.security.get-task-allow, when resigned with restricted entitlements (thusfar only
task_for_pid-allow have been tested) the binary will be able to execute with those restricted entitlements within a grace period of around 40 minutes. Second, a binary with no entitlements blob in its code signature will be allowed to execute regardless.
Little Snitch, however, catches the binary as not being signed. A bit more of an issue than Gatekeeper, as people will care more about their network monitor than Gatekeeper and its incessant warning on Java .jar files.
Before we break down the vulnerability, here’s the proof of concept:
As stated before, codesigning is handled by a myriad of systems in the operating system – from the kernel, to AMFI, to taskgated, and the greater Security.framework. It all starts with a single binary however, aptly named codesign. The man page for codesign, codesign(1), explains it concisely:
“The codesign command is used to create, check, and display code signatures, as well as inquire into the dynamic status of signed code in the system.”
The codesign binary integrates itself with a variety of user-level APIs exported from Security.framework, alongside amfid to a smaller extent. The tool can also replace existing signatures while preserving metadata (such as an entitlements blob), remove the signature and entitlements blob completely, and manipulate detached signatures (signatures not present in the LoadCommands of the binary). The codesign tool is either run manually or automatically, usually as a part of XCode. There are third party libraries and tools available for manipulating code signatures, but codesign is the go-to when it comes to codesigning and most tools integrate it’s functionality, acting as a wrapper.
codesign being the go-to for anything regarding code signatures, it doesn’t seem that Apple’s security team expected someone to overwrite the LoadCommand with that of another binary.
Cerberus + 1
Cerberus, in Greek mythology, is a multi-headed dog (often depicted with three heads). We look at our problems and recognize fourfold, hence the naming. These are as follows.
The majority of
Of these, taskgated and dyld are our most problematic offenders when it comes to enforcing code signature enforcement (alongside the omnipresent eye of AMFI). Thus the following sections will mainly consist of the enforcement and security mechanisms in place for dyld and taskgated (with AMFI tagging along, of course).
It’s worth noting that, while the majority of Security.framework will be left out, it also plays a large role because it’s trivial to circumvent. ocspd, trustd and XprotectService both manage to fall flat when it comes to code signature enforcement. Due to this, they’re not really worth spending any time on. So, we begin with dyld.
The Binder: DYLD – or why we can’t easily bypass the Hardened Runtime
Luckily for us, Apple is kind enough to opensource the code to
dyld. We’ll be using that as a reference instead of disassembly/decompilation for this section. We’ll be referring to
dyld-852 from the macOS 11.4 release on
. You may be familiar with
dyld for its versatility in aiding in code injection by loading shared libraries in Electron applications or other applications with misconfigured entitlements. (VS 2019 for Mac, VS Code, Firefox, Burp Suite….)
It boils down to two functions:
This is a case of “it does what it says it does”.
This one is a bit more complicated. It checks the main binary’s code signature blob to check the Hardened Runtime’s “require library validation” flag. The problem here is that we’re not working with the main binary in attempts to bypass the hardened library. We’re only stealing a “valid” code signature from a library loaded by the main library and adding it to our malicious library.
In theory, this would allow us to use a modified version of dylib hijacking to gain arbitrary control over any application with the hardened runtime. However, due to both the kernel’s built-in checks, the entirety of Security.framework, and AMFI (when it chooses to be active), our code signature is marked as invalid and the binary will crash. As we’re not tampering with the code signature directly, rather the LoadCommand, there’s no way around this. This is why we cannot easily subvert macOS’ Hardened Runtime. A bit disappointing, but the utility gained by the exploit in bypassing taskgated and AMFI more than makes up for this.
taskgated: Will the Real “Gatekeeper” Please Stand Up?
taskgated, as described by its man page, is as such:
It does more than this, however, because it also implements policies for other restricted system services (entitlements). For example, attempting to transpose the LC_CODE_SIGNATURE of rootless-init will end up with the binary killed by taskgated. The same applies for tccd, ps, and…. wait, what?
Didn’t I just execute a binary with the LC_CODE_SIGNATURE of ps in the beginning of the article? And, it wasn’t killed, and AMFI only made a note of it? (Of course, in macOS 11.4, ps has had its entitlements changed to only have read access to task ports. Other avenues do exist, however.)
What’s happening is taskgated is more than its main page explains. Shocking, I know. An Apple service with hidden functionality (looking at you, interpose list exclusions for AMFI – still waiting for someone to exploit Civ 5).
Looking at the pseudocode, it seems that “authorization” can be given to a process via a write to
stdoutp. From best I can tell, that’s standard output redirected to a pipe (remember that everything on POSIX-like systems is a file!). And we also have a function “copying” authorization for a variety of task-port related entitlements.
(A note for any other reverse engineers – IDA Pro may be the gold standard, but it is not the be-all, end-all. Binary Ninja and Hopper (for macOS) both offer different advantages, alongside Ghidra).
Okay, it’s not that much cleaned up, we just have a different viewpoint. But it provides insight.
v16 in IDA, a AuthorizationOpaqueRef (a pointer to an opaque authorization reference structure used by the Security Server (securityd) to store information about a specific authorization session), is granted specific rights at the end of this function via AuthorizationCopyRights. The pointer is then free()’d, alongside C++ memory (operator delete) and Obj-C++ memory (CFRelease), without returning. This can produce undefined behavior, such as the bug we’ve discovered. This can also explain why the bug has a time limit and does not function in a new tty.
I’ll be the first to admit that I am still not completely of how this behavior is produced and if the pseudocode we’re looking at could be the reason, or something else entirely. Worth noting is that early on
&stru10000C140 )is called if
predicate is not equal to -1.
taskgated is a highly complicated beast, and unfortunately due to how unreliable the bug can be. It’s most likely that
predicate is set to -1,
csr_check is not called, and undefined behavior results from (possibly) how the Authorization pointer is
Focusing on the impact instead of the possible root cause, this gives us authorization for the entitlements in the ps code signature, namely access to com.apple.system-task-ports, which allows us to grab the task ports of system processes. This includes amfid, tccd, and even taskgated. The result is that we can perform arbitrary memory read and write using the Mach virtual memory APIs and inject code into system processes. The impact ranges from local privilege escalation, to bypassing or detouring security elements tccd, amfid, or taskgated, to potentially achieving tfp0. The latter is better known as achieving access to the kernel’s task port (a common, though declining technique in jailbreaks). All you need for it is a thin Mach-O binary with a valid code signature and the right entitlement and Python (or dd, if that’s your thing.) And that’s not even including the utility this can provide for implants when used with a code signature that has no entitlements blob.
An Incomplete Conclusion
I have made many breakthroughs to date, such as reproducing executing binaries with “privileged” entitlements, though I’ve unfortunately not been able to make progress in getting those entitlements to work. We’ve managed to make the code signature more valid to the operating system via patching _LINKEDIT, which is a boon. No more warnings from taskgated, and we’ve produced yet more undefined behavior, in which the embedded code signature is believed to be detached.
Due to macOS using the “hash-of-hashes” technique for codesigning, it’s resilient, though that bit of info disclosed by the kernel may aid us in forging a CDHash, though it’s a SHA-1 CDHash, not SHA256. (Recall that SHA-1 is vulnerable to collisions with enough compute power and as of macOS 11.4 seems to be still acceptable for use in a code signature, but I may be wrong.)
It seems slot 17 (and all slots which don’t match) are the problem. It should be possible to retroactively fix this, as patching _LINKEDIT fixed some of the hashing problems. We can either reuse code from libsecurity-codesigning and the XNU kernel or find another way around it. This remains a problem to be solved, but in its current state, this bug is excellent for implants.
Many variables still remain, though given enough time I think I can solve this. The bootstrap for the attack may be phishing with PyObjC or AppleScript to use spctl to add us to the trust cache, or just simply succeeding in breaking the remaining locks on the code signature mechanism. For now, it’s a method for implants. In the future I can see it being used to achieve task_for_pid(0), assuming macOS hasn’t locked itself down even more by that time.
Good luck to any who want to pursue this and happy hacking.
– Max Headroom
As of Friday, July 16th (perhaps earlier, with the release of Big Sur 11.4), it seems Apple issued a stealth patch against this exploit, without notifying us. Code signatures no longer show up via
codesign or other tools, though the kernel is still able to recognize the “detached code signature”, as seen above. It seems that the code signature format may have changed; given tools such as “Apparency” say the code signature is in an invalid format; alongside my script + classic
otool -l refuse to spit out a valid code signature. As for why Apple has been so silent on the communications side of things, we don’t know.
Inadvertently, Apple has reduced their security posture with this patch
Detached code signatures produced by my tool are no longer identified via
file or verifiable via
codesign, nor does the binary display itself as signed when checked with
codesign. The problem herein lies with the fact that by viewing the binary with
otool -l we can see that we are still successfully attaching a valid
LC_CODE_SIGNATURE LoadCommand and that when executed, the kernel is still aware and tries to validate the code signature. This band-aid patch essentially makes it possible for malware to hide a phony code signature, and does nothing on the kernel side to mitigate the vulnerability.
March 19th – disclosed to Apple, 90 day disclosure timeline set
March 19th – bug acknowledged by Apple
July 29th – full disclosure after 90 days.