对Chrome cve-2018-17463的简单分析
环境搭建
Ubuntu20.04LTS
1 | git checkout 568979f4d891bafec875fab20f608ff9392f4f29 |
漏洞分析
patch
1 | [turbofan] Fix ObjectCreate's side effect annotation. |
diff:
1 | diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc |
这个patch是打在#define CACHED_OP_LIST(V)
的宏定义里,其中把CreateObject相对应的operator的properties从Operator::kNoWrite改成了Operator::kNoProperties,properties定义在operator类中:
1 | // src/compiler/operator.h |
根据注释,这个properties应该是标志该operator会有哪些effect,比如满足交换律,不会抛出异常等等,而其中的kNoWrite说明该操作不会有“side-effect”,我个人的理解是带有“side-effect”的操作是能改变对象map(或者内存布局)的操作。可以看到的是,在打了patch之后,现在的CreateObject操作是会有“side-effect”了。
lowering JSCreateObject
为了确定节点是否会有“side-effect”,通常需要查看一下对该节点进行lowering的阶段,JSCreateObject节点会在Generic lowering阶段被lowering:
1 | // src/compiler/js-generic-lowering.cc |
Builtins::kCreateObjectWithoutProperties会调用到Runtime::kObjectCreate,而Runtime::kObjectCreate函数会将对象的map从FastProperties变成DictionaryProperties,测试代码:
1 | let o = {a: 42}; |
输出:
1 | DebugPrint: 0x2b71c098e1a9: [JS_OBJECT_TYPE] |
而这表明Object.create是有“side-effect”的,而turbofan认为其没有,这会导致了后续优化推测出错
Redundancy Elimination
如果一些安全检查的节点(如checkmap)被认为是不需要的,那么在后续的优化阶段,该检查节点就会被删除,比如以下的代码:
1 | function foo(o) { |
首先会生成类似的代码:
1 | CheckHeapObject o |
这是在取得o.a和o.b,很明显这里第二次的CheckHeapObject和CheckMap是Redundancy 的,所以如果Turbofan认为在这两次check之间没有操作会改变o的map的话,就会在后续优化的过程中把第二次的CheckMap删除,这个例子的turbolizer图,inlining阶段:
到了load elimination阶段:
为了消除这些冗余的check节点,turbofan要保证这两次check之间不会有操作改变对象的Map,然而要预测这个是很困难的,因为js本身就很复杂,一个没考虑到的操作就会导致错误的消除,比如这个漏洞里的Object.create
poc
1 | function foo(o) { |
输出结果不是预期的43,查看生成的优化代码:
1 | 0x17685fc40db9 59 48bbd1c9f072fb0a0000 REX.W movq rbx,0xafb72f0c9d1 ;; object: 0x0afb72f0c9d1 <Map(HOLEY_ELEMENTS)> |
我们可以看到只在一开始进行了checkmap,后续在取o.b的时候并没有checkmap,是因为turbofan认为object.create并不会改变o的map
remove checkmap
让我们跟踪下代码来看看CreateObject的Operator::kNoWrite property是怎么影响到这个checkmap的消除的。
在turbolizer中可以看出是在load elimination的时候把第二次的checkmap给消除的,跟踪消除checkmap的代码:
1 | Reduction LoadElimination::ReduceCheckMaps(Node* node) { |
[1]处的代码要求state不能为nullptr,[2]处就是具体消除checkmap的地方了,而该state是checkmap节点的EffectInput节点的state,以上面poc的代码来说就是JSCreateObject,这个也可以通过gdb来看到:
而JSCreateObject节点在loadElimination阶段会经过该函数:
1 | Reduction LoadElimination::ReduceOtherNode(Node* node) { |
如[3]处代码所示,如果该节点有Operator::kNoWrite属性的话,那就不会调用到下面那句state = empty_state();
,也就是说JSCreateObject节点的state不会被清空,那么在调用LoadElimination::ReduceCheckMaps的时候就能保留下map的相关信息,这也就会可以消除checkmap节点了
exploit
memory layout
在开始之前我们需要查看一下FastProperties和DictionaryProperties 两个类型的Map下的properties内存布局是怎么样的
测试代码:
1 | var property_count = 8; |
FastProperties下的properties数组:
1 | DebugPrint: 0x41dac78e239: [JS_OBJECT_TYPE] |
DictionaryProperties 下的properties数组:
1 | DebugPrint: 0x41dac78e239: [JS_OBJECT_TYPE] |
这个DictionaryProperties 下的properties数组应该是类似hashtable的结构,还是3个一组(key,value,hash),且每次运行时这个properties是不一样的,也就是每个组间的顺序可能会变化,相对应的bucket也会变化
但是只要我们定义的属性有很多(以下例子property_count为30),就能出现以下情况:
注意看打箭头的在数组中偏移是一样的,这个代表着在优化后turbofan以为在拿o.p6其实拿到的是o.p22,但是每一次执行这个匹配的位置是会变动的,所以我们需要动态的确认这个匹配的idx。
addrof
以下代码来自 http://phrack.org/issues/70/9.html ,改了一点点
首先需要一个找到匹配idx的函数:
1 | const NUM_PROPERTIES = 32; |
makeObj函数会创建出带有一个inline属性和NUM_PROPERTIES个out-of-line属性的对象,eval创建的函数为(如NUM_PROPERTIES为2):
1 | function hax(o){ |
而把propertyValues都设置成负数是为了更方便的确认,因为NameDictionary数组中除了value会是该负数,key和hash都不会为负数,所以后面的判断条件也只需要判断出返回的数组里的某个偏移为负数且大于-NUM_PROPERTIES就可以,当然如果overlap了自己的是不行的
找到了这一对pair后,就可以构造出addrof了:
1 | // Return the address of the given object as BigInt. |
之所以可以这样是因为在同一次执行中,相同属性构造的Object
,在DictionaryProperties
中的偏移是相同的[2]
arbitrary read/write
上面我们可以泄露地址了,现在我们需要一个能任意地址读写的构造,最简单就是让一个ArrayBuffer(记为ab1)的backing_store指向另一个ArrayBuffer(记为ab2),这样就能操作ab1来修改ab2的backing_store来实现任意地址读写
1 | // Corrupt the backingStore pointer of an ArrayBuffer object and return the |
因为ArrayBuffer的内存布局是这样的:
1 | offset: |
而o的内存布局:
1 | offset: |
所以我们需要改o.x2这个inlline的property
成功修改后就实现了任意地址读写了
一个重要的问题
考虑以下代码:
1 | let o = {} |
在foo被优化后,只会生成一个checkmap节点,该checkmap节点是check对象o的,而不是check o.b的,check o.b的节点也是在load elimination的时候被消除了
参考链接
[1] http://phrack.org/issues/70/9.html
[2] https://gtoad.github.io/2019/09/04/V8-CVE-2018-17463/
[3] https://chromium.googlesource.com/v8/v8.git/+/52a9e67a477bdb67ca893c25c145ef5191976220%5E%21/#F0
[4] https://bugs.chromium.org/p/chromium/issues/detail?id=888923