一个有关 V8 漏洞的细节分析 (三)
2021-03-12 10:38:37 Author: www.4hou.com(查看原文) 阅读量:241 收藏

导语:现在,TurboFan可以以此为前提来相应地修改sea of nodes graph,那么它有什么用呢?它尝试优化MaybeGrowFastElements节点,但是这样做非常小心。

一个有关 V8 漏洞的细节分析 (一)

一个有关 V8 漏洞的细节分析 (二)

现在,TurboFan可以以此为前提来相应地修改sea of nodes graph,那么它有什么用呢?它尝试优化MaybeGrowFastElements节点,但是这样做非常小心。它知道作为推测性优化编译器,它所做的假设可能是错误的。通过创建一个新的CheckBounds节点,可以防止这种错误假设。

在if语句中,我们在[4]处看到此CheckBounds节点的创建。有几个参数传递给这个新节点,但是我们在这里只关心index和length参数。根据CheckBounds节点的工作方式,我们知道到达此新创建的CheckBounds节点时,TurboFan将检查以确保索引的实际值(在这种情况下,x == 7)小于或等于长度的值(在本例中,为一个NumberConstant范围(2,2))。显然7不小于或等于2,因此执行将返回给解释器,并且所有希望都将丢失,除非首先由于该错误导致CheckBounds节点从来没有插入到sea of nodes graph中。

此函数确实存在一个错误,Sergey在Bugtracker中简短地讨论了它,因此有必要更深入地研究它。该错误在标有[5]的行上,其中用两个参数调用ReplaceWithValue,即当前节点(即MaybeGrowFastElements节点)和elements节点,这是LoadField[+8]节点和MaybeGrowFastElements节点之间solid 边缘 的value output。这是corrupting_array的后备存储。

29.png

LoadField 和MaybeGrowFastElements之间的Value 边缘

这里的问题是ReplaceWithValue实际上接受四个参数:

30.png

真正应该发生的是调用带有Check_bounds作为Node *效果参数的ReplaceWithValue(node,elements,check_bounds)。由于check_bounds从未传入,因此Node *效果参数最终为nullptr,这使其成为[1]处的传入效果input node(在本例中,这是另一个CheckBounds节点,与长度+ 1024进行比较)。请记住,节点图海洋上的效果边缘是虚线,而值和控制边缘是实线。 MaybeGrowFastElements节点周围的边缘如下所示:

31.png

MaybeGrowFastElements边缘

[2]处的for循环遍历MaybeGrowFastElements节点的每个输出边缘,并将该边缘的“input node”替换为作为参数传入的相应节点。例如,传入的Node *值参数是LoadField [+8]节点(value input 边缘1),该节点将加载corrupting_array的元素指针。 MaybeGrowFastElements的唯一value output 边缘就是通向StoreElement节点的那条边缘,因此,当for循环在该Value 边缘上迭代时,情况如下:

32.png33.png

实际错误发生在[3],这里的边缘变量是MaybeGrowFastElements节点与StoreField [+12]和EffectPhi节点之间的效果输出边缘。我们希望将此边缘的input node(当前为MaybeGrowFastElements节点)替换为创建的新CheckBounds节点,因为当这一切发生的时候,新的CheckBounds节点会有影响输出本身之间的边缘和StoreField(+ 12)和EffectPhi节点。

为什么具有这种效果的输出边缘如此重要?好吧,如果新的CheckBounds节点具有到另一个节点的输出效果边缘,它会告诉TurboFan,在这个新的CheckBounds节点完成其工作之前,其他节点无法做任何需要做的事情。它告诉TurboFan这个CheckBounds节点很重要。请记住,效果边缘是TurboFan如何确定需要特定执行顺序的节点的顺序。

现在,Node *效果参数已设置为链接到MaybeGrowFastElements节点的先前CheckBounds节点,而不是新创建的CheckBounds节点。这意味着StoreField [+12]和EffectPhi节点最终将其MaybeGrowFastElements节点的效果边缘替换为先前Checkbounds节点的效果边缘。

由于新的CheckBounds节点没有任何效果输出边缘,因此TurboFan最终认为这是一个无用的CheckBounds节点(如果该节点的实际输出对其他任何节点都没有影响,为什么还要检查任何边界?)。

一旦ReplaceWithValue返回,则Replace(check_bounds)调用将MaybeGrowFastElements节点替换为新的CheckBounds节点。由于新的CheckBounds节点没有效果输出边缘,因此将其删除,如下所示:

34.png

删除GrowFastElements

具有重影效果(ghost effect )的CheckBounds节点是新的CheckBounds节点,由于没有任何效果输出边缘,因此已将其删除。可以看到它将来自另一个CheckBounds节点的索引与常数2(这是corrupting_array的长度)进行比较,但是没有其他节点使用它,因此它被认为是冗余的,因此被删除了。

连接到StoreElement节点的另一个CheckBounds节点是起到检查作用,以确保x小于length + 1024的节点。由于x = 7肯定小于length + 1024,因此很高兴让我们无限制地访问corrupting_array [x]。另外该数组也不会增长,因为已删除了MaybeGrowFastElements节点,并且引擎不会抱怨x = 7大于corrupting_array.length = 2,因为应该测试该对象的CheckBounds节点已被删除,越界写入已经实现!

越界写入的具体过程

现在我们已经超出了特定索引的边界(在本例中,x = 7),Sergey选择7一定是有原因的,我们知道corrupting_array是在corrupted_array之前定义的,并且由于V8堆的确定性性质,corrupted_array总是在corrupting_array之后放置。由于我们在末尾处以损坏的长度返回了corrupted_array,所以对下索引7的越界写入操作覆盖了corrupted_array的length属性是有意义的。在GDB中,我们可以通过./d8——allow-native -syntax运行以下代码来看到这一点:

35.png

在GDB中运行这个,在遇到断点后向上滚动,复制corrupted_array的地址并在它之前查看几组quadwords:

36.png

注意,我们从地址中减去1,因为在V8中,指针的最后位总是设置为1。

这里有几点需要注意:

1.V8堆中的所有指针均为32位,但是每个数组中的0.1位指针都表示为它们的64位的IEEE-754 0x3fb9999999999999999a的十六进制表示,这是我同时使用x/wx和x/gx查看堆的部分原因,因为这可以更清楚地了解堆。

2.如果你在其后备存储器的边界之外跟踪ruptinging_array的索引,你会注意到,corrupted_array的元素指针是索引7的低32位,而length属性是索引7的高32位。特定值索引7处的值为0x178308792cfc处的0x2424242400000000值。

3.由于corrupting_array是一种HOLEY_DOUBLE_ELEMENTS类型的数组,因此我们对其执行的任何超出范围的写入操作都会将64位值写入所选的索引。 Sergey将length_as_double设置为0x2424242400000000的浮点表示形式。之所以将高位设置为较大的值,是因为他想覆盖length属性,该属性恰好是此索引的高32位。

这里的一个大问题是,当覆盖length属性时,我们也必须覆盖elements指针。如果你要注意的话,你会注意到%DebugPrint在尝试打印出有关corrupted_array的信息时实际上会导致分段错误。这样做的原因是因为元素指针已被覆盖为NULL,所以在取消引用时,就会出现程序运行崩溃(segfault)。

你可能想知道,引擎如何知道后备存储的位置?现在访问corrupted_array的任何索引肯定会导致segfault,因为它将尝试访问元素指针,我想这似乎是因为corrupted_array是一个已知长度的“快速”数组(我们将其分配为1)。由于从未修改过此长度(使用越界写入进行修改不会计算在内),因此引擎始终在其自身之前以已知偏移量分配其FixedDoubleArray后备存储。引擎可能将此偏移量缓存在某个位置,但是我不确定该如何工作。你只需要知道,只要你不再次通过JavaScript扩展数组的长度,覆盖指向NULL的elements指针就不会造成任何问题。

我们的分析差不多完成了,不过需要解释一下最后一行:

37.png

为什么Sergey选择在包装器数组中同时返回corrupting_array和corrupted_array?如果你仅通过返回corrupted_array来试验概念证明,就会注意到概念证明不再起作用(corrupted_array的长度永远不会被覆盖)。

让我们快速查看一下GDB,使用--allow-natives-syntax标志在GDB中运行以下代码:

38.png

注意到这里的区别了吗?corrupting_array的实际JSArray对象消失了!这里我们只有corrupting_array的后备存储,索引为0和1的为[0.1,0.1] ,然后是用于corrupted_array的后备存储,最后是用于corrupted_array的JSArray对象。由于这个原因,defected_array的长度所在的越界索引现在位于corrupting_array的索引5处,而不是索引7处。不过我们仍然可以看到发生越界写入,但是它位于错误的索引处。

在解释为什么corrupting_array的JSArray对象消失之前,重要的是要注意这个特定的场景仍然是可利用的。实际上,它用一个简单得多的语句替换了当前看似复杂的return语句,从而简化了Trigger函数。我们只需要将x设置为5而不是7,同时将typer类型x作为范围Range(0, 1)。

一种可能的方法是:

39.png

我们修改了乘法和减法,使得我们能够将x设置为5,而Typer仍然使用Range(0,1)输入最终的Math.max调用。如果你尝试使用这种概念验证,就会发现它确实有效,并为我们简化了返回声明。

现在,为什么corrupting_array的JSArray对象消失了?也许它被TurboFan优化了?为了理解这一点,我们必须看看最后一个优化阶段——逃避分析阶段。

逃避分析

Tobias Tebbi有一个关于这个阶段的精彩演讲,但简单地说,V8使用了逃避分析阶段来确定它是否可以优化任何未从函数中逃避的已分配对象。在本例中,“逃避函数”仅意味着通过返回语句或外部作用域中的变量等从函数中返回。

在我们修改过的概念证明案例中,TurboFan正确地推断出corrupting_array实际上不需要分配。它可以分配后备存储,在索引x上进行一个越界写入操作,然后结束。实际的corrupting_array不会在函数中的其他任何地方使用,也不会从函数中返回(因此也没有从函数中逃出)。如果你不从函数中返回corrupted_array,这就是corrupting_array的JSArray对象消失的原因。

如果要在sea of nodes图中查看此图,只需比较负载删除阶段和逃避分析阶段的图形即可。你会注意到,删除负载阶段具有一个额外的Allocate [Array,Young]节点,该节点在逃避分析阶段不存在。这是应该为corrupting_array分配JSArray对象的节点,并且已对其进行优化。

来自第一个POC的相同开发原语

之前我提到过,只要稍微修改一下Trigger函数,就可以从第一个概念证明中获得相同的利用原语。请注意,在“设置”部分中,我提到了一种无需任何编译器优化即可构建发行版本的方法。如果你尝试在禁用编译器优化的情况下运行以下代码,则将需要很长时间才能完成。如果你想尝试一下,建议使用编译器优化函数编译默认发行版。

第一个概念证明的代码如下所示:

40.png

漏洞利用

这个由@r4j0x00编写的漏洞已经被利用了n天。还有许多其他博客文章,描述了一旦实现了如此强大的利用原语,如何在V8中执行代码,因此,我不会详细介绍该主题。以下是可用于从此阶段执行代码的一般步骤列表:

1.实施addrof原语:立即在corrupted_array之后分配一个对象,我们称其为泄漏对象。将要泄漏其地址的对象设置为内联属性,以泄漏方式使用,并使用corrupted_array越界读取此属性的地址。

2.在32位V8堆上实现绝对的r/w原语:数组具有32位元素指针。创建一个具有PACKED_DOUBLE_ELEMENTS类型的数组,该数组在堆上的corrupted_array之后存在。使用来自corrupted_array的越界写入操作,修改这个新数组的elements指针,指向任意32位地址。现在,你可以使用此新数组对V8堆中的任意32位地址读取和写入64位double值。

3. 在64位地址空间上实现绝对r/w原语:V8中的TypedArray对象使用64位的后备存储指针。在堆上的corrupted_array之后创建一个BigUint64Array,并使用corrupted_array的越界写入将BigUint64Array的后备存储指针修改为任意64位地址。然后,你可以使用BigUint64Array读取和写入任意64位地址。

4.实现代码执行:加载WebAssembly模块,泄漏WebAssembly实例对象的地址,使用32位任意读取原语泄漏RWX页面的地址(此地址存储在实例对象上),替换RWX中的代码使用64位任意r / w原语包含shellcode的页面,最后调用WebAssembly函数。

本文翻译自:https://www.elttam.com/blog/simple-bugs-with-complex-exploits/#content如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/WpWX
如有侵权请联系:admin#unsafe.sh