【CSDN 编者按】一行看似普通的系统通知代码,居然能让一部 iPhone 反复重启、卡死在恢复界面、彻底“变砖”?在本篇文章中,作者展示了一个几乎无需交互的 iOS 漏洞利用方法——攻击者只需诱导用户安装一个恶意小组件,手机就可能陷入无限恢复重启的死循环,哪怕重启也无济于事。
原文链接:
https://rambo.codes/posts/2025-04-24-how-a-single-line-of-code-could-brick-your-iphone
今天要讲的,是我在 iOS 系统上发现的一个非常喜欢的漏洞。我之所以喜欢这个漏洞,一方面是因为利用它非常简单,几乎一行代码就能触发;另一方面,它用到的是一个苹果操作系统中历史悠久但仍然存在的功能,这个功能是公开的,很多苹果的系统组件还依赖它,不过不少开发者甚至从来没听说过它。
这个功能就叫做——Darwin 通知机制。
首先,“Darwin 通知”是什么?
你可能听说过 iPhone 上的 App 都是“沙盒运行”的——意思是每个 App 都被限制在自己的一块区域里,不能随便访问别的 App 数据,也不能乱动系统的核心部分。这是苹果为了安全设计的一种机制。
但有时候,一个 App 还是需要和别的 App 或系统进行简单的消息交换,这时候就需要一种“进程之间打招呼”的方法。苹果系统中,有几种方式可以实现这个目的,比如:
NSNotificationCenter:只能在同一个进程内部使用,可传递对象或字符串。
NSDistributedNotificationCenter:可支持不同进程之间的简单通信,并允许附带字符串形式的数据,但使用起来有一些限制。
Darwin 通知:这是苹果系统最底层的通知机制,属于操作系统的核心部分(叫做 CoreOS),能让不同进程不管是不是在沙盒内,都能互相发送简单的通知消息。
简单来说,Darwin 通知最大的特点就是:简单、快速,而且不需要任何额外权限。你只需要知道通知的名字,就可以接收或者发送它。那么 Darwin 通知机制具体要怎么用呢?下面简单举个例子。
假设有一个系统功能,比如“锁屏”操作,它在内部可能监听了一个叫做 com.apple.springboard.toggleLockScreen 的通知(这名字只是一个示例)。
如果某个进程想通知系统“请锁屏”,它就可以调用 notify_post("com.apple.springboard.toggleLockScreen") 这个函数。
如果系统正在监听这个通知,那它就会响应,并执行锁屏操作。
而如果有其他进程想监听这个通知,也可以通过一个叫 notify_register_dispatch 的函数注册监听,当通知被发出时就会收到消息。
更进一步,Darwin 通知还可以附带一个状态值,比如“1”表示开启,“0”表示关闭。某个进程发送这类通知时需要先申请一个句柄(可以理解为“发言资格”),然后就能使用 notify_set_state() 来设置这个状态。同样地,其他进程可以通过 notify_get_state() 来读取这个状态。这就让 Darwin 通知不仅能“广播事件”,还可以存储一些系统级的“全局状态信息”,供系统中任何进程实时读取。
问题来了:这个机制太开放,反而成了漏洞
关键点在于:这个 Darwin 通知机制虽然非常方便,但它的权限检查几乎为零。
任何程序,哪怕是受限的沙盒 App(就是你手机上安装的普通 App),都可以使用它:不需要系统权限,不需要苹果签名,也不需要 Entitlement(苹果用来标记系统权限的认证机制)。当然,这在设计上并不算问题,毕竟苹果的很多系统框架(包括第三方 App 可用的)确实依赖 Darwin 通知来完成某些功能。
虽然 Darwin 通知的数据传输能力有限,不太可能被用来窃取敏感数据,但别忘了:发送通知同样不需要权限。总结一下 Darwin 通知的特点:
接收通知不需要特殊权限;
发送通知不需要特殊权限;
API 是公开的;
没有任何发送方校验机制。
正因为如此,我开始思考一个问题:是否存在某些系统组件会监听特定的 Darwin 通知,并在接收到通知时执行高权限操作?如果有,那么是否可能在沙盒应用中通过伪造通知,实现类似拒绝服务(DoS)的攻击?
你现在正在读这篇文章,说明我已经剧透了:确实存在这样的漏洞。
漏洞利用演示:EvilNotify
在我意识到 Darwin 通知机制可能存在严重漏洞之后,我决定进行深入测试。我下载了一份最新的 iOS 系统镜像(当时是 iOS 18 的早期测试版),开始查找系统中有哪些进程使用了 notify_register_dispatch 或 notify_check 这两个函数——只要某个进程用到了这两个函数,那它就可能“对某些通知做出反应”,从而成为攻击目标。
很快,我找到了不少目标进程,于是做了一个小测试应用,取名叫 “EvilNotify”,专门用来验证我的想法。
可惜,我现在手头连一台容易被攻击的旧 iOS 设备都没有,因此无法录制运行效果的真实视频。不过,上面这个 iOS 模拟器的演示视频,基本展示了大部分概念验证(Proof of Concept, PoC)能实现的效果。虽然有些功能在模拟器上无法运行,但核心部分的演示还是保留了下来。
在视频的最后,你应该能看到一点提示,说明了最终“拒绝服务(DoS)攻击”手段是怎样的。但在那之前,让我先列一下 EvilNotify 利用漏洞可以实现的所有破坏性操作。要注意:这些操作都是系统级的,所以哪怕用户强行退出了 App,也无济于事。
EvilNotify 能通过 Darwin 通知机制,触发以下一系列系统行为:
让状态栏显示出“液体检测”警告图标(通常只有检测到 Lightning 接口有液体时才出现);
触发灵动岛(Dynamic Island)显示“外接显示器已连接”的状态提示;
禁用全系统的滑动手势,比如下拉控制中心、通知中心,甚至无法唤出锁屏界面,整部手机基本“半瘫痪”;
强制系统忽略 Wi-Fi,只使用蜂窝移动网络直接锁定屏幕;
强制锁屏;
弹出“数据传输中”的界面,用户必须手动取消,否则设备无法正常使用
模拟设备进入和退出“查找我的 iPhone”(Find My)中的“丢失模式”,并强制触发 Apple ID 密码验证,要求重新启用 Apple Pay;
让设备进入一个“正在恢复中”状态(Restore in Progress)。
“正在恢复中”
因为我当时想找一个最有效的拒绝服务(DoS)方式,最后一种触发“正在恢复中”的模式看起来最有潜力——设备一旦进入“正在恢复中”状态,唯一的办法是点击“重新启动”按钮,从而强制重启设备。
更妙的是,触发这个模式的代码,只需要一行:
notify_post("com.apple.MobileSync.BackupAgent.RestoreStarted");
就是这一行!只要执行了它,设备就会立刻进入“正在恢复中”模式。由于设备实际上并没有真正进行数据恢复操作,所以过一段时间这个状态就会超时失败,但在此期间,用户除了点击“重新启动”按钮之外别无选择,而点击这个按钮就会导致设备重启。
我进一步分析二进制文件发现,正是系统的主界面管理器 SpringBoard 在监听这条通知,用来触发相应的 UI 界面——正常情况下,这条通知只在通过电脑恢复本地备份时发送。但正如之前所说,由于 Darwin 通知没有权限限制,任何 App 都可以伪造这条通知,轻松骗过系统进入恢复模式。
拒绝服务攻击演示:VeryEvilNotify,用一个小组件让 iPhone 无限重启
在我找到了这样一个可能导致拒绝服务(DoS)的 Darwin 通知机制后,我接下来的目标,就是设法让它在设备重启后仍能反复触发。
一开始这听起来相当棘手。毕竟在 iOS 系统中,应用在后台运行的机会非常有限,许多有副作用的 API 在应用不处于前台时是无法正常工作的。不过我发现 notify_post 在应用处于后台时仍然有效,因此这个问题被排除了。
至于怎样在设备重启后,反复发送通知,我最初也不太确定。不过我猜测,利用应用扩展(App Extension)应该是最有可能成功的方式。
一些类型的第三方应用扩展,可以在设备“第一次解锁之前”就运行。因此,我决定尝试一种我非常熟悉的扩展类型 —— Widget Extension(小组件扩展),并基于此创建了一个新的应用,命名为 “VeryEvilNotify(非常邪恶的通知)”。
在 iOS 系统中,小组件扩展会被周期性唤醒,用于生成快照(snapshot)和时间线(timeline),这些内容将被系统缓存,并展示在锁屏界面、主屏幕、通知中心和控制中心等位置。由于小组件的使用范围广泛,当系统检测到用户安装并首次打开了一个包含小组件的 App 时,会非常积极地尝试加载该小组件扩展,从而为用户提供选择并添加组件到界面的机会。
本质上,小组件扩展就是一个可以运行代码的进程,因此我将那行关键代码添加到了扩展的入口逻辑中。为了最大程度地提升小组件被系统唤醒执行的可能性,我还配置了所有可能的小组件类型。不过这也存在一个问题:小组件扩展生成的内容(占位符、快照、时间线)会被系统缓存以节省资源。即使扩展请求了更高的更新频率,系统也会强制实施“时间预算”策略,对频繁更新的请求进行节流控制。
为了绕过这一限制,我决定让扩展在执行完那行关键代码后故意崩溃。具体做法是:在小组件扩展的所有时间线提供点中调用 Swift 的 fatalError() 函数,强制使扩展崩溃——为什么?因为当系统发现一个小组件崩溃、没有生成任何内容后,它会认为“小组件初始化失败”,下次系统启动后会再次尝试唤醒它来修复问题。
因此在扩展的入口点中,我将 notify_post 的调用置于最前,确保每次扩展被唤醒时,都会执行漏洞触发代码:
import WidgetKit
import SwiftUI
import notify
struct 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.
@main
struct VeryEvilWidgetEntryPoint {
static func main
()
{
notify_post
(
"com.apple.MobileSync.BackupAgent.RestoreStarted"
)
VeryEvilWidgetBundle.main
()
}
}
有了这个小组件扩展之后,只要我在测试设备上安装并启动 VeryEvilNotify 应用,设备就会立即弹出“正在恢复中”的界面,然后因超时失败,要求用户“重新启动”系统。而设备重启后,只要 SpringBoard 初始化完成,系统就会再次唤醒这个小组件,从而无限循环地触发崩溃与恢复。
最终结果就是:设备进入软砖状态(soft-brick)。唯一的恢复方式是彻底抹除所有数据,重新刷机,并从备份恢复系统——而更糟糕的是,如果这个 App 被包含进了用户的备份文件中,那么当用户恢复备份后,该漏洞可能再次被触发……
我猜 iOS 系统确实有考虑“扩展崩溃后不该无限重试”,也设置了某种节流机制。但可能是由于这个扩展触发的时间点太巧了——扩展崩溃的时机与“正在恢复中”状态开始并失败的时间点重叠,刚好打破了系统的保护机制,导致系统无法正常处理崩溃恢复。
在实现了这个验证概念(PoC)后,我将这个漏洞如实上报给了苹果的安全团队。
时间线
以下是关于此次漏洞报告的简要时间线。期间还有来自苹果安全报告系统的自动消息更新,但为了简洁,我在本文没有一一列出。
2024 年 6 月 26 日:向苹果提交了初步报告
2024 年 9 月 27 日:收到苹果通知,称缓解措施正在进行中
2025 年 1 月 28 日:问题被标记为已解决,同时确认符合漏洞赏金条件
2025 年 3 月 11 日:漏洞分配了编号 CVE-2025-24091,并在 iOS/iPadOS 18.3 中修复
漏洞赏金金额:17500 美元(约合人民币 12.7 万元)
虽然 CVE 已经分配,苹果也提供了预定发布公告和致谢的链接,但截至本文发布时,公告尚未正式上线,不过应该很快就会发布。为了保险起见,我下面提前附上了公告内容:
可以注意到,公告中提到“敏感通知现在需要受限权限”,这也暗示了苹果采取的修复措施。关于具体细节,可以继续阅读下面的介绍。
苹果修复漏洞的方式
如苹果在公告中所说,现在要发送敏感的 Darwin 通知,发送进程必须具备受限权限(restricted entitlements)。这并不是一个统一的权限可以通发所有敏感通知,而是采用了前缀式的权限控制,格式为: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 会在允许发送该通知前,先校验发送进程是否具备相应的权限。
同时,监听这些通知的系统组件(例如恢复界面)也会使用这个新的“加前缀”的通知名。这样一来,只有被授权的系统程序能互相通信,普通 App 就完全被挡在外面了。
我没有专门去逐个比对旧版 iOS,查找引入这个机制的确切版本。不过,借助 ipsw-diffs 工具可以看到,这项权限机制最早出现在 iOS 18.2 beta 2(版本号 22C5125e)中。其中,最早采用这一新机制的系统组件是:backupd、BackupAgent2 和 UserEventAgent。
这些组件最先获得了发送“敏感通知”的授权,从而封堵了我在 PoC(概念验证)中展示的最严重的利用手段。在随后的各个 iOS 18 测试版及正式版中,越来越多的系统进程也逐步采用了这种受限通知机制。最终在 iOS 18.3 中,所有我在 PoC 测试中展示的问题均被彻底修复。