Rootpipe Reborn (Part II): CVE-2019-8565 Feedback Assistant Race Condition
Relying on pid to validate IPC peer is unsafe.
There’s a general bug type on macOS. When a privileged (or loosely sandboxed) user space process accepts an IPC message from an unprivileged or sandboxed client, it decides whether the operation is valid by enforcing code signature (bundle id, authority or entitlements). If such security check is based on process id, it can be bypassed via pid reuse attack.
Background
An unprivileged client can send an IPC message, then spawns an entitled process to reuse current pid. The privileged service will then validate on the new process and accept the previous IPC request, leading to privilege escalation or even sandbox escape. The attacker can stably win the race by spawning multiple child processes to fill up the message queue.
Security checks based on pid, like sandbox_check
and SecTaskCreateWithPID
suffer from this attack.
The idea and the initial PoC was borrowed from Ian Beer:
Samuel Groß has also been aware of this senario:
- Don’t Trust the PID! Stories of a simple logic bug and where to find it
- Pwn2Own: Safari sandbox part 2 — Wrap your way around to root
Put another way, the IPC server should never use xpc_connection_get_pid
or [NSXPCConnection processIdentifier]
to check the validity of incoming clients. It should use the audit_token_t
instead (note: there was an exception).
Unfortunately these functions are undocumented and private:
xpc_connection_get_audit_token
[NSXPCConnection auditToken]
Since, as noted, these methods are private, third-party developers are trapped in this issue repeatedly:
Apple please consider opening these functions to developers!
Oh wait. Actually audit_token_t
was not so trustworthy. @5aelo has just pointed out another bug before iOS 12.2 / macOS 10.14.4: Issue 1757: XNU: pidversion increment during execve is unsafe 🤦♂
The bug
The privileged XPC service com.apple.appleseed.fbahelperd has exported the following interface:
@protocol FBAPrivilegedDaemon <NSObject>
- (void)copyLogFiles:(NSDictionary *)arg1;
- (void)gatherInstallLogsWithDestination:(NSURL *)arg1;
- (void)gatherSyslogsWithDestination:(NSURL *)arg1;
- (void)sampleProcessWithPID:(unsigned long long)arg1 withDestination:(NSURL *)arg2;
- (void)runMDSDiagnoseWithDestination:(NSURL *)arg1;
- (void)runTMDiagnoseWithDestination:(NSURL *)arg1;
- (void)runBluetoothDiagnoseWithDestination:(NSURL *)arg1 shortUserName:(NSString *)arg2;
- (void)runWifiDiagnoseWithDestination:(NSURL *)arg1;
- (void)runSysdiagnoseWithDestination:(NSURL *)arg1 arguments:(NSArray *)arg2;
- (void)runSysdiagnoseWithDestination:(NSURL *)arg1;
- (void)runMobilityReportWithDestination:(NSURL *)arg1;
- (void)runSystemProfileWithDetailLevel:(NSString *)arg1 destination:(NSURL *)arg2;
- (void)stopDaemon;
- (void)showPrivileges;
- (void)performReadyCheck;
@end
Look at the implementation of -[FBAPrivilegedDaemon listener:shouldAcceptNewConnection:]
method. It only allows XPC messages from one client: /System/Library/CoreServices/Applications/Feedback Assistant.app/Contents/MacOS/Feedback Assistant
launchctl plist "./System/Library/CoreServices/Applications/Feedback Assistant.app/Contents/Library/LaunchServices/fbahelperd"
{
"CFBundleIdentifier" = "com.apple.appleseed.fbahelperd";
"SMAuthorizedClients" = (
"(identifier com.apple.appleseed.FeedbackAssistant) and anchor apple";
);
};
id -[FBAPrivilegedDaemon authorizedClientReq](FBAPrivilegedDaemon *self, SEL selector)
{
id bundle = NSBundle.mainBundle;
id value = [bundle objectForInfoDictionaryKey:@"SMAuthorizedClients"];
if (value && value.count == 1) {
return value.lastObject;
}
return nil;
}
BOOL -[FBAPrivilegedDaemon listener:shouldAcceptNewConnection:](
FBAPrivilegedDaemon *self,
SEL a2,
id a3,
id a4)
{
id pid = [NSNumber numberWithLong:connection.processIdentifier];
id attr = [NSDictionary dictionaryWithObjects:&pid forKeys:&kSecGuestAttributePid count:1];
if (SecCodeCopyGuestWithAttributes(0, attr, 0, &guest)) {
syslog_DARWIN_EXTSN(3, "FBAPrivilegedDaemon SecCodeCreateWithPID returns error 0x%x\n", attr);
return NO;
}
id reqstr = [self authorizedClientReq];
if (!reqstr) {
syslog_DARWIN_EXTSN(3, "FBAPrivilegedDaemon couldn't find an authorized client requirement\n");
return NO;
}
SecRequirementRef requirement;
if (SecRequirementCreateWithString(reqstr, 0, &requirement)) {
syslog_DARWIN_EXTSN(3, "FBAPrivilegedDaemon SecRequirementCreateWithString returns error 0x%x\n", attr);
return NO;
}
if (SecCodeCheckValidity(guest, 0, requirement)) {
syslog_DARWIN_EXTSN(2, "FBAPrivilegedDaemon SecCodeCheckValidity returns error %d (0x%x)\n", attr, attr);
return NO;
}
// ...
}
But since it performs the security check based on process id, we can bypass it. You can now refer to the proof of concept by Ian Beer entitlement_spoof.c or see my full exploit at the end.
The steps to trigger the race condition are as follows:
Create multiple client processes via posix_spawn
or NSTask
(note: you can’t do this on iOS).
Avoid using fork
because Objective-C runtime may crash between fork
and exec
, which is required by this attack.
On 10.13 you can add an OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
environment variable before process creation or add a __DATA,__objc_fork_ok
section to your executable as a workaround. But these workarounds are not compatible with previous macOS. For more information, please refer to Objective-C and fork() in macOS 10.13.
Send multiple XPC messages to the server to block the message queue.
Ian Beer uses execve
to replace the binary to a trusted one and write to its its buffer to prevent the new process from terminating. Instead, I chose to pass these flags POSIX_SPAWN_SETEXEC | POSIX_SPAWN_START_SUSPENDED
to posix_spawn
to create a suspended child process and reuse the pid of the parent.
Since the child process has been replaced, there won’t be any callback. You have to use a “canary” to detect whether the race is successful based on the server’s behavior, e.g., the existence of a newly created file.
From the console output, the server accepts our request:
Now the check is passed
Give Me Root
Now continue code auditing on FBAPrivilegedDaemon
.
The method copyLogFiles:
accepts one NSDictionary
argument, whose keys as the sources and the correspond NSString
as destination to perform file copy. It supports multiple tasks at once, and the path can be both directory or file.
-[FBAPrivilegedDaemon copyLogFiles:]
if ([src hasPrefix:@"/LibraryLogs"] || [src hasPrefix:@"/var/log"]) {
if (![self canModifyPath:dst]) {
result[src] = [NSString stringWithFormat:@"Invalid destination: %@", dst];
} else {
result[src] = @"File must be copied from a log directory";
}
}
-[FBAPrivilegedDaemon canModifyPath:]
if ([dst hasPrefix:@"/var/folders/"] || [dst hasPrefix:@"/private/var/"] || [dst hasPrefix:@"/tmp/"]) {
return TRUE;
} else {
return [dst rangeOfString:@"Library/Caches/com.apple.appleseed.FeedbackAssistant"] != 0;
}
The source must start with /Library/Logs
or /var/log
, and the destination must match one the following patterns:
- ^\/var\/folders
- ^\/private\/var\/
- ^\/tmp
- Library\/Caches\/com.apple.appleseed.FeedbackAssistant
It will not override an existing destination.
These constraints can be bypassed throuth path traversal. So now we can copy arbitrary file or folder to anywhere unless rootless protected.
NSMutableDictionary *traversal(NSDictionary *mapping) {
NSMutableDictionary *transformed = [[NSMutableDictionary alloc] init];
for (NSString *key in mapping) {
NSString *val = mapping[key];
NSString *newKey = [@"/var/log/../../.." stringByAppendingPathComponent:key];
NSString *newVal = [@"/tmp/../.." stringByAppendingPathComponent:val];
transformed[newKey] = newVal;
}
return transformed;
}
Additionally, after each copy, it will call -[FBAPrivilegedDaemon fixPermissionsOfURL:recursively:]
to set the copied files’ owner to the XPC client process’s gid and uid. This is extremely ideal for macOS LPE CTF challenges. I used this zero day exploit during #35C3 CTF to simply copy the flag and read it, lol.
If you don’t mind reboot, getting root privilege is simple. Copy the executable to the places that will be automatically launched with privilege during startup. For example, the bundles in /Library/DirectoryServices/PlugIns
will be loaded by the process /usr/libexec/dspluginhelperd
, who has root privilege and is not sandboxed.
Can we have an instant trigger solution?
Since it will never override existing file, we can not:
- override administrator account’s password digest (
/var/db/dslocal/nodes/Default/users
) ❌ - override suid binaries (not to mention file permission and rootless) ❌
- override one of the PrivilegedHelpers ❌
And it will fix file permissions, none of these would work:
- add sudoer ❌
- add an entry to /Library/LaunchDaemons to register a new XPC service ❌
We need more primitives.
The daemon has other methods named run*diagnoseWithDestination
. They are various external command wrappers just like those diagnose helpers mentioned from my previous post. What’s interesting is that runTMDiagnoseWithDestination: acts the same as timemachinehelper , thus we can trigger the CVE-2019-8513 command injection.
At first I was looking at runMDSDiagnoseWithDestination: , who launches /usr/bin/mddiagnose that will finally spawn /usr/local/bin/ddt
after around 10 seconds, waiting for the /usr/bin/top
command to end. Remember the previous post? This location does not exist by default and we can put custom executable with the arbitrary file copy bug.
Another exploit path is method runMobilityReportWithDestination:
. It invokes this shell script: /System/Library/Frameworks/SystemConfiguration.framework/Versions/A/Resources/get-mobility-info
The script checks the existence of /usr/local/bin/netdiagnose
. If so, execute it as root. The exploit will success within milliseconds.
PCAP_STARTED=0
if [ -x /usr/local/bin/netdiagnose -a ${NO_PCAP} -ne 1 ]; then
trap stop_pcap SIGINT
/usr/local/bin/netdiagnose -p "${WORKDIR}" start packetcapture 2>&1
PCAP_STARTED=1
fi
#
# get-network-info
#
if [ -x /System/Library/Frameworks/SystemConfiguration.framework/Resources/get-network-info ];
/bin/sh /System/Library/Frameworks/SystemConfiguration.framework/Resources/get-network-info
By the way, I was surprised by how many diagnostic tools depending on the non-existing directory /usr/local/bin
.
The bug has been fixed in macOS 10.14.4 and iOS 12.2.
- About the security content of iOS 12.2
- About the security content of macOS Mojave 10.14.4, Security Update 2019-002 High Sierra, Security…
PoC
https://github.com/ChiChou/sploits/tree/master/CVE-2019-8565