这是一个关于我如何发现我最喜欢的 iOS 漏洞之一的故事。它之所以成为我的最爱之一,是因为实现它的漏洞利用是多么简单。还有一个事实是,它使用了许多苹果操作系统组件仍然依赖的旧版公共 API,而且许多开发人员从未听说过。
Darwin 通知
大多数 iOS 开发者可能习惯使用 NSNotificationCenter,而大多数 Mac 开发者也可能习惯使用 NSDistributedNotificationCenter。前者仅在一个进程内工作,后者允许在进程之间交换简单的通知,并可以选择包含一个字符串,其中包含要与通知一起传输的附加数据。
Darwin 通知 甚至更简单,因为它们是 CoreOS 层的一部分。它们为 Apple 操作系统上的进程之间简单的消息交换提供了一种低级机制。每个通知可以有一个与其关联的 state
,它是一个 UInt64
,并且通常仅用于通过指定 0
或 1
来指示布尔值 true
或 false
,而不是对象或字符串。
该 API 的一个简单用例是,一个进程只想通知其他进程关于给定事件,在这种情况下,它可以调用 notify_post
函数,该函数接受一个字符串,该字符串通常是一个反向 DNS 值,例如 com.apple.springboard.toggleLockScreen
。
有兴趣接收此类通知的进程可以通过使用 notify_register_dispatch
函数进行注册,该函数将在任何其他进程使用指定名称发布通知时,在给定队列上调用一个块。
有兴趣发布带有状态的 Darwin 通知的一个进程必须首先为其注册一个句柄,这可以通过调用 notify_register_check
函数来完成,该函数接受通知的名称和指向 Int32
的指针,函数在该指针处返回一个令牌,该令牌可用于调用 notify_set_state
,该函数还接受一个用于状态的 UInt64
值。
通过相同的 notify_register_check
机制,想要获取通知状态的进程可以调用 notify_get_state
来获取其当前状态。这允许 Darwin 通知用于某些类型的事件,但也保留了一些状态,系统上的任何进程都可以在任何给定时间查询这些状态。
漏洞
苹果操作系统(包括 iOS)上的任何进程都可以在其沙盒内注册,以接收任何 Darwin 通知,而无需特殊权限。考虑到第三方应用程序使用的一些系统框架依赖于 Darwin 通知来实现重要功能,这一点是有道理的。
鉴于通过 Darwin 通知传输的数据量非常有限,即使 API 是公开的,并且沙盒应用程序可以注册通知,Darwin 通知对于敏感数据泄露也不是一个重大风险。
然而,正如系统上的任何进程都可以注册以接收 Darwin 通知一样, 发送通知也是如此。
总之,Darwin 通知:
-
接收不需要特殊权限 -
发送时无需特殊权限 -
可作为公共 API 使用 -
没有用于验证发件人的机制
考虑到这些特性,我开始思考 iOS 上是否有使用 Darwin 通知进行强大操作的地方,这些地方可能会被沙盒应用程序利用,从而作为拒绝服务攻击。
你正在阅读这篇博文,所以我已经剧透了:答案是“是的”。
概念验证:EvilNotify
带着这个问题,我抓取了一份全新的 iOS 根文件系统——当时是早期的 iOS 18 beta 版本之一,我想——并开始寻找使用 notify_register_dispatch
和 notify_check
的进程。
我很快找到了一堆,并制作了一个名为“EvilNotify”的测试应用程序,用于测试。
遗憾的是,我不再拥有可以用来录制真正的设备内视频的易受攻击的设备,但上面的 iOS 模拟器演示展示了概念验证的大部分功能。其中一些在模拟器中不起作用,所以我无法在视频中演示它们。
你可以在视频的结尾看到关于最终拒绝服务攻击的暗示,但让我来提一下它还能做到的其他所有事情。请记住,所有这些都会影响整个系统,即使用户强制退出了应用程序。
-
导致状态栏中出现“液体检测”图标 -
触发灵动岛中显示 Display Port 连接状态 -
阻止系统范围的手势,例如下拉控制中心、通知中心和锁定屏幕 -
强制系统忽略 Wi-Fi,转而使用蜂窝网络连接 -
锁定屏幕 -
触发“数据传输进行中”的 UI,阻止设备使用,直到用户取消 -
模拟设备进入和离开“查找我的”的“丢失模式”,触发 Apple ID 密码对话框提示以重新启用 Apple Pay -
触发设备进入“恢复进行中”模式
“恢复进行中”
由于我正在寻找拒绝服务攻击,最后一个似乎是最有希望的,因为除了点击“重启”按钮外,没有其他出路,这总是会导致设备重启。
它也相当简洁,因为它只包含一行代码:
notify_post("com.apple.MobileSync.BackupAgent.RestoreStarted")
就这样!这行代码足以让设备进入“正在恢复”状态。操作最终会在超时后失败,因为设备实际上并未被恢复,唯一的解决方法是点击“重启”按钮,然后设备就会重启。
查看二进制文件后,SpringBoard 观察到触发 UI 的通知。当设备通过连接的计算机从本地备份恢复时,会触发该通知,但如前所述,任何进程都可以发送该通知并欺骗系统进入该模式。
拒绝服务:VeryEvilNotify
既然我收到了一个可能导致拒绝服务的 Darwin 通知,我就必须想办法在设备重启后反复触发它。
起初,这听起来相当棘手,因为 iOS 上的应用程序在后台处理方面的机会非常有限,而且当应用程序不在前台时,很多具有副作用的 API 都被禁止工作。 后来我发现这不会成为问题,因为我可以验证即使应用程序不在前台,notify_post
也能正常工作。
至于能够让通知在设备多次重启后反复发布,我不太确定,但我直觉认为应用程序扩展最有可能成功。
某些类型的第三方应用程序扩展程序可能会在 iOS 设备首次解锁之前运行,所以我决定尝试我非常熟悉的一种应用程序扩展程序,并创建了一个小部件扩展程序,在一个我称之为“VeryEvilNotify”的新应用程序中。
iOS 会定期在后台唤醒 Widget 扩展。它们有有限的时间来生成快照和时间线,然后系统会在各种地方显示这些内容,包括锁定屏幕、主屏幕、通知中心和控制中心。
由于 Widget 在系统中的使用非常广泛,当安装并启动包含 Widget 扩展的新应用程序时,系统非常渴望执行其 Widget 扩展。这使得应用程序的 Widget 准备好供用户选择并添加到各种支持的位置。
Widget 扩展最终只是一个可以运行代码的进程,所以我将上述代码行添加到我的 Widget 扩展中。我配置了该扩展程序以包含每一种可能的 Widget 类型,只是为了尽可能让 iOS 尽快执行它。
但问题是:widget 扩展会生成占位符、快照和时间线,然后系统会缓存这些内容以节省资源。这些扩展程序不会一直都在后台运行,即使扩展程序请求非常频繁的更新,系统也会强制执行时间预算,如果扩展程序尝试过于频繁地请求更新,则会延迟更新。
为了规避这个问题,我决定尝试让我的 widget 扩展在运行 notify_post
函数后不久就崩溃,我通过在 TimelineProvider
的每个扩展点方法中调用 Swift 的 fatalError()
函数来实现这一点。
调用 notify_post
是作为扩展的入口点的一部分进行的,在将执行交给扩展运行时之前:
import WidgetKitimport SwiftUIimport notifystruct VeryEvilWidgetBundle: WidgetBundle { var body: some Widget { VeryEvilWidget()if#available(iOS 18, *) { VeryEvilWidgetControl() } }}/// Override extension entry point to ensure the exploit code is always run whenever/// our extension gets woken up by the system.@mainstruct VeryEvilWidgetEntryPoint { static func main() { notify_post("com.apple.MobileSync.BackupAgent.RestoreStarted") VeryEvilWidgetBundle.main() }}
有了那个小部件扩展,一旦我在我的安全研究设备上安装了 VeryEvilNotify 应用程序,就显示了“正在恢复”的 UI,然后失败并提示重新启动系统。
重启后,一旦 SpringBoard 初始化,该扩展就会被系统唤醒,因为它之前未能生成任何小部件条目,然后它将重新开始整个过程。
结果是设备软砖,需要擦除设备并从备份中恢复。我怀疑如果该应用程序最终出现在备份中,并且设备从备份中恢复,该错误最终会再次被触发,使其作为拒绝服务更有效。
我的理论是,当小部件扩展崩溃时,iOS 会有一些重试机制,这显然会带有一些节流机制。我仍然认为这是真的,但关于扩展崩溃的时间和恢复开始然后失败的情况,可能阻止了这种机制的运作。
我对我的概念验证感到满意,我向苹果报告了这个问题。
时间线
以下是此漏洞报告的事件摘要时间线。为了简洁起见,我没有包括来自苹果安全报告系统的自动消息的其他状态更新。
-
2024年6月26日:向苹果发送初始报告 -
2024年9月27日:收到苹果的消息,告知我缓解措施正在进行中 -
2025年1月28日:问题被标记为已解决,赏金资格已确认 -
2025 年 3 月 11 日:漏洞被分配了 CVE-2025-24091, 在 iOS/iPadOS 18.3 中得到修复 -
漏洞赏金:17,500美元
尽管 CVE 已经被分配,并且苹果公司提供了一个链接,应该发布公告和致谢,但尚未发生。我被告知它将很快发布,但如果这篇文章发布时还没有发布,你可以在下面阅读公告。
注意公告是如何提到“敏感通知现在需要受限权限”的,暗示了缓解措施是什么。你可以在下一节中阅读更多相关信息。
缓解措施
正如苹果在公告中提到的,发送敏感的 Darwin 通知现在要求发送进程拥有受限权限。这并不是一个允许发布任何敏感通知的单一权限,而是一个前缀权限,形式为 com.apple.private.darwin-notification.restrict-post.<notification>
。
从我对反汇编的简要了解来看,导致通知被“限制”的原因是通知名称中的前缀 com.apple.private.restrict-post.
。
例如, com.apple.MobileBackup.BackupAgent.RestoreStarted
通知现在被发布为 com.apple.private.restrict-post.MobileBackup.BackupAgent.RestoreStarted
,这导致 notifyd
验证发布过程是否具有 com.apple.private.darwin-notification.restrict-post.MobileBackup.BackupAgent.RestoreStarted
授权,然后才允许发布通知。
观察该通知的进程也将使用其新名称,并带有 com.apple.private.restrict-post
前缀,从而阻止任何随机的、未授权的应用程序或进程发布可能对系统产生严重副作用的通知。
我没有机会剖析许多较旧的 iOS 版本来找到引入此机制的确切版本,但多亏了 ipsw-diffs,授权似乎首次出现在 iOS 18.2 build 22C5125e,也就是 iOS 18.2 beta 2。
首批采用者是 backupd
、BackupAgent2
和 UserEventAgent
,它们都获得了与通知系统关于设备恢复相关的授权,从而减轻了我在概念验证中提出的最严重的漏洞利用。
在各种 iOS 18 beta 和发布过程中,越来越多的进程开始采用新的授权,用于受限通知。随着 iOS 18.3 的发布,我在 PoC 中演示的所有问题都得到了解决
原文始发于微信公众号(独眼情报):一行代码如何让你的 iPhone 变砖
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论