Project Zero

来自 Google 的 Project Zero 团队的新闻和更新

2025年3月26日,星期三

突破 WebP 的防线

NSO BLASTPASS iMessage 漏洞分析 Posted by Ian Beer, Google Project Zero

2023年9月7日,Apple 发布了一个针对 iOS 的带外安全更新:

Release notes for iOS 16.6.1 and iPadOS 16.6.1, including CVE-2023-41064 for ImageIO and CVE-2023-41061 for Wallet, detailing security updates and potential exploitation.

大约在同一时间,2023年9月7日,Citizen Lab 发布了一篇博文,将 iOS 16.6.1 中修复的两个 CVE 与 "NSO Group 零点击、零日漏洞利用在野捕获" 联系起来:

"[目标是] 一位受雇于总部位于华盛顿特区的、在国际上设有办事处的民间社会组织的人员... 该漏洞链能够在无需受害者任何交互的情况下,入侵运行最新版本 iOS (16.6) 的 iPhone。 该漏洞利用涉及包含来自攻击者 iMessage 帐户发送给受害者的恶意图像的 PassKit 附件。"

在此前一天,2023年9月6日,Apple 向 WebP 项目报告了一个漏洞,并在报告中表明他们计划第二天为 Apple 客户发布自定义修复程序。

WebP 团队第二天在公共 git 仓库中发布了他们的第一个建议修复,五天后,即9月12日,Google 发布了一个包含 WebP 修复程序的新 Chrome 稳定版本。Apple 和 Google 都将此问题标记为在野利用,提醒 WebP 的其他集成商也应迅速集成修复程序,同时也引起了安全研究界的密切关注...

几周后的 2023年9月21日,前 Project Zero 团队负责人 Ben Hawkes(与 @mistymntncop 合作)在 Isosceles 博客上发布了关于漏洞根本原因的第一个详细文章。几个月后,11月3日,一个名为 Dark Navy 的组织发布了他们的第一篇博文:对 WebP 漏洞的两部分分析(Part 1 - Part 2)和一个针对 Chrome (CVE-2023-4863) 的概念验证漏洞利用。

虽然 Isosceles 和 Dark Navy 的文章非常详细地解释了底层内存损坏漏洞,但他们无法解决谜题的另一个引人入胜的部分:如何在一次性、零点击设置中利用此漏洞?正如我们很快将看到的,损坏原语非常有限。如果没有访问样本,几乎不可能知道。

11月中旬,与 Amnesty International Security Lab 合作,我能够获得一些 BLASTPASS PKPass 样本文件以及来自失败的漏洞利用尝试的崩溃日志。

这篇博文涵盖了我对这些样本的分析以及弄清楚 NSO 最近的零点击 iOS 漏洞之一的实际工作方式的历程。对我来说,这段旅程始于立即休三个月的陪产假,并在 2024 年 3 月恢复,这个故事由此开始:

场景设定

为了详细分析 WebP 漏洞的根本原因及其产生的原语,我建议首先阅读我之前提到的三篇博文(Isosceles, Dark Navy 1, Dark Navy 2.)。我不会在这里重述他们的分析(既因为你应该阅读他们的原创作品,又因为它非常复杂!),而是简要讨论 WebP 和漏洞产生的损坏原语。

WebP

WebP 是一种相对现代的图像文件格式,于 2010 年首次发布。实际上,WebP 实际上是两种完全不同的图像格式:一种基于 VP8 视频编解码器的有损格式和一种单独的无损格式。这两种格式除了都使用 RIFF 容器和第一个块名称字符串 WEBP 之外,没有任何共同之处。从那时起(文件中的 12 个字节),它们就完全不同了。该漏洞存在于无损格式中,其 RIFF 块名称为 VP8L。

无损 WebP 广泛使用 Huffman coding;BLASTPASS 样本中至少存在 10 棵 Huffman 树。在文件中,它们存储为 canonical huffman trees,这意味着只保留代码长度。在解压缩时,这些长度直接转换为两级 Huffman 解码表,其中五个最大的表全部被压缩到同一个预分配缓冲区中。这些表(事实证明并非完全)的最大大小是根据它们编码的符号数量预先计算的。如果你正在研究这部分并且有点迷茫,上面引用的其他三篇博文对此进行了详细解释。

通过控制符号长度,可以定义各种奇怪的树,其中许多是无效的。根本问题在于,WebP 代码仅在构建解码表后才检查树的有效性。但是,解码表的预计算大小仅对有效树是正确的。

正如 Isosceles 博客文章指出的那样,这意味着该漏洞的一个基本部分是,触发该错误会被检测到,尽管是在内存损坏之后,并且图像解析仅在几行代码之后停止。这提出了另一个利用之谜:在零点击上下文中,如何利用每次触发问题也会停止解析任何攻击者控制的数据的错误?

第二个谜团涉及实际的损坏原语。该漏洞会在 Huffman 表缓冲区末尾之后的已知偏移量处写入一个 HuffmanCode 结构:

// Huffman lookup table entry
typedef struct {
uint8_t bits;
uint16_t value;
} HuffmanCode;

正如 DarkNavy 指出的那样,虽然 bits 和 value 字段名义上受攻击者控制,但实际上并没有太大的灵活性。第五个 Huffman 表(预分配缓冲区的末尾,部分内容可能会被写出界)只有 40 个符号,将 value 限制为最大值 39 (0x27),并且 bits 将在 1 到 7 之间(对于二级表条目)。在 bits 和 value 之间有一个填充字节,这使得可能写出界的最大值为 0x00270007。恰好这就是漏洞利用所写入的值 - 并且他们可能对此没有太多的选择。

Huffman 表分配大小也没有太大的灵活性。漏洞利用中的表分配为 12072 (0x2F28) 字节,将被四舍五入以适合 0x3000 字节的 libmalloc 小区域。选择代码长度,以便溢出像这样发生:

Memory layout diagram showing Huffman tables at offset 0x3000 and the structure of a Huffman lookup table entry at offset 0x3058.

总结一下:32 位值 0x270007 将被写入到 0x3000 字节 Huffman 表分配结束后 0x58 字节的位置。然后 WebP 解析将失败,解码器将退出。

似曾相识?

Project Zero 博客的长期读者此时可能会感到似曾相识... 我是否已经写过一篇关于 NSO 零点击 iPhone 零日漏洞的文章,该漏洞利用了 iMessage 附件中解析的图像中使用的一种略显晦涩的无损压缩格式中的漏洞?

确实如此

BLASTPASS 与 FORCEDENTRY 有许多相似之处,我的最初直觉(事实证明是完全错误的)是,此漏洞利用可能采用类似的方法,使用一些更高级的 WebP 功能构建一台奇怪的机器。为此,我首先编写了一个 WebP 解析器,以查看实际使用了哪些功能。

转换

JBIG2 非常相似,WebP 也支持对输入像素数据进行可逆转换:

Screenshot of WebP documentation explaining '4 Transforms' and their role in image compression.

Screenshot of a table listing pixel prediction modes with corresponding formulas for calculating predicted values.

我最初的理论是,该漏洞利用可能以类似于 FORCEDENTRY 的方式运行,并在图像缓冲区的边界之外应用这些转换序列来构建一台奇怪的机器。但是在 python 中实现了足够多的 WebP 格式以解析 VP8L 块的每一位后,很明显它只是触发了 Huffman 表溢出而已。VP8L 块只有 1052 字节,几乎所有字节都是触发溢出所需的 10 个 Huffman 表。

什么是 pass?

虽然 BLASTPASS 通常被称为“WebP 漏洞”的利用,但攻击者实际上并没有只发送 WebP 文件(即使 iMessage 支持这样做)。他们发送一个 PassKit PKPass 文件,其中包含一个 WebP。这一定有原因。因此,让我们退后一步,实际看看我收到的一个示例文件:

171K sample.pkpass
$ file sample.pkpass 
sample.pkpass: Zip archive data, at least v2.0 to extract, compression method=deflate

PKPass zip 存档中有五个文件:

60K background.png
5.5M logo.png
175B manifest.json
18B pass.json
3.3K signature

5.5MB 的 logo.png 是 WebP 图像,只是扩展名为 .png 而不是 .webp:

$ file logo.png:
logo.png: RIFF (little-endian) data, Web/P image

PKPass 格式最接近的规范似乎是 Wallet Developer Guide,虽然它没有明确说明 .png 文件实际上应该是可移植网络图形图像,但这可能是意图。这是与 FORCEDENTRY 的另一个相似之处,在尝试解析 GIF 时,使用了一个类似的技巧来访问 PDF 解析器。

PKPass 文件需要一个有效的签名,该签名包含在 manifest.json 和 signature 中。该签名具有可能是伪造的名称和更多时间戳,表明 PKPass 很可能是在运行时为每次漏洞利用尝试生成和签名的。

pass.json 只是:

{"pass": "PKpass"}

最后是 background.png:

$ file background.png 
background.png: TIFF image data, big-endian, direntries=15, height=16, bps=0, compression=deflate, PhotometricIntepretation=RGB, orientation=upper-left, width=48

好奇。另一个具有误导性扩展名的文件;这次是带有 .png 扩展名的 TIFF 文件。

我们将在后面的分析中返回此 TIFF,因为它在漏洞利用流程中起着关键作用,但现在我们将专注于 WebP,但稍作绕道:

Blastdoor

到目前为止,我只提到了 WebP 漏洞,但我在这篇文章开头链接的 Apple 建议提到了两个单独的 CVE:

第一个,ImageIO 中的 CVE-2023-41064,是 WebP 错误(尽管为了避免与上游 WebP 修复程序的另一个 CVE(即 CVE-2023-4863)混淆 - 但它们是相同的漏洞)。

第二个,"Wallet" 中的 CVE-2023-41061,在 Apple 建议中描述为:"恶意制作的附件可能会导致任意代码执行"。

Isosceles 博客文章 假设:

"Citizen Lab 将此攻击称为 "BLASTPASS",因为攻击者找到了一种巧妙的方法来绕过 "BlastDoor" iMessage 沙箱。我们没有完整的技术细节,但看起来通过将图像漏洞捆绑在 PassKit 附件中,恶意图像将在不同的、非沙箱化的进程中处理。这对应于 Apple 发布的第一个 CVE,即 CVE-2023-41061。"

这个理论是有道理的 - FORCEDENTRY 有一个类似的技巧,其中 JBIG2 错误实际上是在IMTranscoderAgent 中利用的,而不是 BlastDoor 的更严格的沙箱。但在我的所有实验以及我所看到的所有在野崩溃日志中,这种假设似乎并不成立。

PKPass 文件和其中包含的图像确实在 BlastDoor 沙箱中解析,并且崩溃或有效负载执行就在那里发生 - 稍后我们还将看到证据表明最终被评估的 NSExpression 有效负载预计将在 BlastDoor 中运行。

我猜测 CVE-2023-41061 更可能指的是对 PKPasses 的宽松解析,它没有拒绝不是 png 的图像。

在 2024 年末,我收到了另一组在野崩溃日志,其中包括两个实际上强烈表明也存在在 MobileSMS 进程中(在 BlastDoor 沙箱之外)命中 WebP 漏洞的路径!有趣的是,时间戳表明这些设备在 2023 年 11 月(即该漏洞被修补两个月后)成为目标。

在这些情况下,WebP 代码通过 CKAttachmentMessagePartChatItem 创建的 ChatKitCKPassPreviewMediaObject 在 MobileSMS 进程中被访问。

WebP 中有什么?

我提到 WebP 文件中的 VP8L 块只有大约 1KB。但是在上面的文件列表中,WebP 文件是 5.5MB!那么它的其余部分是什么?扩展我的 WebP 解析器,我们看到还有一个 RIFF 块:

EXIF : 0x586bb8
exif is Intel byte alignment
EXIF has n_entries=1
tag=8769 fmt=4 n_components=1 data=1a
subIFD has n_entries=1
tag=927c fmt=7 n_components=586b8c data=2c

它是一个(非常非常大的)EXIF - 相机用于存储图像元数据的标准格式 - 比如相机型号、曝光时间、光圈等。

它是一种基于标签的格式,几乎所有的 5.5MB 都在 id 为 0x927c 的一个标签内。那么那是什么?

浏览 EXIF 标签的在线列表,在镜头 FocalLength 标签下方和 UserComment 标签上方,我们发现了 0x927c:

Screenshot of a table listing EXIF tag definitions. The table includes columns for tag number, tag name, and description. The row for tag number 0x927C with the tag name 'MakerNote' is highlighted in red, with the description 'Manufacturer specific information'.

它听起来非常模糊但又令人着迷:“MakerNote - 制造商特定信息。”

Wikipedia 上查找一些澄清,了解这实际上是什么,我们了解到

“"MakerNote" 标签包含通常采用专有二进制格式的信息。”

修改 WebP 解析器以现在转储 MakerNote 标签,我们看到:

$ file sample.makernote 
sample.makernote: Apple binary property list

Apple 选择的“专有二进制格式”是二进制 plist!

事实上:在 IDA 中浏览 ImageIO 库,可以清楚地看到 WebP 解析器、EXIF 解析器、MakerNote 解析器和二进制 plist 解析器之间的路径。

unbplisting

我之前的 一篇博文 中介绍了二进制 plist 格式。那是我第二次不得不分析一个大型 bplist。第一次(对于 FORCEDENTRY 沙箱逃逸),主要是通过手动使用 plutil 的人类可读输出来实现的。去年,对于 Safari 沙箱逃逸分析,bplist 为 437KB,我必须编写一个自定义 bplist 解析器来弄清楚发生了什么。保持指数曲线继续,今年的 bplist 再次大了 10 倍。

在这种情况下,很明显 bplist 必须是一个堆 groom - 并且在 5.5MB 时,大概是一个相当复杂的 groom。那么它在做什么?

切换视图

我有一种预感,bplist 会使用重复的字典键作为堆 groom 的基本构建块,但是运行我的解析器并没有输出任何... 直到我意识到我的工具在转储它们之前将解析的字典直接存储为 python 字典。修复该工具以改为保留键和值的列表,很明显存在重复的键。很多:

Screenshot of code showing a series of nested dictionary creations / duplicate keys

在 Safari 漏洞利用的撰写中,我描述了如何使用不同的可视化技术来尝试探索对象的结构,寻找可以用来简化正在发生的事情的模式。在这种情况下,修改解析器以发出格式良好的花括号和缩进,然后依靠 VS Code 的自动代码折叠,证明足以浏览和了解 groom 对象的结构。

有时,正确的可视化技术足以弄清楚漏洞利用试图做什么。在这种情况下,当原语是基于堆的缓冲区溢出时,groom 不可避免地会尝试将两个东西彼此相邻地放置在内存中,我想知道“哪两个东西?”

但是无论我凝视和滚动多久,我都无法弄清楚任何事情。是时候尝试不同的东西了。

Instrumentation

我编写了一个小助手来使用与 MakerNote 解析器相同的 API 加载 bplist,并使用 Mac Instruments 应用程序运行它:

Screenshot of Instruments app showing memory allocation with detailed information for the top three categories: 'All Heap & Anonymous...', 'CFString (store)', and 'Malloc 16.00 KiB'

解析单个 5.5MB 的 bplist 会导致近 50 万次分配,消耗近 1GB 的内存。仅通过查看此分配摘要,很明显存在许多 CFString 和 CFData 对象,可能用于堆整形。查看列表的更下方,还有其他有趣的数字:

Memory allocation table showing 'All Heap & Anonymous...' using 990.66 MiB of total bytes, with 660.77 MiB being persistent.

最后一行中的 20'000 过于完整而不能是巧合。此数字与分配的 __NSDictionaryM 对象的数量相匹配:

[Table displaying memory usage broken down by allocation size, showing the number of allocations, the size of each allocation, and the total memory used for each size. In the middle of the image, there are 20000 __NSDictionaryM objects allocated.](https://googleprojectzero.blogspot.com/2025/03/<https:/blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgFVqxO