整理 | 郑丽媛
出品 | CSDN(ID:CSDNnews)
被称为“围墙花园”的苹果,一直以来都是以“安全”为由坚持着各种外界看来十分霸道的规则:为了安全,禁止第三方换产品电池;为了安全,不同意开放第三方应用商店;为了安全,不允许“侧载”应用......
一切都为了安全,一切都为了用户,既然如此,为何 App Store 里恶意软件层出不穷?为何近日有安全研究人员称苹果长期漠视 iOS 15 中仍然存在的三个零日漏洞?
对此,开发者 Denis Tokarev 否定了 App Store 安全的说法,并详细介绍了恶意软件进入 App Store 的过程,直言道:“苹果不允许任何 App Store 替代品的真正原因是,这样他们就能从所有应用内购中抽取 30% 的佣金,这对他们来说是一项非常有利可图的业务。”
被态度敷衍的苹果激怒
Denis Tokarev 之所以突然如此愤慨,是因为就是他在今年 3~5 月陆续向苹果报告了 iOS 中存在的 4 个零日漏洞。苹果曾向他承诺,会将这些漏洞加进安全内容列表,但却食言了。
4 个漏洞中,苹果只悄悄地在 iOS 14.7 更新中修复了其中一个漏洞,并且没有将其加入安全内容列表,至于另外 3 个零日漏洞,直到 iOS 15 中还依旧存在。
苹果的这番举动令 Denis Tokarev 十分不满,他认为苹果不仅对系统漏洞的发现态度敷衍,还掩盖了它在 iOS 14.7 更新中修复漏洞的行为,而这一切,都是为了避免向 Denis Tokarev 支付根据其安全赏金计划应得的 10 万美元。
Denis Tokarev 试图就此问题与苹果进行沟通,却对方始终没有答复,盛怒之下他写了一篇详细介绍那 3 个苹果还未修复的零日漏洞以及批判其安全赏金计划的博客文章,3 个漏洞分别名为 Gamed、Nehelper installed apps 和 Nehelper wifi info,而 iOS 14.7 中已修复的漏洞为 Analyticsd。
在这篇文章引起公众关注后,苹果终于回复了 Denis Tokarev:
“我们看到了你关于此问题的博文和其他报告。对于延迟回复你,我们深表歉意,但我们只是仍在调查并思考要如何解决这些问题以保护客户。再次感谢你抽出时间向我们报告这些问题,我们感谢你的帮助。如果你有问题,请告诉我们。”
可是,一切都晚了,Denis Tokarev 对苹果这种几乎是“被迫”回应的态度再次激怒,也对有人质疑该恶意代码是否真的能进入 App Store 的行为感到可悲:“他们这么怀疑我是可以理解的,因为苹果总是一遍又一遍地重复,让人们相信 App Store 是安全的。”
因此,Denis Tokarev 再度提笔,详细说明了恶意软件是如何通过审核进入 App Store 的整个过程(包括源代码)。
混淆函数名称的字符串即可
首先,Denis Tokarev 指出,当 App 的二进制文件上传到苹果服务器时,会被进行静态分析。但其实这个过程除了根据预定义的私有 API 集(这些 API 只有苹果自己的 App 才允许使用)检查二进制文件中的字符串列表外,并不会做太多其它工作。
如果在 App 中检测到了私有 API 的使用,苹果就不会上传该 App 的二进制文件,并向 App 作者发送一封邮件:
“我们发现你最近交付的 App [APP_NAME_AND_VERSON] 中存在一个或多个问题,请请更正以下问题,然后重新上传。
ITMS-90338:使用非公共 API - App 包含或继承了 [APP_NAME] 中的非公共类:GKFamiliarPlayerInternal,GKFriendPlayerInternal,GKLocalPlayerInternal。如果你源代码中的方法名称与上面列出的私有苹果 API 一致,请更改你的方法名称以避免该 App 在未来提交时被标记。此外,如果上述一个或多个 API 也存在于你 App 附带的静态库中,那它们必须被删除。如需更多信息,请访问 http://developer.apple.com/support/technical/。”
这种情况下,该怎么办呢?Denis Tokarev 给出了两种方法:
如果这是一个 Objective-C 的 API,那么可以通过 Objective-C 运行时对其动态调用。
例如,先找到一个类GKLocalPlayerInternal(这是 Gamed 漏洞中会使用的一个类),就像 NSClassFromString("GKLocalPlayerInternal")。由于 GKLocalPlayerInternal 属于私有 API,因此在分析二进制文件时会被发现。但是,开发者可以通过多种方式对它进行隐藏,比如,可以将 GKLocalPlayerInternal 简单分为几个部分:NSClassFromString(["GKLoc","lPlayerInternal"].joined(separator: "a")),这样就不会被静态分析检测到。
Gamed 漏洞就是通过这种方法混淆了所有私有 API 的使用,因此在接受苹果静态分析时未被发现。
使用凯撒密码(这是一种替换加密的技术,明文中的所有字母都在字母表上向后或向前按照一个固定数目进行偏移后被替换成密文)。
Denis Tokarev 表示,他发现 App Store 上有一个下载量高达几亿次的 App 就采用了这种方法。该 App 支持 iOS 9,因此开发者必须使用私有 API 来解决 UIKit 漏洞,并为那些无法安装最新 iOS 版本的人改善体验(苹果认为有些设备已过时,因此放弃了对它们的支持)。
举个例子,自 iOS 7 开始,苹果就为 App 图标使用了一种特殊的圆角设计,并将这种设计覆盖到了 App 中包括按钮、警报等所有 UI 组件。但是,这种圆角设计只在 iOS 13 中为第三方开发者提供,同时苹果早在 iOS 11 中就开始调用 CALayer 类的私有方法 setContinuousCorners 用于自己的 App 和系统组件,因此,如果应用开发者想让旧 iOS 版本的用户体验到一致的 App 界面,就必须使用私有 API,而这违反了 App Store 规则。
除了 Gamed 漏洞,Denis Tokarev 另外发现的三个漏洞(Analyticsd、Nehelper installed apps 和 Nehelper wifi info)都使用了苹果认为是私有 API 一部分的 C 函数,而他已经更新了这三个漏洞的源代码,使其能动态调用这些函数,使躲过苹果的静态分析。
实操过程及代码
理论有了,接下来,Denis Tokarev 分享了要如何通过凯撒密码具体实现这一过程。
首先,dlopen 和 dlsym 函数可以加载动态库并解析其中的符号,但考虑到这可能会被 App Store 审核团队检测到,因此开发者可以“曲线救国”:每个 iOS 二进制文件都会导入一个名为 dyld_stub_binder 的符号,而它导自与 dlopen 和 dlsym 相同的库——也就是说,开发者可以通过计算 dlopen 和 dlsym 函数在内存中与 dyld_stub_binder 的距离,然后仅使用这些函数的地址来调用它们。
有一点需要注意,Denis Tokarev 表示,这个距离,也就是偏移量,会根据 iOS 版本和设备型号这两个参数而有所变化,他在 Github 上分享的 3 个漏洞源代码中的偏移量是对应 iPhone 7 Plus 和 iOS 15.0 的值,所以如果这与开发者的实际设备不符,则需重新计算。
以下为计算偏移量的方法:
printf("%lld\n",(long long)dyld_stub_binder - (long long)dlopen);
printf("%lld\n",(long long)dyld_stub_binder - (long long)dlsym);
当有了偏移量后,开发者便可以通过定义自己的函数来调用 dlopen 和 dlsym:
// dlopen
void * normal_function1(const char * arg1, int arg2) {
return ((void *(*)(const char *, int))((long long)dyld_stub_binder - 20780))(arg1, arg2);
}
// dlsym
void * normal_function2(void * arg1, const char * arg2) {
return ((void *(*)(void *, const char *))((long long)dyld_stub_binder - 20648))(arg1, arg2);
}
在 Swift 中将其导入后,开发者便可以重写检查 App 是否已安装的代码,在这过程中无需导入和引用任何符号(除了已被二进制文件默认导入的 dyld_stub_binder):
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
}
简单来说,这一切的目的就是为了不被静态分析检测到,所以要使用凯撒密码混淆包含函数名称的字符串,或者像第一种方法那样将其分成几块。
Denis Tokarev 对这两种方法非常自信:“如果苹果敢说这样也能在审查期间发现,那我会想出不同的应对方法并将它全部发布出来。”
主观性很强的审核流程
在“骗”过静态分析后,App 就会进入 App Store 的审核流程,而 Denis Tokarev 认为在这一过程中,主观性很强:基本上就是一个随机分配的测试人员把 App 下载到自己的 iPad 上,然后满屏幕地点击,再根据他们自己对 App Store 规则的理解和主观意见做出是否允许上架的决定。
可是,这个过程具有很明显的缺点:一般而言,恶意软件都会连接到远程服务器,发送有关当前用户会话的详细信息,并询问是否要执行某些恶意操作。而苹果服务器只会检测苹果测试员或普通用户是否正在使用 App,并基于此发送响应。这意味着,审核人员只会看到一个外表“良善”的恶意 App,发现不了任何可疑之处,并允许它进入 App Store。
因此,这些年来 App Store 中的恶意软件总是层出不穷,而苹果也总是在被别人发现后才下架这些 App,甚至某些情况下还会对举报者视若无睹:有一位开发者 Kosta Eleftheriou 曾明确指出 App Store 中存在许多带有虚假评论的盗版 App,它们向用户收取高昂的费用却提供不了什么功能,因此 Eleftheriou 想让苹果删除这些 App。但最终,苹果并没有怎么听取 Kosta Eleftheriou 的建议,这在 Denis Tokarev 看来,是因为苹果也从这些盗版 App 中获益了:高昂订阅费中 30% 的抽成。
另外,Denis Tokarev 还例举了一些他亲身经历过的经验,以证明 App Store 的审核是有多么“主观随性”:
他提交了一款有关占星术、星座运势、手相和算命的免费 App,却被 App Store 审核团队拒绝,理由是“在 App Store 上已经有足够多的此类 App”。可 App Store 中却有一个 8 美元/周、带有虚假评论的星座 App。
他开发的一款名为 Hobo Simulator 的游戏被 App Store 突然下架,理由之一是他们不喜欢“hobo”(流浪的失业工人)这个词以及游戏内容,但彼时甚至直到如今 App Store 中还有许多类似应用,例如“Hobo Life”、“Hobo - Real Life Simulator”——只有 Denis Tokarev 的“hobo”主题游戏被下架了。
在 Hobo Simulator 被下架后,有人完整克隆了 Denis Tokarev 的这款游戏并且成功在 App Store 中上架。在 Denis Tokarev 向苹果提出关于知识产权的投诉时,苹果回复道 Denis Tokarev 应该自己去和那个抄袭 App 的开发者解决这个问题。
2020 年 7 月,Denis Tokarev 在 App Store 中重新创建了一个 ID,并再次提交了他的“Hobo Simulator”App,而这次,App Store 允许它上架了。
因此,Denis Tokarev 表示,苹果这些行为完全属于反竞争,并且还含有对部分开发者歧视的意味,就算最近苹果因美国集体诉讼允许应用的第三方支付,这也还不够。
他呼吁道,人们必须向苹果施加压力,让他们开放平台,允许 App Store 替代品和侧载的存在,并让开发者获得公平待遇:“面对压迫和不公正,我们必须站在一起,为自由而战!”
那么,对于 Denis Tokarev 的做法与呼吁,你有什么看法吗?
参考链接:
https://habr.com/en/post/580272/
https://habr.com/en/post/579714/
https://en.wikipedia.org/wiki/Caesar_cipher
10 月 23-24 日,到长沙参加 1024 程序员节,领略一线的技术专家和议题、和众多开发者朋友交流合作,庆祝程序员自己的节日。大会嘉宾包括倪光南院士、龚克院士、王怀民院士、MySQL 之父、Kubernetes 联合创始人、RISC-V国际基金会 CTO、PostgreSQL 全球开发组联合创始人、MongoDB CTO……
☞阿里旗下App接入微信支付;马斯克成世界首富;PostgreSQL 14 RC 1发布|极客头条
☞干掉 Android 2.3!
☞AI盯上了外包司机,看后视镜就被扣分,奖金拜拜!