CVE-2021-38001复现

漏洞通报

漏洞描述:Issue 1260577 于2021年10月16日天府杯提交,CVE编号为CVE-2021-38001,漏洞描述为Type confusion in V8 in Google Chrome prior to 95.0.4638.69 allowed a remote attacker to potentially exploit heap corruption via a crafted HTML page. 。可以知道该漏洞是位于ChromeJS处理引擎V8中的类型混淆漏洞。

利用难度:Type Confusion (较低)

漏洞影响范围:v8 < 9.5.172.21 ; Chrome < 95.0.4638.54

issue编号:1260577

漏洞导致的结果:远程代码执行

分析报告地址:https://www.cnblogs.com/Rain99-/p/14673789.html

漏洞分析

现该Issuebugs.chromium上还未有公开的文档,但是根据查阅对应版本的patch文件和描述[super ic] Fix receiver vs lookup start object confusion related to module exports,可知漏洞点位于src/ic/ic.ccsrc/ic/accessor-assembler.cc两份代码中,漏洞的主要原因是在属性访问指令LoadSuperIC创建IC handler时处理的对象为holder,而在加载IC handler使用时处理的对象为p->receiver(),由于这两个对象的类型可以不一致,导致了类型混淆漏洞。

找到两份该漏洞的公开POC,分别来自:https://github.com/vngkv123/aSiagaming/tree/master/Chrome-v8-1260577https://github.com/Peterpan0927/TFC-Chrome-v8-bug-CVE-2021-38001-poc 。第一份包含了完整的漏洞利用,第二份仅能触发崩溃。

根据该漏洞描述,能够找到类似的漏洞:CVE-2021-30517,该漏洞也存在于IC创建和处理流程中。对于call_handler类型的IC创建时传入的参数为p->lookup_start_object(),而IC处理时传入的参数为p->receiver(),最终导致了类型混淆漏洞。

前置知识

prototype原型链

JavaScript 只有一种结构:对象,当存在继承时,每个实例对象(object)都有一个私有属性(称之为__proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

原型链具有继承属性,寻找某个成员变量时,会对原型链进行遍历查找。如下代码:

1
2
3
4
5
function doSomething(){}
doSomething.prototype.foo = "bar"; // add a property onto the prototype
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );

如上所示, doSomeInstancing 中的__proto__doSomething.prototype。当访问doSomeInstancing 中的一个属性,首先会查看doSomeInstancing 中是否存在这个属性。

如果 doSomeInstancing 不包含属性信息, 那么就会在 doSomeInstancing__proto__ 中进行查找(同 doSomething.prototype)。 如属性在 doSomeInstancing__proto__ 中查找到,则使用 doSomeInstancing__proto__ 的属性。如果 doSomeInstancing__proto__ 不具有该属性,同理,则检查doSomeInstancing__proto____proto__ 是否具有该属性。

所以,可以通过修改对象的__proto__,来为其增加一个继承的父类。

深入了解,可参考这篇文章:继承与原型链

Inline cache

Inline cacheV8中引入的用于提高属性访问效率的优化机制,其会存储去何处寻找一个对象的属性的相关信息,来减少属性遍历查找的的开销。

用如下示例代码,简单说明其实现的思想:

1
2
3
function getX(point) {
return point.x;
}

这句代码,会对point对象的x属性进行访问。那么一个简单的遍历查找属性的方法,可以总结思路1如下:

  1. 从对象的 Map 中获得 instance_descriptors
  2. instance_descriptors 遍历获取到 key 存储的位置(在对象内还是在 properties 中)。
  3. 调用特定的方法读取属性值。

但是,如果对这行代码多次调用执行时,上面的处理思路因为需要不断遍历获取key值,所以会消耗大量的时间。这里如果我们用一个slot将属性 x 的存取位置和其对应的JSObject<Map>存储下来,就可以在多次执行时减少遍历的消耗。

那么,此时的思路2可以总结为:

  1. 判断对象的 Map 与之前缓存的是否相同。
  2. 如相同,直接从缓存的位置读取。
  3. 如不同,调用第一种思路1通过遍历获取。

但是,如果存在这个对象的Map在不断变化,那么我们思路2中其实 只会不断执行第3步,这会导致我们的缓存命中率降低,优化失效,例如如下代码:

1
2
3
4
5
6
7
for (var i = 0; i < 10000; i++) {
if (i % 2) {
getX({x : 1});
} else {
getX({x : 1, y : 2});
}
}

所以,我们可以将缓存由单一存储改为哈希表模式,将其遇到的 Map 及其属性值存取位置全部缓存下来。这样即使Map值在不断变化,我们也能够迅速找到对应属性。

这里V8会将上述的map和缓存位置handler存储在一个FeedbackSlotCache中,之后相同函数对同一个名称的属性进行访问时,就会共享这些Feedback

这里我们把 point.x 称之为一个 callsite,把 “x” 称之为一条名为 “x” 的消息,而实际执行的 point (无论是 {x : 1},还是 {x : 1, y : 2})称之为这个 callsite 的 receiver。

总结JavaScript 中很多操作的执行过程异常复杂,但对于特定调用(callsite) 来说receiver 类型(Map)的变化一般很小,V8 采用内联缓存(Inline Caches,简称IC)来缓存调用的实现以优化这些操作的执行过程。

IC根据接受到的消息类型,而有了不同的状体:

image

  • 当 IC 刚被创建时为初态(没有调用过),没有接收到任何一种类型信息。
  • 当被调用时:
    • 如果接收到 1 种类型信息会迁移到单态模式(MONOMORPHIC)。
    • 收到大于 1 种小于 4 种类型信息会迁移到多态模式(POLYMORPHIC)。
    • 当接收到大于 4 种类型信息时会迁移到复态模式(MEGAMORPHIC)。

深入了解,可参考这篇文章:JavaScript engine fundamentals: Shapes and Inline Caches

Named property load 处理

以一个示例代码,来说明发生Named property load属性访问时,整个IC的生成和处理流程。

1
2
let o = {x: 1, y: 2};
o.x;

Bytecode & Feedback

首先输出这个示例的 bytecode,如下所示:

1
2
3
4
5
6
7
0x1d260829330e @    0 : 7b 00 00 29       CreateObjectLiteral [0], [0], #41
0x1d2608293312 @ 4 : 25 02 StaCurrentContextSlot [2]
0x1d2608293314 @ 6 : 16 02 LdaCurrentContextSlot [2]
0x1d2608293316 @ 8 : c2 Star1
0x1d2608293317 @ 9 : 2d f9 01 01 LdaNamedProperty r1, [1], [1]
0x1d260829331b @ 13 : c3 Star0
0x1d260829331c @ 14 : a8 Return

从上面的字节码处理中,可以看到其会执行LdaNameProperty属性加载操作,这里LdaNamedProperty r1, [1], [1]字节码的含义是:

LdaNamedPropertyr1 的命名属性加载到累加器中。ri 指向 incrementX() 的第 i 个参数。在这个例子中,我们在 r1 上查找一个命名属性,这是 incrementX() 的第二个参数。该属性名由常量 1 确定。LdaNamedProperty 使用 1 在单独的表中查找名称:

1
2
3
4
5
6
7
8
Constant pool (size = 2)
0x1d26082932dd: [FixedArray] in OldSpace
- map: 0x1d2608002205 <Map>
- length: 2
0: 0x1d26082932c1 <ObjectBoilerplateDescription[5]>
1: 0x1d2608293245 <String[1]: #x>
Handler Table (size = 0)
Source Position Table (size = 0

可以看到,1 映射到了 x。因此这行字节码的意思是加载 o.x

那么值为 1 的操作数是干什么的呢? 它是函数 incrementX() 的反馈向量feedback的索引。反馈向量包含用于IC性能优化的 runtime 信息,其本质就是上文中提到的IC存储的信息。

这里提到的feedback,可以用一个如下示例说明

1
2
3
4
5
6
7
8
9
10
function f(o) {
return o.x
}

%PrepareFunctionForOptimization(f);
%OptimizeFunctionOnNextCall(f);
%DebugPrint(f);
f({ x: 1 });
f({ x: 2 });
%DebugPrint(f);

这里只关注输出的feedback信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- feedback vector: 0x10a30829354d: [FeedbackVector] in OldSpace
- map: 0x10a308002711 <Map>
- length: 2
- shared function info: 0x10a308293339 <SharedFunctionInfo f>
- no optimized code
- optimization marker: OptimizationMarker::kCompileOptimized
- optimization tier: OptimizationTier::kNone
- invocation count: 0
- profiler ticks: 0
- closure feedback cell array: 0x10a30800328d: [ClosureFeedbackCellArray] in ReadOnlySpace
- map: 0x10a308002955 <Map>
- length: 0

- slot #0 LoadProperty UNINITIALIZED {
[0]: 0x10a3080053fd <Symbol: (uninitialized_symbol)>
[1]: 0x10a3080053fd <Symbol: (uninitialized_symbol)>
}


- feedback vector: 0x10a30829354d: [FeedbackVector] in OldSpace
- map: 0x10a308002711 <Map>
- length: 2
- shared function info: 0x10a308293339 <SharedFunctionInfo f>
- no optimized code
- optimization marker: OptimizationMarker::kNone
- optimization tier: OptimizationTier::kNone
- invocation count: 1
- profiler ticks: 0
- closure feedback cell array: 0x10a30800328d: [ClosureFeedbackCellArray] in ReadOnlySpace
- map: 0x10a308002955 <Map>
- length: 0

- slot #0 LoadProperty MONOMORPHIC {
[0]: [weak] 0x10a3082c7aa1 <Map(HOLEY_ELEMENTS)>
[1]: 1668
}

在没有执行函数f时,可以看到此时的feedback状态为UNINTIALIZED,这和我们上面说的IC的5个状态符合。

接着,执行了两次f函数,feedback状态变为了MONOMORPHIC,且有了两条slotslot[0]存储了指向函数fMap信息(用于比较当前对象是否发生改变);slot[1]存储的是的handler(存储用于辅助属性查找的位置)