addrof/fakeobj are the primitives; a JIT bug is one way to get them. This part is the finish line they all share: parlay them into arbitrary read/write, then into code execution. The first half is mechanical; the second is where the last decade of browser hardening lives.
- have
- addrof + fakeobj (or an OOB array)
- step 1
- a TypedArray with a backing-store pointer YOU control -> arbitrary R/W
- step 2
- R/W -> code execution (and the mitigations fighting step 2)
the master primitive: a fake TypedArray#
A TypedArray/DataView is just a header plus a pointer to its backing store. If
you can forge that header with a chosen backing-store pointer, every array[i]
read/write becomes a read/write at any address you set. With fakeobj you do
exactly that:
|
Rewrite the backing-store slot before each access and the same fake array reads or writes anywhere in the process. That's the arbitrary read/write every JS-engine exploit is built to reach. From here you leak the engine's own pointers, walk structures, and find your code-exec target.
step 2: R/W → code execution#
Historically this was trivial: JITed code lived in RWX pages, so you wrote shellcode straight into a JIT'd function's code and called it. The classic reliable variant used WebAssembly — instantiating a WASM module created an RWX page; overwrite its body via your R/W and jump in.
the mitigations that moved the finish line#
Step 1 still works; step 2 is where modern engines fight back, and it's why a present-day browser chain is so much more than "get R/W":
- W^X for the JIT. RWX JIT pages are gone — code is written through a separate, short-lived mapping and executed read-only. No more "write shellcode into the JIT page." You pivot to a ROP/JOP chain or data-only techniques instead.
- The V8 heap sandbox ("Ubercage"). Pointers inside the V8 heap are sandboxed — backing-store pointers become offsets within a reserved cage, so a fake TypedArray can only reach inside the cage, not arbitrary process memory. Arbitrary-R/W-in-the-cage no longer means arbitrary-R/W-in-the-process; you now need a second bug (an "out-of-sandbox" primitive) to escape it.
- Pointer compression changes addresses to 32-bit cage offsets — your
addrof/R/W arithmetic adapts to the compressed representation. - CFI / CET on the host then constrains the control-flow hijack at the end.
arbitrary R/W, then a shell (pre-sandbox era)
the shape of the whole field#
- The chain is layered now. Bug → addrof/fakeobj → in-cage R/W → sandbox escape → W^X-defeating code-exec. Each mitigation added a stage; none removed the one before it.
- Data-only is ascendant. Because code-exec is so guarded, many modern chains stay in data: corrupt interpreter state, function pointers the engine itself calls, or WASM/JIT metadata, never executing injected bytes at all.
- Two bugs are the norm. The sandbox means one memory bug often isn't enough — you bring a renderer bug and a sandbox-escape bug, which is why full chains are team-scale efforts.
That's browser exploitation end to end: a bug becomes addrof/fakeobj, those become a fake TypedArray and arbitrary R/W, and the rest of the chain is a fight against the cage and W^X. The primitives are eternal; the finish line keeps moving, and following where it moves is the whole game.