Browser exploitation looks intimidating from the outside — JIT compilers, garbage collectors, hidden classes. But almost every modern JS-engine exploit funnels through the same two primitives, and once you have them the engine is effectively yours:
addrof(obj)— leak the in-heap address of a JavaScript object.fakeobj(addr)— make the engine treat memory you control as a JS object.
This series builds toward full arbitrary read/write; this part builds the foundation. Everything else is a way of obtaining, or applying, these two.
- bug
- a type confusion between PACKED_DOUBLE and PACKED (object) array elements
- goal
- addrof + fakeobj
doubles and pointers share storage#
An engine stores array elements differently depending on what's in the array. A
[1.1, 2.2] array is PACKED_DOUBLE_ELEMENTS — the backing store holds raw
IEEE-754 doubles, inline. A [{}, {}] array is PACKED_ELEMENTS — the backing
store holds tagged pointers to objects. Same slot, two interpretations: an
8-byte double, or an 8-byte object pointer.
The entire trick is to make the engine put an object into a slot and then read
that slot back as a double (you get the pointer bits → addrof), or write a
double and read it back as an object (the engine trusts your bits as a
pointer → fakeobj). A type-confusion bug that swaps an array's elements-kind
without converting the backing store gives you exactly that swap.
reinterpreting bits#
You move between doubles and integers with an ArrayBuffer aliased two ways:
the two primitives#
Given a bug that hands you a double-array flt and an object-array obj aliasing
the same backing store (the confusion), the primitives fall out:
|
addrof leaks where an object lives; fakeobj conjures an object out of an
address you choose. Point fakeobj at a buffer whose bytes you control and you've
forged a fake object header — a fake array with an attacker-chosen length and
backing-store pointer, which is the doorway to
arbitrary read/write.
the leak that starts every JS pwn
why it's always these two#
- They compose into everything.
fakeobjover a controlled buffer forges aTypedArraywhose data pointer you set → read/write anywhere.addrofgives you the addresses to aim it at. - They're bug-agnostic. A JIT typer bug, an OOB write, a UAF on an array — wildly different bugs all get massaged into "confuse a double array with an object array," because that's the shape that yields these two.
- They survive most mitigations. Pointer compression and the V8 sandbox change the representation (32-bit compressed pointers, a separate heap cage), but the addrof/fakeobj idea adapts; you're still leaking and forging in-heap references.
The mental model for the whole field: a memory bug in a JS engine is only
interesting insofar as it gets you addrof and fakeobj. Reach those two and you
stop thinking about the bug — the next two parts are the same regardless of how you
got here.