• 本文作者: 天融信安全应急响应中心
  • |
  • 2020年4月15日
  • |
  • CVE
  • |

v8 cve-2019-5791:模块耦合导致的类型混淆

近日,作者研究了chrome v8的一个漏洞cve-2019-5791,cve的漏洞描述是由于不合适的优化可以导致越界读,但实际上该漏洞是由于在语法树遍历阶段和实际生成字节码阶段对死结点的判定不一致导致的类型混淆,成功利用该漏洞可以导致rce。

漏洞环境

漏洞的修复网址是https://chromium.googlesource.com/v8/v8/+/9439a1d2bba439af0ae98717be28050c801492c1,这里使用的commit是2cf6232948c76f888ff638aabb381112582e88ad。使用如下命令搭建漏洞环境

漏洞分析

ast visitor

首先看一下漏洞修复的描述,

通过漏洞描述我们大致知道问题出在语法树的遍历阶段,具体点来说就是语法树遍历阶段和字节码生成阶段对死结点的判定不一致。那么现在的问题就是这两个阶段对死结点的判定具体有什么不同,对死结点判定的不一致又会导致什么问题。

在漏洞修复页面查看一下diff的内容看一下修复漏洞改了哪些东西,通过被修改代码的文件名发现只是去掉了语法树遍历阶段的一些代码,加了几行对漏洞分析帮助不大的输出和一个漏洞poc。从这些信息我们可以得到ast visitor处理语法树死结点跳转位置的代码如下,

从上边ast visitor中处理语法树statements的代码我们可以得到stmt->IsJump()为真时会跳出循环不去处理(使用自定义的RECURSE方法)之后的代码。注意这里stmt->IsJump()为真的条件中有一个是IfStatement。

我们跟进ast-traversal-visitor.h,发现这个文件定义了一个继承自AstVisitor的类AstTraversalVisitor,定义了一些处理不同类型语法树结点的操作,而在处理不同类型语法树结点时又使用了RECURSE宏调用相应语法树结点类型的visit方法继续遍历语法树,在遍历节点过程中主要记录语法树深度、检查语法树结点递归时是否栈溢出。

以上是我们可以从漏洞修复得到的信息,但是仅凭这些信息显然没办法了解漏洞的本质。此时我们还需要解决的一个问题是找到v8中真正生成字节码(bytecode-generator)时对死结点处理的代码。

bytecode-generator

我对v8的源代码不是很熟悉,找bytecode-generator处理语法树死结点的代码我这里用的方法是在patch中ast-traversal-visitor.h代码修改的地方(即b ast-traversal-visitor.h:113)下断点,然后不断栈回溯找到的。最终得到v8生成语法树解析生成字节码的大致过程如下(这里有一个点是v8会把js代码分成top-level和non top-level部分,普通语句和函数声明是top-level,函数定义部分是non top-level。)

我们跟进最终生成字节码的函数BytecodeGenerator::GenerateBytecode,

主要是进行了栈溢出检查、范围检查、分配寄存器,然后调用GenerateBytecodeBody()真正生成字节码。

在GenerateBytecodeBody()中主要是根据语法树结点类型调用相应visit函数处理相应结点,注意这里VisitStatements(info()->literal()->body());调用的是如下代码

BytecodeGenerator::VisitStatements即实际生成字节码时处理语法树声明类型结点的代码,这里我们发现在实际生成字节码时builder()->RemainderOfBlockIsDead()条件为真时会跳出循环不去处理之后的代码。这样我们最开始的问题ast visitor和bytecode-generator处理语法树死结点的不同就转化为builder()->RemainderOfBlockIsDead()条件为真和stmt->IsJump()条件为真时的不同。

我们找到builder()->RemainderOfBlockIsDead()的定义,

对比stmt->IsJump()的定义

对比可以发现stmt->IsJump()为真的条件多了IfStatement,也就是说ast visitor不会处理if死结点之后的代码,而实际生成字节码时会处理到这部分ast visitor没有检查过的代码。那接下来的问题就是,这么做会导致什么后果呢?

类型混淆的原因

经过调试发现漏洞版本的v8在处理到形如poc中代码的箭头函数时,

会调用bytecode-generator.cc BytecodeGenerator::AllocateDeferredConstants,此时栈回溯如下

跟进BytecodeGenerator::AllocateDeferredConstants,

BytecodeGenerator::AllocateDeferredConstants主要调用对应的方法处理语法树不同类型节点并将当前语法树结点偏移literal.second的元素视为下一个要处理的当前结点类型入口即视为与当前结点类型一致,例如在构造array对象常量时,会调用SetDeferredConstantPoolEntry设置literal.second为当前数组的下一个入口点,即偏移literal.second的位置视为数组类型,这里literal.second为一个索引值。

由于ast visitor没有检查if死结点之后代码的数据类型,而bytecode-generator在实际生成字节码时会把语法树当前结点偏移literal.second的位置视为当前节点类型从而最终导致类型混淆。

如poc中的代码在执行到compiler.cc :961 maybe_existing = script->FindSharedFunctionInfo(isolate, literal);时,此时literal的内容已经是非法的object对象,debug编译的v8类型检查错误导致崩溃。

patch分析

我们回过头来看patch修改的内容,去掉了ast visitor中对代码块跳转的判断,即不论当前代码块是否会跳转依旧处理之后的代码,这样虽然可能会多遍历检查一部分语法树结点,但确实是修复了这个漏洞。

漏洞利用

cve-2019-5791网上公开的只有一个韩国人写的不稳定exp,成功率大概在40%左右,exp地址:https://github.com/cosdong7/chromium-v8-exploit。作者水平有限没有构造出比较稳定的exp,下面介绍下这个exp的思路。

因为我们利用cve-2019-5791最终要达到的目的还是任意地址读写,所以要做的就是利用if死结点之后代码的类型混淆构造一个可控的array buffer对象,有了可控的array buffer利用写wasm执行shellcode即可。

–print-ast查看一下exp中生成的语法树

注意到run中proxy对象的地址是0x55f76877c100,v1.v2中callFn的地址是0x55f7687838b8,callFn中的元素会覆盖到proxy的位置导致proxy和run类型混淆成array。在run成功被混淆成array类型时我们可以通过run修改proxy的长度得到一个可以越界访问的数组proxy,再通过数组的越界读写利用写wasm执行shellcode即可。这里exp不稳定的原因是run不一定会从jsfunction类型被稳定类型混淆成array类型,导致不一定会得到稳定越界访问的数组proxy。

总结

通过以上分析发现cve-2019-5791这个漏洞根源还是v8在开发时模块之间耦合出现的问题,而为了减少模块之间数据和操作的耦合度,开发时又不得不加入一些模块去分开处理数据和操作。模块之间耦合时可能存在处理不一致导致安全隐患,这可能为我们挖掘漏洞提供一些思路。

参考链接

exp:https://github.com/cosdong7/chromium-v8-exploit

Written by 天融信安全应急响应中心