CVE-2020-9971 Abusing XPC Service mechanism to elevate privilege in macOS/iOS
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 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.
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.
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.
XPC Services added exist in the subdirectory of target process.
The caller user is root.
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 | bool __fastcall sub_10000B440(char *a1, char *a2) |
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 | com.apple.xpc.launchd.domain.pid.systemsoundserverd.308 = { |
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 | xpc_object_t pid_dict = xpc_dictionary_create(NULL, NULL, 0); |
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 | com.r3df09.test.testXPC = { |
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 | <key>LaunchProperties</key> |
We could watch our custom XPC Service listen on the port 9999.
1 | com.r3df09.test.testXPC = { |
We could also check network port 9999 is on listening by launchd
.
1 | Active Internet connections (including servers) |
Any connections to this socket port would make launchd
process start our XPC Service with root privilege.
1 | r3df09@r3df09s-Mac ~ % ps -A | grep test |
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
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!