查看原文
其他

macOS 终于开始解决 XPC 的一个大坑

0xcc 非尝咸鱼贩 2023-05-27

今年的 WWDC 刚过去,苹果生态当中的几大操作系统都宣布了全新的测试版本。比如 macOS 升级到了 12.0,命名为 Monterey



有眼尖的网友在新版文档当中看到了这个新加入的 API:



int xpc_connection_set_peer_code_signing_requirement( xpc_connection_t connection, const char *requirement);


这个 API 属于 XPC 框架。




XPC


XPC 是 macOS 和 iOS 当中一种基于 Mach 消息的跨进程通信机制,其 API 可以很轻松地将计算任务拆分到多个进程中完成。特别是系统会在消息发送的时候自动启动对应的 service,开发者只需关心 connection 的生命周期,而不是具体的进程。


进程隔离可以提升应用的安全性和稳定性。例如在处理格式解析的时候,损坏或者恶意构造的文件可能造成进程崩溃。有了 XPC,开发者可以将这类容易出问题的运算任务放进独立进程中实现,并使用严格的沙箱配置。


一方面 XPC 进程错误不会让主程序崩溃,另一方面沙箱也会限制漏洞的利用。例如 Safari 浏览器和 WKWebView 就利用了这种机制,采用独立进程渲染 Web 内容。


XPC 的目的是简化多进程权限隔离。XPC 服务不一定会采用更严格的沙箱,反之,还有一种特权 XPC 服务的应用。


在 macOS 系统中想要临时以管理员权限执行操作,系统会要求输入密码或者使用 TouchID 验证身份。实际上后台程序临时的权限提升需求很常见,如果每一次都要求认证,会对用户体验造成一定程度的损害。


macOS 将 AuthorizationExecuteWithPrivileges 函数标记为过时,并推荐使用 SMJobBless 替代。但这两个函数的行为并不等价,甚至可以说是大相径庭。AuthorizationExecuteWithPrivileges 类似 sudo,可以创建更高权限的子进程;SMJobBless 则会向系统中安装一个 PrivilegedHelper 并注册为 Mach Service。


这样一来不是直接 sudo 执行命令,而是仅在第一次安装时需要验证管理员身份,之后后台的 XPC Helper 以 root 权限运行,界面等较低权限的程序通过 XPC 暴露的接口远程调用 XPC Helper 里的代码。


这个机制对第三方开发者开放,而另一方面 mac 系统本身有很多内置的 XPC 服务。


这样便产生了跨安全边界的通信,需要考虑恶意输入的问题。做安全设计需要具备一个思维就是,不要轻易信任数据输入。


我们来看一些风险案例。




Rootpipe


在 2014 年,研究人员向(当时还被叫做)OS X 系统报告了一个安全问题,并命名为 Rootpipe。问题组件出在 writeconfig 的 XPC 服务中。这个服务提供了一个可以写入任意内容到任意路径,同时设置文件属性的远程接口。


这个接口没有做任何校验,任何第三方应用程序都可以调用。写入文件属性的参数可以设置 suid 位,因此直接创建一个具有 suid 权限的可执行文件,其再调用 setuid 即可提升到 root。




校验 peer


macOS 针对此类问题采用的策略是限制可访问 XPC 服务的 peer 的代码签名,首先要求对方数字签名来自 Apple,有时候还会要求具有特定的 entitlement。


XPC 主要有两种风格的 API,一种是面向过程的 C API,在此之上又封装了支持复杂 Objective-C 对象序列化的 NSXPC 系列 API。


面向过程的 API 使用 xpc_connection_set_event_handler 设置一个 block(Objective-C 的匿名函数)来处理 connection 的各种事件。当 xpc_get_type(event) 返回 XPC_TYPE_CONNECTION 时,意味着连接刚刚建立。服务可以通过验证 peer 的有效性来决定是 xpc_connection_resume 继续通信,还是忽略连接。


如果是基于 NSXPC,远程调用被封装到 NSXPCListener 类中。校验 peer 的时机在 delegate 类的 listener:shouldAcceptNewConnection: 方法。假如校验合法,则继续连接并返回 YES。


- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)connection { connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(HelperToolProtocol)]; connection.exportedObject = [[HelperTool alloc] init]; [connection resume]; return YES;}




mac 的文档在这里留了一个大坑,就是没有说明到底应该怎么校验传入连接的合法性。


在 SMJobBless 的官方实例 EvenBetterAuthorizationSample 里写了一句非常误导人的描述。代码在程序的 Info.plist 里设置了一个 SMAuthorizedClients 键值标记允许的代码签名要求(Code Signing Requirement)


https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html


Code Signing Requirement 是一个领域特定语言(DSL),用来描述代码签名所需要满足的条件。示例如下:


anchor apple and info [CFBundleShortVersionString] < "17.4"


这个示例让开发者认为只要简单在 Info.plist 里设置好这个值即可。然而事实上这是需要开发者编写代码去实现如下流程:


  • 读取 Info.plist 里的配置

  • SecRequirementCreateWithString 为描述语言生成 SecRequirementRef

  • 获取 peer 的 SecCodeRef

  • SecCodeCheckValidityWithErrors 检查是否满足要求


也就是说仅仅设置 Infp.plist 是没有任何效果的。


另一个更大的坑来自如何正确地获取 peer 连接的 SecCodeRef。


  • SecStaticCodeCreateWithPath:获取路径对应的

  • SecCodeCopySelf:获取自身的

  • SecCodeCopyGuestWithAttributes:使用指定的属性创建


SecCodeCopySelf 指向进程自身,没有检查的意义。


而 SecStaticCodeCreateWithPath 校验的是文件,存在可运行时替换的问题。由于 mac 系统不像 Windows 那样锁定运行中的可执行文件,完全可以先执行恶意程序 A,然后将自身路径替换成可信的程序 B 而不影响运行。这样尝试检查合法性会误认为有效。这个函数仅在运行程序之间有意义。


SecCodeCopyGuestWithAttributes 支持传入一个 CFDictionaryRef,键名包括:


  • kSecGuestAttributeArchitecture

  • kSecGuestAttributeSubarchitecture

  • kSecGuestAttributePid

  • kSecGuestAttributeAudit


https://developer.apple.com/documentation/security/code_signing_services/guest_attribute_dictionary_keys?language=objc


一个示例如下:


SecCodeRef code = NULL;NSDictionary *attributes = @{    kSecGuestAttributePid: @connection.processIdentifier};
SecCodeCopyGuestWithAttributes(0, attributes, kSecCSDefaultFlags, &code);


这里采用了对方进程的 pid 作为参数,看上去没有什么问题,甚至 mac 自己的服务也这么写。




CVE-2019-8565


进程 pid 实际上在 XPC 验证 peer 的场景中不能信任。笔者在 macOS High Mojave 上找到的漏洞证明,即使是 Apple 自己也被这个 API 坑了。


漏洞在 macOS 10.14.4 被修复。漏洞根源在于 com.apple.appleseed.fbahelperd 这个 XPC 服务。在处理传入连接的时候,进程直接使用 pid 作为参数校验。


实际上在 Unix 系统中 pid 的个数是有上限的,但进程的创建和终止让达到这个上限并非难事。因此操作系统有 pid 复用的机制,在不同时刻,同一个 pid 可能对应了不同的进程。


除了将用过的 pid 回收,还有一个技巧可以主动替换 pid 对应的进程,就是 exec 函数。一般使用 exec 执行程序之前都会 fork,但假如故意忽略掉 fork,就会出现 pid 保持原样,进程却被整个替换成全新的命令的情况。使用 posix_spawn 函数的 POSIX_SPAWN_SETEXEC 标志位也可以实现同样效果。


XPC 在处理来自不同进程的请求时复用了同一个消息队列,这就给条件竞争攻击留下了空间。


攻击程序创建十个左右完全一致的子进程,同时向 XPC 服务发起请求。使用非阻塞函数发送消息之后,迅速调用 exec / posix_spawn 将自身进程替换成签名合法的系统自带程序:


#define COUNT 10int pids[COUNT];for (int i = 0; i < COUNT; i++) { int pid = fork(); if (pid == 0) { xpc_connection_t connection = xpc_connection_create_mach_service("Helper", NULL, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED); xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {}); xpc_connection_resume(connection); xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0); xpc_connection_send_message(connection, message); char* target_binary = "/path/to/valid signed binary"; char* target_argv[] = {target_binary, NULL}; exec_blocking(target_binary, target_argv, environ); } else { pids[i] = pid; }}sleep(1);for (int i = 0; i < COUNT; i++) { pids[i] && kill(pids[i], 9);}


这样一来,总有一些请求正好绕过代码签名检查,得以访问受限制的 XPC 服务接口。在结合 fbehelper 服务本身的路径穿越等缺陷,可以在毫秒级别获得 root 权限。




auditToken


既然 pid 不可靠,在需要安全检查的场景又该如何处理?


在 XNU 里有一个 audit_token_t 结构,除了 pid 之外还保存了其他字段,特别是 p_idversion,用来保证相同的 audit_token_t 只能表示同一个进程:


audit_token.val[0] = my_cred->cr_audit.as_aia_p->ai_auid;audit_token.val[1] = my_pcred->cr_uid;audit_token.val[2] = my_pcred->cr_gid;audit_token.val[3] = my_pcred->cr_ruid;audit_token.val[4] = my_pcred->cr_rgid;audit_token.val[5] = p->p_pid;audit_token.val[6] = my_cred->cr_audit.as_aia_p->ai_asid;audit_token.val[7] = p->p_idversion;


校验代码签名的时候用 audit_token_t 比 pid 更为安全(虽然 Project Zero 后来在这里又找了一个漏洞),但是苹果看上去不想让第三方开发者用这个结构。以下两个函数分别可以在面向过程和 NSXPC 的接口中获取 audit token,但它们既没有文档也没有在头文件中提供:


  • xpc_connection_get_audit_token

  • [NSXPCConnection auditToken]


macOS 自带的服务大量使用这两个接口做安全检查,但它们却不向第三方软件提供。这就导致第三方应用不停地出现类似的安全问题:


  • Google Chrome CVE-2020-6574
    Security: Keystone for macOS should use auditToken to validate incoming XPC messages
    https://bugs.chromium.org/p/chromium/issues/detail?id=1102196

  • KeyBase(KB004)
    还被 35c3 做成了真实 CTF 题

  • Office Mac 版(CVE-2018-8412)

  • VMware Fusion (CVE-2018-6962)

  • Adobe CreativeCloud (CVE-2018-4991)




解决方案


回到一开始我们关注到的这个新函数 xpc_connection_set_peer_code_signing_requirement。这个函数的第二个参数接受一个字符串,也就是前文提到的 Code Signing Requirement 语言。


笔者现在其实没有下最新版的 Beta 系统来验证具体的代码实现,不过相信 Apple 此举是为了解决代码校验这个大坑,隐藏具体的 audit_token_t 和 SecCodeRef 等细节,直接让开发者传入一个字符串来限制所期望的签名规则。




未完待续……


不过就算代码签名得到解决,其实在某些情况下还是有办法绕过。首先受信任的 peer 必须开启 Hardened Runtime,以避免代码注入的问题


https://developer.apple.com/documentation/security/hardened_runtime


曾经只需要一个 DYLD_INSERT_LIBRARIES 环境变量就可以向命令注入一个 dylib 库,或者使用其他 dylib 劫持的方式触发 dlopen 载入恶意代码。这样一来校验 peer 是在主进程上做的,并没有递归对所有的动态库做检查。恶意注入的代码模块可以借此绕过检测,包括最新的 xpc_connection_set_peer_code_signing_requirement API 也有被过的可能。


另外一种情况就是代码重用问题。例如经典的 return oriented programming,在没有控制流保护的平台上一但出现本地可以触发的代码执行漏洞,就可以完全以受信任程序的身份执行任意代码。由于确实没有引入新的可执行文件,无从检测。


因此如果有需要调用 XPC Helper 的程序,一定要避免信任脚本解释器、Electron 等天生就为了执行代码而设计的框架。框架本身可能存在脚本调用 dlopen 的可能性,此外类似 v8 等脚本引擎还容易受到 patchgap 导致的 nday 攻击。通过复用代码冒充可信代码签名的绕过基本解,只能尽量避免出现类似问题。



参考资料


  1. xpc_connection_set_peer_code_signing_requirement https://developer.apple.com/documentation/xpc/3755524-xpc_connection_set_peer_code_sig?language=objc

  2. ObjC 中国 - XPC https://objccn.io/issue-14-4/

  3. Rootpipe - WIkipedia https://en.wikipedia.org/wiki/Rootpipe

  4. The Story Behind CVE-2019-13013 https://blog.obdev.at/what-we-have-learned-from-a-vulnerability/

  5. Code Signing Requirement Language https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html

  6. Hardended Runtime https://developer.apple.com/documentation/security/hardened_runtime

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存