v8 Base

学习v8过程中参考了很多资料以及师傅们的博客,在这里把自己实验过的/或者需要速查的内容整理记录下来,里面引用各位师傅的博客的内容都注明了出处,特此致谢。

V8环境配置

终端代理

编译v8必须科学上网,本地使用crashx作为代理。

终端挂代理,将下面的代码写入/etc/bashrc,每次生成终端都会自动进行代理。

1
2
3
export https_proxy=http://127.0.0.1:7890
export http_proxy=http://127.0.0.1:7890
export all_proxy=socks5://127.0.0.1:7891

或者参考自动化命令

1
2
3
4
alias setproxy="export https_proxy=http://127.0.0.1:7890;export http_proxy=http://127.0.0.1:7890;export all_proxy=socks5://127.0.0.1:7891;echo \"Set proxy successfully\" "
alias unsetproxy="unset http_proxy;unset https_proxy;unset all_proxy;echo \"Unset proxy successfully\" "
alias ipcn="curl myip.ipip.net"
alias ip="curl ip.sb"

curl cip.cc检验代理(crash x开全局)

wDfUNM

编译v8

参考链接

安装依赖

1
apt-get install binutils python2.7 perl socat git build-essential gdb gdbserver

1. 准备depot_tools工具

1
2
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc #depot_tools的目录

2. ninja准备

1
2
3
$ 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编译

注意要使用depot_tools下的ninja而不是/usr/bin下的ninja,使用v8gen脚本快速构建toolchain

1
2
3
$ fetch v8 && cd v8&& gclient sync #获取v8源代码同步最新源代码,如果直接clone了fetch命令可以不用
$ tools/dev/v8gen.py x64.debug
$ ninja -C out.gn/x64.debug #如果只编译d8的话,最后加上d8参数即可

4. 运行d8

1
2
$ ./out.gn/x64.debug/d8
$ ./out.gn/x64.debug/v8_shell

v8漏洞利用在browser pwn中一般有两种形式,都需要对v8进行patch然后再进行编译。

  • 直接使用真实的cve,一般提供commit的hash值。

    在编译v8以前,需要使用git reset --hard hash值来对版本进行回溯–>roll a d8

  • 出题人给出一个patch,人为地在v8制造一个漏洞,一般提供一个diff文件/编译好的v8可执行文件–>oob-v8

    需要git apply < diff文件将diff文件加入v8源码分支。

范例题目

例如Star CTF 2019 oob-v8,题目信息,我们需要使用对应版本的v8,然后打上我们的patch。

1
2
3
4
Yet another off by one

$ nc 212.64.104.189 10000
the v8 commits is 6dc88c191f5ecc5389dc26efa3ca0907faef3598.

下载v8源代码并且回溯版本,不过因为编译失败,后来还是选择用fetch。

1
2
3
4
$ git clone https://github.com/v8/v8.git && cd v8 #这一步改为fetch v8
$ git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
Checking out files: 100% (6085/6085), done.
HEAD is now at 6dc88c1 Update V8 DEPS.

将diff补丁写入代码,然后编译即可。

1
$ git apply < oob.diff

diff补丁

oob.diff

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
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- 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();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:

编译v8的问题汇总

chromium-cipd : golang net/http proxy代理

gclient sync的时候提示代理连接失败

1
[P5912 11:57:20.290 client.go:347 W] RPC failed transiently. Will retry in 1s    {"error":"failed to send request: Post https://chrome-infra-packages.appspot.com/prpc/cipd.Repository/GetInstanceURL: proxyconnect tcp: EOF", "host":"chrome-infra-packages.appspot.com", "method":"GetInstanceURL", "service":"cipd.Repository", "sleepTime":"1s"}

我是通过仅代理http(不使用https和sock5)来实现的(192.168.1.106是我mac主机的地址)我在Ubuntu下执行这个命令

export https_proxy=http://192.168.1.106:7890

提示Error: client not configured; see ‘gclient config’

在v8目录下创建一个.gclient文件,然后执行gclient sync

1
2
3
4
5
6
7
8
9
10
solutions = [
{
"managed": False,
"name": "src",
"url": "https://chromium.googlesource.com/chromium/src.git",
"custom_deps": {},
"deps_file": ".DEPS.git",
"safesync_url": "",
},
]

v8调试基础

  • Google提供的gdb插件 gdb-v8-support.py

    可以在/tools目录下找到.gdbinit和gdb-v8-support.py文件,然后在~/.gdbinit文件中source引入py文件,同时将.gdbinit的内容拷贝到~/.gdbinit中

    其中定义了一些方法比如job,可以在gdb中作为辅助使用

wClJ9b

  • Allow-natives-syntax参数

    在运行v8时附加--allow-natives-syntax参数,可以通过该选项调用一些有助于调试到本地运行时函数。

    • %DebugPrint(obj) 输出对象地址
    • %SystemBreak()插入调试中断,重新调试时还会断在此处。
    • Print在遇到CodeStubAssembler的代码时,可以用于输出变量值。
  • readline()添加在js代码中,让程序停下等待输入,方便gdb调试。

  • Polyfill 包含很多js本身实现的js函数,实现逻辑和v8很类似,但是比C++阅读难度低很多。

v8 DEBUG

V8 Object入门

这部分流程基本按照THORN大神的文章思路走下来,增加了从内存角度看的内容,很大一部分大神写的太好了就直接引用了,在这里做声明

V8是怎么跑起来的

在Chrome中查看快照

  • 在console中运行一段js代码

    1
    2
    3
    function Fool(){}
    var a=new Fool(); //构造函数主要是为了更方便地在快照中找到对象
    a.name='aaa'
  • 在Memory中,设置Profiles为Heap snapshot(堆快照),点击左边的小圆点。

    iSwsfJ

v8对象结构

  • v8的对象主要由三个指针构成

    • Hidden Class(隐藏类)
    • Property
    • Element

    隐藏类用于描述对象的结构,Property和Element用于存放对象的属性,两者的区别主要体现在键名能否被索引。仔细的读者应该发现,在对象中还包含着名为__proto__的属性,这是JS对象中的原型链,与JS中NEW的用途有关。

    8B7ERf

Property和Elememt

1
2
3
4
5
// 可索引属性会被存储到 Elements 指针指向的区域
{ 1: "a", 2: "b" }

// 命名属性会被存储到 Properties 指针指向的区域
{ "first": 1, "second": 2 }

参照ECMA规范中的要求,可索引属性需要按照索引值大小升序排列,而命令属性根据创建的顺序升序排列。

让我们运行以下代码。

1
2
var a = { 1: "a", 2: "b", "first": 1, 3: "c", "second": 2 }
var b = { "second": 2, 1: "a", 3: "c", 2: "b", "first": 1 }

j5oJ6n

对象a以可索引属性开头,存储时按照可索引对象升序排列,命名属性first比second要早创建,所以first排列在secnod前。对象b以命名属性开头,存储时与可索引部分与a相同,命名属性按照创建先后排列,second在first之前。而可索引属性和命名属性都很明显是分开的,也验证了两者时分开存储的理论。

结论

  • 索引的属性按照升序排列,命名属性根据创建的顺序升序排列
  • 同时使用两种属性时,两个属性分开存储

让我们来实际论证一下这个理论,通过之前的Heap快照来查看内存中的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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'

//v8调试用
//%DebugPrint(a)
//%DebugPrint(b)
//%SystemBreak()

GTtP0c

a和b都包含命名属性name和text,a还拥有两个可索引属性。

a和b两个对象指向的隐藏类是同一个(图中的Map @111025),即说明a和b的对象具有相同的结构。

思考一下,为什么a和b的属性并不相同,但是两者却拥有相同的结构?THORN大神的文章中解释,可索引属性本身就是有序排列,并不需要通过结构去查找,所以也就不会和隐藏类有什么关系了。两者拥有相同隐藏类的原因还是取决于包含相同的命名属性name和text。

大神还给出另外一个案例

1
a[1111]='aaa'

r3FqGA

在a对象增加了一个可索引属性[1111]。a对象和b对象,两者的隐藏类发生了变化。可索引数组Elememet中的数组也忽然没有了规律。因为写入了a[1111]之后,数组变成了稀疏数组,而为了节省空间,稀疏数组会转换为哈希存储的方式,而不是用一个完整的数组来描述这块空间。有趣的事,上文隐藏类似乎与可索引属性并没有什么关系,但是在这里,Elements从数组变成了稀疏数组,Property并没有变化,但是隐藏类却发生了变化。

大神的分析(咱也不懂,咱也不敢问)

可能是为了描述 Element 的结构发生改变

让我们从调试角度再来回顾一下上文的内容

通过%DebugPrint(a)来查看对象a在内存中的分布,也可以通过job 0x2c671120f451来查看这个结构。

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
d8> %DebugPrint(a)
DebugPrint: 0x2c671120f451: [JS_OBJECT_TYPE]
- map: 0x3ef5ef90ce91 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2c671120f349 <Object map = 0x3ef5ef90cee9>
- elements: 0x2c671120fae9 <FixedArray[19]> [HOLEY_ELEMENTS]
- properties: 0x2296bb502251 <FixedArray[0]> {
#name: 0x200db42b661 <String[3]: aaa> (data field 0)
#text: 0x200db42b661 <String[3]: aaa> (data field 1)
}
- elements: 0x2c671120fae9 <FixedArray[19]> {
0: 0x2296bb502321 <the_hole>
1: 0x200db42b661 <String[3]: aaa>
2-18: 0x2296bb502321 <the_hole>
}
0x3ef5ef90ce91: [Map]
- type: JS_OBJECT_TYPE
- instance size: 104
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 8
- enum length: invalid
- stable_map
- back pointer: 0x3ef5ef90ce39 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x2296bb502629 <Cell value= 1>
- instance descriptors (own) #2: 0x2c671120f831 <DescriptorArray[8]>
- layout descriptor: (nil)
- prototype: 0x2c671120f349 <Object map = 0x3ef5ef90cee9>
- constructor: 0x200db427329 <JSFunction Foo1 (sfi = 0x200db427149)>
- dependent code: 0x2296bb502251 <FixedArray[0]>
- construction counter: 5

{1: "aaa", name: "aaa", text: "aaa"}
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
d8> %DebugPrint(b)
DebugPrint: 0x2c671120f531: [JS_OBJECT_TYPE]
- map: 0x3ef5ef90ce91 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2c671120f349 <Object map = 0x3ef5ef90cee9>
- elements: 0x2296bb502251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x2296bb502251 <FixedArray[0]> {
#name: 0x200db42bd11 <String[3]: bbb> (data field 0)
#text: 0x200db42bd11 <String[3]: bbb> (data field 1)
}
0x3ef5ef90ce91: [Map]
- type: JS_OBJECT_TYPE
- instance size: 104
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 8
- enum length: invalid
- stable_map
- back pointer: 0x3ef5ef90ce39 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x2296bb502629 <Cell value= 1>
- instance descriptors (own) #2: 0x2c671120f831 <DescriptorArray[8]>
- layout descriptor: (nil)
- prototype: 0x2c671120f349 <Object map = 0x3ef5ef90cee9>
- constructor: 0x200db427329 <JSFunction Foo1 (sfi = 0x200db427149)>
- dependent code: 0x2296bb502251 <FixedArray[0]>
- construction counter: 5

{name: "bbb", text: "bbb"}

通过telescope命令(可见显示指针关系),比较接近在Chrome中的观感。需要注意的是v8为了区别对象和值类型的区别,在对象的最低位设置为1,所以使用命令时需要-1才能获得正确的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> telescope 0x2c671120f451-1
00:00000x2c671120f450 —▸ 0x3ef5ef90ce91 ◂— 0xd00002c8ca6a022 //map: 0x3ef5ef90ce91
01:00080x2c671120f458 —▸ 0x2296bb502251 ◂— 0x2c8ca6a023 //properties: 0x2296bb502251 <FixedArray[0]>
02:00100x2c671120f460 —▸ 0x2c671120fae9 ◂— 0x2c8ca6a023 //elements: 0x2c671120fae9 <FixedArray[19]>
03:00180x2c671120f468 —▸ 0x200db42b661 ◂— 0xbe00002c8ca6a024 //properties[name/text](共用): 0x200db42b661
... ↓
05:00280x2c671120f478 —▸ 0x2c8ca6a023b9 ◂— 0x100002c8ca6a022 //slot
... ↓
pwndbg> telescope 0x2c671120f531-1
00:00000x2c671120f530 —▸ 0x3ef5ef90ce91 ◂— 0xd00002c8ca6a022 //map: 0x3ef5ef90ce91
01:00080x2c671120f538 —▸ 0x2296bb502251 ◂— 0x2c8ca6a023 //properties
... ↓
03:00180x2c671120f548 —▸ 0x200db42bd11 ◂— 0xc600002c8ca6a024 //elements
... ↓
05:00280x2c671120f558 —▸ 0x2c8ca6a023b9 ◂— 0x100002c8ca6a022 //properties[name]

验证前文property和element属性的分开存放的假设,初始时两者的值是相同的。properties数组中存放的值如果相同(例如都是’aaa’),也会引用同一块内存用来节省空间,类似隐藏类的实现。

命名属性(property)的不同存储方式

V8 中命名属性有三种的不同存储方式:对象内属性(in-object)、快属性(fast)和慢属性(slow)

nd7e92

  • 对象内属性,保存在对象本身,访问速度最快
  • 快属性,比前者多一次寻址次数
  • 慢属性速度最慢,将属性的完整结构存储在内(前两种属性的结构会将结构放在隐藏类中描述)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//三种不同类型的 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'
}

对象内属性和快属性的结构基本相同,对象内属性因为对象存储空间的限制,所以在超过10个属性之后多余的部分就会进入property(命名属性)中。

对象内属性

u5vm0G

快属性:多出的两个属性被存入property中,并且有序索引。

mzVIMk

慢属性:property中的索引变得无序,说明这个对象已经采用了hash存取结构了。

Kk2crN

至于为什么要采用三种方式进行存储,无非是为了让v8更快一些。这里就不做记录了,有兴趣可以看参考文献。

隐藏类

Hidden Class,即隐藏类,V8借用了类和偏移位置的思想,将本来通过属性名匹配来访问属性值的方法进行了改进,使用类似C++编译器的偏移位置机制来实现。在V8的Memory检查器中,隐藏类被称为Map。在SpiderMonkey中,类似的设计被称为Shape

隐藏类的目的只有两个,运行更快和占内存空间更小。我们这里从节省内存空间讨论。

在 ECMAScript 中,对象属性的 Attribute被描述为以下结构。

  • [[Value]]:属性的值
  • [[Writable]]:定义属性是否可写(即是否能被重新分配)
  • [[Enumerable]]:定义属性是否可枚举
  • [[Configurable]]:定义属性是否可配置(删除)

QajU2V

隐藏类的引入,将属性的 Value 与其它 Attribute 分开。一般情况下,对象的 Value 是经常会发生变动的,而 Attribute 是几乎不怎么会变的。没有没有必要重复Attribute的剩余部分。

隐藏类的创建

对象创建过程中,每添加一个命名属性,都会对应一个生成一个新的隐藏类。在 V8 的底层实现了一个将隐藏类连接起来的转换树,如果以相同的顺序添加相同的属性,转换树会保证最后得到相同的隐藏类。

下面的例子中,a 在空对象时、添加 name属性后、添加 text属性后会分别对应不同的隐藏类。

1
2
3
4
function Foo3 (){};
let a = new Foo3();
a.name = 'migraine1'
a.text = 'migraine2'

隐藏类生成概念图

5WU8yj

查看堆内存快照,在创建name时,Hidden Class1创建了一个Attribute,当Hidden Class2创建的时候,name部分的属性会直接饮用Hidden Class1的Attribute。快照中可以看到back_pointer指向前一个Hidden Class。

mkZCAu

Aqwc3r

隐藏类的结构

让我们从上文DebugPrint(a)的数据来看

1
2
3
4
5
6
7
8
9
10
11
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'

a对象中的map,各个部分的意义在注释中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x3ef5ef90ce91: [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: 0x3ef5ef90ce39 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x2296bb502629 <Cell value= 1>
- instance descriptors (own) #2: 0x2c671120f831 <DescriptorArray[8]>//标识对象实例的属性名与其值的存取位置(a和b的描述符相同,结构也相同)
- layout descriptor: (nil)
- prototype: 0x2c671120f349 <Object map = 0x3ef5ef90cee9>
- constructor: 0x200db427329 <JSFunction Foo1 (sfi = 0x200db427149)>
- dependent code: 0x2296bb502251 <FixedArray[0]>
- construction counter: 5

job查看其中的对象实例

1
2
3
4
   pwndbg> job 0x2c671120f831
Descriptor array #2:
[0]: #name (data field 0:h, p: 0, attrs: [WEC]) @ Any
[1]: #text (data field 1:h, p: 1, attrs: [WEC]) @ Any

通过内存实验,验证一下MAP创建的过程。

1
2
3
4
function Foo3 (){};
let a = new Foo3();
a.name = 'migraine1'
a.text = 'migraine2'

创建a.name之后,a.text之前的MAP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x296dce70cf41: [Map]
- type: JS_OBJECT_TYPE
- instance size: 104
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 9
- enum length: invalid
- stable_map
- back pointer: 0x296dce70ccd9 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0xc850a002629 <Cell value= 1>
- instance descriptors (own) #1: 0x39ca33d90331 <DescriptorArray[5]>
- layout descriptor: (nil)
- prototype: 0x39ca33d8f379 <Object map = 0x296dce70cde1>
- constructor: 0x3e90a04a7329 <JSFunction Foo3 (sfi = 0x3e90a04a7149)>
- dependent code: 0xc850a002251 <FixedArray[0]>
- construction counter: 6

{name: "migriane1"}
-----
pwndbg> job 0x39ca33d90331 //对象实例name
Descriptor array #1:
[0]: #name (data field 0:h, p: 0, attrs: [WEC]) @ Any

创建a.text之后的MAP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0x296dce70cff1: [Map]
- type: JS_OBJECT_TYPE
- instance size: 104
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 8 //a.text 占用了对象内属性空间
- enum length: invalid
- stable_map
- back pointer: 0x296dce70cf41 <Map(HOLEY_ELEMENTS)> //之前之前的MAP地址
- prototype_validity cell: 0xc850a002629 <Cell value= 1>
- instance descriptors (own) #2: 0x39ca33d90891 <DescriptorArray[8]>
- layout descriptor: (nil)
- prototype: 0x39ca33d8f379 <Object map = 0x296dce70cf99>
- constructor: 0x3e90a04a7329 <JSFunction Foo3 (sfi = 0x3e90a04a7149)>
- dependent code: 0xc850a002251 <FixedArray[0]>
- construction counter: 6

{name: "migriane1", text: "migraine2"}
---
pwndbg> job 0x39ca33d90891
Descriptor array #2:
[0]: #name (data field 0:h, p: 1, attrs: [WEC]) @ Any
[1]: #text (data field 1:h, p: 0, attrs: [WEC]) @ Any

V8内存模型

v8 exploit

V8的Object内存继承关系如下,继承比较复杂看源代码的时候需要注意从基类继承下来的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
Object
Smi
HeapObject
HeapNumber
PropertyCell
Name
String
Oddball
JSReceiver
JSObject
JSFunction
JSArray
JSArrayBuffer

Object

Object主要包含两种类型

  • Smi(Samll Integer) <–整数
    • 整数由带符号的31位范围表示
    • 整数由带符号的32位范围表示
      • 最后一bit(LSB)始终为0
  • HeapObject. <–V8大部分存储类型的基类
    • 除了整数以外的其他类(包括范围在Smi表达以外的整数,例如Double)
      • 始终有个指向Map的指针。
      • 最后一bit位始终是1,在解引用的时候要做-1操作
    • HeapObject基本上由GC管理,因此位于GC区域,而不是HEAP的区域(堆喷射中一连串均匀的非HEAP内存,那个一般就是JS的GC堆)

Tagged Values

同时表示指向Smi和HeapObject的指针的机制(gdb中通过job查看内存会自动进行区分)

0zUFQE

  • 指向SMI时(LSB=0)
    • 32bit :通过右移一位获得原始值
    • 64bit:通过右移32位获得原始值(64bit原始值在高32位)

5wvZ0a

  • 指向HeapObject时

    • 32bit:Ptr-1直接指向HeapObject
    • 64bit: Ptr-1 直接指向HeapObject

    因为GC上的CHUNK都是4/8字节对齐的,所以指针在不人为改动的情况下HeapObject最低位LSB始终是0,所以Pointer不需要和Smi一样左移来空出多余的一位。

HeapNumber

当对象的值位Double,数据无法在Smi范围内表达,就需要借用HeapObject来存储数据。

V8中的HeapObject内部没有定义成员变量,这些内部指针是通过偏移量实现的。

w2Cl0B

也同样做一遍实验,存储Smi(0xdeadbee)和Double(0xdeadbeef),后者要大于0x7fffffff,所以需要放在数组中。

oG6cDV

1
2
3
4
5
6
7
pwndbg> job 0x10bf2190d441
0x10bf2190d441: [FixedArray]
- map: 0x2f1f4b9826d1 <Map>
- length: 3
0: 233495534
1: 0x236edc27151 <Number 3.73593e+09>
2: 0x236edc27021 <String[6]: 123123>

写入内存之后,通过job可以看到这个数组内容,不过这次我们需要直接用x/看

Smi类型是直接存在FixedArray中高32位置

zRoK95

非Smi类型,存放在指针中,解引用的时候需要-1

0x41ebd5bdde0000是0xdeadbeef的double存储方式

noMmcP

flphXO

HeapNumber对象和其他对象连续,中间没有任何间隙(自己做的图好丑)

4iMv3n

这部分还有一些坑,之后再填,先学下面的部分

JSObject

这部分在上文有过比较详细实验,简单总结一下。

JSObject用于表示Javascript中的对象

​ 继承自Object->HeapObject->JSReceiver

vdqCSg

  • JSObject从JSReceiver继承Property(命名属性),用于存放命名属性
  • JSObject定义了Elements(可索引属性),用于存放可索引属性

JSFunction

用于指向Javascript Function的对象

​ 继承自 Object->HeapObject->JSRecviver->JSObject

Lp1jtk

实验一下

cY5Oz0

1
2
3
4
5
6
pwndbg> x/20xg  0x6d0e21a7308  <-- function f()
0x6d0e21a7308: 0x00000dd381902519 <--KMapOffset* 0x000030bd03f82251
0x6d0e21a7318: 0x000030bd03f82251 0x000006d0e21a7129
0x6d0e21a7328: 0x000006d0e2183eb1 0x000006d0e21a72e9
0x6d0e21a7338: 0x000014a2d8e1fe41 <--kCodeEntry 0x000030bd03f82321
0x6d0e21a7348: 0x00001bdd17582a41
  • kCodeEntryOffset是一个指向JIT code(可读可执行的区域),很多exploit都会在这个部分写入shellcode。当然前提是这个funciton变热才会使用JIT

    • V8 6.7版本之后JIT的区域不再可写,可能需要结合JIT Spray才能利用。(这个知识点还不太懂,没试过)

    • 向JIT的RWX内存实现利用可以看我写的JS Engine Exploit-qwn2own

    YxmYWC

    GC区域中 标注位Code的部分大概就是JIT的执行区域

JSArray

继承自 Object->HeapObject->JSReceiver->JSObject

923PSg

JSArrayBuffer

ArrayBuffer & TypedArray

​ ArrayBuffer是一个直接从Javascript访问内存的特殊数组

  • ArrayBuffer仅仅准备了一个内存缓冲区

  • BackingStore,可以使用TypedArray指定的类型读取和写入该区域

    • BackingStore指针,属于HEAP而不是GC管理区域

    uNlKzE

  • 实际应用中,有必要和TypedArray和DataView一起使用

    vBi7I1

    一些Demo

    • 不使用TypeArray

      1
      d8> var a=new ArrayBuffer(8);
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
        d8> %DebugPrint(a)
      DebugPrint: 0x2be37c28d469: [JSArrayBuffer]
      - map: 0x2ed11a983fe9 <Map(HOLEY_ELEMENTS)> [FastProperties]
      - prototype: 0x34a749712981 <Object map = 0x2ed11a984041>
      - elements: 0x9d3c4182251 <FixedArray[0]> [HOLEY_ELEMENTS]
      - embedder fields: 2
      - backing_store: 0x55b2de6d7c40 -->指向HEAP区而不是GC区
      - byte_length: 8
      ...
      }
    • 使用TypeArray

      制定长度,初始化为0

      t_arr=new Uint8Array(128)//ArrayBuffer被创建在内部

      使用特定值初始化

      t_arr=new Uint8Array([1,2,3,4])//ArrayBuffer被创建在内部

    事先构建缓冲区并使用它

    1
    2
    3
      arr_buf=new ArrayBuffer(8);
    t_arr1=new Uint16Array(arr_buf);//创建一个Uint16数组
    t_arr2=new Uint16Array(arr_buf,0,4);//或者,您也可以指定数组的开始和结束位置

8hWdrz

  • BackingStore指向存储ArrayBuffer数据的HEAP。

    • 所以在攻击中,覆盖BackingStore指针来实现一个oob是一个很好的思路
    1
    2
    3
    4
    5
    arr=new ArrayBuffer(0x20);
    u32=new Uint32Array(arr);
    u32[0]=0x1234;
    u32[1]=0x5678;
    %DebugPrint(u32);

39RjhN

  • ArrayBuffer也可以在不同的TypedArray之间共享

    • 准备ArrayBuffer

    var ab = new ArrayBuffer(0x100);

    • 向ArrayBuffer写入Float64的值
1
2
var t64 = new Float64Array(ab);
t64[0] = 6.953328187651540e-310;//字节序列是0x00007fffdeadbeef

当某些地址在V8上泄露时,通常在大多数情况下被迫将其解释为双精度值,为了正确计算偏移量等,需要将其转换为整数值。 对于完成该转换,ArrayBuffer是最佳的。

  • 从ArrayBuffer读取两个unit32

    1
    2
    var t32 = new Uint32Array(ab);
    k = [t32[1],t32[0]]

    k是6.953328187651540e-310,将字节序列按照4个字节去分开,然后解释为Uint32,于是得到:
    k=[0x00007fff,0xdeadbeef]

代码阅读引导

摘自

V8 javascript engine代码阅读

目录:

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
src ---+
|
+---arm
+---arm64
+---mips
+---mips64
A +---ia32
+---x64
+---ppc
+---s390
+---wasm
+---asmjs
|
+---ast
+---compiler
B +---compiler-dispatcher
+---interpreter
+---parsing
|
+---js
+---builtins
C +---runtime
+---snapshot
+---regexp
+---profiler
|
D +---ic
|
+---heap
E +---heap-symbols.h
+---zone
+---objects
|
F +---inspector
|
+---base
+---debug
+---tracing
+---extensions
G +---libplatform
+---libsampler
+---third_party
+---trap-handler
|
+---*.cc/*.h
.
.
.
  • A:存储汇编代码,反汇编程序,宏汇编程序,模拟器等,对于不同CPU不同。
  • B:code generation系统,例如parse, compile, interpreter, etc.
  • C:JS built-in function和runtime helper function
  • D:Inline Cache code
  • E:object model(对象模型)和memory(内存)相关代码
  • F:Inspector
  • G:Debugging and platform abstraction layer codes are stored.

必读部分

  • api.h/api.cc
    An API for Embedder is defined.
  • objects.h/objects.cc
    定义了v8的所有对象模型
  • compiler/compiler.cc
    编译的入口点
  • compiler/pipeline.cc
    和compiler.cc关联,放置TurboFan
  • runtime/runtime-*.cc
    A runtime function is defined.
  • builtins/builtin-*.cc
    A faster runtime function group. It is described in CodeStubAssembler (commentary) or Assembler.
  • interpreter/*.cc
    Ignition解释器
  • ic/*.cc
    Inline Caching的实现
    存储Runtime(?)

扩展阅读:

从Chrome源码看JS Object的实现

V8 是怎么跑起来的 —— V8 的 JavaScript 执行管道

js中v8引擎的详解

探究JS V8引擎下的“数组”底层实现 <–JSArray是个特殊的JSObject

Array.from() <–比起内容,更重要的是认识到Polyfill这个开源项目

V8 小整数(smi)和指针

奇技淫巧学 V8 之二,对象在 V8 内的表达

V8 Ignition:JS 引擎与字节码的不解之缘

V8 Object 内存结构与属性访问详解

nodejs深入学习系列之v8基础篇 <–v8编译的几种方式

如何用GN编译V8引擎

什么是JS原型链

JavaScript engine fundamentals: Shapes and Inline Caches

v8漏洞利用-韩语

V8引擎内存管理(译文)