首页 >> 网络安全 >>漏洞分析 >> 利用CVE-2019-17026-Firefox JIT错误
详细内容

利用CVE-2019-17026-Firefox JIT错误

时间:2020-09-16        阅读

内容

介绍

  1. IonMonkey基础

  2. 漏洞概述

  3. 别名分析

  4. 全球价值编号

  5. 重新检查PoC

  6. 利用基元

  7. 阵列概述

  8. 越界

  9. 弱读/写

  10. 弱地址

  11. 读得更强

  12. 准时喷雾

  • 关于沙盒逃生的注意事项

  • 最后说明

介绍

浏览器利用是安全研究中一个极为独特的领域。随着浏览器不断发展以支持新媒体和协议,其攻击面也在不断发展。甚至JavaScript引擎本身也在不断改进。在Google为以更快的速度JIT编译和执行JavaScript铺平道路之后,其他现代浏览器也开始添加此功能以进行竞争。这导致这些引擎包含其自己的优化编译器,对代码执行复杂的分析以将其缩减为仅必要的指令,并使Web应用程序比以前想象的更快。但是,JIT编译很难正确完成,尤其是在使用一种已经存在的最动态的语言时,因此已成为浏览器研究的最大趋势之一。


早在5月,我就Internet Explorer漏洞(CVE-2020-0674)写了另一篇博客文章。它最初是由Qihoo 360捕获的,同时还有另一个针对Firefox Spidermonkey JIT引擎(IonMonkey)的漏洞(CVE-2019-17026)。这篇博客文章旨在分析根本原因,并讨论攻击者可以利用此漏洞类别的方式。


IonMonkey基础

SpiderMonkey使用一个名为IonMonkey的JIT编译器,该编译器将JavaScript代码转换为已编译的代码,以便更快地运行。但是,仅转移到机器代码还不足以显着提高性能。优化和类型推断都用于将机器代码简化为基本指令,从而使浏览器像优化编译器一样工作。


IonMonkey通过执行几个步骤来做到这一点,总结如下:


  • 中级中间表示(MIR)生成

  1. 将解释器使用的原始字节码转换为控制流图(CFG)中使用的中间表示(IR)节点。

  2. 这样做是因为字节码对于解释器来说很容易使用,但对于JIT编译器则不容易使用。

优化

  1. 在生成的MIR节点上使用了许多优化过程,以确定可以减少功能大小和执行时间的方式。

  2. 常见的优化过程包括别名分析,全局值编号,恒定折叠,不可达代码消除,死代码消除和边界检查消除。

降低

  1. 降低阶段将MIR代码转换为低级中间表示(LIR)。虽然MIR并不依赖于给定的体系结构,但是LIR确实如此,因为LIR是准备优化代码生成的关键步骤。

  2. 此阶段还使用虚拟寄存器,并使用寄存器分配来分配寄存器或堆栈位置。

代码生成

  1. 从LIR生成机器代码。

  2. 通过将机器代码与可执行JIT代码的一组假设一起分配给可执行存储器来链接机器代码。

漏洞概述

Mozilla将该漏洞以IonMonkey type confusion与StoreElementHole 和 FallibleStoreElement的形式列出,描述如下:

IonMonkey JIT编译器中用于设置数组元素的别名信息不正确可能会导致类型混乱。我们知道野蛮滥用此漏洞的针对性攻击。

let arr1 = [];
let arr2 = [1.1, 2.2, , 4.4];
arr2.__defineSetter__("-1", function(x) {
    delete arr1.x;
});
{
    function f(b, index) {
        let ai = {};
        let aT = {};
        arr1.x = ai;
        if (b)
            arr1.x = aT;
        arr2[index] = 1.1;
        arr1.x.x4 = 0;
    }
    delete arr1.x;
    for (let i = 0; i < 0x1000; i++) {
        arr2.length = 4;
        f((i & 1) === 1, 5);
    }
    f(true, -1);
}

通读补丁程序,它显示两个文件已被更改:

  • AliasAnalysis.cpp

  • MIR.h

关注的主要领域是对MIR代码的更改。无论MStoreElementHole和MFallibleStoreElement不再占用getAliasSet方法和返回AliasSet ::商店(AliasSet :: ObjectFields | AliasSet ::元)。综观默认getAliasSet的方法StoreDependency(父类的MStoreElementHole),你可以看到它,而不是返回AliasSet ::商店(AliasSet ::任何的);


别名分析

0vercl0k对两次优化遍历(别名分析和全局值编号)的工作方式进行了非常详尽的撰写,成为Mozilla所需的缺少的文档。因此,在这篇文章中,我将讨论一个更高级的观点,以帮助缓解不熟悉编译器理论和进行的那种优化的读者。有关这两个阶段如何工作的更多信息,强烈建议您阅读该博客文章,以补充本文的高级观点。


别名分析是从加载指令和存储指令中识别依赖关系的过程。可以在以后的阶段中使用它来删除冗余代码。别名分析算法相对来说比较琐碎,可以通过遍历MIR指令来识别依赖关系:


如果getAliasSet函数返回一个Store AliasSet(称为AliasSet :: Store),则将其保存以供以后与Load指令进行比较。

如果getAliasSet函数返回Load AliasSet(被称为AliasSet :: Load),则将使用一种算法来查找对任何当前找到的Store指令的依赖关系。通过执行三个步骤可以找到此依赖关系:

  • 匹配的加载/存储别名

  1. 每个节点都定义了一组别名,这些别名与它们具有的效果类型相对应(通过重写getAliasSet方法)

  2. 在别名分析阶段,当一条指令返回AliasSet :: Load时,将遍历当前找到的AliasSet :: Store节点的数组,以检查是否有任何重叠的效果。

  3. 例如,AliasSet :: Load(AliasSet :: Element)与AliasSet :: Store(AliasSet :: Element | AliasSet :: ObjectFields)相交。

匹配的操作数类型集

  • 加载和存储指令具有操作数。

  • 函数AliasAnalysis :: GetObject用于获取节点并找到其操作的根节点

  1. 例如,InitializedLength节点获取数组元素的初始化长度。第一个操作数是Elements节点。Elements节点的第一个操作数是Constant Array对象节点。因此,GetObject将返回此节点作为根节点。

IonMonkey跟踪潜在的节点类型。此信息存储在TypeSets中。

如果两个指令都在操作的对象的潜在类型不重叠,则不能将它们视为别名。例如,如果我们有一个对象A和一个对象B,它们的类型集(均为TYPE_FLAG_ANYOBJECT)相交(在这种情况下,它们是完全相同的),因此被视为潜在依赖项。

最近比赛

  • 从可能相关的存储指令列表中(具有与Load指令重叠的AliasSet和TypeSets),选择最新的指令。然后在该Store指令上为Load指令创建一个依赖项。

为了更好地说明这一点,请考虑以下JavaScript代码:

function jit(obj_1, obj_2) {
    delete obj_1.x;
    return obj_2.y;
}

此功能分为许多MIR指令:

image.png

image.png


别名分析完成后,在loadfixedslot7和deleteproperty6之间仅创建了一个依赖项。这是因为只有一个AliasSet :: Load指令,并且满足上述所有三个条件的最新节点是deleteproperty6节点。


全球价值编号

尽管“别名分析”阶段是漏洞的发源地,但“全球价值编号”阶段才使此漏洞可以被利用。考虑以下代码:


function f(o) {
    o.x.a = 23;
    o.x.a = 24;
}

MIR指令如下:

-参数节点
0参数THIS_SLOT
1参数0
2常量未定义
3常量未定义
4开始

-验证是否未达到堆栈递归限制
5 checkoverrecursed
6常量未定义

-取操作1的结果(参数o)和将属性'x'作为对象获取
7将参数1取消装箱到对象(可靠)

-检查函数是否太热并且需要重新编译
8重新编译检查

-从操作7的结果中加载属性'a'(未装箱的对象指向到属性x)
9 loadfixedslot unbox7:Object-

保持常数' 23'10
常数23

-将操作10(数字23)中的常数存储在操作9(属性'a')
11中获得的插槽中。storefixedslot loadfixedslot9:Object constant10:Int32-

从操作7的结果再次加载属性'a'(未装箱属性x指向的对象
12 loadfixedslot unbox7:Object-

保持常数' 24'13
常数24-

将操作13(数字24)的常数存储在操作12(属性'a')获得的插槽中
14 storefixedslot loadfixedslot12:Object constant13:Int32-装箱

返回值(未定义,因为该函数不返回值)
15 box constant3:Undefined-

从操作15返回装箱的值
16 return box15:Value

从上面的节点可以看到,属性“ a”被提取了两次(操作9和12),这似乎是多余的。这就是GVN的目的;识别并解决某些冗余。为了解决这个问题,这是在GVN通过之后MIR减少到的内容:

-参数节点
0参数THIS_SLOT
1参数0
2常数undefined
4开始

-验证是否未达到堆栈递归限制
5 checkoverrecursed-

将操作1的结果(参数o)设为'x'对象
7将参数1装箱到对象(可靠)

-检查函数是否太热并且需要重新编译
8重新编译检查

-从操作7的结果中加载属性'a'(由属性x指向的未装箱的对象)
9 loadfixedslot unbox7:Object-

保持常数' 23'10
常数23-

将操作10(数字23)的常数存储在操作9(属性'a')获得的插槽中
11 storefixedslot loadfixedslot9:Object常数10:Int32-

保持常数' 24'13
常数24-

将操作13(数字24)中的常数存储在操作12中获得的插槽中(属性'a')
14 storefixedslot loadfixedslot9:Object constant13 :Int32-

装箱返回值(未定义,因为函数不返回值)
15 box constant3:Undefined-

从操作15返回装箱的值
16 16 return box15:Value

这里的关键点是操作12(第二个loadfixedslot)已被删除。


GVN通过允许每个MIR节点重写两个功能来工作:


一致

  • 此函数检查两个节点是否是相似的表达式。如果是,则删除第二个节点,并用第一个替换对它的引用,以消除冗余指令。

折叠到

  • 此函数标识节点中的公共表达式,并允许将它们折叠。通常的示例是MNot节点,该节点当然会执行not操作。如果此操作的参数也是MNot节点,则两个节点都可以用它们正在操作的值(内部MNot的操作数)替换。

为了使两个节点一致,两个节点在提供给另一个节点时必须从各自的congruentTo函数返回相同的值。但是,仅当两个节点都依赖于同一节点时才检查一致性(别名分析阶段的结果)。关于前面的示例,两个loadfixedslot节点都依赖于start4。


您可能还想知道为什么删除第二个loadfixedslot而不删除第二个storefixedslot,因为考虑到同一索引中的两个storefixedslot似乎是多余的。答案在于MStoreFixedSlot的congruentTo函数。该节点不会覆盖默认的congruentTo函数,因此改用MDefinition :: congruentTo,它仅返回false。


一个更有趣且相关的示例是如何处理数组元素。

a = [1, 2];

function store(index, val) {
    a[index] = val;
    a[index] = val;
}

上面的代码产生以下MIR代码:

-参数节点
0参数THIS_SLOT
1参数0
2参数1
3常量未定义
4常量未定义
5开始

-验证是否未达到堆栈递归限制
6 checkovercursed-

检查参数是否为整数
7常量未定义
8取消框参数1到Int32(
无错误9取消参数框2到Int32(无错误)

-检查函数是否太热并且需要重新编译
10重新编译检查

-将数组作为可引用节点
11常量对象(Array)

-在值上运行ECMA ToNumber以从unbox8获得整数(由于我们已经知道它是int32,因此为冗余-由于congruentTo将被删除)
12 tonumberint32 unbox8:Int32-

获取数组的元素
13个元素constant11:Object

-从操作13获取元素结构的initializedLength成员
14 initializedlength elements13:Elements-

检查操作12中的int32数字是否在操作14中的int范围内
15 boundscheck tonumberint3212:Int32 initializedlength14:Int32
16 spectremaskindex boundscheck15: Int32 initializedlength14:

Int32-将参数2(unbox9)的值存储到操作13的元素数组中的操作16的偏移量中
17个存储元素元素13:元素spectremaskindex16:Int32 unbox9:Int32-

再次将数组作为可引用节点
18常量对象(Array)

-对值运行ECMA ToNumber以再次从unbox8获取整数
19 tonumberint32 unbox8:Int32-

获取数组中的元素
20个元素constant18:Object-

从操作20
21 获取elements结构的initializedLength成员initializedlength elements20:Elements-

检查操作19的int32数字是否在操作21的int范围之内
22 boundscheck tonumberint3219:Int32 initializedlength21:
Int23 23 spectremaskindex boundscheck22:Int32 initializedlength21:Int32

-存储从参数2(unbox9)到从操作20从元件阵列中的操作23的偏移值
24 storeelement elements20:的Int32:元素spectremaskindex23:的Int32 unbox9

- Box中的值'未定义'
25框constant4:未定义

-从操作25返回装箱的值
26返回box25:Value

GVN进程发生后,将保留以下节点:

-参数节点
0参数THIS_SLOT
1参数0
2参数1
3常量undefined
5 start-

验证我们尚未达到堆栈递归限制
6 checkoverresured-

检查参数是否为整数
8取消将parameter1设置为Int32(无误)
9将参数2取消框为Int32(可靠)

-检查函数是否太热并且需要重新编译
10重新编译检查

-将数组作为可引用节点
11常量对象(数组)

-获取数组的元素
13个元素constant11: Object-

从操作13获取elements结构的initializedLength成员
14个initializedlength元素13:Elements-

检查操作8中的int32数字是否在操作14中int的边界内
15 15 boundscheck unbox8:Int32 initializedlength14:Int32
16 spectremaskindex boundscheck15:Int32 initializedlength14:Int32-

存储参数2中的值( unbox9)到操作13的元素数组中距操作16的偏移量
17 storeelement elements13:Elements spectremaskindex16:Int32 unbox9:Int32-

将参数2(unbox9)的值存储到操作13的元素数组中距操作16的偏移量
24个存储元素13:Elements spectremaskindex16:Int32 unbox9:Int32-

将值'undefined'
装箱25框常数3:Undefined

-从操作25返回装箱值
26返回box25:Value

在GVN阶段删除了许多节点,但是特别有一个有趣的节点:MBoundsCheck,该节点负责确保索引在给定数组的范围内。因为两个MBoundsCheck节点都依赖于start5,所以它们的congruentTo函数已运行。这将检查两个节点的类型是否均为MBoundsCheck,是否都检查相同的最小值和最大值,并且两者都是可错误的还是两者都是可靠的指令。这样可以确保两个MBoundsCheck节点相同,因此第二个节点是冗余的并且可以删除;一个称为边界检查消除的过程。


这里要注意的重要一点是,如果两个边界检查都依赖于同一节点,则它们只能运行congruentTo。在这种情况下,这意味着JIT编译器将第一个MStoreElement节点视为没有副作用。如果可以通过该节点触发副作用,并且仍然消除了MCheckBounds,则可以使用副作用来减少数组的大小,这可能导致在存储发生时进行越界访问。这是利用此漏洞的关键。实际上,边界检查消除已在JIT开发中得到了广泛的应用,以至于v8的开发人员选择一起删除消除,以加强引擎以防止恶意使用。



.
更多

1589982338979126.png


ots网络社区

www.ots-sec.cn

联系方式
更多

投稿邮箱:1481840992@qq.com

交流群2群:622534175

ots网络社区3群:1078548359

关注我们
更多
技术支持: 建站ABC | 管理登录