
CVE-2024-29943, but with wasm
An end-to-end exploit chain for CVE-2024-29943 that achieves arbitrary
native code execution by smuggling shellcode into SpiderMonkey's WebAssembly
JIT code page as f64.const immediates — no ROP, no VirtualProtect, no
external toolchain.
Verified working against the SpiderMonkey shell from the
2024-03-20 mozilla-central nightly (JavaScript-C126.0a1, linux x86-64,
pre-fix): the chain runs to completion and the payload executes
(write(1, "PWNED by wasm!\n"); exit(0)).
CVE-2024-29943 is a sec-critical IonMonkey range-analysis bug used at Pwn2Own 2024 by Manfred Paul, affecting Firefox < 124.0.1 (Bugzilla 1886849, CVSS 9.8).
MObjectKeysLength::computeRange returned an incorrect integer range for
Object.keys(x).length after Object.keys was made elidable (regressed by
bug 1845728). Ion range analysis concluded a loop counter could not go
negative and eliminated bounds checks that were still reachable, yielding an
out-of-bounds read/write on a Uint8Array.
Object.keys range-analysis bug -> OOB r/w on Uint8Array
-> corrupt adjacent ArrayBuffer -> addrof / fakeobj / arbitrary R/W
-> instantiate hand-built WASM module (shellcode as f64.const immediates)
-> WasmInstanceObject -> wasm::Instance -> wasm::Code -> CodeTier
-> ModuleSegment.bytes_ (the JIT code page, RX)
-> scan page for marker constant
-> overwrite FuncExport.eagerInterpEntryOffset_ (plain heap)
with the shellcode's offset within the page
-> call the export through the interpreter -> payload runs
Two details cost us a segfault each to learn, so they're written down:
eagerInterpEntryOffset_ in the
heap-resident FuncExport instead — the interpreter computes
codeBase + offset and jumps there.The WASM-JIT-page shellcode technique is from WasmBlazeFox; this repo demonstrates that the technique composes with a modern, shipping-browser bug in place of the original 2018 training bug.
poc.js — minimal trigger for the range-analysis bug.exploit.js — full chain: primitives + WASM JIT shellcode stage.gen_wasm.py — assembles the WASM module embedding shellcode and verifies
the constant round-trip. python3 gen_wasm.py mysc.bin to use a different
payload. The default payload is a pure-syscall
write(1, "PWNED by wasm!\n"); exit(0) proof that needs no symbol
resolution (so it also works in the bare jsshell). For the classic
libxul-based system("gnome-calculator") payload, see WasmBlazeFox ex6.test.gdb — breakpoints and ptype /o helpers for deriving object-model
offsets on a given build.afbdf6822c9e9f9b6d44b9ea6904cb10878126b1 (Firefox ~124, pre-124.0.1),
Linux x86-64 — or grab a pre-fix nightly jsshell, e.g.
archive.mozilla.org/pub/firefox/nightly/2024/03/2024-03-20-21-16-35-mozilla-central/jsshell-linux-x86_64.zip.LD_LIBRARY_PATH=<jsshell dir> ./js --no-threads --ion-offthread-compile=off \
--spectre-mitigations=off poc.js # trigger only (segfault)
LD_LIBRARY_PATH=<jsshell dir> ./js --no-threads --ion-offthread-compile=off \
--spectre-mitigations=off exploit.js # full chain ("PWNED by wasm!")
--spectre-mitigations=off is required because the bounds-check elimination
relies on index masking being disabled (see the Bugzilla comments).
f64.const immediates are emitted inline
in the code segment. Ion compilation could constant-fold them.WASM_INSTANCE_OFF_CODE = 0xa8, MetadataTier.funcExports + 448,
FuncExport + 8, etc.) were derived from the vulnerable commit's headers
and confirmed against the 2024-03-20 nightly jsshell; re-derive with
test.gdb for other builds.