
Prova di concetto educativa per CVE-2024-4947, una confusione di tipo in V8 Maglev, che dimostra una catena completa dall'innesco fino all'esecuzione di codice arbitrario su una build d8 senza sandbox.
Una catena di sfruttamento completa e autonoma per CVE-2024-4947 (type confusion in V8 Maglev), dal trigger iniziale di type confusion fino all'esecuzione arbitraria di codice. Il PoC viene eseguito su una build non sandboxed di d8 e stampa CVE-2024-4947-PWNED su stdout come prova dell'esecuzione di codice.
⚠️ Questo è un PoC di ricerca / didattico per una vulnerabilità pubblica e corretta. Ha come bersaglio una build di sviluppo della shell V8 (
d8 --allow-natives-syntax) — non un vero browser Chrome. Vedi Disclaimer.
$ ./v8-build-nosandbox.sh # build the vulnerable d8 (WSL2 Ubuntu, ~10-20 min)
$ d8 --allow-natives-syntax --module exploit/exploit_rce.mjs
[engine] dblData0=0004f470 class=0
[inst] addr=0x001dc274 trusted_data(tagged)=0x00202bd5 td=0x00202bd4
[jt] jump_table_start = 0x00000967426cd000 (external code space)
[bridge] memory0_start -> jt, memory0_size -> huge (r/w reach jt+off)
[slot0] before: e9 3b 08 00 00
[shell] wrote 52 bytes @ jt+0x0100: VERIFIED
CVE-2024-4947-PWNED
$ echo $? # 0
Lo shellcode da 52 byte è write(1, "CVE-2024-4947-PWNED\n", 20); exit_group(0).
CVE-2024-4947 è una type confusion nel compilatore JIT Maglev di V8, corretta in Chrome 125.0.6422.60 (commit b3c01ac1e60a). È stata sfruttata in the wild dal gruppo APT Lazarus. Quando Maglev compila uno store su un oggetto module namespace (JSModuleNamespace), usa un AccessInfo errato — lo store viene compilato come un semplice mov [[obj + 4], rax] che scrive un valore controllato nel campo map di un oggetto adiacente invece di passare dal normale percorso di store delle proprietà.
Lo sfruttamento procede come segue:
NAME_DICTIONARY_TYPE (0xB2), che cambia dove V8 memorizza l'hash dell'oggetto.new WeakRef(...) così che la scrittura dell'hash finisca nello slot di lunghezza di un oggetto adiacente → accesso out-of-bounds.Da qui si ottiene il classico toolkit di V8: addrOf/fakeObj, quindi lettura/scrittura arbitraria in-cage.
CVE-2024-4947 trigger (fake NAME_DICTIONARY map + WeakRef hash write)
└─► OOB write ─► corrupt doubleArray length
└─► in-cage 4/8-byte arbitrary R/W (the "engine")
└─► overwrite WasmTrustedInstanceData.memory0_start (+0x18)
& memory0_size (+0x20) → huge
└─► wasm load8_u / store8 = clean 64-bit R/W bridge
(no software bounds check in compiled code)
└─► read jump_table_start (+0x38) — external code space
└─► jump table region is RWX (this build)
└─► write shellcode into the slack (jt+0x100)
└─► repoint func0's `e9 rel32` slot at it
└─► call func0 → shellcode → RCE
| # | Fase | Dettaglio |
|---|---|---|
| 1 | Trigger | opt() scrive una fake map tramite lo store confuso; new WeakRef(m) porta corruptArray.length fuori dai limiti. |
| 2 | Motore | addrOf/fakeObj; corrompe la lunghezza di doubleArray → R/W arbitrario di 4 byte a qualsiasi indirizzo caged allineato a 4. |
| 3 | Bridge a 64 bit | WasmTrustedInstanceData.memory0_start (+0x18) è un puntatore grezzo a 64 bit usato dal wasm compilato i32.load8_u/i32.store8 senza bounds check software (fuori intervallo colpisce una guard page → SIGSEGV → trap wasm). Reindirizzalo a qualsiasi indirizzo + imposta memory0_size (+0x20) enorme → R/W arbitrario a 64 bit. |
| 4 | Trova il code space | jump_table_start (+0x38) è un puntatore grezzo a 64 bit nello external code space (fuori dalla cage da 4 GB). |
| 5 | Scrivi lo shellcode | In questa build la regione della jump table è RWX (V8 applica patch alle voci a runtime): scrivi lo shellcode da 52 byte nello slack a jt+0x100. |
| 6 | Ripunta lo slot | Lo slot della jump table di func0 è un e9 <rel32> di 5 byte (target = slot + 5 + rel32). Imposta rel32 → jt+0x100. |
| 7 | Trigger del dispatch | inst.exports.r(0) → JSToWasmWrapper effettua il dispatch tramite lo slot 0 → lo shellcode viene eseguito. |
La maggior parte dei PoC pubblici per CVE-2024-4947 ha come bersaglio la d8 sandboxed o un vero renderer Chrome (v8_enable_sandbox=true) e si ferma alla primitiva di type confusion / OOB. Questo PoC ha come bersaglio una d8 no-sandbox e porta la catena fino all'esecuzione di codice. Le differenze che contano:
| Dimensione | Approccio comune dei PoC pubblici | Questo PoC |
|---|---|---|
| Build bersaglio | d8 sandboxed / renderer Chrome | v8_enable_sandbox=false d8 — i trusted pointer sono diretti, external code space attivo |
| Bridge R/W a 64 bit | sovrascrittura di JSTypedArray.external_pointer | Quel percorso crasha quando legge il code range (verificato empiricamente); si sovrascrive invece memory0_start e si usano load/store wasm grezzi |
| Percorso finale per l'esecuzione di codice | il code space è W^X → JIT-spray + redirect indiretto | la regione della jump table è RWX → scrittura diretta dello shellcode + patch dello slot e9 rel32 |
| Formato slot jump table | i documenti assumono comunemente movabs rax, imm64; jmp rax (12 byte) | misurato: e9 <rel32> di 5 byte, target = slot + 5 + rel32 |
| Offset dei campi | layout della build sandboxed | no-sandbox WasmTrustedInstanceData: jump_table_start@+0x38, memory0_start@+0x18, memory0_size@+0x20; WasmInstanceObject.trusted_data@+0x0c |
| Uscita pulita | — | d8 è multi-thread: bisogna usare exit_group (231), non exit (60), altrimenti il processo si blocca |
Vedi docs/walkthrough.md per l'analisi tecnica completa, inclusi i risultati empirici alla base di ogni riga.
.
├── exploit/
│ ├── Module.mjs # module namespace object corrupted by the trigger
│ ├── exploit_rce.mjs # the full chain (trigger → arbitrary R/W → RCE)
│ └── shellcode.S # assembly source for the 52-byte payload
├── build/
│ └── v8-build-nosandbox.sh # build the vulnerable no-sandbox d8 from V8 source
└── docs/
└── walkthrough.md # deep dive: bridge mechanics, layouts, gotchas
L'exploit importa Module.mjs (il vulnerabile oggetto module namespace) dalla propria directory, quindi tieni i due file insieme (o adatta il percorso import).
git e circa 10 GB di spazio libero su discodepot_tools (git clone https://chromium.googlesource.com/chromium/tools/depot_tools)# 1. fetch V8 at the vulnerable tag (12.4.254.16 is the pre-fix release)
export PATH="$HOME/depot_tools:$PATH"
cd ~/v8w && fetch v8 && cd v8
git checkout 12.4.254.16 # or the commit just before b3c01ac1e60a
# 2. first build a normal release d8 (needed to seed args.gn), then:
./v8-build-nosandbox.sh # copies args.gn and appends v8_enable_sandbox = false
v8-build-nosandbox.sh usa ninja -C out.gn/x64.release_nosandbox -j6 d8.
out.gn/x64.release_nosandbox/d8 --allow-natives-syntax --module exploit/exploit_rce.mjs
L'output atteso termina con:
CVE-2024-4947-PWNED
e il processo termina con stato 0.
--allow-natives-syntaxè necessario per le chiamate runtime%PrepareFunctionForOptimization/%OptimizeMaglevOnNextCall. Questo è il motivo per cui il PoC non può girare in un browser reale: è una proof-of-concept nella shell d8 by design.
I pezzi davvero riutilizzabili (e non ovvi) per la ricerca sullo sfruttamento di V8:
JSTypedArray.external_pointer fallisce su una regione bersaglio, reindirizza invece WasmTrustedInstanceData.memory0_start. I load/store di byte wasm compilati non hanno bounds check software; il signal trap handler converte solo i fault su guard page, quindi le regioni mappate ma non scrivibili crashano invece di generare una trap.try/catch; una trap wasm "out of bounds" indica una guard page (non mappata), un vero SIGSEGV indica mappata-ma-RX.e9 rel32 — salto relativo di 5 byte, non la forma movabs ipotizzata da molti write-up. Verificato tramite il disassembly di --print-wasm-code.Questo repository è fornito solo per ricerca educativa e di sicurezza difensiva. Dimostra lo sfruttamento di una vulnerabilità corretta contro una build di V8 solo di sviluppo. Non è un'arma contro Chrome moderno, non bypassa la sandbox di V8 e richiede il flag non predefinito --allow-natives-syntax. L'autore non è responsabile di qualsiasi uso improprio.
La primitiva trigger (fake map NAME_DICTIONARY_TYPE + scrittura dell'hash via WeakRef) segue le analisi pubbliche dell'exploit Lazarus in-the-wild pubblicate dopo la patch — inclusi i write-up di Google ed Exodus Intelligence su CVE-2024-4947. Le fasi successive (sondaggio della jump table, bridge, patch dello slot) sono state derivate empiricamente contro questa specifica build durante questo lavoro. Il concetto di fallback JIT-spray si basa su tecniche pubbliche di exploit di V8 (ad esempio il pattern make_array di CVE-2024-5830).
MIT — vedi LICENSE.