A deep dive into an NSO zero-click iMessage exploit: Remote Code Execution(译文)

tang2019 2021-12-17 09:50:00
APT

0x00 背景

今年上半年的时候,Citizen Lab设法捕获了NSO组织针对iMessage的zero-click exploit,它被用来发动定点攻击。在这个由两篇组成的系列文章中,我们将首次披露一个在野针对iMessage的zero-click exploit的运行机制。

从技术上讲,它可以称得上是我们所见过的最复杂的exploit之一,这进一步证明了NSO组织所具备的实力,与以前认为只有少数几个国家才具有的实力不分伯仲。

需要指出的是,本文中讨论的漏洞已于2021年9月13日在iOS 14.8中得到修复,该漏洞的编号为CVE-2021-30860。

0x01 NSO

NSO Group是最著名的“访问即服务”提供商之一,出售完整的黑客解决方案,使原本没有网络入侵能力的组织,只要付费就能获得这种能力。多年来,Citizen Lab和Amnesty International等组织一直在跟踪NSO移动间谍软件包“Pegasus”的滥用情况,已经发现大量与此有关的黑客攻击活动。

在本文中,我们将对Citizen Lab从被入侵的iPhone上提取的Pegasus exploit进行深入的分析,以考察NSO针对iPhone的攻击能力。据我们所知,NSO也对外销售针对Android设备的零点击攻击代码,但是,我们目前手头上面还没有这方面的样本,但如果您有的话,欢迎联系我们。

0x02 From One to Zero

在之前的攻击案例中,攻击者会通过短信向目标发送相应的链接,例如:

0.jpg
Citizen Lab于2016年采集的钓鱼SMS样本

由于被攻击者只有在点击链接后才会被入侵,所以,这种技术被称为one-click exploit。然而,最近有资料显示,NSO正在为他们的客户提供zero-click攻击技术:即使被攻击的对象经验老道,就算他们不会点击网络钓鱼链接,也会神不知鬼不觉的中招。这是因为,当攻击者使用zero-click入侵技术的时候,完全不需要用户进行任何互动。这意味着,攻击者连网络钓鱼信息都不用发,就能让exploit在后台悄悄地运行。除非压根不用手机,否则,攻击目标就难逃one-click exploit的魔咒;也就是说,这是一种防不胜防的武器。

0x03 One weird trick

在攻击iPhone时,恶意软件Pegasus选择的切入点是iMessage。这意味着,攻击者只要知道受害者的电话号码或AppleID的用户名,就能发动定点攻击。

iMessage原生支持GIF图片,这是一种在meme文化中非常流行的动画图片,其最大的特点是体积小且画质低。用户通过iMessage聊天时,可以发送和接收GIF,它们会显示在聊天窗口中。对于苹果公司来说,它们想让这些GIF无休止地循环,而不是只播放一次,因此,在iMessage的解析和处理流水线的早期(在收到信息后,但远在信息显示之前),iMessage会通过IMTranscoderAgent进程调用以下方法(在“BlastDoor”沙箱之外),并将收到的扩展名为.gif的图像文件传递给它:

  [IMGIFUtils copyGifFromPath:toDestinationPath:error]

从选择器的名称来看,这里的意图可能只是在编辑循环计数字段之前复制GIF文件,但该方法的语义却并非如此。在底层,它使用CoreGraphics API将源图像呈现为目标路径上的新GIF文件。尽管这里的源文件名必须以.GIF结尾,但是这并不意味着它就是一个GIF文件。

正如前面Project Zero的博客文章中详细描述的那样,ImageIO库用于推测源文件的正确格式并对其进行解析,并且根本不管文件的扩展名是啥。使用这种“假GIF”技巧,攻击者一下就能让20个图像编解码器“华丽变身为”iMessage zero-click攻击链的一部分,其中包括一些非常晦涩和复杂的图像格式,并可能藉此远程暴露数十万行的代码。

注意:苹果公司告诉我们,他们从iOS 14.8.1(2021年10月26日)开始限制了IMTranscoderAgent可访问的ImageIO格式,并从iOS 15.0(2021年9月20日)开始完全删除了IMTranscoderAgent的GIF代码路径,GIF解码将完全在Blastdoor沙箱内进行。

0x04 A PDF in your GIF

NSO组织就是使用“假冒gif”的伎俩来利用CoreGraphics PDF解析器中的一个安全漏洞的。

大约十年前,由于PDF的普遍性和复杂性,它逐渐成为一个受欢迎的攻击目标。此外,PDF中的javascript的可用性使得开发可靠的exploit变得容易得多。CoreGraphics PDF解析器似乎不能解析javascript代码,但NSO设法在CoreGraphics PDF解析器中找到了同样强大的东西……

0x05 Extreme compression

在20世纪90年代末,带宽和存储容量比现在要稀缺得多。正是在这种环境下,JBIG2标准应运而生了。JBIG2是一个特定领域的图像编解码器,专门用于压缩二值图像,也就是像素的颜色只能是黑色或白色的图像。

它的开发是为了对文本文件的扫描实现超高的压缩率,并在高端办公扫描仪/打印机设备中实现和使用,如下面所示的XEROX WorkCenter设备。如果您在十年前使用了像这样的设备的扫描到PDF的功能,那么生成的PDF中就可能含有一个JBIG2流。

0.jpg
一台使用JBIG2实现scan-to-pdf功能的Xerox WorkCentre 7500系列多功能打印机

这些扫描仪生成的PDF文件特别小,可能只有几千字节。JBIG2使用了两种新技术来实现这些超高的压缩率,并且这些技术都与本文介绍的漏洞有关。

技术1:分割和替换

实际上,每个文本文件,特别是那些用英语或德语等小字母写成的文件,都由每页上许多重复的字母(也称为字形)组成。JBIG2试图将每一页分割成字形,然后使用简单的模式匹配来匹配看起来一样的字形。

0.jpg

通过简单的模式匹配,就可以找到页面上所有看起来相似的形状,例如所有的字母“e”

JBIG2实际上对字形一无所知,也不进行OCR(光学字符识别)。相反,JBIG编码器只是寻找像素的连接区域,并将相似的区域归为一组。而压缩算法则是简单粗暴地用其中一个区域的副本替换所有看起来足够相似的区域:

0.jpg
通过将所有相似的字形替换为一个字形的副本这种方式所生成的文档,不仅内容很容易辨认,而且压缩率也很高。

在这种情况下,输出内容是完全可读的,而要存储的信息量则显著减少。因为这种方法不需要存储整个页面的所有原始像素信息,而只需要存储每个字符的“参考字形”的压缩版本和应该复制该字形的所有位置的相对坐标。然后,解压缩算法将输出页面视为画布,并在存储的相应位置处“绘制”完全相同的字形即可。

这种方案有一个重要的问题:对于一个糟糕的编码器来说,将相似的字符弄混的情况时有出现,这可能会产生有趣的后果。D.Kriesel的博客有一些令人振奋的例子,由发票扫描而成的PDF可能出现不同的数字,或者扫描施工图得到的PDF最终生成了错误的数据。不过,这些都不是我们要讨论的问题,但它们是JBIG2被淘汰出通用压缩格式的一个重要原因。

技术2:精细化编码

如上所述,基于替换的压缩输出是有损的。经过一番压缩和解压缩后,得到的输出与输入并不完全吻合。但是JBIG2也支持无损压缩以及“低损耗”压缩模式。

实际上,它还通过存储(和压缩)替换字形和每个原始字形之间的差异来实现这一点。下面的一个例子显示了左边的替换字符和中间的原始无损字符之间的差异掩码:

0.jpg
利用位图上的异或算子计算差分图像

在这个简单的例子中,编码器可以存储右边显示的差值掩码,然后在解压缩过程中,可以将差值掩码与替换字符进行异或运算,以恢复构成原始字符的精确像素。实际上,还有一些超出本文介绍范围的技巧,它们可以使用替换字符的中间形式作为压缩的“上下文”来进一步压缩差异掩码。

它不是一次性地对整个差异进行完全编码,而是可以分步骤进行,每次迭代都使用逻辑运算符(AND、OR、XOR或XNOR之一)来设置、清除或翻转二进制位。每一个连续的细化步骤都使呈现的输出更接近原始输出,这允许对压缩的“损失”进行一定程度的控制。这些精细化编码步骤的实现非常灵活,它们还能够“读取”输出画布上已经存在的值。

0x06 A JBIG2 stream

虽然大多数CoreGraphics PDF解码器好像都是苹果公司的专有代码,但JBIG2的实现则是一个例外:它来自Xpdf公司,并且源代码是免费提供的。

JBIG2格式是由一系列的段组成的,也可以将其看作是一系列的绘图命令,这些命令在一次传递中顺序执行。CoreGraphics JBIG2解析器支持19种不同的段类型,包括定义新页面、解码Huffman表或将位图渲染到页面上给定的坐标处等操作。

实际上,这些段是由类JBIG2Segment及其子类JBIG2Bitmap和JBIG2Symboldict来表示的。

JBIG2Bitmap表示在矩形区域内排列的一组像素。它的数据字段指向一个包含渲染画布的后备缓冲区(backing-buffer)。

JBIG2SymbolDict将JBIG2Bitmaps组合起来。目标页面被表示为一个JBIG2Bitmap,单个字形也是如此。

JBIG2Segments可以通过段号来引用,而向量类型的GList则存储了所有JBIG2Segments的指针。当根据段号查找一个段时,GList会按顺序进行扫描。

0x07 The vulnerability

该漏洞是一个典型的整数溢出漏洞,出现在对引用的段进行排序的代码中:

  Guint numSyms; // (1)

  numSyms = 0;

  for (i = 0; i < nRefSegs; ++i) {

    if ((seg = findSegment(refSegs[i]))) {

      if (seg->getType() == jbig2SegSymbolDict) {

        numSyms += ((JBIG2SymbolDict *)seg)->getSize();  // (2)

      } else if (seg->getType() == jbig2SegCodeTable) {

        codeTables->append(seg);

      }

    } else {

      error(errSyntaxError, getPos(),

            "Invalid segment reference in JBIG2 text region");

      delete codeTables;

      return;

    }

  }

...

  // get the symbol bitmaps

  syms = (JBIG2Bitmap **)gmallocn(numSyms, sizeof(JBIG2Bitmap *)); // (3)

  kk = 0;

  for (i = 0; i < nRefSegs; ++i) {

    if ((seg = findSegment(refSegs[i]))) {

      if (seg->getType() == jbig2SegSymbolDict) {

        symbolDict = (JBIG2SymbolDict *)seg;

        for (k = 0; k < symbolDict->getSize(); ++k) {

          syms[kk++] = symbolDict->getBitmap(k); // (4)

        }

      }

    }

  }

numSyms是一个在(1)处声明的32位整数。通过提供精心设计的引用段,有可能在(2)处的重复执行加法,并导致numSyms溢出到一个受控的小值。

这个较小的值被用于在(3)处指定堆空间的大小,这意味着syms指向了一个“容量不足”的缓冲区。

在(4)处的最内层循环中,JBIG2Bitmap的指针值被写入了一个容量不足的syms缓冲区中。

如果没有其他技巧,这个循环将把超过32GB的数据写入空间不足的syms缓冲区,当然会导致系统崩溃。为了避免崩溃,需要对堆进行适当的处理,以便让越过syms缓冲区边界后的前几次写操作,破坏的是GList后备缓冲区。这个GList存储的是所有已知的段,供findSegments例程使用,以便将通过refSegs传递的段号映射到JBIG2Segment指针。这里的溢出导致GList中的JBIG2Segment指针被(4)处的JBIG2Bitmap指针覆盖。

由于JBIG2Bitmap继承自JBIG2Segment,所以即使在启用了指针身份验证(指针身份验证用于对虚拟调用执行弱类型检查)的设备上,seg->gettype()虚拟调用也能成功,但是返回的类型现在将不再是jbig2SegSymbolDict,从而导致不能在(4)处进行进一步的写操作,并限制了内存覆盖的范围。

0.jpg
堆溢出发生时的内存布局示意图,其中包括GList后备缓冲区和JBIG2Bitmap,以及一个容量不足的缓冲区

0x08 Boundless unbounding

在拿下段的GList缓冲区之后,攻击者需要立即对代表当前页面(当前绘图命令渲染的地方)的JBIG2Bitmap对象进行相应的处理。

JBIG2Bitmap是后备缓冲区的简单封装器,用于存储缓冲区的宽度和高度(以位为单位)以及一个行值,该行值定义了每行存储的字节数。

0.jpg
JBIG2Bitmap对象的内存布局,其中显示了在溢出过程中被破坏的segnum、w、h和line字段。

通过精心地构造refSegs,攻击者能够在段的GList缓冲区后面再写入三个JBIG2Bitmap指针后停止溢出。这将覆盖vtable指针和表示当前页面的JBIG2Bitmap的前四个字段。由于iOS地址空间布局的特性,这些指针很可能位于第二个4GB虚拟内存中,地址介于0x100000000到0x1FFFFFFFF之间。由于所有iOS硬件都是小endian(这意味着w和line字段可能会被0x1即JBIG2Bitmap指针的高位部分的值所覆盖),而segNum和h字段则可能会被这些指针的低位部分的值所覆盖,这些值通常是相当随机的,具体取决于堆布局和ASLR,通常介于0x100000到0xFFFFFFF之间。

这就为当前目标页JBIG2Bitmap提供了一个未知但非常大的H值。由于该h值用于边界检查,并且应该反映页面后备缓冲区的分配大小,因此这具有“取消”绘图画布边界的效果。这意味着后续的JBIG2段命令可以读写页面后备缓冲区原始边界之外的内存。

通过对堆内存进行相应的处理,攻击者还可以让当前页面的后备缓冲区紧靠在syms缓冲区后面,这样当页面JBIG2Bitmap边界外拓之后,原先的越界访问,现在就变成了读写自己的字段了:

0.jpg
上面的内存布局阐释了拓展边界后的位图后备缓冲区为何能够引用JBIG2Bitmap对象并修改其中的字段,因为从内存位置来看,它正好位于后备缓冲区之后

通过在正确的画布坐标处渲染4字节位图,攻击者就可以对页面JBIG2Bitmap的所有字段进行写操作,另外,通过仔细选择w、h和line的新值,他们还可以对页面后备缓冲区中的任意偏移量处执行写操作。

这样的话,只要知道页面后备缓冲区中的绝对内存地址的偏移量,就能对任意绝对内存地址处执行写操作。但是,如何计算这些偏移量呢?到目前为止,该漏洞利用的方式与“规范”脚本语言的利用方式非常相似,在Javascript中,最终只能获得一个可以访问任意内存的无界ArrayBuffer对象。但是在这种情况下,由于攻击者能够运行任意Javascript代码,这显然可以用来计算偏移量和执行任意计算。如何在单向传递的图像解析器中做到这一点呢?

0x09 My other compression format is turing-complete!

如前所述,实现JBIG2细化的步骤序列非常灵活。细化步骤既可以引用输出位图,也可以引用以前创建的任何段,还可以将输出渲染到当前页或段。通过精心制作细化解压缩过程中与上下文相关的部分,就可以设计出只有细化组合运算符才起作用的段序列。

实际上,这意味着可以在距当前页面的JBIG2Bitmap后备缓冲区的任意偏移处的内存区域之间应用AND、OR、XOR和XNOR逻辑运算符。并且由于它是无界的……,所以,攻击者可以在任意越界偏移量处的内存单元上执行这些逻辑操作:

0.jpg
内存布局示意图,展示了如何越界应用逻辑运算符

当我们把它带到最极端的形式时,事情开始变得非常有趣。设想一下,如果不是在字形尺度的子矩形上进行操作,而是在单个二进制位上进行操作,结果会怎样?

结果就是:您现在可以提供一系列JBIG2段命令作为输入,由于这些命令不仅实现了应用于页面的一系列逻辑位操作,而且由于页面缓冲区是无界的,所以,这些位操作可以在任意内存上进行。

只要随手画几幅草图,你就可以让自己相信,只要有AND、OR、XOR和XNOR逻辑运算符,你就可以实现任何可计算的函数——最简单的证据是,您可以通过与1进行XOR运算,然后将AND门放在逻辑NOT运算符前面来形成NAND门,从而创建逻辑NOT运算符:

0.jpg
与XOR门的一个输入端相连的AND门。另一个XOR门输入连接到常量1,形成一个NAND门

NAND门是一种通用逻辑门,可以用来构建所有其他门;在此基础之上,我们就可以构建相应的电路来实现任何可计算的函数。

0x0A Practical circuits

尽管JBIG2不具备脚本功能,但当它与漏洞结合时,它确实能够模拟在任意内存上运行的任意逻辑门电路。既然如此,那为何不直接用它来构建自己的计算机体系结构并编写脚本呢!?实际上,这个exploit就是这么干的。攻击者使用了70,000多个段命令来定义各种逻辑位操作,并在此基础之上定义了一个小型计算机架构,实现了寄存器和完整的64位加法器和比较器等功能,用于搜索内存和执行算术运算。虽然它没有Javascript快,但在计算上是等价的。

沙箱逃逸的引导操作被写成在这个逻辑电路上运行,整个过程都是在这个奇怪的、模拟的环境中运行的,而这个环境则是通过一个JBIG2流的单一解压过程创建的。这是非常不可思议的,同时也是非常可怕的。

在下一篇文章(很快将于读者见面)中,我们将详细介绍它们是如何实现IMTranscoderAgent沙箱逃逸的。

原文地址:https://googleprojectzero.blogspot.com/2021/12/a-deep-dive-into-nso-zero-click.html?m=1

评论

T

tang2019

这个人很懒,没有留下任何介绍

随机分类

渗透测试 文章:154 篇
区块链 文章:2 篇
Java安全 文章:34 篇
iOS安全 文章:36 篇
MongoDB安全 文章:3 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录