首页
社区
课程
招聘
[原创]chrome v8 issue 1486342浅析
发表于: 2024-6-22 02:58 7679

[原创]chrome v8 issue 1486342浅析

2024-6-22 02:58
7679

作者: coolboy

首先,这是一个issue,不是一个漏洞。但不排除它可能可以被利用,通过issue修复者的邮件回复可以印证这一观点。虽然它不是一个漏洞,但弄清它的前因后果,对理解turbofan的sea of nodes以及编译过程有极大的帮助,进一步有助于代码审计去发现新的漏洞、维护v8及各类浏览器的安全。接下来我将从这个思路出发,陆续分析多个issue,加深对v8的理解,以期发现新的安全问题。
这是一个系列文章,本文是第五篇。

什么是sea of nodes?

以函数get_number为例,来观察turbofan Inlining阶段 CommonOperatorReducer优化策略。

优化前它的sea of nodes图如下:

优化后如下:

对比优化前后,可以发现是将merge节点(21, 25)删除,将return(27)删除。新建两个return节点(32,33),分别采用ifture和iffalse的输出作为control输入,同时输出control边到end。接下来看代码。

结合sea of nodes图,看上述代码注释,可以更加清晰的了解。

先来看issue的patch,它位于src/compiler/js-call-reducer.cc文件ReduceArrayIterator函数,属于Inlining阶段call_reducer优化选项,当出现数组迭代器指令的时候,对这个内置函数进行优化。

patch逻辑很简单,把RelaxControls(node);替换成ReplaceWithValue(node, node, node, control);
RelaxControls代码如下:

我们再来看看ReplaceWithValue函数,它的代码如下:

这个函数的作用是修正node输出的边。参数分别node本身,node的value输出、effect输出、control输出。当effect或者control传递nullptr的时候,将不会发生替换。假设node A和node B存在control的边,这条边为node A的输出,B的输入。
此时执行ReplaceWithValue(nodeA, nodeA, nodeA, nodeC),那么A的输出将不再为B的输入,而nodeC和nodeB之间将建立边,为C的输出,B的输入。
再回过头去看补丁,补丁前的代码将node的输出设置为空(即不变),而应该是control。结合补丁前面的代码查看:

对比patch前后的--trace-turbo得到的json如下:

通过调试也可以打印出control id如下:

应该由397指向358,而非350。而397即为"label 1"处代码新得到的control id。
通过对比sea of nodes也可以得到结论:
patch前:

350指向了358,而397的control 输出为空。
patch后:

397正确指向了358。

为什么说这个issue有非常大的可能可以被利用?此处issue导致的最后结果是程序的控制流不符合预期,这可能导致修改程序逻辑,跳过sea of nodes的某些必须要执行的节点,而这些节点如果是checkMap的话,那么将导致Map不被检查,将导致优化函数里面对数据结构的假设不再成立,从而实现类型混淆,进一步实现漏洞。那么如何构造出这样一个精巧的sea of nodes,让它跳过checkMap节点呢?这是一个问题。

TurboFan JIT Design
chromium issues

# 推荐香港服务器,可以避免网络问题导致的编译失败。
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
 
mkdir ~/v8
cd ~/v8
fetch v8
cd v8
 
# 补丁前一笔提交
git checkout 24861cbefe4
gclient sync
alias gm=~/v8/tools/dev/gm.py
gm x64.release
gm x64.debug
 
# test
./out/x64.release/d8 --help
# 推荐香港服务器,可以避免网络问题导致的编译失败。
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=/path/to/depot_tools:$PATH
 
mkdir ~/v8
cd ~/v8
fetch v8
cd v8
 
# 补丁前一笔提交
git checkout 24861cbefe4
gclient sync
alias gm=~/v8/tools/dev/gm.py
gm x64.release
gm x64.debug
 
# test
./out/x64.release/d8 --help
// test.js
const o13 = {
  "maxByteLength": 5368789,
};
const v14 = new ArrayBuffer(129, o13);
const v16 = new Uint16Array(v14);
 
function f3(param) {
  for (let i = 0; i < 5; i++) {
    try {"resize".includes(v14); } catch (e) {}
    v14.resize(3.0, ..."resize", ...v16);
  }
 
  let f = function() { return param; }
}
 
%PrepareFunctionForOptimization(f3);
f3();
%OptimizeFunctionOnNextCall(f3);
f3();
// 运行./out/x64.deubg/d8 --allow-natives-syntax test.js,将会得到崩溃堆栈
// test.js
const o13 = {
  "maxByteLength": 5368789,
};
const v14 = new ArrayBuffer(129, o13);
const v16 = new Uint16Array(v14);
 
function f3(param) {
  for (let i = 0; i < 5; i++) {
    try {"resize".includes(v14); } catch (e) {}
    v14.resize(3.0, ..."resize", ...v16);
  }
 
  let f = function() { return param; }
}
 
%PrepareFunctionForOptimization(f3);
f3();
%OptimizeFunctionOnNextCall(f3);
f3();
// 运行./out/x64.deubg/d8 --allow-natives-syntax test.js,将会得到崩溃堆栈
function get_number(x) {
    return x > 0 ? 1 : 0;
}
// 多次执行get_number,触发优化
for (var i = 0; i < 20000; i++) {
    get_number(3);
}
 
get_number(3);
/*
    ./out/x64.deubg/d8 --allow-natives-syntax --trace-turbo test.js
    --trace-turbo 将生成turbo-get_number-1.json文件, v8提供了一个在线查看工具:https://v8.github.io/tools/head/turbolizer/index.html,加载turbo-get_number-1.json 可以查看sea of nodes
*/
function get_number(x) {
    return x > 0 ? 1 : 0;
}
// 多次执行get_number,触发优化
for (var i = 0; i < 20000; i++) {
    get_number(3);
}
 
get_number(3);
/*
    ./out/x64.deubg/d8 --allow-natives-syntax --trace-turbo test.js
    --trace-turbo 将生成turbo-get_number-1.json文件, v8提供了一个在线查看工具:https://v8.github.io/tools/head/turbolizer/index.html,加载turbo-get_number-1.json 可以查看sea of nodes
*/
function get_number(x) {
    return x > 0 ? 1 : 0;
}
function get_number(x) {
    return x > 0 ? 1 : 0;
}
// common-operator-reducer.cc
// ReduceReturn 优化return 节点
Reduction CommonOperatorReducer::ReduceReturn(Node* node) {
  ...
  // return节点的effect输入
  Node* effect = NodeProperties::GetEffectInput(node);
  // return节点的第一个value输入
  Node* pop_count = NodeProperties::GetValueInput(node, 0);
  // return节点的第二个value输入,get_number函数里它为phi(1|0)
  Node* value = NodeProperties::GetValueInput(node, 1);
  // control输入
  Node* control = NodeProperties::GetControlInput(node);
  if (value->opcode() == IrOpcode::kPhi &&
      NodeProperties::GetControlInput(value) == control &&
      control->opcode() == IrOpcode::kMerge) {
    /* 3个条件均满足,参考sea of nodes图
    1. value类型为phi
    2. value的control输入 跟 return的control输入 是同一个节点
    3. control输入是一个merge节点
    */
     
    // control为merge节点(25),control_inputs代表了merge节点的两个输入:iftrue, iffalse
    Node::Inputs control_inputs = control->inputs();
    Node::Inputs value_inputs = value->inputs();
    DCHECK_NE(0, control_inputs.count());
    DCHECK_EQ(control_inputs.count(), value_inputs.count() - 1);
    DCHECK_EQ(IrOpcode::kEnd, graph()->end()->opcode());
    DCHECK_NE(0, graph()->end()->InputCount());
    // control作为node和value的输入,满足
    // value作为node的输入,满足
    if (control->OwnedBy(node, value) && value->OwnedBy(node)) {
      // control_inputs: iftrue, iffalse两个分支
      for (int i = 0; i < control_inputs.count(); ++i) {
        // newNode(操作码,pop_count, value_input, effect_input, control_input)
        // newNode函数参数如上,使用newNode创建两个新的return节点
        Node* ret = graph()->NewNode(node->op(), pop_count, value_inputs[i],
                                     effect, control_inputs[i]);
        // 将新建return节点的输出control指向end节点                                    
        MergeControlToEnd(graph(), common(), ret);
      }
      // merge节点丢弃
      Replace(control, dead());
      // 原来的return节点丢弃
      return Replace(dead());
    }
    ...
  }
  return NoChange();
}
// common-operator-reducer.cc
// ReduceReturn 优化return 节点
Reduction CommonOperatorReducer::ReduceReturn(Node* node) {
  ...
  // return节点的effect输入
  Node* effect = NodeProperties::GetEffectInput(node);
  // return节点的第一个value输入
  Node* pop_count = NodeProperties::GetValueInput(node, 0);
  // return节点的第二个value输入,get_number函数里它为phi(1|0)
  Node* value = NodeProperties::GetValueInput(node, 1);
  // control输入
  Node* control = NodeProperties::GetControlInput(node);
  if (value->opcode() == IrOpcode::kPhi &&
      NodeProperties::GetControlInput(value) == control &&
      control->opcode() == IrOpcode::kMerge) {
    /* 3个条件均满足,参考sea of nodes图
    1. value类型为phi
    2. value的control输入 跟 return的control输入 是同一个节点
    3. control输入是一个merge节点
    */
     
    // control为merge节点(25),control_inputs代表了merge节点的两个输入:iftrue, iffalse
    Node::Inputs control_inputs = control->inputs();
    Node::Inputs value_inputs = value->inputs();
    DCHECK_NE(0, control_inputs.count());
    DCHECK_EQ(control_inputs.count(), value_inputs.count() - 1);
    DCHECK_EQ(IrOpcode::kEnd, graph()->end()->opcode());
    DCHECK_NE(0, graph()->end()->InputCount());
    // control作为node和value的输入,满足
    // value作为node的输入,满足
    if (control->OwnedBy(node, value) && value->OwnedBy(node)) {
      // control_inputs: iftrue, iffalse两个分支
      for (int i = 0; i < control_inputs.count(); ++i) {
        // newNode(操作码,pop_count, value_input, effect_input, control_input)
        // newNode函数参数如上,使用newNode创建两个新的return节点

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//