• Chrome漏洞分析与利用(十三)——issue-1182647(CVE 2021-21195)漏洞分析


    POC

    let s = [];
    function foo() {
        var arr = Array(1000);
        
        function bar() {
            try { 
                const err_obj = {a: p4nda, b: arr}; 
            } catch(e) {
                return e;
            }
        }
        for (var i = 0; i < 25; i++) bar();
        
        var ret = bar();
        /p4nda/.test({});  // Deopt here.
        s[0] = arr;
        arr.shift();
        return ret;
    }
    
    %PrepareFunctionForOptimization(foo);
    foo();
    foo();
    %OptimizeFunctionOnNextCall(foo);
    let r = foo();
    gc();
    gc();
    
    • 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

    由于调用了gc函数,所以在使用d8执行时需要加上–expose-gc 参数,执行后。

    漏洞分析

    调试

    issue 1182647
    通过issue页面对漏洞的描述大致可知当在优化函数中发生错误时会去创建JSError对象,而在创建该对象的过程中会去调用TranslatedValue::GetValue函数,该函数会通过寄存器以及堆栈中存储的信息生成一些对象,而当代码发生解优化时同样也会去调用TranslatedValue::GetValue函数此时就会导致同样一个对象被创建了两次,对其中一个对象进行一些操作时,例如Array.prototype.shift。对另一个对象并不会产生影响,所以就会导致另一个对象指针悬留在旧内存空间。
    对解优化JSArray对象调用Array.prototype.shift后,通过JSErro获取到保存在Function对象中的JSArray对象arr。
    在这里插入图片描述
    对解优化JSArray对象调用Array.prototype.shift后,解优化后的JSArray对象arr。
    在这里插入图片描述
    首次创建arr副本是在创建JSError对象过程中所调用的OptimizedFrame::Summarize函数中,该函数会打包发生错误的函数对象及其上下文。
    在这里插入图片描述
    在这里插入图片描述
    通过function对象的context获取其上下文内容,并通过其上下文找到arr对象副本。
    在这里插入图片描述
    通过观察可以发现其对象地址为0x69082d2eb1,elements地址为0x690814eaf9
    第二次创建arr对象是在poc代码执行到/p4nda/.test({});时触发解优化,此时会去调用Deoptimizer::MaterializeHeapObjects函数。
    在这里插入图片描述
    此时会直接取到函数的context,通过上下文可以获取到arr对象。
    在这里插入图片描述
    此时arr对象地址为0x69082d3c61,而elements地址为0x690814eaf9,此时就得到了一个与JSError中arr对象使用相同elements的不同数组对象。
    在这里插入图片描述
    Array.prototype.shift函数会将数组中的第一个元素返回并删除,此操作会改变数组elements的长度并且会将其余数据向后移动。而此函数只会影响在Deoptimizer::MaterializeHeapObjects函数中生成arr对象,而保存在JSError对象中的arr对象则不会受影响此时JSError中的arr对象elements将会指向一个填充对象
    在这里插入图片描述
    在这里插入图片描述
    在第一次GC后可以发现JSError中创建的arr对象elements已经被释放
    在这里插入图片描述
    当执行至第二次gc时会从arr elements取出map地址并从中获取数据,由于释放后的填充值并非一个合法的指针于是导致触发Crash
    在这里插入图片描述
    通过观察访问的地址可知该值位于arr对象map地址的+0x6偏移处,通过另一个在解优化过程中创建的arr对象map可知此偏移处为visitor_id字段
    在这里插入图片描述
    在这里插入图片描述

    源码分析

    逃逸分析

    逃逸分析阶段对此漏洞有很大的影响,解优化的结果会直接影响到Summarize与MaterializeHeapObjects函数中是否会具体化某个对象,通过turbolizer图可以找到arr对象对应的Allocate节点:
    在这里插入图片描述
    而在执行过逃逸分析阶段后,此节点将会变成不可达节点:
    在这里插入图片描述
    然后结合代码分析,在逃逸分析阶段对于Allocate节点会一律为该节点创建并初始化一个VirtualObject对象:
    在这里插入图片描述
    VirtualObject对象有两个函数:SetEscaped与HasEscaped,这两个函数一般用于设置与获取对象的逃逸状态,默认为非逃逸,而对象的逃逸状态将会决定节点是否会在逃逸分析阶段的最后被放松变得不可达,同时也决定着对象是否需要被具体化:
    在这里插入图片描述
    然后查看Allocate节点的所有output节点:
    在这里插入图片描述
    有Allocate节点的地方肯定会有FinishRegion节点,当遇到FinishRegion节点时会将自身节点的第一个输入节点的VirtualObject对象设为自身的VirtualObject对象,事实上通常FinishRegion节点的第一个输入都是是Allocate节点,此节点不会操作对象的逃逸状态:
    在这里插入图片描述
    在这里插入图片描述
    StoreField节点负责保存对象的各属性:
    在这里插入图片描述
    当遇到StoreField节点时会获取第一和第二个输入节点,第一个输入通常是Allocate节点,第二个输入通常是要保存的内容例如elements地址,length等,随后判断其输入的Allocate节点的VirtualObject是否不为空以及是否还是未逃逸,当Vobject不为空且未逃逸就将StoreField节点标记为Dead,否则就将其对应的Allocate节点及其value设为逃逸:
    在这里插入图片描述
    除此以外FinishRegion节点的output节点也会影响对象的逃逸状态,而在21195的原POC中FinishRegion只有一个StoreField节点,该节点用于将arr对象存入function对象context中,处理方式同上,所以对于21195的原POC arr对象将会是默认的非逃逸对象,而非逃逸对象会在逃逸分析阶段的最后被放松成为不可达节点:
    在这里插入图片描述
    除此以外非逃逸对象也会在解优化与Summarize函数中被具体化(重复构建)。

    Error对象构建

    在构造Error对象时会通过栈追踪信息来遍历迭代器获取所有StackFrame,再根据不同类型的Frame去调用对应的Summarize函数:
    在这里插入图片描述
    当在迭代器中找到未优化函数时会调用UnoptimizedFrame::Summarize函数,该函数的function与receiver都是从内存缓存中直接获取的:
    在这里插入图片描述
    在这里插入图片描述
    当在迭代器中找到优化函数后就会调用OptimizedFrame::Summarize函数,Summarize函数会初始化一个TranslatedState迭代器通过此迭代器先依次遍历获取栈中每个Frame的kind并进行比较当有满足条件的Frame时就继续执行获取对应的TranslatedValue对象:
    在这里插入图片描述
    按照我对TranslatedFrame的理解,每个TranslatedFrame对应一个FrameState节点,TranslatedFrame中保存的信息都是通过FrameState节点收集的。
    随后通过TranslatedValue::GetValue函数具体化并返回对象或函数,google将此处视为一个正确性漏洞,在最新版的代码中已经在此处添加了CHECK进行检查,但在CVE 2021-21195的环境中不存在此CHECK:
    在这里插入图片描述
    TranslatedValue::GetValue函数会先判断Object是否是smi类型如果是就需要为其创建一个HeapNumber并返回,然后再判断Object是否是只读如果不是就直接返回
    在这里插入图片描述
    如果以上两个分支都不满足并且Frame属于捕获或者重复对象时,就去具体化对象,当在处理function对象时还会将其上下文中的关联且未逃逸的对象也进行具体化,这其中就包括arr数组对象。
    在这里插入图片描述
    至此JSError中保存的arr对象创建完成

    解优化

    在对JIT代码进行解优化时也会对所需的对象进行具体化,而arr就是所需对象之一,所以在该函数内arr对象会被再次创建一次
    在这里插入图片描述

    漏洞利用

    原版POC无法获取到Error对象中创建的arr对象,因为arr对象保存在function的context中,所以原POC是无法被利用的,但是可以在原POC的基础上进行一些微调

    // --allow-natives-syntax --expose-gc 
    var e = {C: null, M: null};
    
    function vul(flg) {
        const arr = Array(1000);
        function inlined_func(a, b, c) {
            Error.prepareStackTrace = function(t, B) {
                return B[c].getThis();
            };
            let p = Error().stack;
            return p;
        }
        for(var i = 0;i<25;i++){
            inlined_func(1,2,1); 
        }
        e.M = inlined_func.call(arr, 4, 5, 0);
        if(flg){
            /p4nda/.test({}); // deopt here
            arr.fill({});  // modify element in -*runtime*-
            e.C = arr;
        }
    }
    
    //print("------------------- run as builtin------------------");
    for(let i = 0; i < 0x3000; i++) vul(false);  
    //print("------------------- run as jit------------------");
    
    var res = vul(true);
    %DebugPrint(e.M);
    %DebugPrint(e.C);
    %SystemBreak();
    
    • 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

    通过这种方法就可以拿到两个地址不同但elements相同的arr数组对象:
    在这里插入图片描述
    在这里插入图片描述
    从目前的情况来看,就可以通过像cve 2022-1364那样在一个数组对象里删除元素填入hole然后再从另一个数组对象取出hole继续后续的利用步骤,但是需要注意的是此时的两个数组对象及其elements都是HOLEY_ELEMENTS此种类型的FixedArray在获取元素时有较为严苛的检查机制,这会导致我们拿不到hole,这一点可以从GetEntryForIndexImpl函数中看出,当elements类型是HOLEY时,在获取元素下标InternalIndex对象时,会先判断其index是否小于数组长度并且元素值不能为hole,如果这两个条件有一个不满足的话就会返回NotFound,实际上就是一个max值,而相反的如果elements不是HOLEY类型,就只是检查index是否小于数组长度:
    在这里插入图片描述

    在这里插入图片描述
    所以现在需要将数组转化为PACKED_ELEMENTS类型,可以尝试对arr对象进行以下方式的初始化:

    const arr = [
    	1, 2, 3, 4, 5, 6, 7, 8,
    	1, 2, 3, 4, 5, 6, 7, 8,
    	1, 2, 3, 4, 5, 6, 7, 8,
    	1, 2, 3, 4, 5, 6, 7, 8,
    	1, 2, 3, 4, 5, 6, 7, 8,
    	1, 2, 3, 4, 5, 6, 7, 8,
    	1, 2, 3, 4, 5, 6, 7, 8,
    	1, 2, 3, 4, 5, 6, 7, 8,
    ];
    ......
    //注释掉数组填充操作
    //arr.fill({});
    .....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    理论上来讲只要元素足够就可以成功:
    在这里插入图片描述
    在这里插入图片描述
    事实上从表面上看也确实是成功了,但是要注意的是此时elements虽然是PACKED_SMI_ELEMETS类型,但是有COW标记,COW是“写入时拷贝”的缩写,大致就是说当同一个资源/内存,被不同的对象所引用时就会将其标记为只读,当使用对象向其中写入或修改数据时就会创建一个新的副本返回给调用者并且保证原内容依然保持原样,所以当我们向上文中拿到的两个arr对象的其中一个写入或者删除数据时,这两个数组对象就会指向不同的elements最终导致我们无法达到目的。除此之外还尝试了其他很多关于将数组对象转换为PACKED数组的方法,但是基本上都会因为逃逸或者COW原因无法成功。
    最后得出结论数组对象的各种检查机制过于苛刻在此处不可用该对象来进行利用,或许可能还有其他好的办法但我没有找到,现在还剩一个思路那就是更换其他对象,目前已知符合要求的对象就是arguments对象,但在此POC中直接使用arguments对象会导致arguments对象逃逸从而导致无法被具体化,但目前也没有找到其他合适的对象,所以最终采取直接套用CVE 2022-1364POC,在其原POC的基础上进行一些调整,在CVE 2022-1364原POC中获取到的两个指向同一elements的arguments对象都是在优化阶段通过创建Error对象时获得的,我们只需要让其获取一次,另一次在解优化后获取即可变成cve 2021-21195的漏洞触发流程,最后获取到hole后就可以进行读写原语的构造,最终效果:
    在这里插入图片描述

  • 相关阅读:
    Qt | windows Qt6.5.3安卓环境搭建成功版(保姆级教程)
    UE5实战篇二(对话系统1):导语
    js的算法-插入排序(直接插入排序)
    小学生Python编程 —— 欢乐钢琴
    前端css实现特殊日期网页变灰功能
    Flutter学习:使用CustomPaint绘制路径
    软件测试缺陷报告
    Day 45 | 70. 进阶爬楼梯 & 322. 零钱兑换 & 279.完全平方数
    【数据结构初阶】C语言从0到1实现希尔排序
    Windows开启监控
  • 原文地址:https://blog.csdn.net/weixin_43815930/article/details/124582645