V8-pwn入门(1)——对象模型、特性

Sl0th Lv4

0x00 前言💬

下面说的都是chrome80以前的v8,无sandbox、指针压缩

image-20241115181353869
image-20241115181353869

0x01 基础🪨

JS Engine CTF的常见模式

  1. 由出题⼈⾃⼰编写的patch引⼊的漏洞

  2. 历史漏洞的CVE被出题⼈重新引⼊

攻略方法

  1. 创建⼀个⽤于调试的js环境,如果有出题⼈提供的引⼊漏洞的patch,那么打上这个patch

  2. 分析这个patch并判断这个patch引⼊的是js引擎的哪个阶段

​ a. Runtime(CSA or Torque), TurboFan, Ignition, AST, InlineCache, …

  1. 构造poc去触发漏洞

  2. 利⽤漏洞去构造任意地址读写/任意对象地址的泄露/伪造任意对象等原语

  3. getshell,常⽤的⽅法是利⽤v8⾥的rwx地址区域(wasm中就有)直接写⼊shellcode代码来实现任意代码执⾏

0x02 V8对象模型⚙️

objects.h

v8源码中的objects.h里写了对象模型的结构,可以看出最大的是Object,Object总体上分为两类SmiHeapObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
// Most object types in the V8 JavaScript are described in this file.
//
// Inheritance hierarchy:
// - Object
// - Smi (immediate small integer)
// - HeapObject (superclass for everything allocated in the heap)
// - JSReceiver (suitable for property access)
// - JSObject
// - JSArray
// - JSArrayBuffer
// .........
//
// Formats of Object::ptr_:
// Smi: [31 bit signed int] 0
// HeapObject: [32 bit direct pointer] (4 byte aligned) | 01

凡是分配在v8堆上的,都会继承自HeapObject,Smi类似于cpu中的立即数

v8中的各种类不是c++语法实现的,因此没有构造和析构函数,也没有任何的字段/成员属性,是直接在v8 heap上通过AllocateRaw函数分配出来的内存

再根据不同object的结构,对不同偏移的内存写入值,🧐这样的目的是为了通过v8 heap来管理内存、实现精准GC

《垃圾回收的算法与实现》v8篇

V8-Object分类

分为Smi和HeapObject

  • smi表示有符号的31/32位小整数
  • 指向HeapObject的指针,由于内存对齐(8/4字节)所以指针的最低位为0

这里为了更方便使用smi,意思是不另外用一个指针指向一块内存空间,里面再存smi,而且直接用32/64bit表示,具体来说,HeapObject指针最低位肯定为0,但是由于smi的使用更频繁点,所以选择让smi左移一位让最低位变成0,而HeapObject指针则是把最低位置1,使用时再恢复。这种区分Smi和指向HeapObject的指针的方法叫Tagged Values

Smi(小整数)

  • LSB始终为0
  • 在32位上,smi右移1位可以获得原始值,64位要右移32位。smi表示的整数范围是有符号的31/32位整数
1
2
3
graph LR
A["Signed Value (31 bit) | 0"]
C["Signed Value (32 bit) | 0-Padding (31 bit) | 0"]

指向HeapObject的指针

  • LSB为1
  • 下图为32/64位示例
1
2
3
graph LR
A["Pointer (31 bit) | 1"] --> B["HeapObject"]
C["         Pointer (63 bit)         | 1"] --> D["HeapObject"]

🤓👆🏻使用Tagged Value区分smi和指向HeapObject的指针可以节省堆空间

0x03 关键的HeapObject🧐

HeapNumber

前面也提到smi只能表示有符号的31/32位整数,超过这个范围的整数可以以double值的形式保存在HeapNumber里面,HeapNumber结构如下

1
2
graph LR
C["         Pointer (63 bit)         | 1"] --> D["Map* | Value"]

其中Map*表示整个HeapObject的类型,表示的整数就以double的形式存在Value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 存储了数字的堆对象
class HeapNumber : public HeapObject {
public:
// 返回存储的 double 类型的值
inline double value() {
return READ_DOUBLE_FIELD(this, kValueOffset);
}
// 写入 double 值到对象
inline void set_value(double value) {
WRITE_DOUBLE_FIELD(this, kValueOffset, value);
}
// 将对象转换为 HeapNumber 指针
static inline HeapNumber* cast(Object* obj);

// 将 HeapNumber 转换为布尔值
Object* HeapNumberToBoolean();

// 内存布局描述
// kSize 之前的空间存储 map 对象的指针
static const int kValueOffset = HeapObject::kSize; //偏移
// kValueOffset 表示存储数字值的偏移量,类型为 double
static const int kSize = kValueOffset + kDoubleSize;

private:
// 禁止隐式构造函数
DISALLOW_IMPLICIT_CONSTRUCTORS(HeapNumber);
};

举个例子🌰

写一个object array,第一个元素是smi,第二个超过smi范围,第三个是字符串

1
2
3
4
5
let arr =[0xdeadbee,0xdeadbeef,"sloth"];
%DebugPrint(arr);
%SystemBreak();
//output
0x02aaab84dda9 <JSArray[3]>

gdb看一下这块内存(-1是因为tagged value),这里可能是版本问题,这个版本的v8把array又做了一层封装,封装到FixedArray中

1
2
3
pwndbg> x/4gx 0x02aaab84dda9-1
0x2aaab84dda8: 0x00000a0c4dc82f79(Map*) 0x00001179f91c0c71(FixedArray)
0x2aaab84ddb8: 0x000002aaab84dd09(指向真实array的pointer) 0x0000000300000000(表示长度)

继续访问真实的数组

1
2
3
4
5
6
7
pwndbg> x/8gx 0x000002aaab84dd09-1
0x2aaab84dd08: 0x00001179f91c0851(Map*) 0x0000000300000000(数组长度)
0x2aaab84dd18: 0x0deadbee00000000(Smi) 0x0000064aeec1f311(HeapNumber Pointer)
0x2aaab84dd28: 0x0000064aeec1f229("sloth") 0x00001179f91c0851
0x2aaab84dd38: 0x0000000400000000 0x000013ab2a603b29
pwndbg> job 0x0000064aeec1f229
#sloth

跟进HeapNumber

1
2
3
4
pwndbg> x/2gx 0x0000064aeec1f311-1
0x64aeec1f310: 0x00001179f91c0561 0x41ebd5b7dde00000(0xdeadbeefdouble表示)
pwndbg> job 0x41ebd5b7dde00000
Smi: 0x41ebd5b7 (1105974711)

String

结构如下

1
2
graph LR
C["Pointer (63 bit)| 1"] --> D["Map* | HashField|Length|String[0:8]|String[8:16]"]

举个例子🌰

1
2
3
4
5
let arr =["passion","free","sloth"];
%DebugPrint(arr);
%SystemBreak();
//output
0x20c46b60dda1 <JSArray[3]>

查看这块内存,跟进真正的array

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> x/4gx 0x20c46b60dda1-1
0x20c46b60dda0: 0x00001f8f0c182f79 0x000028e799a40c71
0x20c46b60ddb0: 0x000020c46b60dd01 0x0000000300000000
pwndbg> x/6gx 0x000020c46b60dd01-1
0x20c46b60dd00: 0x000028e799a40851(Map*) 0x0000000300000000
0x20c46b60dd10: 0x00001fa5bd15f229("passion") 0x00001fa5bd15f241("free")
0x20c46b60dd20: 0x00001fa5bd15f259("sloth") 0x000028e799a40851
pwndbg> job 0x00001fa5bd15f229
#passion
pwndbg> job 0x00001fa5bd15f241
#free
pwndbg> job 0x00001fa5bd15f259
#sloth

跟进第一个字符串passion

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/10gx 0x00001fa5bd15f229-1
0x1fa5bd15f228: 0x000028e799a40461(map*) 0x00000007a35b4466(前四字节是length 7,后四字节是HashField)
0x1fa5bd15f238: 0x006e6f6973736170("passion") 0x000028e799a40461(map*)
0x1fa5bd15f248: 0x0000000401ff50c2(length 7 HashField) 0x0000000065657266("free")
0x1fa5bd15f258: 0x000028e799a40461(map*) 0x00000005a5bcda06(length 6 HashField)
0x1fa5bd15f268: 0x00000068746f6c73("sloth") 0x000028e799a40461
pwndbg> python print(bytes.fromhex("6e6f6973736170").decode('ascii'))
noissap
pwndbg> python print(bytes.fromhex("65657266").decode('ascii'))
eerf
pwndbg> python print(bytes.fromhex("68746f6c73").decode('ascii'))
htols

倒着存

JSObject

  • 继承⾃Object,HeapObject,JSReceiver
  • Properties通过⼀个FixedArray(定⻓数组)保存所有的命名属性
  • Elements通过⼀个FixedArray保存所有的数字索引的属性

结构如下

1
2
graph LR
C["Pointer (63 bit)| 1"] --> D["Map* | Properties* | Elements*"]

JSArray

继承Object, HeapObject, JSReceiver, JSObject,结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
graph TD
A["Pointer (63 bit) | 1"] --> B[JSArray]
B --> C[Map*]
B --> D[Properties*]
B --> E[Elements*]
B --> F[Length]
E --> G[FixedArray]
G --> H[Map*]
G --> I[Length]
G --> J["Elements[0]"]
G --> K["Elements[1]"]
G --> M["...."]

JSArray中Elements的类型

  • https://v8.dev/blog/elements-kinds
  • ⼩整数,也称为 Smi
  • Double,⽤于不能表⽰为 Smi 的浮点数和整数。
  • 常规元素,⽤于⽆法表⽰为Smi或double的值。
1
2
3
4
5
6
7
8
9
const array = [1, 2, 3];
// elements kind: PACKED_SMI_ELEMENTS
array.push(4.56);
// elements kind: PACKED_DOUBLE_ELEMENTS
array.push('x');
// elements kind: PACKED_ELEMENTS
array.length; // 5
array[9] = 1; // array[5] until array[8] are now holes
//array[5] -- array[8] :elements kind: HOLEY_ELEMENTS

举个例子🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let arr = [1, 1.1];
%DebugPrint(arr);
%SystemBreak();
//output
0x08e88570ddc9 <JSArray[2]>
pwndbg> job 0x08e88570ddc9
0x8e88570ddc9: [JSArray]
- map: 0x2e172c042ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x0afb86dd1111 <JSArray[0]>
- elements: 0x08e88570dda9 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x17eb01180c71 <FixedArray[0]> {
#length: 0x07cfb6f401a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x08e88570dda9 <FixedDoubleArray[2]> {
0: 1
1: 1.1
}
pwndbg> x/4gx 0x08e88570ddc9-1
0x8e88570ddc8: 0x00002e172c042ed9 0x000017eb01180c71
0x8e88570ddd8: 0x000008e88570dda9 0x0000000200000000
pwndbg> x/10gx 0x000008e88570dda9-1
0x8e88570dda8: 0x000017eb011814f9 0x0000000200000000
0x8e88570ddb8: 0x3ff0000000000000(1) 0x3ff199999999999a(1.1的hex表示)
0x8e88570ddc8: 0x00002e172c042ed9 0x000017eb01180c71
0x8e88570ddd8: 0x000008e88570dda9 0x0000000200000000
0x8e88570dde8: 0x0000000000000000 0x0000000000000000
pwndbg> job 0x000008e88570dda9
0x8e88570dda9: [FixedDoubleArray]
- map: 0x17eb011814f9 <Map>
- length: 2
0: 1
1: 1.1

可以看到这里1.1没有用HeapNumber,double值在double array里是直接存的,可以节省内存空间

稍微修改一下变成object array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let arr = [1, 1.1];
%DebugPrint(arr);
%SystemBreak();
//output
0x17dcd1e4ddf9 <JSArray[3]>
pwndbg> x/8gx 0x17dcd1e4dd99-1
0x17dcd1e4dd98: 0x000004fddb340801 0x0000000300000000
0x17dcd1e4dda8: 0x0000000100000000(1) 0x00003aaf2dc1f2f9(HeapNumber Pointer)
0x17dcd1e4ddb8: 0x000017dcd1e4ddc1 0x0000376224680459
0x17dcd1e4ddc8: 0x000004fddb340c71 0x000004fddb340c71
pwndbg> job 0x17dcd1e4ddf9
0x17dcd1e4ddf9: [JSArray]
- map: 0x376224682f79 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x3aaf2dc11111 <JSArray[0]>
- elements: 0x17dcd1e4dd99 <FixedArray[3]> [PACKED_ELEMENTS]
- length: 3
- properties: 0x04fddb340c71 <FixedArray[0]> {
#length: 0x2d9d450001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x17dcd1e4dd99 <FixedArray[3]> {
0: 1
1: 0x3aaf2dc1f2f9 <HeapNumber 1.1>
2: 0x17dcd1e4ddc1 <Object map = 0x376224680459>
}
pwndbg> job 0x3aaf2dc1f2f9
1.1
pwndbg> x/2gx 0x3aaf2dc1f2f9-1
0x3aaf2dc1f2f8: 0x000004fddb340561 0x3ff199999999999a

可以看到这里1.1就变成用HeapNumber了

💡可以看出double array和object array的存储方式不同,可以利用这个特性,假设有一个double array和object array的类型混淆,使得object array使用double array的方式直接把元素读出来,这样可以leak任意指针的地址/伪造任意object

JSArrayBuffer

保存有⼀个被称作BackingStore的buffer的对象

BackingStore 是一种用于存储数据的独立内存区域,主要用于存放 JavaScript 中 TypedArrayArrayBuffer 这样的二进制数据。

  • 不受 GC 管理:这块内存区域是独立分配的,不会被 V8 的 GC 自动回收,因此向BackingStore的指针不是Tagged Value(末尾不能为1)
  • 分配方式:
    • 在 Chrome 中使用 PartitionAlloc 分配。
    • 在 d8(V8 的独立运行环境)中使用 ptmalloc(一个常见的 malloc 实现)模拟分配。
  • 用途:它是一种高效存储二进制数据的方法,避免了与 GC 交互时的额外开销。

举个例子🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const buf = new ArrayBuffer(0x20);
%DebugPrint(buf);
%SystemBreak();
//output
0x3d9ab9b8dd79 <ArrayBuffer map = 0x2027fb1021b9>
pwndbg> job 0x3d9ab9b8dd79
0x3d9ab9b8dd79: [JSArrayBuffer]
- map: 0x2027fb1021b9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x28593e5ce981 <Object map = 0x2027fb102209>
- elements: 0x15b6957c0c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x559b9dc779f0 //* 不是tagged value
- byte_length: 32
- detachable
- properties: 0x15b6957c0c71 <FixedArray[0]> {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
pwndbg> x/20gx 0x559b9dc779f0
0x559b9dc779f0: 0x0000000000000000 0x0000000000000000
0x559b9dc77a00: 0x0000000000000000 0x0000000000000000
0x559b9dc77a10: 0x0000000000000000 0x0000000000000031
0x559b9dc77a20: 0x0000000000000000 0x0000559b9dbef010
0x559b9dc77a30: 0x3031626637323032 0x0000003e39303232
0x559b9dc77a40: 0x0000000000000000 0x00000000000003c1
0x559b9dc77a50: 0x00007f8ec2273be0 0x00007f8ec2273be0
0x559b9dc77a60: 0x0000559b694a8081 0x0000000100440000
0x559b9dc77a70: 0x0072005000000008 0x0000559b9dc77a30
0x559b9dc77a80: 0x0000559b9dc83e80 0x0000000000000000
pwndbg> x/20gx 0x559b9dc779f0-0x10
0x559b9dc779e0: 0x0000082856788f19 0x0000000000000031 (meta头)
0x559b9dc779f0: 0x0000000000000000 0x0000000000000000
0x559b9dc77a00: 0x0000000000000000 0x0000000000000000
0x559b9dc77a10: 0x0000000000000000 0x0000000000000031
0x559b9dc77a20: 0x0000000000000000 0x0000559b9dbef010
0x559b9dc77a30: 0x3031626637323032 0x0000003e39303232
0x559b9dc77a40: 0x0000000000000000 0x00000000000003c1
0x559b9dc77a50: 0x00007f8ec2273be0 0x00007f8ec2273be0
0x559b9dc77a60: 0x0000559b694a8081 0x0000000100440000
0x559b9dc77a70: 0x0072005000000008 0x0000559b9dc77a30

带meta头,可见是ptmalloc分配出来的

  • 虽然在ArrayBuffer中描述了⼤⼩,但如果将此值重写为较⼤的值,就可以越界读写了。
  • 同样,可以重写BackingStore指针,则可以读取和写⼊任意内存地址

JSArrayBuffer结构如下

1
2
3
4
5
6
7
8
9
graph TD
A["Pointer (63 bit) | 1"] --> B[JSArrayBuffer]
B --> C[Map*]
B --> D[Properties*]
B --> E[Elements*]
B --> F[ByteLength]
B --> G[BackingStore*]
B --> H[BitField]
G[BackingStore*] -->I1[BackingStore]

JSTypedArray

JSArrayBuffer只是个buffer,在js的设计⾥,对BackStore的读写需要依赖于TypedArray或者DataView

结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
graph TD
A["Pointer (63 bit) | 1"] -->B1[JSTypedArray]
B1[JSTypedArray]-->B2[Map*]
B1[JSTypedArray]-->B3[Properties*]
B1[JSTypedArray]-->B4[Elements*]
B1[JSTypedArray]-->B5[Buffer*]
B1[JSTypedArray]-->B6[BufferOffset]
B1[JSTypedArray]-->B7[ByteLength]
B1[JSTypedArray]-->B8[Length]
B1[JSTypedArray]-->B9[ExternalPointer*]
B1[JSTypedArray]-->B10[BasePointer*]
B5[Buffer*]--> B[JSArrayBuffer]
B --> C[Map*]
B --> D[Properties*]
B --> E[Elements*]
B --> F[ByteLength]
B --> G[BackingStore*]
B --> H[BitField]
G[BackingStore*] -->I1[BackingStore]

JSTypedArray在漏洞利⽤中的⼀种常⻅⽤途,由于两个不同的TypedArray可以共享同样的ArrayBuffer,所以实际上如果我们⽤⼀个Float64Array去向ArrayBuffer⾥写⼊⼀个double值(8字节),然后⽤Uint32Array读取出来,就可以读出两个4字节的unsigned integer,并拼凑成8字节的unsigned integer,这样就实现了double到整形的转换;同理,可以反过来这样将⼀个整形转换为double

💡由于js对浮点数的精度问题,对于⼤于 2^53-1 的数据,这样转换可能会造成细微的偏差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const buf = new ArrayBuffer(8);
const f64 = new Float64Array(buf);
const u32 = new Uint32Array(buf);
//f64和u32共享同一个ArrayBuffer
// Floating point to 64-bit unsigned integer
function d2u(val) {
f64[0] = val;
let tmp = Array.from(u32);
return tmp[1] * 0x100000000 + tmp[0]; //<<4
}
// 64-bit unsigned integer to Floating point
function u2d(val) {
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
u32.set(tmp);
return f64[0];
}
function hex(i) {
return "0x" + i.toString(16).padStart(8,"0");
}
print(hex(d2u(1.1)));
print(u2d(0x3ff199999999999a));
---->
0x3ff1999999999a00
1.1000000000000227

JSDataView

也是⽤来读写ArrayBuffer的BackingStore的内容的对象,常⽤于最后的任意地址读写原语的构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
graph TD
A["Pointer (63 bit) | 1"] -->B1[JSDataView]
B1-->B2[Map*]
B1-->B3[Properties*]
B1-->B4[Elements*]
B1-->B5[Buffer*]
B1-->B6[BufferOffset]
B1-->B7[ByteLength]
B1-->B10[DataPointer*]
B5[Buffer*]--> B[JSArrayBuffer]
B --> C[Map*]
B --> D[Properties*]
B --> E[Elements*]
B --> F[ByteLength]
B --> G[BackingStore*]
B --> H[BitField]
G[BackingStore*] -->I1[BackingStore]

0x04 V8特性

Hidden Class

从一个最简单的对象开始分析

1
let obj = {a: "foo", b: "bar"};
  • a和b叫命名属性(name properties,后面简称为property),没有任何整数索引
    • 整数索引,数组 ["foo","bar"]有两个整数索引属性:0,值为“foo”,1,值为“bar”。整数索引属性也叫元素element

在JS Object的结构中,element和property存储在两个独⽴的FixedArray中

image-20241115181225550
image-20241115181225550

elements的key就是在Object的属性数组中的对应位置的索引

而property的key通常是字符串,无法简单地通过key来判断某个property在属性数组中的对应位置

💡Tips:在js中,以下两个obj的属性数组不同,即属性结构不同

1
2
let obj1= {a: "foo", b: "bar"};
let obj2= {b: "bar", a: "foo"};

🤔那么怎么解决property位置的问题?V8中使用Hidden Class来描述,每个js Object都有关联的Hidden Class,也就是之前画的HeapObject结构中排第一个的Map*

image-20241115181237410
image-20241115181237410

HiddenClasses的基本假设:具有相同属性结构的对象共享同一个HiddenClass

举个例子🌰

1
2
3
4
5
6
7
8
let o1 = {a: "foo",b:"bar"};
%DebugPrint(o1);
let o2 = {a: "foo1",b:"bar2"};
%DebugPrint(o2);
%SystemBreak();
//output
0x296e53ccdda1 <Object map = 0x14378b9cab89>
0x296e53ccde41 <Object map = 0x14378b9cab89>

可以看到map是相同的

再看一下连续添加属性的过程中map的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
let o1 = {a: "foo"};
%DebugPrint(o1);
let o2 = {a: "foo1"};
%DebugPrint(o2);
o1.b = "bar";
%DebugPrint(o1);
o2.d = "bar2";
%DebugPrint(o2);
o1.c = "baz";
%DebugPrint(o1);
o2.c = "baz2";
%DebugPrint(o2);
%SystemBreak();
//output
0x2d3b7204de11 <Object map = 0x218575f4ab39>
0x2d3b7204de61 <Object map = 0x218575f4ab39>
0x2d3b7204de11 <Object map = 0x218575f4ab89>
0x2d3b7204de61 <Object map = 0x218575f4abd9>
0x2d3b7204de11 <Object map = 0x218575f4ac29>
0x2d3b7204de61 <Object map = 0x218575f4ac79>

pwndbg> job 0x218575f4ab39 //看看HC1
0x218575f4ab39: [Map]
- type: JS_OBJECT_TYPE
- instance size: 32
- inobject properties: 1
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x218575f4aae9 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x2379e4b40609 <Cell value= 1>
- instance descriptors #1: 0x2d3b7204df61 <DescriptorArray[3]>
- layout descriptor: (nil)
- transitions #2: 0x39b2971dfa91 <TransitionArray[6]>Transition array #2: //v8维护的transition tree
#b: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x218575f4ab89 <Map(HOLEY_ELEMENTS)> //属性为a,b的map
#d: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x218575f4abd9 <Map(HOLEY_ELEMENTS)> //属性为a,d的map

- prototype: 0x39b2971c2091 <Object map = 0x218575f40229>
- constructor: 0x39b2971c20c9 <JSFunction Object (sfi = 0x2379e4b457e9)>
- dependent code: 0x1a62eb8002c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

可以用以下流程图(也表示transition tree结构)来表示map(HiddenClass)的变化

1
2
3
4
5
6
7
graph LR
A[HC 0] -->|"Add 'a'"| B[HC 1]
B -->|"Add 'b'"| C[HC 2]
C -->|"Add 'c'"| D[HC 3]
B -->|"Add 'd'"| E[HC 4]
E -->|"Add 'c'"| F[HC 5]

再举一个例子🌰,看看Descriptor,这里o1 o2一开始属性一样,共享HiddenClass,后面加入不同属性,transition tree产生了分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let o1 = {a: "foo"};
%DebugPrint(o1);
let o2 = {a: "foo1"};
%DebugPrint(o2);
o1.b = "bar";
%DebugPrint(o1);
o2.d = "bar2";
%DebugPrint(o2);
%SystemBreak();
//output
0x35d08114ddd1 <Object map = 0x3a1af794ab39>
0x35d08114de21 <Object map = 0x3a1af794ab39>
0x35d08114ddd1 <Object map = 0x3a1af794ab89>
0x35d08114de21 <Object map = 0x3a1af794abd9>
pwndbg> job 0x3a1af794ab89 //看看o1的Hidden Class
0x3a1af794ab89: [Map]
- type: JS_OBJECT_TYPE
- instance size: 32
- inobject properties: 1
- elements kind: HOLEY_ELEMENTS
- unused property fields: 2
- enum length: invalid
- stable_map
- back pointer: 0x3a1af794ab39 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x30dcecd9f9f1 <Cell value= 0>
- instance descriptors (own) #2: 0x35d08114de41 <DescriptorArray[2]> //这里存储Descriptor
- layout descriptor: (nil)
- prototype: 0x30dcecd82091 <Object map = 0x3a1af7940229>
- constructor: 0x30dcecd820c9 <JSFunction Object (sfi = 0x3d338a2857e9)>
- dependent code: 0x0f08ee9c02c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
pwndbg> job 0x35d08114de41 //跟进DescriptorArray
0x35d08114de41: [DescriptorArray]
- map: 0x0f08ee9c0271 <Map>
- enum_cache: empty
- nof slack descriptors: 0
- nof descriptors: 2
- raw marked descriptors: mc epoch 0, marked 0
[0]: #a (const data field 0:h, p: 0, attrs: [WEC]) @ Any //#a a是key,data field后面的0是在原属性数组中的位置索引
[1]: #b (const data field 1:h, p: 1, attrs: [WEC]) @ Any

transition tree中的所有HiddenClass(Map)不会被移除,因为将来遇到相同结构还会复用,举个例子🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let o1 = {a: "foo"};
%DebugPrint(o1);
let o2 = {a: "foo1"};
%DebugPrint(o2);
o1.b = "bar";
%DebugPrint(o1);
o2.d = "bar2";
%DebugPrint(o2);
let o3 = {a: "foo2"};
%DebugPrint(o3);
%SystemBreak();
//output
0x2301f01cddf9 <Object map = 0x3dcc32d4ab39>//*
0x2301f01cde49 <Object map = 0x3dcc32d4ab39>//*
0x2301f01cddf9 <Object map = 0x3dcc32d4ab89>
0x2301f01cde49 <Object map = 0x3dcc32d4abd9>
0x2301f01cdf49 <Object map = 0x3dcc32d4ab39>//map复用

💡属性名称一样但顺序不同的话,map也不同

property

property⼜分为in-object property和普通的property

  • in-object property,直接保存在js object⾥的property

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    let o1 = {}
    for(let i=0;i <4; i++){
    o1["p" + i.toString()] = i;
    }
    %DebugPrint(o1);
    %SystemBreak();
    //output
    0x3794877cdd99 <Object map = 0x3f3bffd4abd9>
    pwndbg> job 0x3794877cdd99
    0x3794877cdd99: [JS_OBJECT_TYPE]
    - map: 0x3f3bffd4abd9 <Map(HOLEY_ELEMENTS)> [FastProperties]
    - prototype: 0x0de3c5002091 <Object map = 0x3f3bffd40229>
    - elements: 0x38e8d9140c71 <FixedArray[0]> [HOLEY_ELEMENTS]
    - properties: 0x38e8d9140c71 <FixedArray[0]> { //4个property直接内嵌在object中,本来用于放property的FixedArray大小为0
    #p0: 0 (const data field 0)
    #p1: 1 (const data field 1)
    #p2: 2 (const data field 2)
    #p3: 3 (const data field 3)
    }
    pwndbg> x/8gx 0x3794877cdd99-1
    0x3794877cdd98: 0x00003f3bffd4abd9 0x000038e8d9140c71
    0x3794877cdda8: 0x000038e8d9140c71 0x0000000000000000 //0
    0x3794877cddb8: 0x0000000100000000 0x0000000200000000 //1 2
    0x3794877cddc8: 0x0000000300000000 0x000038e8d9141f49 //3
  • 普通的property:给object的property超过一定数量后,后面的property就会存到PropertyArray中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    let o1 = {}
    for(let i=0;i <8; i++){
    o1["p" + i.toString()] = i;
    }
    %DebugPrint(o1);
    %SystemBreak();
    //output
    0x1e9b0634dd99 <Object map = 0x1501a910ad19>
    pwndbg> job 0x1e9b0634dd99 //看看Object
    0x1e9b0634dd99: [JS_OBJECT_TYPE]
    - map: 0x1501a910ad19 <Map(HOLEY_ELEMENTS)> [FastProperties]
    - prototype: 0x31170e202091 <Object map = 0x1501a9100229>
    - elements: 0x0a7913900c71 <FixedArray[0]> [HOLEY_ELEMENTS]
    - properties: 0x1e9b0634e331 <PropertyArray[6]> {//前面4个是in-object property,后面就是普通的property
    #p0: 0 (const data field 0)
    #p1: 1 (const data field 1)
    #p2: 2 (const data field 2)
    #p3: 3 (const data field 3)
    #p4: 4 (const data field 4) properties[0]
    #p5: 5 (const data field 5) properties[1]
    #p6: 6 (const data field 6) properties[2]
    #p7: 7 (const data field 7) properties[3]
    }
    pwndbg> job 0x1e9b0634e331 //看看PropertyArray
    0x1e9b0634e331: [PropertyArray]
    - map: 0x0a7913901909 <Map>
    - length: 6
    - hash: 0
    0: 4
    1: 5
    2: 6
    3: 7
    4-5: 0x0a79139004d1 <undefined>

    🤔PropertyArray的空间似乎是按3的倍数来,后来试试一个有12个属性的object,去掉4个in-object property,剩下8个,但是创建了一个大小为9的PropertyArray

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    let o1 = {}
    for(let i=0;i <12; i++){
    o1["p" + i.toString()] = i;
    }
    %DebugPrint(o1);
    %SystemBreak();
    //output
    0x005b0240dd99 <Object map = 0x1c32d7d8ae59>
    pwndbg> job 0x005b0240dd99
    0x5b0240dd99: [JS_OBJECT_TYPE]
    - map: 0x1c32d7d8ae59 <Map(HOLEY_ELEMENTS)> [FastProperties]
    - prototype: 0x330042642091 <Object map = 0x1c32d7d80229>
    - elements: 0x139f6b440c71 <FixedArray[0]> [HOLEY_ELEMENTS]
    - properties: 0x005b0240e5f9 <PropertyArray[9]> {
    #p0: 0 (const data field 0)
    #p1: 1 (const data field 1)
    #p2: 2 (const data field 2)
    #p3: 3 (const data field 3)
    #p4: 4 (const data field 4) properties[0]
    #p5: 5 (const data field 5) properties[1]
    #p6: 6 (const data field 6) properties[2]
    #p7: 7 (const data field 7) properties[3]
    #p8: 8 (const data field 8) properties[4]
    #p9: 9 (const data field 9) properties[5]
    #p10: 10 (const data field 10) properties[6]
    #p11: 11 (const data field 11) properties[7]
    }

fast property&slow property

propert也可以分为fast property和slow property

  • fast property:保存在线性排序的数组⾥的property,前面提到的在PropertyArray中的都是fast property

  • slow property:为了支持频繁地添加/删除属性而不更新HiddenClass与DescriptorArray,v8支持slow property,有slow property的Object有一个自包含的属性字典来存储属性信息

    image-20241115181255404
    image-20241115181255404

举个例子🌰添加100次属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let o1 = {}
for(let i=0;i <100; i++){
o1["p" + i.toString()] = i;
}
%DebugPrint(o1);
%SystemBreak();
//output
0x2d695c28dd99 <Object map = 0x3221bcb463a9>
pwndbg> job 0x2d695c28dd99
0x2d695c28dd99: [JS_OBJECT_TYPE]
- map: 0x3221bcb463a9 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
- prototype: 0x31c8c1a42091 <Object map = 0x3221bcb40229>
- elements: 0x151002c00c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x2d695c290969 <NameDictionary[773]> { //这个时候再看已经变成了NameDictionary
#p67: 67 (data, dict_index: 68, attrs: [WEC])
#p96: 96 (data, dict_index: 97, attrs: [WEC])
#p36: 36 (data, dict_index: 37, attrs: [WEC])
#p98: 98 (data, dict_index: 99, attrs: [WEC])
//......

Map结构

可以看一下src/objects/map.h中的注释

主要关注4个连续的Int

  • 第一个int值主要是一些大小相关,instance_size..
  • 🌟第二个int值比较关键,代表了type,描述map对应的object的类型
  • 第三个int值主要是一些descriptors,比如前面说的properties长度之类的
  • 第四个int值主要是64相关的一些额外信息,不太重要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// All heap objects have a Map that describes their structure.
// A Map contains information about:
// - Size information about the object
// - How to iterate over an object (for garbage collection)
//
// Map layout:
// +---------------+---------------------------------------------+
// | _ Type _ | _ Description _ |
// +---------------+---------------------------------------------+
// | TaggedPointer | map - Always a pointer to the MetaMap root |
// +---------------+---------------------------------------------+
// | Int | The first int field |
// `---+----------+---------------------------------------------+
// | Byte | [instance_size] |
// +----------+---------------------------------------------+
// | Byte | If Map for a primitive type: |
// | | native context index for constructor fn |
// | | If Map for an Object type: |
// | | inobject properties start offset in words |
// +----------+---------------------------------------------+
// | Byte | [used_or_unused_instance_size_in_words] |
// | | For JSObject in fast mode this byte encodes |
// | | the size of the object that includes only |
// | | the used property fields or the slack size |
// | | in properties backing store. |
// +----------+---------------------------------------------+
// | Byte | [visitor_id] |
// +----+----------+---------------------------------------------+
// | Int | The second int field |🌟
// `---+----------+---------------------------------------------+
// | Short | [instance_type] |
// +----------+---------------------------------------------+
// | Byte | [bit_field] |
// | | - has_non_instance_prototype (bit 0) |
// | | - is_callable (bit 1) |
// | | - has_named_interceptor (bit 2) |
// | | - has_indexed_interceptor (bit 3) |
// | | - is_undetectable (bit 4) |
// | | - is_access_check_needed (bit 5) |
// | | - is_constructor (bit 6) |
// | | - has_prototype_slot (bit 7) |
// +----------+---------------------------------------------+
// | Byte | [bit_field2] |
// | | - is_extensible (bit 0) |
// | | - is_prototype_map (bit 1) |
// | | - is_in_retained_map_list (bit 2) |
// | | - elements_kind (bits 3..7) |
// +----+----------+---------------------------------------------+
// | Int | [bit_field3] |
// | | - enum_length (bit 0..9) |
// | | - number_of_own_descriptors (bit 10..19) |
// | | - is_dictionary_map (bit 20) |
// | | - owns_descriptors (bit 21) |
// | | - has_hidden_prototype (bit 22) |
// | | - is_deprecated (bit 23) |
// | | - is_unstable (bit 24) |
// | | - is_migration_target (bit 25) |
// | | - is_immutable_proto (bit 26) |
// | | - new_target_is_base (bit 27) |
// | | - may_have_interesting_symbols (bit 28) |
// | | - construction_counter (bit 29..31) |
// | | |
// +*************************************************************+
// | Int | On systems with 64bit pointer types, there |
// | | is an unused 32bits after bit_field3 |
// +*************************************************************+
// | TaggedPointer | [prototype] |
// +---------------+---------------------------------------------+
// | TaggedPointer | [constructor_or_backpointer] |
// +---------------+---------------------------------------------+
// | TaggedPointer | If Map is a prototype map: |
// | | [prototype_info] |
// | | Else: |
// | | [raw_transitions] |
// +---------------+---------------------------------------------+
// | TaggedPointer | [instance_descriptors] |
// +*************************************************************+
// ! TaggedPointer ! [layout_descriptors] !
// ! ! Field is only present if compile-time flag !
// ! ! FLAG_unbox_double_fields is enabled !
// ! ! (basically on 64 bit architectures) !
// +*************************************************************+
// | TaggedPointer | [dependent_code] |
// +---------------+---------------------------------------------+

  • 标题: V8-pwn入门(1)——对象模型、特性
  • 作者: Sl0th
  • 创建于 : 2024-11-12 21:21:50
  • 更新于 : 2024-11-21 16:01:10
  • 链接: http://sl0th.top/2024/11/12/V8-pwn入门-1-——对象模型、特性/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论