参考:

阅读这种英文文献,我感觉是必须边读边写的

如果读翻译的话……可能是我理解力不够,反正我看不懂

JS引擎回顾

VM

js的VM具有可以直接执行发出的字节码的解释器。它的VM通常被实现为一个基于栈的机器,它的各种操作都是围绕着栈来实现的。这对我来说已经在wasm的反编译和v8字节码的反编译上见识过了

对象和数组

js的对象基本上是基于键值对存储的,可以用点运算符或者方括号来访问属性

数组就是一个特殊的对象,它的键是连续的从0开始到比length小1的连续整型。比如声明一个数组var a = [1,2,3],用a[1]访问会得到2,用a['1']访问也会得到2,但是用a.1来访问就会报错了

在内部,js内核将属性和元素存储在通过一个内存区域中,并在对象本身中存储指向该区域的指针。这个指针指向区域的中间,区域的左边(低地址)是属性,右边(高地址)是元素,长度在指针的刚刚左边(??),这看起来就像是蝴蝶(????)。程序员的奇妙思想,为什么不叫麻雀呢

剩下的看不懂

函数

函数执行的时候,会有两个特殊的变量,一个是arguments,用来访问函数的参数,另一个就是this,指向函数的调用者:

  • 如果函数以new func()的方式调用的话,this就指向一个新创建的对象,或者函数本身返回一个对象的话,this就指向这个对象,它具有.prototype属性
  • 如果函数以obj.func()的方式调用的话,this就指向的是obj
  • 如果函数以func()的方式效用的话,this就指向当前的全局对象,大概就是根对象吧

函数还有两个有趣的属性,.call.apply,它们可以让你用指定的this来调用函数。call()方法的作用和apply()方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。

装饰器:

1
2
3
4
5
6
7
8
function decorate(func) {
return function() {
for (var i = 0; i < arguments.length; i++) {
// do something with arguments[i]
}
return func.apply(this, arguments);
};
}

内置函数和方法通常由两种方式之一实现:C++或者js本身

例如,Math.pow()就是用c++实现的

1
2
3
4
5
6
7
8
EncodedJSValue JSC_HOST_CALL mathProtoFuncPow(ExecState* exec)
{
// ECMA 15.8.2.1.13
double arg = exec->argument(0).toNumber(exec);
double arg2 = exec->argument(1).toNumber(exec);

return JSValue::encode(JSValue(operationMathPow(arg, arg2)));
}

能看出来:

  • 本地js的特色
  • 参数是如何用argument函数提取的(若参数不存在则返回undefined
  • 参数是如何转化为它们所需要的类型的
  • 实际的操作是如何本地数据类型来操作的
  • 结果是如何返回给caller

不同操作的核心实现都是被移入了分别的函数中,所以它们能直接被JIT编译的代码调用

CVE-2016-4622

该漏洞是WebKit中JSC的一个漏洞

问题出现在Array.prototype.slice函数(或者说,方法),它的用法如下:

1
2
3
var a = [1,2,3,4];
var s = a.slice(1,3);
//是就是索引为[1,3)的所有元素,即[2,3]

为了可读性,原作者将ArrayPrototype.cpp的代码格式化之后精简了一下

JSC中它的代码如下:

照例写注释

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
EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
{
/* [[ 1 ]] */
JSObject* thisObj = exec->thisValue();
//获取this指针,即调用它的数组
.toThis(exec, StrictMode)
.toObject(exec);
if (!thisObj)
return JSValue::encode(JSValue());

/* [[ 2 ]] */
unsigned length = getLength(exec, thisObj);
//获取数组的长度
if (exec->hadException())
return JSValue::encode(jsUndefined());

/* [[ 3 ]] */
unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
unsigned end =
argumentClampedIndexFromStartOrEnd(exec, 1, length, length);

/* [[ 4 ]] */
//检查是否使用了构造函数 (why?)
std::pair<SpeciesConstructResult, JSObject*> speciesResult =
speciesConstructArray(exec, thisObj, end - begin);

// We can only get an exception if we call some user function.
if (UNLIKELY(speciesResult.first ==
SpeciesConstructResult::Exception))
return JSValue::encode(jsUndefined());

/* [[ 5 ]] */
//如果数组是具有密集存储的本地数据,则使用fastslice进行切片
if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath &&
isJSArray(thisObj))) {
if (JSArray* result =
asArray(thisObj)->fastSlice(*exec, begin, end - begin))
return JSValue::encode(result);
}

//或者使用简单循环来切片
JSObject* result;
if (speciesResult.first == SpeciesConstructResult::CreatedObject)
result = speciesResult.second;
else
result = constructEmptyArray(exec, nullptr, end - begin);

unsigned n = 0;
for (unsigned k = begin; k < end; k++, n++) {
JSValue v = getProperty(exec, thisObj, k);
if (exec->hadException())
return JSValue::encode(jsUndefined());
if (v)
result->putDirectIndex(exec, n, v);
}
setLength(exec, result, n);
return JSValue::encode(result);
}

那么,这个漏洞就和plaidCTF那个很像了

由于获取length位于获取begin和end之前,而如果在获取了begin和end的时候修改了数组长度,那么就会因为实际数组长度比程序以为的数组长度小,就会导致数组越界