更多全球网络安全资讯尽在邑安全
第一部分:写在前面:
环境:
Ubuntu 18.04
turbofan 图表都是在 release的v8下产生的命令
./d8 poc.js --allow-natives-syntax --trace-turbo
1.1:在v8执行时,在poc.js后面加上--trace-turbo执行参数,会产生turbofan(优化过程各个阶段的处理逻辑)图表。
1.2:分析turbofan的图表时,最重要的是找到第一个出现错误的地方。v8 优化的漏洞有一点不同于一般的溢出或UAF是,漏洞的根源通常很难在调试器中直接体现出来。不像溢出和UAF,我们经常可以制造一个崩溃,来体现漏洞发生的直接原因,看v8优化的POC经常给人一种,怎么地,就溢出了的感觉。究其原因,比较个人感觉合理的解释是其漏洞出现的根源往往是在v8工程师自己设计的逻辑层面。
1.3:0xFFFFFFFF是个什么?或者说v8会把他当作什么?
首先我们可以简单排除v8把他当成int64或unsigned int64的可能性。(这样会造成资源上的浪费),可能的解释为一个int32数字或一个unsigned int 32数字。如果解释为int32类型,那么我们打印就会输出-1,如果解释为unsigned int 类型,那么我们打印就会为4294967295。那到底v8会将其解释为什么类型呢,我们可以写个简单的代码验证一下:
var x=0xFFFFFFFF
console.log(x);
图1.1.1
显然v8把0xFFFFFFFF当成了unsigned int32 的类型。换一句话说v8把0xFFFFFFFF当成unsigned int32才是其规定的合法操作,如果当成了int32类型,则会出现前后不一致,会导致出现错误的结果。
这里写的有点啰嗦,主要是这点对理解这个漏洞挺重要。
1.4:v8优化的过程中往往会会对计算的结果进行预估,成为一个范围,预估的范围本身,会影响到后面优化的过程,我们分析优化的漏洞通常就是分析其预估的范围是否有误,以及对后面优化的影响。
这点有点像那种综艺节目,第一个人看到一个东西,然后口述给第二个人听,然后第二个人口述给第三个人听,以此信息传递下去,中间有一个人理解错误的话,就会导致后面全部人理解错误。
第二部分:poc的turbofan过程分析。
2.1:POC的简单研究
原始的poc1.js
(function(){
Function foo(b){
let x=-1;
if(b)x=0xFFFFFFFF;
return -1<Math.max(0,x,-1)
}
console.log(foo(true));
%PrepareFunctionForOptimization(foo);
console.log(foo(false));
%PrepareFunctionForOptimization(foo);
console.log(foo(true));
})
分别输出的是:
图2.1.1
这里可以看到经过优化后,参数x=0xFFFFFFFF的情况下,-1<Math.max(0,x,-1)的结果为false。和优化之前的结果不一样!
这里把poc1.js调整下,以下将其称为poc2.js:
(function(){
Function foo(b){
let x=-1;
if(b)x=0Xffffffff;
return Math.max(0,x,-1)
}
console.log(foo(true));
%PrepareFunctionForOptimization(foo);
console.log(foo(false));
%PrepareFunctionForOptimization(foo);
console.log(foo(true));
})
图 2.1.2
从图2.1.2可以看到,v8对Math.max(0,x,-1)优化前后计算的值都是一样,那为什么前面poc1.js经v8优化后的-1<Math.max(0,x,-1)会返回false呢?
我们可以推测经过v8的优化,在poc1.js在运算完Math.max(0,x,-1)之后,对其结果0xFFFFFFFF(4294967295)的解释出现了问题,原本应该解释为unsigned int32 类型的,结果确解释为int32类型。0xFFFFFFFF如果解释为int32的话,结果就会为-1,这样的话-1<-1,最终结果自然就会变为false。
2.2:turbofan图表分析
关于v8 turbofan图表分析这一块本人也是新手,看别人写的都是直接看几个重要的阶段,本人不是太懂,就用笨一点的方法,把每个图相关过程都看一下。
第一个看到有对结果进行范围判断的是:
2.2.1:V8.TFTypedLowering 57
图 2.2.1
a):这一阶段的优化初始化过程为节点13和节点19合并产生20节点Phi[kRepTagged]Range(-1, 4294967295) 。
b):这一阶段的优化是将poc.js中的Math.max(0,x,-1)拆分为两个NumberMax,将初始化数值分别与节点31的常数0和节点13的常数-1运算。
这里并未看出存在什么问题。
2.2.2:V8.TFLoopPeeling57
图 2.2.2
2.2.3:V8.TFLoadElimination 57
2.2.4:V8.TFEscapeAnalysis 57
图 2.2.4
以上几个优化过程大同小异,基本和第一个图表没什么区别。
2.2.5:V8.TFSimplifiedLowering 77
图2.2.5
a):这个一阶段,初始化将74节点和75节点经过20节点Phi[kRepFloat64]运算后得到预估范围为Range(-1,4294967295),再通过节点65 ChangeFloat64ToInt64运算。
b):图2.2.5 中所示在这一阶段将poc1.js中的Math.max(0,x,-1)拆分为两个Int64LessThan,分别与66节点Int64常数0和70节点Int64常数-1运算。这里得到的范围就应该是Range(0,4294967295)
流程到这里,是没有什么问题的,这里的Int64LessThan其实只是把32位数放入64位寄存器计算。
但是紧接进行68节点的:Truncation64Int32运算,这个节点问题就非常大了,他是把结果进行int32转化,按照这个逻辑进行推断的话,结果就会从Range(0,4294967295)变成Range(0,-1),对原本正确的计算结果进行错误的解释,导致出现了错误的结果。
由此,我们可以推测,这个Truncation64Int32节点的生成是这里计算错误根本原因。如果说在这里还不能清晰的说明对返回结果的影响的话,那么翻到后面的V8.TFLateOptimization 190阶段就很明显了。
2.2.6 V8.TFLateOptimization 190
图 2.2.6
a) 如图2.2.6所示:在这个阶段经过初始化20节点Phi[kRepFloat64]的运算,结果为Range(-1,4294967295)
b) 在这个阶段Math.max(0,x,-1)变成分别和节点66Int64常数0,节点70Int64常数-1进行Phli[kRepWord64]运算,可以推断出其结果为(0,4292967295)。
再经过节点68 TruncateInt64ToInt32就会转化为int类型,结果就为(0,-1),最后和节点67 int32常数-1进行节点40 Int32LessThan运算(这边用Int32LessThan可以进一步证明v8已经把上面结果解释为Int32),得到的结果就为(true,false),然后做一些常规合法性校验,紧接着就是返回。
也就是为什么poc1.js在优化后当参数x=0xFFFFFFFF的情况下,结果得到的计算为false。
第三部分:exp核心分析
function foo(a){
let x=-1;
if(a) x=0xFFFFFFFF;
var arr = new Array(Math.sign(0-Max.max(0,x,-1)));
arr.shift();
let local_arr=Array(2);
local_arr[0] = 5.1;
let buff = new LeakArrayBuffer(0x1000)
arr[0]=0x1122;
return [arr, local_arr, buff];
}
a)根据前面的分析,v8在优化过程中Math.max(0,x,-1) 产生了错误,使得结果为(0,-1)。在Math.sign(0-Math.max(0,x,-1))运算后就会变成(0,1),产生了一个意外的1,使得优化后,如果参数x=0xFFFFFFFF的话,arr的结果就会为new Array(1),产生了一个有效数组。
b)然而在v8的预估判断中,Math.max(0,x,-1)结果为(0,0),arr长度始终为0,不会是其他的值,为无效数组,所以arr.shift()这代码的优化结果就直接在长度的那里减1,变成0xFFFFFFFF(在内存中存储的为-1*2为0xFFFFFFFE),优化后会直接在代表数组长度的内存位置中填入0xFFFFFFFE,没有别的操作。但是因为此时arr为有效数组,结果就产生了一个长度为0xFFFFFFFF的数组。(这里的利用手法和issue 1196683的利用手法一样)
c) 紧接着申请一个浮点数组和一个0x1000的Buffer,然后用前面的超长数组越界改写浮点数组的长度,用于对对象的地址进行泄露。这里的原exp是通过修改DataView的相关指针来实现对任意地址的读写,这些都是v8漏洞利用的常规套路了,这里不做细究了。不过要注意这里用于写shellcode的Buffer是0x1000,shellcode长度要是超过这个值要重新修改DataView的相关指针,不过在情况实际中应用中貌似也不太可能发生......
原文来自: bbs.pediy.com
原文链接: https://bbs.pediy.com/thread-268752.htm
推荐文章
1
2
本文始发于微信公众号(邑安全):CVE-2021-21224 - Chrome v8 issue 1195777分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论