#Chrome V8 Pwn 学习(二)#前置知识#chrome 查看快照首先打开浏览器开发者工具,在控制台中运行一段 js 代码:

然后在内存选项卡查看快照 
#v8 JsObject 简介这里我们要先区分一个概念,即 JavaScript 语言层面和 V8 引擎内部实现层面上对于数据结构的理解是不一样的。
我们先说V8 引擎内部实现层面,在这个层面上,V8 引擎将所有类型大体划分为 smi 和 HeapObject,这个划分的方式称作 Tagged Value 技术,它利用了最低位来区别 Smi 和对象指针,当最低位为 0 时,表明这是一个 Smi;当最低位为 1 时,表明这是一个对象指针。
下面介绍一下 v8 对象(JsObject)的结构。
#v8 对象结构简介首先有一张很经典的图:
v8 的对象由三个部分组成:
总结:
测试代码如下:
function Foo1 () {};
var a = new Foo1();
var b = new Foo1();
a.name = 'aaa';
a.text = 'aaa';
b.name = 'bbb';
b.text = 'bbb';
a[1] = 'aaa';
a[2] = 'aaa';
%DebugPrint(a);
%DebugPrint(b);
%SystemBreak();
function Foo1 () {};
var a = new Foo1();
var b = new Foo1();
a.name = 'aaa';
a.text = 'aaa';
b.name = 'bbb';
b.text = 'bbb';
a[1] = 'aaa';
a[2] = 'aaa';
%DebugPrint(a);
%DebugPrint(b);
%SystemBreak();
调试信息如下:
0x154024d8def9 // 这是 a 的
0x154024d8df61 // 这是 b 的
pwndbg> job 0x154024d8def9
0x154024d8def9: [JS_OBJECT_TYPE]
- map: 0x0ac0e3a0ac79 0x154024d8def9 // 这是 a 的
0x154024d8df61 // 这是 b 的
pwndbg> job 0x154024d8def9
0x154024d8def9: [JS_OBJECT_TYPE]
- map: 0x0ac0e3a0ac79 可以发现他们的 Map(隐藏类)是相同的(尽管 a 比 b 多了很多可索引属性),然后这两种属性也是分开储存的,符合上面的结论。
#命名属性的不同存储方式V8 中命名属性有三种的不同存储方式:对象内属性(in-object)、快属性(fast)和慢属性(slow)。

对象内属性和快属性的结构基本相同,对象内属性因为对象存储空间的限制,所以在超过10个属性之后多余的部分就会进入property(命名属性)中。
而当使用慢属性时,可以发现 property 中的索引变得无序,说明这个对象已经采用了 hash 存取结构了。
至于为什么要采用三种方式进行存储,无非是为了让v8更快一些。
例子如下:
//三种不同类型的 Property 存储模式
function Foo2() {}
var a = new Foo2()
var b = new Foo2()
var c = new Foo2()
for (var i = 0; i < 10; i ++) {
a[new Array(i+2).join('a')] = 'aaa'
}
for (var i = 0; i < 12; i ++) {
b[new Array(i+2).join('b')] = 'bbb'
}
for (var i = 0; i < 30; i ++) {
c[new Array(i+2).join('c')] = 'ccc'
}
//三种不同类型的 Property 存储模式
function Foo2() {}
var a = new Foo2()
var b = new Foo2()
var c = new Foo2()
for (var i = 0; i < 10; i ++) {
a[new Array(i+2).join('a')] = 'aaa'
}
for (var i = 0; i < 12; i ++) {
b[new Array(i+2).join('b')] = 'bbb'
}
for (var i = 0; i < 30; i ++) {
c[new Array(i+2).join('c')] = 'ccc'
}
自行在浏览器中使用快照技术查看内存即可。
#隐藏类在 V8 的 Memory 检查器中,隐藏类被称为 Map。隐藏类的目的只有两个,运行更快和占内存空间更小。我们这里从节省内存空间讨论。
#隐藏类的概念在 ECMAScript 中,对象属性的 Attribute 被描述为以下结构。
隐藏类的引入,将属性的 Value 与其它 Attributes(也就是 Writable、Get 等)分开。一般情况下,对象的 Value 是经常会发生变动的,而 Attribute 是几乎不怎么会变的。没有没有必要重复Attribute的剩余部分。
#隐藏类的创建对象创建过程中,每添加一个命名属性,都会对应一个生成一个新的隐藏类。在 V8 的底层实现了一个将隐藏类连接起来的转换树,如果以相同的顺序添加相同的属性,转换树会保证最后得到相同的隐藏类。
下面的例子中,a 在空对象时、添加 name属性后、添加 text属性后会分别对应不同的隐藏类。
function Foo3 (){};
let a = new Foo3();
a.name = 'migraine1'
a.text = 'migraine2'
function Foo3 (){};
let a = new Foo3();
a.name = 'migraine1'
a.text = 'migraine2'
生成概念图:
可以理解为,按照代码顺序一步一步添加命名属性,中间每添加一个属性都对应一个不同的 map,同时 map 中会维护一个叫 back_pointer 的指针,实现一个单链表结构。
#隐藏类的结构示例代码:
function Foo1 () {};
var a = new Foo1();
var b = new Foo1();
a.name = 'aaa';
a.text = 'aaa';
b.name = 'bbb';
b.text = 'bbb';
a[1] = 'aaa';
a[2] = 'aaa';
%DebugPrint(a);
%DebugPrint(b);
%SystemBreak();
function Foo1 () {};
var a = new Foo1();
var b = new Foo1();
a.name = 'aaa';
a.text = 'aaa';
b.name = 'bbb';
b.text = 'bbb';
a[1] = 'aaa';
a[2] = 'aaa';
%DebugPrint(a);
%DebugPrint(b);
%SystemBreak();
pwndbg> job 0x0ac0e3a0ac79
0xac0e3a0ac79: [Map]
- type: JS_OBJECT_TYPE // 实例类型
- instance size: 104 // 实例大小
- inobject properties: 10 // 对象内属性存储空间
- elements kind: HOLEY_ELEMENTS
- unused property fields: 8 // 未使用slot数
- enum length: invalid
- stable_map // 处于快属性模式 (dictionary_map:慢属性/字典模式)
- back pointer: 0x0ac0e3a0ac29 // 维护一个单链表
- prototype_validity cell: 0x1e9491b1f881
// 标识对象实例的属性名与其值的存取位置(a 和 b 的描述符相同,结构也相同)
- instance descriptors (own) #2: 0x154024d8e031
- layout descriptor: (nil)
- prototype: 0x154024d8ddf9
- constructor: 0x1e9491b1f6a9
- dependent code: 0x14ae053c02c1
- construction counter: 5
pwndbg> job 0x154024d8e031
0x154024d8e031: [DescriptorArray]
- map: 0x14ae053c0271
- enum_cache: empty
- nof slack descriptors: 0
- nof descriptors: 2
- raw marked descriptors: mc epoch 0, marked 0
[0]: #name (const data field 0:h, p: 1, attrs: [WEC]) @ Any
[1]: #text (const data field 1:h, p: 0, attrs: [WEC]) @ Any
| pwndbg> job 0x0ac0e3a0ac79
0xac0e3a0ac79: [Map]
- type: JS_OBJECT_TYPE // 实例类型
- instance size: 104 // 实例大小
- inobject properties: 10 // 对象内属性存储空间
- elements kind: HOLEY_ELEMENTS
- unused property fields: 8 // 未使用slot数
- enum length: invalid
- stable_map // 处于快属性模式 (dictionary_map:慢属性/字典模式)
- back pointer: 0x0ac0e3a0ac29 // 维护一个单链表
- prototype_validity cell: 0x1e9491b1f881
// 标识对象实例的属性名与其值的存取位置(a 和 b 的描述符相同,结构也相同)
- instance descriptors (own) #2: 0x154024d8e031
- layout descriptor: (nil)
- prototype: 0x154024d8ddf9
- constructor: 0x1e9491b1f6a9
- dependent code: 0x14ae053c02c1
- construction counter: 5
pwndbg> job 0x154024d8e031
0x154024d8e031: [DescriptorArray]
- map: 0x14ae053c0271
- enum_cache: empty
- nof slack descriptors: 0
- nof descriptors: 2
- raw marked descriptors: mc epoch 0, marked 0
[0]: #name (const data field 0:h, p: 1, attrs: [WEC]) @ Any
[1]: #text (const data field 1:h, p: 0, attrs: [WEC]) @ Any
| 参考注释可以看的很清楚了,通过访问 backpointer 里的值可以看到在添加 text 属性前 a 的 map 结构:
pwndbg> job 0x0ac0e3a0ac29
0xac0e3a0ac29: [Map]
- type: JS_OBJECT_TYPE
- instance size: 104
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 9
- enum length: invalid
- back pointer: 0x0ac0e3a0aae9
- prototype_validity cell: 0x1e9491b1f881
- instance descriptors #1: 0x154024d8e031 // 这时 map 对应的命名属性只存了一个 `name`,所以只有 1
- layout descriptor: (nil)
- transitions #1: 0x0ac0e3a0ac79
#text: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x0ac0e3a0ac79
- prototype: 0x154024d8ddf9
- constructor: 0x1e9491b1f6a9
- dependent code: 0x14ae053c02c1
- construction counter: 5
| pwndbg> job 0x0ac0e3a0ac29
0xac0e3a0ac29: [Map]
- type: JS_OBJECT_TYPE
- instance size: 104
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 9
- enum length: invalid
- back pointer: 0x0ac0e3a0aae9
- prototype_validity cell: 0x1e9491b1f881
- instance descriptors #1: 0x154024d8e031 // 这时 map 对应的命名属性只存了一个 `name`,所以只有 1
- layout descriptor: (nil)
- transitions #1: 0x0ac0e3a0ac79
#text: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x0ac0e3a0ac79
- prototype: 0x154024d8ddf9
- constructor: 0x1e9491b1f6a9
- dependent code: 0x14ae053c02c1
- construction counter: 5
| 再往前也可以继续看到,不再赘述。
#V8 内存模型首先贴一个继承关系图,这一节主要介绍的内存模型有以下这些,我们从 Smi 开始依次往下介绍。
+--------------------------------+
| V8 Memory Layout |
+--------------------------------+
| Object |
| ├─ Smi |
| └─ HeapObject |
| ├─ HeapNumber |
| ├─ PropertyCell |
| └─ JSReceiver ─────┐ |
| └─ JSObject ────┤ |
| ├─ JSFunction |
| ├─ JSArray |
| └─ JSArrayBuffer |
+--------------------------------+
+--------------------------------+
| V8 Memory Layout |
+--------------------------------+
| Object |
| ├─ Smi |
| └─ HeapObject |
| ├─ HeapNumber |
| ├─ PropertyCell |
| └─ JSReceiver ─────┐ |
| └─ JSObject ────┤ |
| ├─ JSFunction |
| ├─ JSArray |
| └─ JSArrayBuffer |
+--------------------------------+
#SmiSmi 是 Small Integer 的缩写,也就是专门用来表示小整数值。
基于 tagged value 技术,通过最低位是 0 或者 1 来区分是立即数还是一个内存指针。
其内存结构如下图:
Smi on 32Bits
+----------------------------+-----+
| | |
| Signed Value(31Bits) | 0 |
| | |
+----------------------------+-----+
Smi on 64Bits
+-------------------------------+-----------------------+-----+
| | | |
| Signed Value(32Bits) | 0-Padding(31Bits) | 0 |
| | | |
+-------------------------------+-----------------------+-----+
Smi on 32Bits
+----------------------------+-----+
| | |
| Signed Value(31Bits) | 0 |
| | |
+----------------------------+-----+
Smi on 64Bits
+-------------------------------+-----------------------+-----+
| | | |
| Signed Value(32Bits) | 0-Padding(31Bits) | 0 |
| | | |
+-------------------------------+-----------------------+-----+
#Heap Object同样基于 Tagged Value 技术,通过将一个内存值最低位置为 1 来表示这是一个内存指针。
这个就不画图了,只需要记得不管是 32 位还是 64 位情况下,最低位都是 1。
#HeapNumber继承自 Object->HeapObject,对象的数值范围为 double,一般是用来表示无法在 Smi 范围内表示的整数值。
它的内存结构如下图:
+------------------+---+
| Object Pointer | 1 +----+
+------------------+---+ |
|
+-----------+
|
| +-----------+-----------+
+------>| (Map*) | (Value) |
+-----------+-----------+
^ ^
| |
KMapOffset KValueOffset
=0 =8
+------------------+---+
| Object Pointer | 1 +----+
+------------------+---+ |
|
+-----------+
|
| +-----------+-----------+
+------>| (Map*) | (Value) |
+-----------+-----------+
^ ^
| |
KMapOffset KValueOffset
=0 =8
这里的 Object Pointer 的意思是,当我们查询一个 HeapNumber 的内存时,会先得到一个指向其具体内存结构的指针(也就是这个 Object Pointer),而其具体的内存结构则是在 offset=0 处存放一个指向 map 的指针,而在 offset=8 处存放一个 IEEE754 编码的 double 型 value。
也就是说,实际上在源码中,V8 的诸如 HeapNumber 这类 class 基本没有成员变量,它们都是通过偏移量独立表示的。为了方便画图,将它画成下面这个样子:
+------------------+---+
| Object Pointer | 1 +----+
+------------------+---+ |
|
+-----------+
|
| +--------------+--------------+
+------>| KMapOffset* | KValueOffset|
+--------------+--------------+
+------------------+---+
| Object Pointer | 1 +----+
+------------------+---+ |
|
+-----------+
|
| +--------------+--------------+
+------>| KMapOffset* | KValueOffset|
+--------------+--------------+
后面我们都会用这种方式画图。
#JsObject为了完整性,这里简单回顾一下 JsObject,具体细节可以看上一大节。
在V8中,JsObject 内存结构如下所示:
[ hiddenClass / map ] -> ... ; 指向Map
[ properties ] -> [empty array]
[ elements ] -> [empty array]
[ reserved #1 ] -\
[ reserved #2 ] |
[ reserved #3 ] }- in object properties,即预分配的内存空间
............... |
[ reserved #N ] -/
[ hiddenClass / map ] -> ... ; 指向Map
[ properties ] -> [empty array]
[ elements ] -> [empty array]
[ reserved #1 ] -\
[ reserved #2 ] |
[ reserved #3 ] }- in object properties,即预分配的内存空间
............... |
[ reserved #N ] -/
Map 中存储了一个对象的元信息,包括对象上属性的个数,对象的大小以及指向构造函数和原型的指针等等。同时,Map中保存了Js对象的属性信息,也就是各个属性在对象中存储的偏移。然后属性的值将根据不同的类型,放在 properties、element 以及预留空间中。properties 指针,用于保存通过属性名作为索引的元素值,类似于字典类型elements 指针,用于保存通过整数值作为索引的元素值,类似于常规数组reserved #n,为了提高访问速度,V8在对象中预分配了的一段内存区域,用来存放 in-object 属性,当向 object 中添加属性时,会先尝试将新属性放入这些预留的槽位。当 in-onject 槽位满后,V8才会尝试将新的属性放入 properties 中。#ArrayBuffer && TypedArray简单的说,ArrayBuffer就代表一段原始的二进制数据,而TypedArray代表了一个确定的数据类型,当TypedArray与ArrayBuffer关联,就可以通过特定的数据类型格式来访问内存空间。
这在我们的利用中十分重要,因为这意味着我们可以在一定程度上像C语言一样直接操作内存。
内存结构如图:

在 ArrayBuffer 中存在一个 BackingStore 指针,这个指针指向的就是 ArrayBuffer 开辟的内存空间,可以使用 TypedArray 指定的类型读取和写入该区域,并且,这片内存区域是位于系统堆中的而不是属于GC管理的区域。
测试用例:
arr = new ArrayBuffer(0x20);
u32 = new Uint32Array(arr);
u32[0] = 0x1234;
u32[1] = 0x5678;
%DebugPrint(u32);
%SystemBreak();
readline();
arr = new ArrayBuffer(0x20);
u32 = new Uint32Array(arr);
u32[0] = 0x1234;
u32[1] = 0x5678;
%DebugPrint(u32);
%SystemBreak();
readline();

可以清楚的看到上面的结构和结论。
常见利用有:
#JsFunction内存结构如图:

其中,CodeEntry 是一个指向 JIT 代码的指针(RWX区域),如果具有任意写能力,那么可以向JIT代码处写入自己的 shellcode,实现任意代码执行。
但是,在 v8 6.7 版本之后,function 的 code 不再可写,所以不能够直接修改 jit 代码了。
另外,我自己测试的时候不知道是不是版本原因,这里实际上是 kLiteralsOffset 指向函数区域。
测试代码:
function func() {
let sum = 0;
for (let i = 0; i < 100; ++i)
sum += i;
return sum;
}
for (let i = 0; i < 100; ++i) {
func();
}
%DebugPrint(func);
readline();
function func() {
let sum = 0;
for (let i = 0; i < 100; ++i)
sum += i;
return sum;
}
for (let i = 0; i < 100; ++i) {
func();
}
%DebugPrint(func);
readline();
调试结果如下图所示:

#参考文章