深入解析V8引擎TurboFan的漏洞利用技术

本文详细分析了V8引擎TurboFan优化编译器中的一个漏洞,通过类型混淆和范围分析错误实现越界读写,最终利用WebAssembly的RWX内存执行任意代码。文章涵盖了从漏洞原理到完整漏洞利用链的构建过程。

深入解析V8引擎TurboFan的漏洞利用技术

引言

很久以前我写过一篇关于内核池的博客,而今天我们要探索的是节点海洋!当前趋势是攻击JavaScript引擎,特别是优化JIT编译器如V8的TurboFan、SpiderMonkey的IonMonkey、JavaScriptCore的DFG & FTL或Chakra的SimpleJIT & FullJIT。

本文将重点讨论TurboFan及其使用的"节点海洋"结构,然后研究Google CTF 2018中由@_tsuro编写的一个易受攻击的优化阶段,并为其编写漏洞利用代码。我们将在x64 Linux环境下进行,但Windows平台上的利用过程完全相同(只需使用不同的shellcode)。

环境搭建

构建V8

构建V8非常简单,只需使用depot_tools获取源码并构建:

1
2
3
4
fetch v8
gclient sync
./build/install-build-deps.sh
tools/dev/gm.py x64.release

d8 shell

V8提供了一个方便的d8 shell,为加快构建可以只编译它:

1
~/v8$ ./tools/dev/gm.py x64.release d8

准备Turbolizer

Turbolizer是调试TurboFan节点海洋图的工具:

1
2
3
4
cd tools/turbolizer
npm i
npm run-script build
python -m SimpleHTTPServer

编译流水线

考虑以下代码:

1
2
3
4
5
let f = (o) => {
  var obj = [1,2,3];
  var x = Math.ceil(Math.random());
  return obj[o+x];
}

使用--trace-opt可以观察到函数f最终会被TurboFan优化。

节点海洋

TurboFan工作在称为"节点海洋"的程序表示上。节点可以表示算术运算、加载、存储、调用、常量等。有三种边类型:

  1. 控制边:控制流图中的边,用于分支和循环
  2. 值边:数据流图中的边,显示值依赖关系
  3. 效果边:排序操作如读取或写入状态

实验优化阶段

处理NumberAdd

考虑以下函数:

1
2
3
4
5
function opt_me() {
  let x = Math.random();
  let y = x + 2;
  return y + 3;
}

观察不同类型阶段的图形变化:

  1. 图形构建阶段:生成JSCall、NumberConstant和SpeculativeNumberAdd节点
  2. 类型阶段:TurboFan访问每个节点并尝试简化它们
  3. 类型降低阶段:运行更多简化器,将SpeculativeNumberAdd替换为NumberAdd

范围类型

考虑带有条件分支的代码:

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

Phi节点的类型是两个范围的联合,即Range(5,10)。

CheckBounds节点

考虑以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function opt_me(b) {
  let values = [42,1337];
  let x = 10;
  if (b == "foo") x = 5;
  let y = x + 2;
  y = y + 1000;
  y = y * 2;
  y = y & 10;
  y = y / 3;
  y = y & 1;
  return values[y];
}

在简化降低阶段,TurboFan会比较索引和长度。如果索引的最小范围值大于等于0且最大范围值小于长度值,则会删除CheckBounds节点。

不同类型的加法操作码

V8有多种加法操作码:

  1. SpeculativeSafeIntegerAdd:推测整数加法
  2. SpeculativeNumberAdd:推测数字加法
  3. Int32Add:32位整数加法
  4. JSAdd:复杂对象加法
  5. NumberAdd:普通数字加法

DuplicateAdditionReducer挑战

理解简化过程

DuplicateAdditionReducer优化类似x + 1 + 1的情况为x + 2。但由于IEEE-754双精度浮点数的精度限制,这种转换可能不正确。

理解漏洞

当处理大于2^53-1的数字时,会有精度损失。例如:

1
2
3
4
5
d8> var x = Number.MAX_SAFE_INTEGER + 1
d8> x + 1 + 1
9007199254740992
d8> x + 2
9007199254740994

这种精度损失导致x += 1; x += 1;不等同于x += 2

漏洞利用

改进原始利用

通过多次加法放大精度损失:

1
2
3
4
5
d8> var x = Number.MAX_SAFE_INTEGER + 1
d8> 10 * (x + 1 + 1)
90071992547409920
d8> 10 * (x + 2) 
90071992547409940

完整利用步骤

  1. 破坏FixedDoubleArray
  2. 破坏JSArray并泄漏ArrayBuffer的后备存储
  3. 获取伪造对象
  4. 构建任意读写原语
  5. 覆盖WASM的RWX内存
  6. 执行shellcode

WASM RWX内存

通过实例化WASM模块,从其函数中获取包含RWX内存指针的WASM实例对象:

1
2
3
4
5
6
let WasmOffsets = { 
  shared_function_info : 3,
  wasm_exported_function_data : 1,
  wasm_instance : 2,
  jump_table_start : 31
};

结论

本文详细分析了TurboFan优化编译器中的一个漏洞,通过类型混淆和范围分析错误实现越界读写,最终利用WebAssembly的RWX内存执行任意代码。这种攻击展示了现代JIT编译器复杂优化过程中可能出现的安全问题。

推荐阅读

  • V8的TurboFan文档
  • Benedikt Meurer的演讲
  • Mathias Bynens的网站
  • Vyacheslav Egorov的博客
  • Samuel Groß关于攻击客户端JIT编译器的BlackHat演讲
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计