Blasting Past Webp
NSO BLASTPASS iMessage漏洞利用分析
作者:Ian Beer,Google Project Zero
2023年9月7日,苹果发布了iOS的带外安全更新:
大约在同一时间,Citizen Lab发布了一篇博客文章,将iOS 16.6.1中修复的两个CVE与"在野捕获的NSO Group零点击零日漏洞利用"联系起来:
“目标是一名在华盛顿特区设有国际办事处的民间社会组织雇员… 该漏洞利用链能够在受害者无任何交互的情况下,攻破运行最新iOS版本(16.6)的iPhone。 漏洞利用涉及从攻击者iMessage账户发送给受害者的包含恶意图像的PassKit附件。”
前一天,即2023年9月6日,苹果向WebP项目报告了一个漏洞,并在报告中表示他们计划在第二天为苹果客户发布定制修复程序。
WebP团队第二天在公共git仓库中发布了他们的第一个建议修复程序,五天后即9月12日,谷歌发布了包含WebP修复程序的新Chrome稳定版本。苹果和谷歌都将此问题标记为在野利用,提醒其他WebP集成商应快速集成修复程序,同时也引起了安全研究社区的密切关注…
几周后的2023年9月21日,前Project Zero团队负责人Ben Hawkes(与@mistymntncop合作)在Isosceles博客上发布了关于该漏洞根本原因的第一份详细分析。几个月后的11月3日,一个名为Dark Navy的组织发布了他们的第一篇博客文章:对WebP漏洞的两部分分析(第一部分 - 第二部分)以及针对Chrome的概念验证利用(CVE-2023-4863)。
虽然Isosceles和Dark Navy的帖子详细解释了底层的内存损坏漏洞,但他们未能解决另一个有趣的问题:如何在一击即中的零点击设置中成功利用此漏洞?正如我们即将看到的,损坏原语非常有限。如果没有样本访问权限,几乎无法知晓。
11月中旬,在与国际特赦组织安全实验室的合作中,我能够获得一些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广泛使用霍夫曼编码;BLASTPASS样本中至少存在10个霍夫曼树。在文件中,它们存储为规范霍夫曼树,意味着只保留代码长度。在解压缩时,这些长度直接转换为两级霍夫曼解码表,五个最大的表都被挤压到同一个预分配缓冲区中。这些表的最大大小(结果证明并不完全)基于它们编码的符号数量预先计算。如果你读到这部分有点困惑,上面引用的其他三篇博客文章详细解释了这一点。
通过控制符号长度,可以定义各种奇怪的树,其中许多是无效的。根本问题是WebP代码只在构建解码表后检查树的有效性。但解码表的预计算大小仅对有效树是正确的。
正如Isosceles博客文章指出的,这意味着漏洞的一个基本部分是触发错误会被检测到,尽管在内存损坏之后,图像解析仅在几行代码后停止。这提出了另一个利用之谜:在零点击上下文中,如何利用一个每次触发问题都会停止解析任何攻击者控制数据的错误?
第二个谜涉及实际的损坏原语。该漏洞将在霍夫曼表缓冲区末尾的已知偏移处写入一个HuffmanCode结构:
|
|
正如DarkNavy指出的,虽然bits和value字段名义上由攻击者控制,但实际上灵活性不大。第五个霍夫曼表(位于预分配缓冲区末尾的那个,部分可以越界写入)只有40个符号,将value限制在最大值39(0x27),bits将在1到7之间(对于二级表条目)。bits和value之间有一个填充字节,使得可能越界写入的最大值为0x00270007。而恰好这正是漏洞利用写入的值——他们可能没有太多选择。
霍夫曼表分配大小的灵活性也不大。漏洞利用中的表分配为12072(0x2F28)字节,将向上舍入以适应0x3000字节的libmalloc小区域。选择代码长度使得溢出如下发生:
总结:32位值0x270007将被写入0x3000字节霍夫曼表分配末尾的0x58字节处。然后WebP解析将失败,解码器将退出。
似曾相识?
Project Zero博客的长期读者可能会在此时有一种似曾相识的感觉…我不是已经写过一篇关于NSO零点击iPhone零日的博客文章, exploiting一个稍微晦涩的无损压缩格式中的漏洞,该格式用于从iMessage附件解析的图像中吗?
确实。
BLASTPASS与FORCEDENTRY有许多相似之处,我最初的直觉(结果完全错误)是这个漏洞利用可能采用类似的方法,使用一些更高级的WebP功能构建一个怪异机器。为此,我开始编写一个WebP解析器,看看实际使用了哪些功能。
转换
与JBIG2非常相似,WebP也支持对输入像素数据的可逆转换:
我最初的理论是,漏洞利用可能以类似于FORCEDENTRY的方式操作,并在图像缓冲区边界外应用这些转换序列来构建一个怪异机器。但在用python实现足够的WebP格式来解析VP8L块的每一位后,很明显它只触发了霍夫曼表溢出,没有更多。VP8L块只有1052字节,几乎全部是触发溢出所需的10个霍夫曼表。
通行证里有什么?
虽然BLASTPASS通常被称为"WebP漏洞"的漏洞利用,但攻击者实际上并不只是发送一个WebP文件(尽管iMessage支持)。他们发送一个PassKit PKPass文件,其中包含一个WebP。这一定有原因。让我们退后一步,实际查看我收到的一个样本文件:
171K sample.pkpass
|
|
PKPass zip存档中有五个文件:
|
|
5.5MB的logo.png是WebP图像,只是扩展名为.png而不是.webp:
|
|
PKPass格式最接近的规范似乎是Wallet Developer Guide,虽然它没有明确说明.png文件实际上应该是便携式网络图形图像,但这大概是意图。这是与FORCEDENTRY的另一个相似之处,其中使用了类似的技巧在尝试解析GIF时到达PDF解析器。
PKPass文件需要有效的签名,该签名包含在manifest.json和signature中。签名有一个可能是假的名字和更多的时间戳,表明PKPass很可能是为每次漏洞利用尝试动态生成和签名的。
pass.json只是这样:
|
|
最后是background.png:
|
|
好奇。另一个具有误导性扩展名的文件;这次是一个带有.png扩展名的TIFF文件。
我们将在分析后期回到这个TIFF,因为它在漏洞利用流程中扮演关键角色,但现在我们将专注于WebP,并稍作 diversion:
Blastdoor
到目前为止,我只提到了WebP漏洞,但我在本文开头链接的苹果公告提到了两个独立的CVE:
第一个,ImageIO中的CVE-2023-41064,是WebP错误(尽管与上游WebP修复的CVE-2023-4863保持混淆 - 它们是相同的漏洞)。
第二个,“Wallet"中的CVE-2023-41061,在苹果公告中描述为:“恶意制作的附件可能导致任意代码执行”。
Isosceles博客文章假设:
“Citizen Lab称此攻击为’BLASTPASS’,因为攻击者找到了一种聪明的方法来绕过’BlastDoor’ iMessage沙箱。我们没有完整的技术细节,但看起来通过将图像漏洞利用捆绑在PassKit附件中,恶意图像将在不同的、未沙箱化的进程中处理。这对应于苹果发布的第一个CVE,CVE-2023-41061。”
这个理论有道理——FORCEDENTRY有一个类似的技巧,其中JBIG2错误实际上是在IMTranscoderAgent内部利用的,而不是BlastDoor的限制性更强的沙箱。但在我所有的实验中,以及我见过的所有在野崩溃日志中,这个假设似乎不成立。
PKPass文件及其中的图像确实在BlastDoor沙箱内解析,崩溃或有效负载执行发生在那里——稍后我们还将看到证据,表明最终被评估的NSExpression有效负载期望在BlastDoor内部运行。
我的猜测是,CVE-2023-41061更可能指的是宽松的PKPass解析,它没有拒绝不是png的图像。
2024年底,我收到了另一组在野崩溃日志,包括两个实际上强烈表明也存在在小MobileSMS进程中到达WebP漏洞的路径,在BlastDoor沙箱之外!有趣的是,时间戳表明这些设备在2023年11月成为目标,即漏洞被修补两个月后。
在这些情况下,WebP代码在MobileSMS进程内通过ChatKit CKPassPreviewMediaObject到达,该对象由CKAttachmentMessagePartChatItem创建。
WebP里有什么?
我提到WebP文件中的VP8L块只有大约1KB。然而在上面的文件列表中,WebP文件是5.5MB!那么其余部分是什么?扩展我的WebP解析器,我们看到还有一个RIFF块:
|
|
这是一个(非常非常大的)EXIF——相机用于存储图像元数据的标准格式——比如相机型号、曝光时间、光圈等。
它是一个基于标签的格式,几乎全部5.5MB都在一个id为0x927c的标签内。那么那是什么?
查看在线的EXIF标签列表,在镜头焦距标签下方和用户评论标签上方,我们发现了0x927c:
它是听起来非常模糊而迷人的:“MakerNote - 制造商特定信息。”
查看维基百科以澄清这实际上是什么,我们了解到
“MakerNote标签包含通常为专有二进制格式的信息。”
修改webp解析器以现在转储MakerNote标签,我们看到:
|
|
苹果为"专有二进制格式"选择的格式是二进制plist!
确实:在IDA中查看ImageIO库,在WebP解析器、EXIF解析器、MakerNote解析器和二进制plist解析器之间有一条清晰的路径。
unbplisting
我在之前的博客文章中介绍了二进制plist格式。那是我第二次必须分析大型bplist。第一次(对于FORCEDENTRY沙箱逃逸)大部分是手工完成的,只使用plutil的人类可读输出。去年,对于Safari沙箱逃逸分析,bplist是437KB,我不得不编写一个自定义bplist解析器来弄清楚发生了什么。保持指数曲线,今年的bplist又大了10倍。
在这种情况下,相当清楚bplist必须是一个堆布局——并且为5.5MB,可能是一个相当复杂的堆布局。那么它在做什么?
切换视图
我有一个直觉,bplist将使用重复的字典键作为堆布局的基本构建块,但运行我的解析器时没有输出任何…直到我意识到我的工具在转储它们之前将解析的字典直接存储为python字典。修复工具以改为保留键和值的列表,很明显有重复的键。很多:
修改解析器以发出格式良好的花括号和缩进,然后依赖VS Code的自动代码折叠被证明足以浏览和感受布局对象的结构。
有时正确的可视化技术足以弄清楚漏洞利用试图做什么。在这种情况下,原语是基于堆的缓冲区溢出,布局将不可避免地尝试将两个东西在内存中放在一起,我想知道"哪两个东西?”
但无论我盯着和滚动多久,我都无法弄清楚任何事情。是时候尝试一些不同的东西了。
仪器化
我写了一个小助手来使用与MakerNote解析器相同的API加载bplist,并使用Mac Instruments应用程序运行它:
解析单个5.5MB的bplist导致近五十万次分配,消耗近千兆字节的内存。仅仅查看这个分配摘要,很明显有很多CFString和CFData对象,可能用于堆整形。进一步查看列表,还有其他有趣的数字:
最后一行中的20'000太圆了,不可能是巧合。这个数字与分配的__NSDictionaryM对象的数量匹配:
最后,在列表的底部,还有两个更突出的分配模式:
有两组非常大的分配:八十个1MB分配和四十四个4MB分配。
我再次修改了我的bplist工具,以转储每个唯一的字符串或数据缓冲区,以及它的计数和哈希值。查看文件列表,有一个清晰的模式:
对象大小 | 计数 |
---|---|
0x3FFFFF | 44 |
0xFFFFF | 80 |
0x3FFF | 20 |
0x26A9 | 24978 |
0x2554 | 44 |
0x23FF | 5822 |
0x22A9 | 4 |
0x1FFF | 2 |
0x1EA9 | 26 |
0x1D54 | 40 |
0x17FF | 66 |
0x13FF | 66 |
0x3FF | 322 |
0x3D7 | 404 |
0xF | 112882 |
0x8 | 3 |
有大量分配刚好落在十六进制"round"数字下方:0x3ff、0x13ff、0x17ff、0x1fff、0x23ff、0x3fff…这强烈暗示它们的大小恰好落在某些分配器大小桶内。
几乎所有的分配都只填充了零或’A’。但1MB的那个非常不同:
|
|
在1MB对象的hexdump中,明显有一个NSExpression有效负载——这个有效负载在WebP文件上运行strings时也可见。iVerify的Matthias Frielingsdorf在BlackHat Asia上做了一个关于此NSExpression有效负载的初步分析的演讲,我们将在本文末尾回到这一点。
同样引人注目(并在上面的hexdump中可见): clearly有指针在里面。在分析的这个阶段,还不知道这是一个以某种方式重新定位的有效负载,还是有一个单独的ASLR披露步骤。
在稍高的层次上,这个hexdump看起来有点像Objective-C或C++对象,尽管有些东西很奇怪。为什么前24字节都是零?为什么没有isa指针或vtable?看起来像是有一些整数字段在指针之前,但它们是什么?在分析的这个阶段,我不知道。
动态思考
我尝试了很多在真实设备上重现漏洞利用原语;我构建了工具来动态生成和签名合法的PKPass文件,我可以通过iMessage发送到测试设备,我可以使很多崩溃,但我似乎从未深入漏洞利用——堆布局工作的iOS版本范围似乎很小,我没有确切的设备和iOS版本来测试。
无论我尝试什么:通过iMessage发送原始漏洞利用,发送带有触发器和布局的自定义PKPass,在测试应用程序中直接渲染WebP,或尝试使用PassKit API渲染PKPass文件,我动态管理的最好结果是触发堆元数据完整性检查失败,我假设这表明漏洞利用失败。
(有趣的是,使用合法API在应用程序内渲染PKPass失败,错误是PKPass文件格式错误。确实,漏洞利用样本PKPass格式错误:它缺少多个必需文件。但"安全"的PKPass BlastDoor解析器入口点(PKPassSecurePreviewContextCreateMessagesPreview)至少在这方面不那么严格,将尝试渲染不完整和无效的PKPass。)
虽然让整个PKPass被解析证明很棘手,但通过一些逆向工程,可以调用正确的底层CoreGraphics API来渲染WebP,并让EXIF/MakerNote被解析。然后通过在分配霍夫曼表时设置断点,我希望很明显什么是溢出目标。但实际上完全不清楚以下对象是什么:(这里X3指向霍夫曼表的开始,大小为0x3000字节)
|
|
第一个qword(0x111800000)是一个有效的指针,但这显然不是Objective-C对象,也没有看起来像任何其他可识别的对象,或者与bplist或WebP有多大关系。但运行测试几次,有一个好奇的模式:
|
|
霍夫曼表是0x2F28字节,分配器向上舍入到0x3000。在这两次测试运行中,将分配大小添加到霍夫曼表指针产生了一个可疑的round数字。这不可能是巧合。运行更多测试,表+0x3000指针总是8MB对齐。我记得从我读过的一些关于iOS用户空间分配器的演示中,8MB是一个有意义的数字。这里是Synaktiv的一个:
或者Angelboy的这个:
8MB是iOS用户空间默认分配器的小架区域的大小。看起来他们可能试图布局分配器而不是针对应用程序特定数据,而是针对分配器元数据。是时候深入研究一些libmalloc内部了!
libmalloc
我建议阅读上面链接的两个演示,以很好地概述iOS默认用户空间malloc实现。Libmalloc在四个抽象级别管理内存。从大到小,这些是:架、杂志、区域和块。微小、小和大架之间的大小分割取决于平台。此漏洞利用的几乎所有相关分配都来自小架,所以这是我将关注的。
阅读libmalloc源代码,我注意到区域预告片,虽然仍称为预告片,但现在已移动到区域对象的开头。小区域以8MB的块管理内存。那8MB被分成(对于我们的目的)三个相关部分:头、元数据字数组,然后是形成分配的512字节块:
前0x28字节是一个头,其中前两个字段形成小区域的双向链表:
|
|
小区域以512字节的单位管理内存,称为块。在iOS上,来自小区域的分配由最多31个块的连续运行组成。每个块有一个相关的16位元数据字,称为小元字,它本身被细分为一个最高有效位中的"空闲"标志和一个15位计数。
为了将连续的块运行标记为正在使用(属于分配),第一个元字清除其空闲标志,并将计数设置为运行中的块数。在释放时,分配首先被放置在lookaside列表上,以便快速重用而不释放。但一旦分配真正被释放,分配器将尝试贪婪地合并相邻块。虽然正在使用的运行不能超过31个块,但空闲运行可以增长到包含整个区域。
布局
下面你可以看到小区域的元字数组的状态,该区域直接跟在包含霍夫曼表作为其最后一个分配的区域之后:
|
|
通过一些简单的数学,我们可以将元字数组中的索引转换为它们对应的堆指针。这样做可以转储与上面显示的分配相关联的内存。较大的0x19、0x18和0x1c分配似乎都是通用的布局分配,但两个0x3块分配显得更有趣。第一个(第一个元字在0x14800005a,以黄色显示)是code_lengths数组,在霍夫曼表构建失败后直接释放。蓝色的0x3块运行(第一个元字在0x148000090)是来自MakerNote的CFSet对象的支持缓冲区,包含对象指针。
回想一下,损坏原语将在0x3000分配末尾的0x58字节处写入dword 0x270007(而该分配恰好直接位于此小区域之前)。该损坏具有以下效果(以粗体显示):
|
|
它将正在使用的分配的大小从3块更改为39(或从1536到19968字节)。我之前提到,正在使用的分配的最大大小应该是31块,但这似乎不是在每个单独的释放路径中都检查。如果事情不太顺利,你会遇到运行时检查。但如果事情顺利,你最终会得到这样的情况:
|
|
黄色(0x8027)分配现在超出其原始三个块,并完全重叠以下绿色(0x18)和蓝色(0x3)以及紫色(0x1c)分配的开始。
但一旦这种损坏发生,WebP解析就会失败,并且不会进行任何其他分配。那么他们在做什么?他们如何能够利用这些重叠的分配?我非常困惑。
一个理论是,也许它是某个内部ImageIO或BlastDoor特定对象,重新分配了重叠的内存。另一个理论是,也许漏洞利用有两个部分;这个第一部分将重叠的条目放在分配器空闲列表上,然后发送另一个文件来利用那个?也许我缺少那个文件?但是,为什么会有那个带有NSExpressions的巨大1MB有效负载?这说不通。
拼图碎片
像经常发生的情况一样,退后一步,暂时不考虑问题,我意识到我完全忽视和忘记了一些关键的东西。在分析的一开始,我对PKPass中的所有文件运行了file,并注意到background.png实际上不是png而是TIFF。然后我完全忘记了这一点。但现在解决方案似乎很明显:使用PKPass而不是仅仅WebP的原因是因为PKPass解析器将按顺序渲染多个图像,并且TIFF中必须有某些东西用有用的东西重新分配重叠的分配。
Libtiff带有一套用于解析tiff文件的工具。tiffdump显示头和EXIF标签:
|
|
四个15KB缓冲区的存在是值得注意的,但它们似乎 mostly只是零。这里是tiffinfo的输出:
|
|