作者:Carl [email protected]墨云科技VLab Team
原文链接:https://mp.weixin.qq.com/s/PY_QiNgEk9F3nSSgxECfTg
10月28日,谷歌Chrome在发布95.0.4638.69版本时修复了天府杯上昆仑实验室提交的漏洞CVE-2021-38001。由于此漏洞的PoC非常简洁使得作者对V8引擎产生了强烈的兴趣,分析此漏洞也是作者对V8的一次学习。V8是谷歌用C++编写的JavaScript和WebAssembly引擎,在Chrome和Node.js中都有使用。
该漏洞与内联缓存(Inline Caching)有关,内联缓存是一种运行时环境(runtime environment)的优化技巧。由于动态语言必须在运行时进行方法绑定(method binding),此优化手法对于动态语言来说十分重要,举一个例子:
def foo(a,b):
a.func(b)
这段代码的Python bytecode如下:
Disassembly of <code object foo at 0x000000000356EEA0, file "<dis>", line 2>:
3 0 LOAD_FAST 0 (a)
2 LOAD_METHOD 0 (func)
4 LOAD_FAST 1 (b)
6 CALL_METHOD 1
8 POP_TOP
10 LOAD_CONST 0 (None)
12 RETURN_VALUE
在执行时,LOAD_METHOD
会去确认a
的类型,然后利用a
的类型寻找add
。
如果没有IC,那第二次执行a.func(b)
时就必须重复做同样的事情(在同样的context下)。这样做逻辑上是比较严谨的,但是执行效率会很低,那么有没有什么方法可以提速呢?其实,程序员写代码时,大概率会写成下面的形式:
def foo(a,...):
a.func(b)
a.func(c)
...
a.func(z)
在上面代码中,a
的类型是不变的。Deutsch和Schiffman在他们的文章(http://web.cs.ucla.edu/~palsberg/course/cs232/papers/DeutschSchiffman-popl84.pdf)中提到:'在代码执行的某个时点,接收者(receiver)的类型通常和上次此时点的类型一样'。比如说上面例子中,a
的类型并未发生变化,所以这里可以将a
的类型进行缓存以便后面使用。
V8使用的是Data-driven IC,这种IC将属性的加载存储信息编码成数据结构。其他函数(例如LoadIC
和StoreIC
)会读取这个结构然后执行相应的操作。以下是V8之前的Patching IC和现在的Data-Driven IC的区别。
这里右边的图中的FeedbackVector的功能是记录和管理所有执行反馈,此数据结构对于JavaScript的执行效率提升十分关键。同时,在图中可以发现有Fast-path,Slow-path和Miss。Miss很好理解,即需要运行时确认类型。那么Slow-path和Fast-path分别对应了什么情况呢?通过以下例子可以理解:
let a = {foo:3}
let b = {foo:4}
这里a
和b
的架构一样,在处理上就没有必要为这两个对象建立不同的架构。V8的处理方式是将对象的架构与值分成对象的形状(Object Shapes)和一个带有值的vector,对象形状在V8中被称为Maps。上面例子中,V8会先创造一个形状Map[a]
。此形状拥有属性foo
位于偏移0,在对应vector[0]
的值为3。在创建对象b
的时候,只需将b
的Map指向Map[a]
,然后让对应的vector[0]=4
即可。这个即为Fast-path。
假设后面是
a.foo1 = 4
那么V8会新建一个Map[a1]
并将a
的Maps改为Map[a1]
。Map[a1]
拥有属性foo1
位于偏移1并指向Map[a]
,同时将对应的vector[1]
设为4。即为Slow-path。
以下例子将会包含以上三种情况:
function load(a) {
return a.key;
}
//IC of load: [{ slot: 0, icType: LOAD, value: UNINIT }]
let first = { key: 'first' } // shape A
let fast = { key: 'fast' } // the same shape A
let slow = { foo: 'slow' } // new shape B
load(first) //IC of load: [{ slot: 0, icType: LOAD, value: MONO(A) }] --> Miss
load(fast) //IC of load: [{ slot: 0, icType: LOAD, value: MONO(A) }] --> Fast
load(slow) //IC of load: [{ slot: 0, icType: LOAD, value: POLY[A,B] }] --> Slow. Now it needs to check 2 shapes.
该漏洞的修复Commit修改了两个函数HandleLoadICSmiHandlerLoadNamedCase
和ComputeHandler
。对这两个函数进行追踪可以发现以下调用链:
ComputeHandler
^
UpdateCaches
^
Load
^
Runetime_LoadWithReceiverIC_Miss
和
HandleLoadICSmiHandlerLoadNamedCase
^
HandleLoadICSmiHandlerCase
^
HandleLoadICHandlerCase
^
GenericPropertyLoad
从函数名可以看出,这里是在加载属性,那么可以联想到在了解IC时讨论的属性加载的问题。通过查看bytecode,可以发现属性加载是通过LdaNamedProperty
来实现的。通过搜索发现以下代码:
// LdaNamedProperty <object> <name_index> <slot>
//
// Calls the LoadIC at FeedBackVector slot <slot> for <object> and the name at
// constant pool entry <name_index>.
IGNITION_HANDLER(LdaNamedProperty, InterpreterAssembler) {
TNode<HeapObject> feedback_vector = LoadFeedbackVector();
// Load receiver.
TNode<Object> recv = LoadRegisterAtOperandIndex(0);
// Load the name and context lazily.
LazyNode<TaggedIndex> lazy_slot = [=] {
return BytecodeOperandIdxTaggedIndex(2);
};
LazyNode<Name> lazy_name = [=] {
return CAST(LoadConstantPoolEntryAtOperandIndex(1));
};
LazyNode<Context> lazy_context = [=] { return GetContext(); };
Label done(this);
TVARIABLE(Object, var_result);
ExitPoint exit_point(this, &done, &var_result);
AccessorAssembler::LazyLoadICParameters params(lazy_context, recv, lazy_name,
lazy_slot, feedback_vector);
AccessorAssembler accessor_asm(state());
accessor_asm.LoadIC_BytecodeHandler(¶ms, &exit_point);
.....
}
注意最后一行,追踪LoadIC_BytecodeHandler
发现此函数处理了所有关于属性访问的情况。第一次访问时并不会FeedbackVector
所以会进入LoadIC_NoFeedBack
函数。
void AccessorAssembler::LoadIC_NoFeedback(const LoadICParameters* p,
TNode<Smi> ic_kind) {
Label miss(this, Label::kDeferred);
TNode<Object> lookup_start_object = p->receiver_and_lookup_start_object();
GotoIf(TaggedIsSmi(lookup_start_object), &miss);
TNode<Map> lookup_start_object_map = LoadMap(CAST(lookup_start_object));
GotoIf(IsDeprecatedMap(lookup_start_object_map), &miss);
TNode<Uint16T> instance_type = LoadMapInstanceType(lookup_start_object_map);
{
// Special case for Function.prototype load, because it's very common
// for ICs that are only executed once (MyFunc.prototype.foo = ...).
Label not_function_prototype(this, Label::kDeferred);
GotoIfNot(IsJSFunctionInstanceType(instance_type), ¬_function_prototype);
GotoIfNot(IsPrototypeString(p->name()), ¬_function_prototype);
GotoIfPrototypeRequiresRuntimeLookup(CAST(lookup_start_object),
lookup_start_object_map,
¬_function_prototype);
Return(LoadJSFunctionPrototype(CAST(lookup_start_object), &miss));
BIND(¬_function_prototype);
}
GenericPropertyLoad(CAST(lookup_start_object), lookup_start_object_map,
instance_type, p, &miss, kDontUseStubCache);
BIND(&miss);
{
TailCallRuntime(Runtime::kLoadNoFeedbackIC_Miss, p->context(),
p->receiver(), p->name(), ic_kind);
}
}
在这里找到了GenericPropertyLoad
。同时发现无论如何都会执行Runtime::kLoadNoFeedbackIC_Miss
。这个函数其实就是RUNTIME_FUNCTION(Runtime_LoadWithReceiverIC_Miss)
。
至此完整的调用链已经找到了,那根据此调用链,可以发现在第一次访问属性时,由于没有FeedbackVector
,会调用LoadIC_NoFeedback
。假设lookup_start_object
不是小整数且没有被淘汰(被回收),那么就会调用GenericPropertyLoad
,随后再调用LoadNoFeedbackIC_Miss
。在ComputeHandler
中,发现修改的判断分支检查了holder
是否在IsJSModuleNamespace
,但是在HandleLoadICSmiHandlerLoadNamedCase
中却加载的是receiver
,此对象并不在JSModuleNamespace
中。所以当FeedbackVector
被创建后,内部的IC中的类型记录可能与真正调用时的类型不符,假设此时使用IC中储存的对象类型调用JSModuleNamespace
中的某些属性,那么V8会根据FastPath
使用IC中存储的类型,但是由于receiver
不是此类型,就会导致类型混淆。
综上所述,复现此漏洞需要以下条件:
JSModuleNamespace
中放置一个可以随时调用的属性/函数此条件可以通过export文件中的函数或属性即可,比如说在“一个文件”中:
export let foo = {}
//或者(笔者使用的方法)
export function foo()
{
....
}
在“另一个文件”中:
import * as foo from "一个文件.mjs";
%DebugPrint(foo)
会有以下结果:
/*
DebugPrint: 000003BF080496D9: [JSModuleNamespace]
- map: 0x03bf082077f9 <Map(DICTIONARY_ELEMENTS)> [DictionaryProperties]
- prototype: 0x03bf08002235 <null>
- elements: 0x03bf08003295 <NumberDictionary[7]> [DICTIONARY_ELEMENTS]
- module: 0x03bf081d3229 <Other heap object (SOURCE_TEXT_MODULE_TYPE)>
- properties: 0x03bf080496ed <NameDictionary[17]>
- All own properties (excluding elements): {
0x03bf08005669 <Symbol: Symbol.toStringTag>: 0x03bf080049f5 <String[6]: #Module> (data, dict_index: 1, attrs: [___])
f: 0x03bf081d3349 <AccessorInfo> (accessor, dict_index: 2, attrs: [WE_])
}
- elements: 0x03bf08003295 <NumberDictionary[7]> {
- max_number_key: 0
}
000003BF082077F9: [Map]
- type: JS_MODULE_NAMESPACE_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: DICTIONARY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- dictionary_map
- may_have_interesting_symbols
- non-extensible
- prototype_map
- prototype info: 0x03bf081d3369 <PrototypeInfo>
- prototype_validity cell: 0x03bf08142405 <Cell value= 1>
- instance descriptors (own) #0: 0x03bf080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x03bf08002235 <null>
- constructor: 0x03bf081c3bed <JSFunction Object (sfi = 000003BF08144745)>
- dependent code: 0x03bf080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
*/
lookup_start_object
和holder
不同并符合holder
为JSModuleNamespaceType
。之后,调用“一个文件”中的函数(访问属性),以触发LoadWithReceiverIC_Miss
导致的UpdateCaches
。import * as foo from "一个文件.mjs";
class Test(){}
class Test1(){}
let tmp = new Test();
Test.prototype.__proto__=Test1;//修改lookup_start_object
Test.prototype.__proto__.__proto__=foo;//修改holder
TNode<Module> module =
LoadObjectField<Module>(CAST(p->receiver()), JSModuleNamespace::kModuleOffset);
认为这里会提供一个foo
,但是p->receiver
并不是foo
。此时便会触发类型混淆。
修复该漏洞只需保证ic.cc
和accessor-assembler.cc
中使用的对象类型是相同的即可,V8选择的方式为在HandleLoadICSmiHandlerLoadNamedCase
中使用holder
(而不是receiver
)作为Load的参数。并在ComputeHandler
中为smi
类别单独开分了一个判断分支,以确保在处理HandleLoadICSmiHandlerLoadNmaedCase
中一定会拿到smi_handler
。
修复Commit内容如下:
近期Google官方修复了包括在天府杯中披露的和已发现存在在野利用的多个高危漏洞,建议Chrome用户积极将程序升级到最新稳定版以免受到攻击,目前最新稳定版本为96.0.4664.77。
参考链接
https://github.com/v8/v8/commit/e4dba97006ca20337deafb85ac00524a94a62fe9
https://github.com/maldiohead/TFC-Chrome-v8-bug-CVE-2021-38001-poc
http://web.cs.ucla.edu/~palsberg/course/cs232/papers/DeutschSchiffman-popl84.pdf
http://www.cnnvd.org.cn/web/xxk/ldxqById.tag?CNNVD=CNNVD-202110-2070
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/2007/