这道题中,主要涉及v8中的dependency机制,由于patch文件删除了某些添加依赖(dependency)的代码,导致在生成的JIT代码中,即使某些元素类型发生了变化也不会触发deoptimize,从而导致type confusion。
在这篇writeup里我主要记录我分析的过程,因为我事先从已有的wp中知道到了一些结论性的东西,所以我试图找到一个从零逐步寻找得到最后结果的逻辑,这个过程中可能会显得比较啰嗦。
调试环境
具体环境搭建步骤就不详述了,patch文件在这里下载
1 | git reset --hard eefa087eca9c54bdb923b8f5e5e14265f6970b22 |
漏洞分析
首先分析题目patch文件
1 | diff --git a/src/compiler/access-info.cc b/src/compiler/access-info.cc |
AccessInfoFactory::ComputeDataFieldAccessInfo
函数中,有两处unrecorded_dependencies.push_back
被删除掉,同时让constness
始终被赋值为PropertyConstness::kConst
先浏览一下整个函数的功能(以下为patch后的代码),首先获取了map
中的instance_descriptors
(存储了对象属性的元信息),然后通过descriptor
定位到了一个具体的属性。
1 | PropertyAccessInfo AccessInfoFactory::ComputeDataFieldAccessInfo( |
依次判断属性的类型,在进行一定的检查后,将属性加入到unrecorded_dependencies
中。patch导致了一些本应该加入到unrecorded_dependencies
的属性没有被加入进去。
1 | if (details_representation.IsNone()) { |
最后,因为patch的修改,使得所有属性都被标注为KConst
1 | PropertyConstness constness; |
在这里,这个unrecorded_dependencies
显然是问题的关键。
继续跟踪函数返回值可以发现最终返回的是一个PropertyAccessInfo对象,而unrecorded_dependencies
则是被初始化赋值给私有成员unrecorded_dependencies_
1 | PropertyAccessInfo::PropertyAccessInfo( |
查找引用该私有成员的代码,主要有两个函数
1 | bool PropertyAccessInfo::Merge(PropertyAccessInfo const* that, |
其中Merge函数中合并了两个unrecorded_dependencies_
,RecordDependencies函数中将unrecorded_dependencies_
转移到了CompilationDependencies
类的私有成员dependencies_
并清空了自身
浏览CompilationDependencies
类所在的compilation-dependency.cc(.h)
文件,从注释中可以得知该类用于收集和安装正在生成的代码的依赖。
在文件中查找dependencies_
,发现主要引用的代码均为遍历dependencies_
并调用IsValid()
。
IsValid()
被CompilationDependencies
的每个子类所重载,根据代码,其功能我的理解是用于判断某个元素是否已经改变或者过时。
为了进一步了解该类的作用,我在搜索了引用该头文件的代码。可以发现,结果中几乎都是用于JIT优化的文件。
逐个跟进文件查看后,我在compilation-dependencies.cc
中注意到了以下部分代码。从代码中可以看出,Ruduce过程中,可以通过添加dependency的方式来将CheckMaps节点删除,我认为这便是道题的root cause.
1 | Reduction TypedOptimization::ReduceCheckMaps(Node* node) { |
1 | // Record the assumption that {map} stays stable. |
总结
结合一些资料,对dependency我的理解是
对于JS类型的不稳定性,v8中有两种方式被用来保证runtime优化代码中对类型假设的安全性
- 通过添加CheckMaps节点来对类型进行检查,当类型不符合预期时将会bail out
- 以dependency的方式。将可能影响map假设的元素添加到dependencies中,通过检查这些dependency的改变来触发回调函数进行deoptimize
该题目中,因为删除了某些添加dependency的代码,这就导致在代码runtime中,某些元素的改变不会被检测到从而没有deoptimize,最终造成type confusion。
构造POC
patch删除了details_representation.IsHeapObject()
分支中的unrecorded_dependencies.push_back
操作,这意味HeapObject
类型不会被加入dependencies中。
运行以下代码
1 | var obj = {}; |
以上代码中,将字典{a: 1.1}
加入到obj
中,函数leaker
返回o.c.a
将obj作为参数传入leaker,生成JIT代码后,用{b: buf_to_leak}
替换掉原来的字典,再次调用leaker(obj),可以发现并没有触发deoptimize,而是输出了一个double值(buf_to_leak的地址)
其原因正是因为{a: 1.1}
对象并没有被添加到dependency中,导致后期修改时并没有被检测到,从而导致问题。
注意:修改obj.c时不能使用同属性名,如{a: buf_to_leak},因为事实上仍然存在一些依赖会影响到deoptimize,这点我没有找到更详细的解释,希望有师傅能够解释一下。参考:https://twitter.com/itszn13/status/1173627505485516801?s=20
使用Turbolizer可视化程序IR,验证我们的猜想
1 | cd tools/turbolizer |
使用以下命令执行代码,并使用浏览器访问127.0.0.1:8000
打开生成的文件
1 | ./out.gn/x64.debug/d8 --trace-turbo ../../../exps/accessible/poc.js --trace-turbo-path ../ |
可以看到,在TyperLowering时还存在两次CheckMaps,分别对应obj和obj.c
而到了SimplifiedLowering时已经只有对obj的CheckMaps了,这说明obj.c的转为使用dependency的方式来进行检查。
漏洞利用
既然存在type confusion,那么我们可以用JSArray来伪造一个ArrayBuffer,即可控制到BackingStore,从而实现任意读写。
对象地址泄露
在poc中我们已经实现了该功能
1 | var obj1 = {c: {x: 1.1}}; |
伪造ArrayBuffer
JSArray内存模型
我们首先进行如下调试
1 | d8> var arr = [1.1, 2.2, 3.3] |
从地址很容易可以看出,在Elements的后面紧跟的就是JSArray对象的Map,布局如下图
1 | Elements--->+-------------+ |
这意味着我们可以通过JSArray对象的地址来计算得到其Elements的地址,这为我们之后伪造ArrayBuffer后寻找其地址提供了便利。
trick:在调试过程中会发现,Elements并不是始终紧邻JSArray的,有些时候两者会相距一段距离。在师傅们的wp中提到可以使用splice
来使该布局稳定,例如
1 | var arr = [1.1, 2.2, 3.3].splice(0); |
具体原理我没有找到相关资料。。可能只有等以后读了源码才知道吧(有师傅知道的话可以说说吗
ArrayBuffer内存模型
在伪造ArrayBuffer的时候需要同时也伪造出它的Map的结构(当然,也可以对内存中ArrayBuffer的Map地址进行泄露,但是就麻烦了),通过找到JSArray的地址,+0x40即为map的地址,再将map地址填入JSArray的第一项即可。
这部分可以通过调试一个真正的ArrayBuffer并将其Map复制下来(这里并不需要全部的数据)。关于Map的内存模型可以参考这里。
1 | var fake_ab = [ |
获取伪造的ArrayBuffer
和poc的代码类似,不过反了过来,先将填入一个ArrayBuffer进行优化,然后在ArrayBuffer处写入地址,则该地址将作为ArrayBuffer被解析
1 | var ab = new ArrayBuffer(0x1000); |
WASM
在v8利用中总是需要布置shellcode,那么在内存中找到一块具有RWX权限的区域将会十分有帮助。wasm(WebAssembly)详细概念就不在这介绍了,这里值得注意的是是用wasm可以在内存中开辟出一块RWX的内存空间。
这里可以将C语言编写的代码转换为wasm格式。当然,编写的c语言代码不能够调用库函数(不然就可以直接写rce了),但是只要通过漏洞,将我们的shellcode覆盖到内存中wasm代码所在rwx区域即可。
下文将展示如何定位到rwx内存区域
1 | //test.js |
即instance+0x80
处即存放了RWX区域的地址
1 | wasm_inst_addr = leak_obj(wasm_instance) - 1; |
完整利用
1 | function success(str, val){ |