Chrome Turbofan远程代码执行漏洞
一、漏洞概要
在这篇安全公告中,我们介绍了Chrome 59版本浏览器中存在的一个类型混淆问题,这个问题最终会导致远程代码执行漏洞。
Chrome浏览器中存在一个类型混淆漏洞。该漏洞出现在用于优化JavaScript代码的turbofan编译器中,导致访问程序在访问对象数组以及数值数组之间存在混淆,如果以数值形式来读取对象,那么就可以像访问数值那样访问对象(因此可以通过内存地址来读取这些值),反之亦然,也可以将数值写入到某个对象数组中,因而能够完全伪造对象。
二、漏洞细节
2.1 背景知识
对象映射图(object map)
每个对象都有一个映射图与之对应,用来表示对象的结构(键值以及值的类型)。具有相同结构的两个对象(但这两个对象的值不同)会使用相同的映射图。对象最常见的表示方法如下所示:
如上图所示,映射图的字段(指向映射图的某个指针)保存着具体的对象图。两个固定的数组分别保存着额外的命名属性以及编号属性。编号属性通常也称为“元素(Elements)”。
图转换
当我们往对象中添加新的属性时,对象的图会处于失效状态。新的图会被创建,以适应新的对象结构,原始图中会添加一个转换描述符(transition descriptor),以描述如何将原始图转换为新的图。
例如:
Var obj = {}; // Map M0 is created and assigned to the object
obj.x = 1; // Map M1 created, shows where to store the value x. A transition “x” is added to M0 with target M1.
obj.y=1; // Map M2 created, shows where to store the value y. A transition “y” is added to M1 with target M2.
随后,当内联缓存(inline cache)没有命中时,编译器可以利用这些转换描述符来重新优化函数代码。
元素类型
如上所述,对象的元素就是编号属性的值。这些值存储在对象指向的常规数组中。对象图中有个名为ElementsKind的特殊字段。这个字段描述了元素数组中的值是否为boxed、unboxed、contiguous、sparse还是其他值。仅通过元素类型进行区分的图不会通过转换关系进行连接。
V8数组
v8引擎中的数组属于有类型(typed)数组,可以使用“boxed”或者“unboxed”值。这基本上就决定了数组是否只能存储双精度值(整数也使用双精度来表示),或者也能存储更加复杂的值,在后一种情况下,这些值实际上是指向对象的指针。
简单描述一下这两种情况,如下所示:
(数组本身的类型就决定了其值是boxed还是unboxed。)
因此,如果我们有一个数组(如上图左图所示),然后我们将某个复杂对象(如数组对象)分配给其中一个数组位置,那么整个数组就会变成boxed数组,所有已有值也会相应地变为boxed类型的值。
V8优化
V8编译器首先会分析javascript代码,以生成JIT编译代码,这一过程使用到了内联缓存(inline cache),同时对类型的处理没有那么严格。
从Google的V8官方文档中,我们找到如下解释:
“在首次执行时,V8会将JavaScript源代码直接编译成机器码。其中不存在中间字节码以及解释器。对属性的访问由内联缓存代码来处理,当V8执行时可能会使用其他机器执行来修改内联缓存代码……”
“V8会根据预测来优化属性访问过程,依据的是同一代码在未来访问所有对象时也会用到当前这个对象的类,然后会根据类中的信息来修改内联缓存代码,以便使用隐藏类。如果V8预测正确,那么属性的值就可以在一次操作中完成分配(或者读取)。如果预测错误,V8会修改代码来删除优化策略。”
因此,编译器只会编译适用于特定类型的那些代码。如果下一次代码(或者函数)所执行的类型与编译的类型不符,那么就会出现“内联缓存丢失”现象,导致编译器重新编译代码。
比如,假设我们有一个函数f,以及两个对象o1和o2,如下所示:
f(arg_obj) {
return arg_obj.x;
}
var o1 = {“x”:1, “y”:2}
var o2 = {“x”:1, “t”:2}
当使用o1第一次调用函数时,编译器会生成如下代码:
(ecx holds the argument)
cmp [ecx + ],
jne - this will execute compiler code
mov eax, [ecx + ]
如果使用o2再次调用这个函数,就会出现缓存丢失现象,编译器代码就会修改函数对应的JIT代码。
2.2 具体漏洞
元素类型转换
当出现缓存丢失现象并且编译器想重新优化函数代码时,编译器会使用已保存的转换关系,也会使用Map::FindElementsKindTransitionedMap函数,即时生成所需的ElementsKindTransitions(转换到仅在元素类型上有所差别的另一张图)。之所以使用即时方式完成这一过程,原因在于编译器只需要修改ElementsKind这个字段,而不需要完全修改整张图。
稳定(stable)图
当访问图所属元素的代码已完成优化,此时图就会被标记为稳定(stable)状态。
当优化编译器认为函数已经使用得差不多,可以进一步“减少”时(即编译器想进一步优化代码,减少代码大小),此时就会出现漏洞。此时,ReduceElementAccess函数会被调用,以减少对某个对象元素的访问。该函数会继续调用ComputeElementAccessInfos。
ComputeElementAccessInfos这个函数也会搜索可能的元素类型转换,以进一步优化代码。
问题在于这种转换会不会从一张稳定图中生成及使用。原因在于,如果使用了这样一种转换,那么它只会影响当前的函数,使用同一张稳定图的其他函数不会去考虑这种元素类型转换。
具体过程为:首先,某个函数以某种方式被优化减少,使其修改了某张稳定图的元素类型。然后,第二个函数以某种方式被优化减少,使其存储/加载了同一张稳定图中的某个属性。现在,这张图的某个对象被创建。第一个函数被调用,使用该对象作为函数参数,然后元素的类型会被修改。
一、漏洞概要
在这篇安全公告中,我们介绍了Chrome 59版本浏览器中存在的一个类型混淆问题,这个问题最终会导致远程代码执行漏洞。
Chrome浏览器中存在一个类型混淆漏洞。该漏洞出现在用于优化JavaScript代码的turbofan编译器中,导致访问程序在访问对象数组以及数值数组之间存在混淆,如果以数值形式来读取对象,那么就可以像访问数值那样访问对象(因此可以通过内存地址来读取这些值),反之亦然,也可以将数值写入到某个对象数组中,因而能够完全伪造对象。
二、漏洞细节
2.1 背景知识
对象映射图(object map)
每个对象都有一个映射图与之对应,用来表示对象的结构(键值以及值的类型)。具有相同结构的两个对象(但这两个对象的值不同)会使用相同的映射图。对象最常见的表示方法如下所示:
如上图所示,映射图的字段(指向映射图的某个指针)保存着具体的对象图。两个固定的数组分别保存着额外的命名属性以及编号属性。编号属性通常也称为“元素(Elements)”。 本文来自无奈人生安全网
图转换
当我们往对象中添加新的属性时,对象的图会处于失效状态。新的图会被创建,以适应新的对象结构,原始图中会添加一个转换描述符(transition descriptor),以描述如何将原始图转换为新的图。
例如:
Var obj = {}; // Map M0 is created and assigned to the object
obj.x = 1; // Map M1 created, shows where to store the value x. A transition “x” is added to M0 with target M1.
obj.y=1; // Map M2 created, shows where to store the value y. A transition “y” is added to M1 with target M2.
随后,当内联缓存(inline cache)没有命中时,编译器可以利用这些转换描述符来重新优化函数代码。
元素类型
如上所述,对象的元素就是编号属性的值。这些值存储在对象指向的常规数组中。对象图中有个名为ElementsKind的特殊字段。这个字段描述了元素数组中的值是否为boxed、unboxed、contiguous、sparse还是其他值。仅通过元素类型进行区分的图不会通过转换关系进行连接。
V8数组
v8引擎中的数组属于有类型(typed)数组,可以使用“boxed”或者“unboxed”值。这基本上就决定了数组是否只能存储双精度值(整数也使用双精度来表示),或者也能存储更加复杂的值,在后一种情况下,这些值实际上是指向对象的指针。
简单描述一下这两种情况,如下所示:
(数组本身的类型就决定了其值是boxed还是unboxed。)
因此,如果我们有一个数组(如上图左图所示),然后我们将某个复杂对象(如数组对象)分配给其中一个数组位置,那么整个数组就会变成boxed数组,所有已有值也会相应地变为boxed类型的值。
V8优化
V8编译器首先会分析javascript代码,以生成JIT编译代码,这一过程使用到了内联缓存(inline cache),同时对类型的处理没有那么严格。
从Google的V8官方文档中,我们找到如下解释:
“在首次执行时,V8会将JavaScript源代码直接编译成机器码。其中不存在中间字节码以及解释器。对属性的访问由内联缓存代码来处理,当V8执行时可能会使用其他机器执行来修改内联缓存代码……”
“V8会根据预测来优化属性访问过程,依据的是同一代码在未来访问所有对象时也会用到当前这个对象的类,然后会根据类中的信息来修改内联缓存代码,以便使用隐藏类。如果V8预测正确,那么属性的值就可以在一次操作中完成分配(或者读取)。如果预测错误,V8会修改代码来删除优化策略。” 本文来自无奈人生安全网
因此,编译器只会编译适用于特定类型的那些代码。如果下一次代码(或者函数)所执行的类型与编译的类型不符,那么就会出现“内联缓存丢失”现象,导致编译器重新编译代码。
比如,假设我们有一个函数f,以及两个对象o1和o2,如下所示:
f(arg_obj) {
return arg_obj.x;
}
var o1 = {“x”:1, “y”:2}
var o2 = {“x”:1, “t”:2}
当使用o1第一次调用函数时,编译器会生成如下代码:
(ecx holds the argument)
cmp [ecx + ],
jne - this will execute compiler code
mov eax, [ecx + ]
如果使用o2再次调用这个函数,就会出现缓存丢失现象,编译器代码就会修改函数对应的JIT代码。
2.2 具体漏洞
元素类型转换
当出现缓存丢失现象并且编译器想重新优化函数代码时,编译器会使用已保存的转换关系,也会使用Map::FindElementsKindTransitionedMap函数,即时生成所需的ElementsKindTransitions(转换到仅在元素类型上有所差别的另一张图)。之所以使用即时方式完成这一过程,原因在于编译器只需要修改ElementsKind这个字段,而不需要完全修改整张图。 本文来自无奈人生安全网
稳定(stable)图
当访问图所属元素的代码已完成优化,此时图就会被标记为稳定(stable)状态。
当优化编译器认为函数已经使用得差不多,可以进一步“减少”时(即编译器想进一步优化代码,减少代码大小),此时就会出现漏洞。此时,ReduceElementAccess函数会被调用,以减少对某个对象元素的访问。该函数会继续调用ComputeElementAccessInfos。
ComputeElementAccessInfos这个函数也会搜索可能的元素类型转换,以进一步优化代码。
问题在于这种转换会不会从一张稳定图中生成及使用。原因在于,如果使用了这样一种转换,那么它只会影响当前的函数,使用同一张稳定图的其他函数不会去考虑这种元素类型转换。
具体过程为:首先,某个函数以某种方式被优化减少,使其修改了某张稳定图的元素类型。然后,第二个函数以某种方式被优化减少,使其存储/加载了同一张稳定图中的某个属性。现在,这张图的某个对象被创建。第一个函数被调用,使用该对象作为函数参数,然后元素的类型会被修改。
内容来自无奈安全网