CVE-2021-38001复现
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. 。可以知道该漏洞是位于Chrome的JS处理引擎V8中的类型混淆漏洞。
利用难度:Type Confusion (较低)
漏洞影响范围:v8 < 9.5.172.21 ; Chrome < 95.0.4638.54
issue编号:1260577
漏洞导致的结果:远程代码执行
分析报告地址:https://www.cnblogs.com/Rain99-/p/14673789.html
漏洞分析
现该Issue在bugs.chromium上还未有公开的文档,但是根据查阅对应版本的patch文件和描述[super ic] Fix receiver vs lookup start object confusion related to module exports,可知漏洞点位于src/ic/ic.cc和src/ic/accessor-assembler.cc两份代码中,漏洞的主要原因是在属性访问指令LoadSuperIC创建IC handler时处理的对象为holder,而在加载IC handler使用时处理的对象为p->receiver(),由于这两个对象的类型可以不一致,导致了类型混淆漏洞。
找到两份该漏洞的公开POC,分别来自:https://github.com/vngkv123/aSiagaming/tree/master/Chrome-v8-1260577 和 https://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 | function doSomething(){} |
如上所示, doSomeInstancing 中的__proto__是 doSomething.prototype。当访问doSomeInstancing 中的一个属性,首先会查看doSomeInstancing 中是否存在这个属性。
如果 doSomeInstancing 不包含属性信息, 那么就会在 doSomeInstancing 的 __proto__ 中进行查找(同 doSomething.prototype)。 如属性在 doSomeInstancing 的 __proto__ 中查找到,则使用 doSomeInstancing 中 __proto__ 的属性。如果 doSomeInstancing 中 __proto__ 不具有该属性,同理,则检查doSomeInstancing 的 __proto__ 的 __proto__ 是否具有该属性。
所以,可以通过修改对象的__proto__,来为其增加一个继承的父类。
深入了解,可参考这篇文章:继承与原型链
Inline cache
Inline cache是V8中引入的用于提高属性访问效率的优化机制,其会存储去何处寻找一个对象的属性的相关信息,来减少属性遍历查找的的开销。
用如下示例代码,简单说明其实现的思想:
1 | function getX(point) { |
这句代码,会对point对象的x属性进行访问。那么一个简单的遍历查找属性的方法,可以总结思路1如下:
- 从对象的
Map中获得instance_descriptors。 instance_descriptors遍历获取到key存储的位置(在对象内还是在properties中)。- 调用特定的方法读取属性值。
但是,如果对这行代码多次调用执行时,上面的处理思路因为需要不断遍历获取key值,所以会消耗大量的时间。这里如果我们用一个slot将属性 x 的存取位置和其对应的JSObject<Map>存储下来,就可以在多次执行时减少遍历的消耗。
那么,此时的思路2可以总结为:
- 判断对象的
Map与之前缓存的是否相同。 - 如相同,直接从缓存的位置读取。
- 如不同,调用第一种思路1通过遍历获取。
但是,如果存在这个对象的Map在不断变化,那么我们思路2中其实 只会不断执行第3步,这会导致我们的缓存命中率降低,优化失效,例如如下代码:
1 | for (var i = 0; i < 10000; i++) { |
所以,我们可以将缓存由单一存储改为哈希表模式,将其遇到的 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根据接受到的消息类型,而有了不同的状体:

- 当 IC 刚被创建时为初态(没有调用过),没有接收到任何一种类型信息。
- 当被调用时:
- 如果接收到 1 种类型信息会迁移到单态模式(MONOMORPHIC)。
- 收到大于 1 种小于 4 种类型信息会迁移到多态模式(POLYMORPHIC)。
- 当接收到大于 4 种类型信息时会迁移到复态模式(MEGAMORPHIC)。
深入了解,可参考这篇文章:JavaScript engine fundamentals: Shapes and Inline Caches
Named property load 处理
以一个示例代码,来说明发生Named property load属性访问时,整个IC的生成和处理流程。
1 | let o = {x: 1, y: 2}; |
Bytecode & Feedback
首先输出这个示例的 bytecode,如下所示:
1 | 0x1d260829330e @ 0 : 7b 00 00 29 CreateObjectLiteral [0], [0], #41 |
从上面的字节码处理中,可以看到其会执行LdaNameProperty属性加载操作,这里LdaNamedProperty r1, [1], [1]字节码的含义是:
LdaNamedProperty 将 r1 的命名属性加载到累加器中。ri 指向 incrementX() 的第 i 个参数。在这个例子中,我们在 r1 上查找一个命名属性,这是 incrementX() 的第二个参数。该属性名由常量 1 确定。LdaNamedProperty 使用 1 在单独的表中查找名称:
1 | Constant pool (size = 2) |
可以看到,1 映射到了 x。因此这行字节码的意思是加载 o.x。
那么值为 1 的操作数是干什么的呢? 它是函数 incrementX() 的反馈向量feedback的索引。反馈向量包含用于IC性能优化的 runtime 信息,其本质就是上文中提到的IC存储的信息。
这里提到的feedback,可以用一个如下示例说明
1 | function f(o) { |
这里只关注输出的feedback信息如下:
1 | - feedback vector: 0x10a30829354d: [FeedbackVector] in OldSpace |
在没有执行函数f时,可以看到此时的feedback状态为UNINTIALIZED,这和我们上面说的IC的5个状态符合。
接着,执行了两次f函数,feedback状态变为了MONOMORPHIC,且有了两条slot,slot[0]存储了指向函数f的Map信息(用于比较当前对象是否发生改变);slot[1]存储的是的handler(存储用于辅助属性查找的位置)



