UTCTF2025 - ECorp (v8pwn)

After finishing my FYP Part 1, I finally have some free time to learn browser v8 exploitaiton based on the UTCTF challenge, E-Corp Part 2

Challenge Files

chevron-rightserver.pyhashtag
!/usr/bin/env python3 

import os
import subprocess
import sys
import tempfile

print("Size of Exploit: ", flush=True)
input_size = int(input())
print("Script: ", flush=True)
script_contents = sys.stdin.read(input_size)
with tempfile.NamedTemporaryFile(buffering=0) as f:
    f.write(script_contents.encode("utf-8"))
    print("Running. Good luck! ", flush=True)
    res = subprocess.run(["/d8", f.name], timeout=20, stdout=1, stderr=2, stdin=0)
    print("Done!", flush=True)

We are given a server file which asks for the size and content of our exploit. It will then call d8 with our exploit.

chevron-rightDockerfilehashtag
FROM ubuntu:20.04

RUN apt-get update
RUN apt-get update && apt-get install -y build-essential socat libseccomp-dev python3

ARG FLAG
ENV FLAG $FLAG

WORKDIR /
COPY start.sh /start.sh
RUN chmod 755 /start.sh
COPY d8 /d8
RUN chmod 755 /d8
COPY snapshot_blob.bin /snapshot_blob.bin
RUN chmod 755 /snapshot_blob.bin
COPY server.py /server.py
RUN chmod 755 /server.py

# random flag filename
RUN FLAG_FILE=$(cat /dev/urandom | tr -cd 'a-f0-9' | head -c 32); \
    echo $FLAG > $FLAG_FILE; \
    chmod a=r $FLAG_FILE; \
    unset FLAG_FILE

EXPOSE 9000

CMD ["/start.sh"]

Dockerfile of the challenge

d8

We are also given a d8 file, which is the standalone binary of the JavaScript engine V8. V8 is the JavaScript engine of Google Chrome.

chevron-rightargs.gnhashtag

The args.gn file is a list of arguments that will be used by d8. We can see that the V8 sandbox is disabled. So we don't need to escape the sandbox.

chevron-rightpatchhashtag

You're looking at a custom patch applied to the V8 engine for UTCTF 2025, which introduces a new built-in function called Array.prototype.confuse which is defined in builtins-array.c.

Additional Knowledge

Pointer Tagging

Pointer tagging is a mechanism used to efficiently store and differentiate between different types of data, such as pointers, immediate small integers (Smis), and doubles.

  • Doubles - actual 64 bit representation without any changes

  • Pointers - points to objects, the least significant bit is set to 1 (eg: 0x5678 is represented as 0x5679)

  • SMI - integer that is bitshifted left by 1 bit when in memory (eg: 3 will be stored as 0b110)

Pointer Compression

Typically, pointers are 8 bytes and takes up alot of memory. In pointer compression, the actual pointer values can be represented in 4 bytes. These 4 bytes will store the offset of the object. To calculate the actual address, we can do base_addr + offsetto get the actual address. Example :

Base address = 0x00007fff_00000000

Offset = 0xdeadbeef

Actual address = base + offset = 0x7fff_deadbeef

Helpful Functions

chevron-righthelpful_functions.jshashtag

Any address leaks that you get is in double precision floating point since their 64 bit representation is not modified. We would need these js functions to parse them

chevron-righthelpful_functions_2.jshashtag

These DebugPrint() native JS functions that can give you extra information

Debugging

chevron-rightdebug.pyhashtag

Goal

Most of the time, when exploiting a JavaScript engine, we want the primitives addrof and fakeobj.

With addrof, you can leak the address of an object. With fakeobj, you can create a fake object, allowing you to control it using two variables.

Objects in v8

Lets create object a and view it in memory

object a

We can see our JSArray at 0x227c00042b10

JSArray

In memory, it stores pointers as 32 bit values due to pointer compression. The first value 0x14b8ed corresponds to a MAP object at 0x227c0014b8ec. Next, 0x725 is our properties. Then, 0x15354d is our elements pointer. Finally, 0x00000004is the length of our object bitshifted to the left once (original value would be 2).

Map object

In V8, every JavaScript object, including arrays, is linked to a Map object. The Map in V8 contains metadata and layout information about the structure of the object. It's essentially the blueprint of how the object is structured and how it should behave.

Pointer to elements array

Elements refer to the individual items or values that are stored within the array

Methods of Getting RCE

WASM to RCE

We can create a sample wasm file like such and then compile it using wat2wasm from the web assembly toolkit.

Then we can use xxd to extract the bytes

With the bytes we have, we can put it in the Uint8Array

Now theres a pointer to trusted data

Now, there is a new RWX memory allocated. However, this method did not work for this challenge as the CPU has a feature called pkeyarrow-up-right which prevents writing to that new memory region.

JIT to RCE

In V8 (the JavaScript engine), when a function is called many times, it becomes "hot" and the engine tries to speed it up by compiling it using a Just-In-Time (JIT) compiler like Maglev or Turbofan. This compiled code is stored in memory that can be executed (RWX: Read, Write, Execute). So, if you make a function that returns raw bytes (your shellcode) and force V8 to compile it by calling it many times, V8 will place that compiled code — including your shellcode — in executable memory. Then, with a vulnerability, you can hijack the control flow to jump to that memory and run the shellcode, leading to remote code execution (RCE).

This function will be placed in a RWX memory region which we will abuse for RCE.

Last updated