-
-
[原创]r4j0x00发布的V8的1DAY漏洞分析
-
发表于: 2020-10-14 18:49 5970
-
一.漏洞简介
漏洞来源https://twitter.com/r4j0x00/status/1298682570448674817
作者发布了自己的exploit以及google的修改记录:
https://github.com/r4j0x00/exploits/tree/master/chrome-exploit
https://github.com/v8/v8/commit/85bc1b0cab31cc064efc65e05adb81fee814261b#diff-2e2c5645d87dabecd3793b1f10300974
二.漏洞初步分析
漏洞的核心触发点为这个被优化的函数:
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];
}
字面意思可以推测出corrupting_array 越界踩踏了corrupted_array,于是来调试证明一下。
在优化完成后用%DebugPrint 先后打印出corrupting_array和 corrupted_array的地址:
DebugPrint: 0x20859364f19: [JSArray]
可以看到corrupting_array 的地址正好在corrupted_array前面一点,gdb打印出内存:
首先,corrupting_array首地址偏移8的位置,也就是最右侧上面那个红框里,59364f01,最后个bit为1,代表这是个指针,和左侧红框里isolate指针0x20800000000相加再减去那个1,指向0x20859364f00。这个位置存放的就是elements,这在DebugPrint 的打印里也可以找到,同理可以对应找到map,prototype等信息。
而59364f01左边,也就是corrupting_array首地址偏移12的位置,存放的是corrupting_array数组的大小0x4,这个数据最后个bit是0,因此是个smi,也就是整形数据,需要右移1bit,因此真正的大小是2,这也的确是corrupting_array最初定义时的大小。
关于V8的isolate,指针和SMI知识,可查阅其它文档,但需要说下的是,这部分貌似最近有了变化,因为从其它文档看,指针和SMI都占位了64bit,但现在却缩到了32bit,估计是为了节约内存吧。
接下来再看elements,也就是刚才计算出来的0x20859364f00,首地址存储的是elements的属性信息,略过不关注,再往后就是连续的两个浮点数0x3fb999999999999a,也就是真正的数组数据,两个0.1。这样corrupting_array的解析就基本完成了。
这时,再看看corrupting_array是怎么越界的,从trigger函数里分析,x的值计算出来必定是7,也就是corrupting_array的大小理应扩展为8,但实际上从调试来看,它的大小只有2,那么越界写就这样产生了,右边第2个红框处就是越界写的位置,这个位置正好是corrupted_array的首地址偏移8的位置,也就是存放它数组大小以及elements首指针的位置。
这样的话,corrupted_array数据的数组访问范围就变成了从isolate首地址+8开始的(0x24242424/2)这么大的空间,基本涵盖了整个isolate,数据泄露就这么产生了。
要了解这里面的代码流程,可以从DebugPrint的实现入手,断点断在JSArray::JSArrayPrint,一步步看下去。
后面的利用流程大致说下,在search_space中,作者把isolate中可能的空间范围分段搜索他感兴趣的数据,之所以分段,是因为这段内存并不是连续可读的,这里不同版本的浏览器是不一样的,甚至同一版本浏览器每次启动都可能发生变化,这里需要自行调整出一个合适适度的范围。
此外,由于corrupted_array是个浮点数组,只能以U64的方式进行读取,而内存数据有可能并不是刚好按作者想象的16进制对齐,需要自己设法调整。
三.漏洞原理分析
显然,和以前大多数漏洞一样,这个越界之所以产生,是优化的过程出了问题。但在分析优化流程之前,首先还是看看google给出的修改方案,关键点在这里:
这个xx.tq代码将会在xx-tq-csa.cc中被翻译成一段冗长的C++代码。
可以看出,这里将会对数组的最大值做判断,超过了就会报OOM。这个地方是否就是针对优化流程做的修改呢?
找一个chrome85版本做实验,这个版本已经修改了这个bug,通过插入日志可以发现,根本走不到trigger函数,在前面的giant_array.splice那一步就发生了OOM。
(图略)
再分析前面的js代码,可以发现splice以及前面的流程,强行把giant_array数组撑破了,超出了最大值67108862,达到了67108863。因此fix的代码才会阻止这个步骤。
那么这和之后的trigger优化流程有什么联系呢?
感谢谷歌的turbolizer优化流程分析工具,使得这个问题的分析变得简单了,甚至都不需要去看代码(这貌似不是什么好事)。
这个“节点之海”(sea of nodes)看起来深不可测,但如果只针对这个问题,还是比较好理解的。
绿色部分代表了优化后机器指令,20,38,40分别是减,乘,减,正好对应trigger函数里的三次算术操作。
蓝色的97,104对应trigger函数里调用Math.max的操作。
所有的操作下面都有个Range,代表这步做完后,这个值可能所在的范围,“节点之海”可以根据这个Range决定是否优化掉115节点,也就是边界检查这步,它检查是否超出了数组的最大范围。
如果最后计算出来的Range都没有超出数组的范围,那显然是可以不检查的,这个逻辑并没有问题。
但问题在于最开始的87节点,它推测的是var x = array.length的范围,按照数组的最大值,这理所当然被推测成了(0,67108862),但却没想到实际传进来的是个非法的被撑破的数组,它的大小刚好超过了67108862,是67108863。
所以,失去边界检查后,corrupting_array[x] = length_as_double;也就没人能阻止越界了。
四.参考文档:
1.[原创] 手把手教你入门V8漏洞利用:
https://bbs.pediy.com/thread-258431.htm
2.TurboFan TechTalk presentation
https://docs.google.com/presentation/d/1sOEF4MlF7LeO7uq-uThJSulJlTh--wgLeaVibsbb3tc/edit#slide=id.g5499b9c42_01778
3.introduction-to-turbofan
https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/
4.从漏洞利用角度介绍Chrome的V8安全研究
https://www.4hou.com/posts/21Y1
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课