rambo.codes

一行代码如何搞垮你的 iPhone

2025年4月26日 上午11:00

这是我目前发现的最喜欢的 iOS 漏洞之一的故事。之所以喜欢它,是因为实现利用程序非常简单。而且它使用了一个遗留的公共 API,许多 Apple 操作系统组件仍然依赖它,但很多开发者从未听说过。

Darwin Notifications

大多数 iOS 开发者可能习惯使用 NSNotificationCenter,而大多数 Mac 开发者可能也习惯使用 NSDistributedNotificationCenter。前者仅在单个进程中有效,后者允许进程之间交换简单的通知,还可以包含一个字符串,其中包含与通知一起传输的附加数据。

Darwin notifications 甚至更简单,因为它们是 CoreOS 层的一部分。它们为 Apple 操作系统上的进程之间的简单消息交换提供了一个低级机制。每个通知可以关联一个 state,它是一个 UInt64 类型的值,通常仅用于通过指定 01 来指示布尔值 truefalse,而不是对象或字符串。

该 API 的一个简单用例是,一个进程只想通知其他进程关于给定的事件,在这种情况下,它可以调用 notify_post 函数,该函数接受一个字符串,通常是一个反向 DNS 值,例如 com.apple.springboard.toggleLockScreen

有兴趣接收此类通知的进程可以使用 notify_register_dispatch 函数进行注册,每当另一个进程发布具有指定名称的通知时,该函数都会在给定的队列上调用一个 block。

有兴趣发布带有状态的 Darwin 通知的进程必须首先为其注册一个句柄,这可以通过调用 notify_register_check 函数来完成,该函数接受通知的名称和一个指向 Int32 的指针,该函数在其中返回一个 token,该 token 可用于调用 notify_set_state,该函数还接受一个 UInt64 值作为状态。

通过相同的 notify_register_check 机制,想要获取通知状态的进程可以调用 notify_get_state 来获取其当前状态。 这允许 Darwin 通知用于某些类型的事件,但也保留一些系统上任何进程可以随时查询的状态。

漏洞

Apple 操作系统(包括 iOS)上的任何进程都可以注册以接收关于任何 Darwin 通知的通知,从其 sandbox 内部,而无需特殊的 entitlements。 考虑到第三方应用程序使用的某些系统框架依赖 Darwin 通知来实现重要的功能,这一点是有道理的。

鉴于通过它们传输的数据量非常有限,即使 API 是公开的,并且沙盒应用程序可以注册通知,Darwin 通知对于敏感数据泄漏的风险也不大。

然而,正如系统上的任何进程都可以注册以接收 Darwin 通知一样,发送它们也是如此。

总结一下,Darwin 通知:

考虑到这些属性,我开始怀疑 iOS 上是否有地方使用 Darwin 通知来进行强大的操作,这些操作可能会被利用为来自沙盒应用程序的拒绝服务攻击。

既然您正在阅读这篇博文,我已经剧透了:答案是“是”。

概念验证:EvilNotify

考虑到这个问题,我抓取了一个全新的 iOS 根文件系统副本——当时是 iOS 18 的早期 beta 版本之一,我想——并开始寻找使用 notify_register_dispatchnotify_check 的进程。

我很快发现了一堆,并制作了一个名为“EvilNotify”的测试应用程序,可用于测试。

不幸的是,我不再有可用于录制适当的设备视频的易受攻击的设备,但上面的 iOS Simulator 演示显示了概念验证能够做到的事情。其中一些在 Simulator 中不起作用,所以我无法在视频中演示它们。

您可以在视频结尾处看到最终拒绝服务的提示,但让我提一下它能够做的所有其他事情。 请记住,即使在用户强制退出应用程序的情况下,所有这些都会影响整个系统。

“恢复进行中”

由于我正在寻找拒绝服务攻击,因此最后一个似乎是最有希望的,因为除了点击“重新启动”按钮之外,没有其他方法可以退出它,这总是会导致设备重新启动。

它也非常简洁,因为它只包含一行代码:

notify_post("com.apple.MobileSync.BackupAgent.RestoreStarted")

就是这样! 仅仅一行代码就足以使设备进入“恢复进行中”状态。 该操作在超时后不可避免地会失败,因为设备实际上并未被恢复,唯一的补救措施是点击“重新启动”按钮,然后该按钮将重新启动设备。

通过查看二进制文件,SpringBoard 正在观察该通知以触发 UI。 当通过连接的计算机从本地备份恢复设备时,会触发该通知,但是如前所述,任何进程都可以发送该通知并欺骗系统进入该模式。

拒绝服务:VeryEvilNotify

现在我有一个可能导致拒绝服务的 Darwin 通知,我只需要找到一种方法来在设备重启后重复触发它。

起初,这听起来非常棘手,因为 iOS 上的应用程序的后台处理机会非常有限,并且当应用程序不在前台时,会阻止许多具有副作用的 API 工作。 后来我发现后者不是问题,因为我可以验证即使应用程序不在前台,notify_post 也可以工作。

至于能够在设备多次重启时一次又一次地发布通知,我不太确定,但我预感 app extension 最有可能成功。

某些类型的第三方 app extension 可以在 iOS 设备上首次解锁之前运行,因此我决定尝试一种我非常熟悉的 app extension 类型,并创建了一个 widget extension,在一个名为“VeryEvilNotify”的新应用程序中。

Widget extension 会定期被 iOS 在后台唤醒。 它们有有限的时间来生成快照和时间线,然后系统会在各个位置(包括 Lock Screen、Home Screen、Notification Center 和 Control Center)显示这些快照和时间线。

由于 widget 在系统中的使用非常广泛,因此当安装并启动包含 widget extension 的新应用程序时,系统非常渴望执行其 widget extension。 这使应用程序的 widget 准备好供用户选择并添加到各种支持的位置。

Widget extension 最终只是一个可以运行代码的进程,因此我将上述代码行添加到我的 widget extension 中。 我已将 extension 配置为包含每种可能的 widget 类型,只是为了使 iOS 尽可能快地执行它。

但是有一个问题:widget extension 会生成占位符、快照和时间线,然后系统会缓存这些快照和时间线,以保留资源。 这些 extension 不会一直都在后台运行,即使 extension 请求非常频繁的更新,如果 extension 尝试过于频繁地请求更新,系统也会强制执行时间预算并延迟更新。

为了规避这种情况,我决定尝试使我的 widget extension 在运行 notify_post 函数后不久总是崩溃,我通过在其 TimelineProvider 的每个 extension 点方法中调用 Swift 的 fatalError() 函数来实现此目的。

notify_post 的调用是在 extension 的入口点处进行的,然后再将执行交给 extension 运行时:

import WidgetKit
import SwiftUI
import notify
struct VeryEvilWidgetBundle: WidgetBundle {
  var body: some Widget {
    VeryEvilWidget()
    if #available(iOS 18, *) {
      VeryEvilWidgetControl()
    }
  }
}
/// 覆盖 extension 入口点以确保每次我们的 extension 被系统唤醒时,都会运行 exploit 代码。
@main
struct VeryEvilWidgetEntryPoint {
  static func main() {
    notify_post("com.apple.MobileSync.BackupAgent.RestoreStarted")
    VeryEvilWidgetBundle.main()
  }
}

有了该 widget extension,一旦我在我的安全研究设备上安装 VeryEvilNotify 应用程序,就会显示“恢复进行中”UI,然后由于提示重启系统而失败。

重新启动后,一旦 SpringBoard 初始化,extension 就会被系统唤醒,因为它之前未能生成任何 widget 条目,然后该过程将重新开始。

结果是设备被软砖了,需要擦除设备并从备份恢复。 我怀疑如果该应用程序最终出现在备份中并且设备是从备份恢复的,则该错误最终将被再次触发,从而使其作为拒绝服务更加有效。

我的理论是,当 widget extension 崩溃时,iOS 会有一些重试机制,这显然会有某种限制机制。 我仍然认为这是真的,但是 extension 崩溃的时间和恢复开始然后失败的时间可能阻止了这种机制的运作。

对我的概念验证感到满意后,我向 Apple 报告了该问题。

时间线

以下是此漏洞报告的事件摘要时间表。 有来自 Apple 安全报告系统的其他状态更新,为了简洁起见,我没有包括这些更新。

即使已经分配了 CVE,并且 Apple 已经提供了一个应该发布咨询和信用信息的链接,但尚未发生。 我被告知它将很快发布,但是如果此帖子发布时该咨询尚未发布,您可以在下面阅读该咨询。

Apple 已将 CVE-2025-24091 分配给此问题。 CVE 是用于唯一标识漏洞的唯一 ID。 以下描述了此问题的影响和描述。 影响 - 应用程序可能导致拒绝服务。 描述 - 应用程序可以模拟系统通知。 敏感通知现在需要受限 entitlements。

请注意,该咨询如何提到“敏感通知现在需要受限 entitlements”,这暗示了缓解措施是什么。 您可以在以下部分中阅读有关该内容的更多信息。

缓解措施

正如 Apple 在咨询中提到的那样,发送敏感 Darwin 通知现在要求发送进程拥有受限 entitlements。 这不是允许发布任何敏感通知的单一 entitlement,而是 com.apple.private.darwin-notification.restrict-post.<notification> 形式的前缀 entitlement。

从我对反汇编程序的简要了解中,导致通知受到“限制”的原因是通知名称中包含前缀 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 entitlement,然后才允许发布该通知。

观察通知的进程也将使用其带有 com.apple.private.restrict-post 前缀的新名称,从而阻止任何未经授权的随机应用程序或进程发布可能对系统产生严重副作用的通知。

我没有机会剖析许多较旧的 iOS 版本来找到引入此机制的确切版本,但是感谢 ipsw-diffs,似乎该 entitlement 首次出现在 iOS 18.2 build 22C5125e 中,又名 iOS 18.2 beta 2。

最初的采用者是 backupdBackupAgent2UserEventAgent,所有这些都获得了与通知系统有关设备恢复的 entitlements,从而缓解了我的概念验证中提出的最恶劣的 exploit。

在各种 iOS 18 beta 版本和版本中,越来越多的进程开始采用用于受限通知的新 entitlement,并且随着 iOS 18.3 的发布,我的 PoC 中演示的所有问题都得到了解决。

© 2025 Guilherme Rambo

Guilherme Rambo 写关于他的编码和逆向工程冒险。 使用 Publish 生成