0x00 前言💬 下面说的都是chrome80以前的v8,无sandbox、指针压缩
image-20241115181353869
0x01 基础🪨 JS Engine CTF的常见模式
由出题⼈⾃⼰编写的patch引⼊的漏洞
历史漏洞的CVE被出题⼈重新引⼊
攻略方法
创建⼀个⽤于调试的js环境,如果有出题⼈提供的引⼊漏洞的patch,那么打上这个patch
分析这个patch并判断这个patch引⼊的是js引擎的哪个阶段
a. Runtime(CSA or Torque), TurboFan, Ignition, AST, InlineCache, …
构造poc去触发漏洞
利⽤漏洞去构造任意地址读写/任意对象地址的泄露/伪造任意对象等原语
getshell,常⽤的⽅法是利⽤v8⾥的rwx地址区域(wasm中就有)直接写⼊shellcode代码来实现任意代码执⾏
0x02 V8对象模型⚙️ objects.h v8源码中的objects.h里写了对象模型的结构,可以看出最大的是Object,Object总体上分为两类Smi
和HeapObject
凡是分配在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的指针
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 : inline double value () { return READ_DOUBLE_FIELD (this , kValueOffset); } inline void set_value (double value) { WRITE_DOUBLE_FIELD (this , kValueOffset, value); } static inline HeapNumber* cast (Object* obj) ; Object* HeapNumberToBoolean () ; static const int kValueOffset = HeapObject::kSize; 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 (); 0x02aaab84dda9 <JSArray [3 ]>
gdb看一下这块内存(-1是因为tagged value),这里可能是版本问题,这个版本的v8把array又做了一层封装,封装到FixedArray中
1 2 3 pwndbg> x/4 gx 0x02aaab84dda9 -1 0x2aaab84dda8 : 0x00000a0c4dc82f79 (Map*) 0x00001179f91c0c71 (FixedArray)0x2aaab84ddb8 : 0x000002aaab84dd09 (指向真实array的pointer) 0x0000000300000000 (表示长度)
继续访问真实的数组
1 2 3 4 5 6 7 pwndbg> x/8 gx 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/2 gx 0x0000064aeec1f311 -1 0x64aeec1f310 : 0x00001179f91c0561 0x41ebd5b7dde00000 (0xdeadbeef 的double 表示)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 (); 0x20c46b60dda1 <JSArray [3 ]>
查看这块内存,跟进真正的array
1 2 3 4 5 6 7 8 9 10 11 12 13 pwndbg> x/4 gx 0x20c46b60dda1 -1 0x20c46b60dda0 : 0x00001f8f0c182f79 0x000028e799a40c71 0x20c46b60ddb0 : 0x000020c46b60dd01 0x0000000300000000 pwndbg> x/6 gx 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/10 gx 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的类型
1 2 3 4 5 6 7 8 9 const array = [1 , 2 , 3 ];array.push (4.56 ); array.push ('x' ); array.length ; array[9 ] = 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 27 28 29 30 31 32 33 let arr = [1 , 1.1 ];%DebugPrint (arr); %SystemBreak (); 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 (); 0x17dcd1e4ddf9 <JSArray[3 ]>pwndbg> x/8 gx 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/2 gx 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 中 TypedArray
、ArrayBuffer
这样的二进制数据。
不受 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 (); 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 - byte_length: 32 - detachable - properties: 0x15b6957c0c71 <FixedArray[0 ]> {} - embedder fields = { 0 , aligned pointer: (nil) 0 , aligned pointer: (nil) } pwndbg> x/20 gx 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/20 gx 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);function d2u (val ) { f64[0 ] = val; let tmp = Array .from (u32); return tmp[1 ] * 0x100000000 + tmp[0 ]; } 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
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
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 (); 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 (); 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 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 : #b : (transition to (const data field, attrs : [WEC ]) @ Any ) -> 0x218575f4ab89 <Map (HOLEY_ELEMENTS )> #d : (transition to (const data field, attrs : [WEC ]) @ Any ) -> 0x218575f4abd9 <Map (HOLEY_ELEMENTS )> - 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 (); 0x35d08114ddd1 <Object map = 0x3a1af794ab39 >0x35d08114de21 <Object map = 0x3a1af794ab39 >0x35d08114ddd1 <Object map = 0x3a1af794ab89 >0x35d08114de21 <Object map = 0x3a1af794abd9 >pwndbg> job 0x3a1af794ab89 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 ]> - 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 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 [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 (); 0x2301f01cddf9 <Object map = 0x3dcc32d4ab39 >0x2301f01cde49 <Object map = 0x3dcc32d4ab39 >0x2301f01cddf9 <Object map = 0x3dcc32d4ab89 >0x2301f01cde49 <Object map = 0x3dcc32d4abd9 >0x2301f01cdf49 <Object map = 0x3dcc32d4ab39 >
💡属性名称一样但顺序不同的话,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 (); 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 ]> { #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 0x3794877cddb8 : 0x0000000100000000 0x0000000200000000 0x3794877cddc8 : 0x0000000300000000 0x000038e8d9141f49
普通的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 (); 0x1e9b0634dd99 <Object map = 0x1501a910ad19 >pwndbg> job 0x1e9b0634dd99 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 ]> { #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 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 (); 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
举个例子🌰添加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 (); 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 ]> { #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