内容简介:今天我们文章介绍的是CVE-2018-8391,对应的在这里我除了介绍漏洞本身以外,还介绍了在引入了Spectre Mitigation之后的一种通用的Array OOB RW利用方法.关于这个漏洞,我们还有后续的Story2.这里i就是inductionVariable,并且arr2用i进行数组访问.优化的详情在doLowerBoundCheck和doUpperBoundCheck这两个函数中.这里用doUpperBoundCheck作为例子.
Author: Qixun Zhao(@S0rryMybad) of Qihoo 360 Vulcan Team
今天我们文章介绍的是CVE-2018-8391,对应的 patch commit . 这是一个关于Loop循环的越界读写漏洞,漏洞的成因十分有趣.我们都知道零乘以无限等于零,但是开发人员在写代码的时候忽略了这样的一种特殊情况.
在这里我除了介绍漏洞本身以外,还介绍了在引入了Spectre Mitigation之后的一种通用的Array OOB RW利用方法.关于这个漏洞,我们还有后续的Story2.
实验环境: chakraCore-2018-8-15附近的commit
0x0 关于Loop的优化
在之前的文章中我们已经简单介绍过关于Loop的优化,在编译器的优化过程中,我们需要把很多在Loop中不需要变化的指令hoist到LandingPad中,不然每次循环会执行很多没必要的指令.而在针对数组的边界检查中,有一种特殊的优化处理方法,这种优化是针对在循环inductionVariable并且用inductionVariable进行数组访问的情况.inductionVariable就是循环中的自变量.举个例子最直接:
这里i就是inductionVariable,并且arr2用i进行数组访问.优化的详情在doLowerBoundCheck和doUpperBoundCheck这两个函数中.这里用doUpperBoundCheck作为例子.
我们可以看到最下面有一个|CreateBoundsCheckInstr|的函数,用于生成一个boundcheck指令,用于检查|indexSym <= headSegmentLength + offset (src1 <= src2 + dst)|(注释已经很清楚).只需要通过这个检查,在下面的循环中就不会再有任何边界检查,因为已经hoist到LandingPad中,问题的关键就出在这个边界检查中.所以关键是这个检查是怎么保证在循环中数组的访问一定不会发生越界呢?
HeadSegmentLength很清楚就是数组的长度,问题就在于这个indexSym是怎么得来的,通过阅览代码我们可以发现是在上面的函数|GenerateSecondaryInductionVariableBound|(生成的hoistInfo.IndexSym最终用于初始化lowerBound这个Opnd).
0x1 GenerateSecondaryInductionVariableBound的计算方法
这个函数根据字面意思已经很清楚,就是计算inductionVariable的取值范围,只有inductionVariable的最大值少于HeadSegmentLength,循环中的数组访问必定不会越界.
至于计算的方法其实代码的注释已经十分清楚,下面我截取代码的注释来解释:
inductionVariable就是我们i的初始化值,也就是我们上图的start,而loopCountMinusOne的计算方法在GenerateLoopCount函数中:
在这里我用我小学毕业的数学知识把这个公式变换一下,得到如下的等式,当然要注意运算符号的顺序: (left - right + offset) / minMagnitudeChange * maxMagnitudeChange + inductionVariable
这里简单结合js代码介绍一下各个变量的含义:
函数中:
Left对应的是end变量,right对应的是start,至于offset我们不用太在乎,如果判断条件是|i 结合我们文章的题目,聪明的读者肯定已经想到问题出在哪里. 上述的公式在计算i的取值范围的时候已经十分保守了,因为没可能每一次循环i都是增加最大值,但是它忽略了一种特殊情况:zero.当(end- start - 1) / 1等于0的时候,无论它后面乘以多大的数,结果都是0,最后边界检查就是只需要start < headSegmentLength即可,而这个边界检查是不安全的(试想maxMagnitudeChange 远远大于headSegmentLength). 有了越界读写的能力,下一步就是如何利用了,chakraCore在这个 commit 中加入了一个mitigation, 这个commit简单来说在每一次数组访问的时候都会再次检查index是否少于数组的长度,如果不少于就直接crash,本来是用于防御Spectre,但是也把这些越界读写漏洞堵住了.换句话说,即使bypass了boundcheck,还要这些mask指令需要bypass.在刚引入的时候,很多人都觉得这种越界读写的漏洞不能再利用了. 这些指令的引入是十分拖累速度的,千辛万苦才消去了boundcheck的检查,又引入这个措施等于boundcheck的消去毫无意义,特别是在Loop中,每一次的循环都要运行这些没必要的mask指令,因此微软很快就引入了一个优化措施,在某些情况下hoist这些mask指令到循环外.由于这个优化措施比较复杂,这里只能简单介绍一下,它存在于Backward阶段的processBlock中,相关代码如下: 首先遍历所有的opnd,查看这个opnd的有没有type-specialized,这里我们可以理解成有没有针对特定类型的优化,例如Float64等等,如果没有则记录下这个Sym的id,记录下的id最终在这里进行判断: 如果这里满足两个条件,如果是LdElemI_A指令并且之前没有把Opnd的Sym记录下来,则把这个指令SetIsSafeToSpeculate(true);意思是不需要添加mask指令,最终在一个air block中加入防御指令: 这个指令是架构相关的,不同架构有不同实现,这里与我们讨论的无关,不再展开. 换句话说,第一数组的访问必须在loop里面,触发它的loop优化机制,第二我们只能进行数组的load并且数组是int32类型或者float64类型,则我们可以把mask指令hoist到loop外.但是单单有这样的越界读(除非再多一个object数组的越界读)是不够的,我们需要更多的东西去RCE. 有了越界读,我们是可以越界读取一个missingValue的值的,只要我们首先初始化一个数组,然后把这个数组的length重新设置,例如: 则在它的index 4的地方有一个missingValue,同时也满足了HasNoMissingValue为true,如果不满足在后续我们JIT取出该值的时候是要bailout的,内存区域如下: 这时候如果我们能off by one index,我们就能读取到这个missingValue,然后我们可以用这个missingValue创建一个evil Array:HasNoMissingValue为true,但是headSegment中带有missingValue,最终创建evil Array的PoC如下: 有了这样的数组,离RCE还远吗,网上已经有大量的利用例子.可以参考我们的第一篇文章或者 project-zero 或者 From zero to zero day 剩下的就作为读者的练习吧.0x2 Mom,零乘以无限等于多少?
0x3 Hi, MissingValue Again
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
软件预构艺术(中文版)
Ken Pugh / O'Reilly Taiwan公司 / 东南大学 / 2010-6 / 26.00元
利用经验累积而得到的洞察力开发新的解决方案被称为预构。透过重构而获得的专业知识也属于这类经验,而预构的词源即重构。重构是修改程序或软件系统内部结构的实践,以此在保留其现有行为的基础上改良设计。重构的原因有多种:方便后期增加功能、提高可维护性、提升性能。 本书作者是经验老道的软件开发人员。书中,作者运用他个人和其他众多开发人员的丰富经验,展示由其推衍而得的各项实践方针。这些方针把优秀的开发人员......一起来看看 《软件预构艺术(中文版)》 这本书的介绍吧!