文章首发于安全KER https://www.anquanke.com/post/id/224317
0x00 前言
通过N1CTF2020 Escape一题学习V8的逃逸分析机制
0x01 前置知识
逃逸分析
概念
逃逸分析(escape-analysis)就是JIT阶段用来分析对象的作用域的一种机制,分析对象的作用域是为了更好的优化代码,生成高效率的JIT代码。
如下的代码中,对象a发生了逃逸
,因为a是在函数中创建的对象,通过return返回给外部使用。
1 | function func() { |
如下的代码也同样发生逃逸
1 | var a; |
逃逸的对象不会在函数执行完毕不会被收回,因此JIT对此类对象不做优化。
优化未逃逸的对象
如果对象未发生逃逸,JIT会将其优化为局部变量的形式,如下的代码中,v未发生逃逸
1 | function func(a) { |
那么该函数会被优化为
1 | function func(a) { |
从中可用看出,逃逸分析可以优化那些未逃逸的对象,去掉不必要的对象申请,使得代码更加高效。
构造一个逃逸
如下,将另一个函数作为一个参数,并在当前这个函数里调用另一个函数,JIT将无法在编译时确定foo会做什么,由此,o会发生逃逸
1 | function (foo) { |
JIT逃逸分析如何确定变量类型
In a CFG: One map per basic block, updated imperatively when traversing the
block
- In an unscheduled graph: One map per effectful node.
This is expensive! Solution: A purely functional map: - Copy: O(1)
- Update/Access: O(log n)
This can be achieved with any tree-based map datastructure.
We chose a hash-tree.
从Escape Analysis in V8
文献中可以看出,在逃逸分析时,使用树结构来保存各个节点的checkmap
,这样进行复制时,只需要O(1)
的时间,进行状态更新和访问时,只需要O(log n)
的时间。checkmap
决定了这个节点生成的JIT该以什么方式去操作对象。如果checkmap
缺失,将导致生成的JIT代码有问题,发生类型混淆。
0x02 漏洞分析
patch分析
1 | diff --git a/src/compiler/escape-analysis.cc b/src/compiler/escape-analysis.cc |
从中可用看出,patch文件在 VirtualObject
类中增加了几个变量和函数,并在一些位置进行调用,利用git apply patch.diff
将patch文件应用,然后我们分析完整的escape-analysis.cc
文件,在ReduceNode
函数中的IrOpcode::kStoreField
分支时
1 | case IrOpcode::kStoreField: { |
上面的代码可以体现出逃逸分析
中的变量替换
思想,即对没有逃逸的对象进行优化。
接下来继续看IrOpcode::kCheckMaps
分支补丁上去的代码
1 | case IrOpcode::kCheckMaps: { |
前面我们介绍过,所有节点的checkmap保存在一棵树上,因此为了方便进行删除,这里用的是MarkForDeletion()
,只需要O(1)
的时间即可将当前这个节点的checkmap标记为删除。checkmap被删除的话,那么JIT在处理这个节点时将无法知道其当前的类型,由此会造成类型混淆(Type Confusion)
。
再来看打到default
分支上的补丁
1 | default: { |
可以看出这里又清除了map_
变量的值
POC构造与分析
首先得让vobject->_map
这个变量被赋值,那么就是发生在没有逃逸的时候,会进入分支
1 | if (vobject && !vobject->HasEscaped() && |
然后得让变量进入逃逸状态,这样当进入case IrOpcode::kCheckMaps:
时能够进入else if (vobject) { //逃逸状态}
分支,但要执行到current->MarkForDeletion();
语句,还得保证Node* cache_map = vobject->Map();
不为空。
首先构造如下的代码
1 | function opt(foo) { |
运行后发现不能像我们预期的那样发生类型混淆,通过gdb调试看一下,在三个patch点下断点
1 | b escape-analysis.cc:585 |
通过调试发现仅能在585这一个分支断下,添加-print-opt-code
选项可以看到整个代码都被JIT优化了
这样的话JIT编译器可以确定foo做了什么,我们的opt函数就会退化为
1 | function opt(foo) { |
因此我们得仅让opt这一个函数被优化,由此应该这样
1 | function opt(foo) { |
这样运行,会发现opt的JIT生成了两次,也就是说print(opt((o)=>{o[0] = x;}));
这句的opt调用并没有匹配到之前opt生成的JIT代码,查看第一次生成的JIT代码(关键部分)
1 | 0x131a00084fa9 e9 49ba009784e7ad7f0000 REX.W movq r10,0x7fade7849700 (CreateShallowArrayLiteral) ;; off heap target |
查看第二次JIT生成的关键代码
1 | f8 488945b8 REX.W movq [rbp-0x48],rax |
可以看出,第一次并没有匹配参数,而是直接 deopt-soft deoptimization bailout
,而第二次有匹配参数,判断函数地址是否为指定值,因此,我们再增加几个opt调用看看有什么变化。
1 | function opt(foo) { |
我们看到,最后一个标号为2,也就是总共生成了opt函数的3份JIT代码,而我们的js里有4个opt函数调用,也就是说,最后的print(opt((o)=>{o[0] = x;}));
成功匹配了JIT代码。
我们查看最后一份的JIT代码
1 | 0x2a000854c0 0 488d1df9ffffff REX.W leaq rbx,[rip+0xfffffff9] |
可以看到,最后一份JIT代码中,已经不再对参数进行匹配了,也就是说,即使我们记下来继续调用opt(),参数无论为什么,都会匹配到,我们测试一下
1 | function opt(foo) { |
可以看到也只生成了3份JIT代码,最后两句的调用都直接走opt的JIT成功了。
于是,我们的代码可以用for循环来精简一下
1 | function opt(foo) { |
运行后,发现仍然不能发生类型混淆,继续调试
先是 (v8::internal::compiler::VirtualObject *) 0x5643165e5410
设置了map_
值
然后(v8::internal::compiler::VirtualObject *) 0x5643165e5770
设置了map_
值
接下来发现(v8::internal::compiler::VirtualObject *) 0x5643165e5770
的map_
值被清空
接下来到这里,这个分支是当检测到对象逃逸时才会到达,由于前一步把这个vobject
的map_
给清空了,导致条件不成立,无法执行到current->MarkForDeletion();
上述POC失败的原因是因为在case IrOpcode::kCheckMaps:
之前先进入了default
把map_
值给清空了,我们可以再对象里再裹一层对象试试。
1 | function opt(foo) { |
接下来我们重新调试,我们发现(const v8::internal::compiler::VirtualObject *) 0x558f95f216c0
这个节点的checkmaps
被删除了,因此将造成类型混淆
继续运行,发现输出了对象的地址,发生了类型混淆
1 | pwndbg> p vobject |
如下是有漏洞的JIT代码
1 | 0x2343000857c8 1a8 488b7d18 REX.W movq rdi,[rbp+0x18] |
如下是无漏洞的JIT代码
1 | 0x286d000857b0 1b0 49ba405e010f7e7f0000 REX.W movq r10,0x7f7e0f015e40 (Call_ReceiverIsNullOrUndefined) ;; off heap target |
可以发现,由于逃逸分析时把checkmaps
删除了,使得生成的JIT代码里调用完函数后少了如下的检查代码,由此发生类型混淆
1 | 0x286d000857bd 1bd 488b4dd8 REX.W movq rcx,[rbp-0x28] |
0x03 漏洞利用
利用类型混淆,构造addressOf和fakeObj原语,然后利用两个原语伪造一个ArrayBuffer,实现任意地址读写。然后可以创建一个div
对象,利用任意地址读写篡改其虚表,然后执行对应的操作劫持程序流
1 | <!DOCTYPE html> |
0x04 感想
在写这篇文章的过程中,某些疑难点无形中理解了,以后得坚持写文章记录过程。
0x05 参考
JVM之逃逸分析
深入理解Java中的逃逸分析
[JVM] 逃逸分析(Escape Analysis)
Escape Analysis in V8
Pwn2Win OmniTmizer