开发
了解 JScript VAR 和字符串
由于在这篇博文的其余部分中,我们将大量讨论 JScript VAR 和字符串,因此在深入了解这些漏洞的工作原理之前先描述这些内容是很有用的。
JScript VAR 是一个 24 字节(在 64 位版本上)结构,它表示一个 JavaScript 变量,并且本质上与此 MSDN 文章中描述的 VARIANT 数据结构相同。在大多数情况下(足以跟踪漏洞利用),它的内存布局如下所示:
抵消 | 尺寸 | 描述 |
---|---|---|
0 | 2 | 变量类型,3 表示整数,5 表示双精度,8 表示字符串等。 |
8 | 8 | 根据类型,立即数或指针 |
16 | 8 | 大多数类型未使用 |
例如,我们可以用 VAR 表示一个双精度数,在前 2 个字节中写入 5(表示双精度类型),后跟偏移 8 处的实际双精度值。最后 8 个字节将不使用,但它们如果从该 VAR 复制另一个 VAR 的值,则将被复制。
JScript 字符串是类型为 8 的 VAR 类型和偏移量 8 处的指针。指针指向此处(https://msdn.microsoft.com/en-us/library/windows/desktop/ms221069(v=vs.85).aspx)描述的 BSTR 结构。在 64 位版本上,BSTR 布局如下所示:
抵消 | 尺寸 | 描述 |
---|---|---|
0 | 4 | 没用过 |
4 | 4 | 以字节为单位的字符串长度,不包括最后的空字符 |
8 | 长度 2 | 字符串字符(16 位)后跟一个空字符 |
String VAR 直接指向字符数组,这意味着,要获得 String 的长度,需要将指针减 4 并从那里读取长度。请注意,BSTR 由 OleAut32.dll 处理并分配在单独的堆上(即与用于其他 JScript 对象的堆不同)。
释放 BSTR 也与大多数对象不同,因为在调用 SysFreeString 时,它不是直接释放 BSTR,而是首先将字符串放入由 OleAut32.dll 控制的缓存中。这个机制在 JavaScript 中的堆风水中有详细描述。
第 1 阶段:信息泄漏
infoleak 的目的是获取我们完全控制其内容的内存中字符串的地址。在这一点上,我们不会泄露任何可执行模块地址,这将在稍后发布。相反,我们的目标是击败高熵堆随机化,并使漏洞利用的第二阶段可靠,而无需使用堆喷射。
对于信息泄漏,我们将在 RegExp.lastParen 中使用这个错误。要了解这个错误,让我们首先仔细看看 jscript!RegExpFncObj 的内存布局,它对应于 JScript RegExp 对象。在偏移量 0xAC RegExpFncObj 包含 20 个整数的缓冲区。实际上这些是 10 对整数:对的第一个元素是输入字符串的开始索引,第二个元素是结束索引。每当带有 RegExp 参数的 RegExp.test、RegExp.exec 或 String.search 遇到捕获组(RegExp 语法中的括号)时,匹配的开始和结束索引都存储在这里。显然,缓冲区中只有 10 个匹配项的空间,因此只有前 10 个匹配项存储在此缓冲区中。
但是,如果 RegExp.lastParen 被调用并且有超过 10 个捕获组,RegExpFncObj::LastParen 会很乐意使用捕获组的数量作为缓冲区的索引,从而导致越界读取。这是一个 PoC:
var r= new RegExp(Array(100).join('()'));
''.search(r);
警报(RegExp.lastParen);
2 个索引(我们称它们为start_index和end_index )在缓冲区边界之外读取,因此可以任意大。假设第一次越界访问不会导致崩溃,如果这些索引中的值大于输入字符串的长度,那么将发生第二次越界访问,这允许我们读取a 在输入字符串的范围之外。像这样越界读取的字符串内容将在一个可以检查的字符串变量中返回给调用者。
我们将要使用第二次越界读取,但首先我们需要弄清楚如何将受控数据放入start_index和end_index 。好在看RegExpFncObj的布局,在索引缓冲区结束后还有我们控制的数据:RegExp.input值。通过将 RegExp.input 设置为整数值并使用由 41 组空括号组成的 RegExp,当 RegExp.lastParen 被调用时,start_index将为 0,而end_index将是我们写入 RegExp.input 的任何值。
如果我们让一个输入字符串与一个被释放的字符串相邻,那么通过读取输入字符串的边界,我们可以获得堆元数据,例如指向其他空闲堆段的指针(红黑中的Left,Right和Parent节点堆块树,请参阅Windows 10 段堆内部了解更多信息)。图 1 显示了信息泄漏时的相关对象。
图 1:堆信息泄漏布局
我们使用 20000 字节长的字符串作为输入,以便它们不会被分配到低碎片堆上(LFH 只能用于 16K 字节或更小的分配),因为 LFH 的堆元数据不同并且不包括Windows 10 段堆中的有用指针。此外,LFH 引入了随机性,这会影响我们将输入字符串放置在已释放字符串旁边的能力。
通过从返回的字符串中读取堆元数据,我们可以获得一个已释放字符串的地址。然后,如果我们分配一个与释放的字符串大小相同的字符串,它可能会被放置在这个地址,我们就实现了我们的目标,即我们知道我们控制其内容的字符串的内存地址。
整个信息泄露过程如下所示:
- 分配 1000 个 10000 个字符的字符串(注意:10000 个字符 == 20000 个字节)。
- 每隔一秒免费一次。
- 触发信息泄漏错误。使用剩余的字符串之一作为输入字符串并读取 20080 个字节。
- 分析泄漏的字符串并获取指向已释放字符串之一的指针。
- 使用特制内容分配 500 个与已释放字符串(10000 个字符)长度相同的字符串。
特制琴弦的内容现阶段不重要,但在下一阶段会很重要,所以会在此进行说明。另请注意,通过检查堆元数据,我们可以轻松确定进程正在使用哪个堆实现(段堆与 NT 堆)。
图像 2 和 3 显示了在信息泄漏前后使用堆历史查看器创建的堆可视化。绿色条纹代表分配的块(被字符串占用),灰色条纹代表分配的块,然后被稍后再次分配的释放(我们释放并在触发信息泄漏错误后重新分配的stings),白色条纹代表从未分配的数据(守卫页)。您可以看到随着时间的流逝如何分配字符串,然后释放其中一半(灰色),稍后再次分配(条纹变为绿色)。
我们可以看到,每 3 次这样大小的分配后都会有保护页。我们的漏洞利用永远不会真正触及任何这些保护页面(它读取的数据太少超出了字符串的末尾),但在 1/3 的情况下,在输入字符串之后不会有空闲字符串infoleak,因此预期的堆元数据将丢失。然而,我们可以很容易地检测到这种情况,或者使用另一个输入字符串触发 infoleak 错误,或者静默中止漏洞利用(注意:到目前为止,我们没有触发任何内存损坏)。
图 2:堆图:显示堆随时间的演变
图 3:泄露指向字符串的指针的分步说明。
第 2 阶段:溢出
在漏洞利用的第 2 阶段,我们将使用这个堆溢出漏洞在 Array.sort 中。如果 Array.sort 的输入数组中的元素数大于 Array.length / 2,JsArrayStringHeapSort(如果未指定比较函数则由 Array.sort 调用)将分配一个相同大小的临时缓冲区作为当前数组中的元素数(注意:可以小于array.lenght)。然后它将尝试检索从 0 到 Array.length 的每个数组索引的相应元素,如果该元素存在,则将其添加到缓冲区并转换为字符串。如果数组在 JsArrayStringHeapSort 的生命周期内没有改变,这将正常工作。但是,JsArrayStringHeapSort 将数组元素转换为可以触发 toString() 回调的字符串。如果在其中一个 toString() 回调中元素被添加到之前未定义的数组中,
为了更好地理解这个错误及其可利用性,让我们仔细看看我们将溢出的缓冲区的结构。已经提到该数组将具有与当前输入数组中的元素数相同的大小(准确地说,它将是元素数 1)。数组的每个元素的大小将是 48 字节(在 64 位构建中),具有以下结构:
抵消 | 尺寸 | 描述 |
---|---|---|
0 | 8 | 将偏移量 16 处的原始 VAR 转换为字符串后指向字符串 VAR 的指针 |
8 | 4 | 当前元素的索引 (int) |
16 | 24 | VAR 保存原始数组元素 |
40 | 4 | int 0 或 1 取决于偏移 16 处的 VAR 类型 |
在 JsArrayStringHeapSort 期间,检索索引 < array.length 的数组的每个元素,如果定义了该元素,则会发生以下情况:
- 数组元素在偏移量 16 处读入 VAR
- 原始的 VAR 被转换为字符串 VAR。指向字符串 VAR 的指针被写入偏移量 0。
- 在偏移量 8 处,写入数组中当前元素的索引
- 根据原始 VAR 类型,在偏移量 40 处写入 0 或 1
看临时缓冲区的结构,很多我们并没有直接控制。如果数组成员是一个字符串,那么在偏移量 0 和 24 处我们将有一个指针,当取消引用时,在偏移量 8 处包含另一个指向我们控制的数据的指针。然而,这比在大多数情况下对我们有用的间接级别要大一级。
但是,如果数组的成员是双精度数,那么在偏移量 24(对应于原始 VAR 的偏移量 8)处,该数字的值将被写入,并且它直接在我们的控制之下。如果我们创建一个与在阶段 1 中获得的指针具有相同双精度表示的数字,那么我们可以使用溢出来用指向我们直接控制的内存的指针覆盖缓冲区结束后某处的指针。
现在问题变成了,我们可以用这种方式覆盖什么来推进漏洞利用。如果我们仔细研究对象在 JScript 中是如何工作的,那么其中一个可能的答案就会出现。
每个对象(更具体地说,一个 NameList JScript 对象)都有一个指向哈希表的指针。这个哈希表只是一个指针数组。当访问 Object 的成员元素时,将计算元素名称的哈希值。然后,取消引用对应于哈希最低位的偏移量的指针。这个指针指向一个对象元素的链表,并且遍历这个链表,直到我们到达一个与请求元素同名的元素。如图 4 所示。
图 4:JScript 对象元素内部