文章首发于安全KER https://www.anquanke.com/post/id/224765
0x00前言
从一题学习v8引擎对property access的相关JIT优化
0x01 前置知识
简介
在js中,字典类型的键称为属性(property),如下,dict是一个对象,其中a是它的一个属性
1 | var dict = {a:"haivk",b:"hai"}; |
当你要访问a时,首先是从这个对象里面查找键的内容a,找到后从中取出其对应的值。
优化
空间优化
假如有多个具有相同键的对象,其排列顺序也一样,那么可以不必为每一个对象都存储这些键的值,单独存储一份键的模板,我们称之为Shape
,比如上述的dict其键模板为
1 | a |
然后每个对象只需要保存一份键模板的指针即可,这样就节省了大量的空间。
运行如下的代码,并打印JIT代码
1 | var obj = {a:"haivk",b:"hai"}; |
发现生成的JIT代码如下(部分)
1 | 0x38af000851a2 c2 48b88d2c2d08af380000 REX.W movq rax,0x38af082d2c8d ;; object: 0x38af082d2c8d <HeapNumber 1.1> |
可以发现,这里直接用数组下标寻址的方式进行了属性的赋值和访问
1 | movl [rdi+0xf],rax |
Inline Caches (ICs)
如果要多次访问字典类型的数据,那么查找键的时间耗费是比较大的,因此v8引擎使用了一种叫Inline Caches (ICs)
的机制来缓解这种查找的时间耗费。假如有如下函数
1 | function (obj) { |
如果要调用该函数对同一个对象进行多次访问,那么可以将该函数里的访问过程进行优化,即不必再从查找键开始,将该键对应的数据缓存下来,这样下次访问时先校验,然后直接从缓存中加载。如下,我们对同一个对象进行了多次访问
1 | var obj = {a:"haivk",b:"hai"}; |
对应的JIT代码如下(部分)
1 | 0x12f100084fd7 117 b81e000000 movl rax,0x1e |
可以看到最后一个print调用时,直接使用LoadGlobalICTrampoline
函数从缓存中加载了数据,而不必再从对象中查找。
与LoadGlobalICTrampoline
对应函数是StoreGlobalICTrampoline
,可以将数据保存到缓存中。
0x02 漏洞分析
patch点分析
patch文件如下
1 | diff --git a/src/compiler/access-info.cc b/src/compiler/access-info.cc |
可以看到,patch文件通过#if
和#endif
将两处unrecorded_dependencies.push_back(dependencies()->FieldTypeDependencyOffTheRecord(map_ref, descriptor));
给注释掉了,并且constness = PropertyConstness::kConst;
将constness
设为了PropertyConstness::kConst
从源码中的注释
// Store is not safe if the field type was cleared.
我们可以知道,字典对象的property
的类型是很重要的,并且在程序中会被保存到unrecorded_dependencies
容器里,而patch正是patch掉了这个操作,除了Double
和SMI
类型的对象,其他的对象的类型都不会被push到unrecorded_dependencies
,unrecorded_dependencies
最终包装给一个对象,然后返回
1 | return PropertyAccessInfo::DataConstant( |
为了方便追踪,我们用gdb动态调试,设置断点,然后运行文章开始的示例脚本
1 | b AccessInfoFactory::ComputeDataFieldAccessInfo |
此时,unrecorded_dependencies
是空的
然后return到js-heap-broker.cc
里的GetPropertyAccessInfo
函数里
接着继续最终,来到js-native-context-specialization.cc
里的FilterMapsAndGetPropertyAccessInfos
函数
然后来到js-native-context-specialization.cc
里的ReduceNamedAccess
,发现这里有引用到dependencies()
,打印其值,是一个容器,内容为空
到这里,发现使用access_info.receiver_maps
来BuildCheckMaps
跟进BuildCheckMaps
函数,来到property-access-builder.cc
里
1 | void PropertyAccessBuilder::BuildCheckMaps( |
跟进DependOnStableMap(receiver_map);
函数
1 | 387 void CompilationDependencies::DependOnStableMap(const MapRef& map) { |
如果map.CanTransition()
成立,就会修改property
的类型
继续跟踪,来到graph-reducer.cc
里的GraphReducer::Reduce
函数
1 | 85 auto skip = reducers_.end(); |
poc构造
从上述的分析可知,如果DependOnStableMap(receiver_map);
里的map.CanTransition()
不成立,那么property
的类型就不会被改变,由于const MapRef& map
参数来自access_info.receiver_maps()
,而access_info
里的部分数据来自unrecorded_dependencies
,而由于patch的原因,某些类型不会加入到unrecorded_dependencies
了,那么意味着一些原本该进行类型转换的操作将不会进行。
首先构造
1 | var obj = {a:"haivk",b:"hai"}; |
发现不能造成类型混淆,其JIT代码如下(部分)
1 | 0x23565c142c38 118 49b971404c31240e0000 REX.W movq r9,0xe24314c4071 ;; object: 0x0e24314c4071 <String[#1]: a> |
主要是在执行obj.a = 1.1;
的时候没有使用优化的方法,而是使用SetNamedProperty
的普通js方法来进行赋值,那么就不会触发到漏洞点。那么,我们在{}
里再包含一个{}
试试
1 | var obj = {a:{b:"haivk"}}; |
仍然没有发生类型混淆,查看JIT代码
1 | 0x3b6d7cc2dfe 19e 48b991c2313f02140000 REX.W movq rcx,0x14023f31c291 ;; object: 0x14023f31c291 <String[#5]: print> |
其中opt函数优化为如下,可以看到其被优化为了数组寻址的方法
1 | 0x3e95ea042b5b 3b 55 push rbp |
考虑到是ICS
缓存机制的原因,o.a.b
的类型被缓存,因此存入1.1
时仍然是以HOLEY_ELEMENTS
的方式将1.1
打包为HeapNumber
,存为了对象,那么我们尝试这样修改
1 | var obj = {a:{b:"haivk"}}; |
上述,我们改了
1 | obj.a = {c:2.2}; |
即将a改成了另一个Shape
形的字典对象,然后调试,可以发现,这回因为没有缓存的原因,obj.a = {c:2.2};
是以unboxed double
的形式将数据写入
而opt
函数仍然能够访问obj.a.c
是因为opt被优化为了数组寻址
的方式,并且opt中仅比较了obj.a
的类型是否合法,而没有比较obj.a.b
的类型
1 | 0x1c8800bc2b80 60 48b9b9a678b713050000 REX.W movq rcx,0x513b778a6b9 ;; object: 0x0513b778a6b9 <Map(HOLEY_ELEMENTS)> |
继续运行,发现发生了类型混淆,1.1被当成一个对象地址,然后取出了一个对象
由此,我们可以构造如下两个原语
1 | function addressOf(obj) { |
注意事项
由于ICS
缓存机制的原因,上述两个原语仅能使用一次
,因为调用后,里面的字典对象相关信息会被缓存,因此想要多次利用的话,需要构造多个原语函数,并且每个函数里的字典对象的key互不相同,这里,我们也可以看到,在addressOf
里面,我们用的是var obj1 = {a:{b:1.1}};
,而在fakeObj里面,我们用的是var obj2 = {x:{y:buf}};
0x03 漏洞利用
本题的v8版本为7.9.33
,在低版本中,还没有compression pointer(指针压缩)
机制,因此addressOf
可以直接泄露出8字节地址,然后利用fakeObj伪造一个ArrayBuffer
实现任意地址读写,由于没有关闭wasm
,我们可以利用任意地址读写,修改wasm的shellcode,然后执行wasm就可以执行到我们的shellcode。
1 | <!DOCTYPE html> |
0x04 感想
通过本题加深了对v8的字典对象的理解,同时学习了wasm
在浏览器漏洞中的利用手法。浏览器PWN虽然难但是很有趣。
0x05 参考
Shapes and Inline Caches
[译] JavaScript 引擎基础:Shapes 和 Inline Caches
JavaScript engine fundamentals: optimizing prototypes
简明扼要地谈谈v8的隐藏类和Inline Cache(內联缓存