
Prueba de concepto educativa para CVE-2024-4947, una confusión de tipos de V8 Maglev, que demuestra una cadena completa desde el disparador hasta la ejecución arbitraria de código en una compilación d8 sin sandbox.
Una cadena de explotación completa y autocontenida para CVE-2024-4947 (confusión de tipos en V8 Maglev), desde el disparador inicial de la confusión de tipos hasta la ejecución arbitraria de código. El PoC se ejecuta contra una compilación de d8 sin sandbox e imprime CVE-2024-4947-PWNED en stdout como prueba de la ejecución de código.
⚠️ Este es un PoC de investigación / educativo para una vulnerabilidad pública y parcheada. Apunta a una compilación de desarrollo del shell de V8 (
d8 --allow-natives-syntax) — no a un navegador Chrome real. Ver Descargo de responsabilidad.
$ ./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
El shellcode de 52 bytes es write(1, "CVE-2024-4947-PWNED\n", 20); exit_group(0).
CVE-2024-4947 es una confusión de tipos en el compilador JIT Maglev de V8, corregida en Chrome
125.0.6422.60 (commit b3c01ac1e60a). Fue explotada en ataques reales por el grupo APT
Lazarus. Cuando Maglev compila un store a un objeto de espacio de nombres de módulo
(JSModuleNamespace), usa una AccessInfo incorrecta: el store se compila como un
mov [[obj + 4], rax] simple que escribe un valor controlado en el campo map de un
objeto adyacente en lugar de pasar por la ruta correcta de almacenamiento de propiedades.
La explotación procede de la siguiente manera:
NAME_DICTIONARY_TYPE (0xB2), lo que
cambia dónde almacena V8 el hash del objeto.new WeakRef(...) para que la escritura del hash caiga en el slot de longitud
de un objeto adyacente → acceso fuera de límites.Desde ahí obtenemos el kit clásico de V8: addrOf/fakeObj, y luego lectura/escritura
arbitraria dentro de la 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
| # | Etapa | Detalle |
|---|---|---|
| 1 | Disparador | opt() escribe un map falso mediante el store confundido; new WeakRef(m) hace que corruptArray.length quede fuera de límites. |
| 2 | Motor | addrOf/fakeObj; corromper la longitud de doubleArray → R/W arbitraria de 4 bytes en cualquier dirección de la cage alineada a 4. |
| 3 | Puente de 64 bits | WasmTrustedInstanceData.memory0_start (+0x18) es un puntero directo de 64 bits usado por wasm compilado i32.load8_u/i32.store8 sin comprobación de límites por software (fuera de rango golpea una guard page → SIGSEGV → trampa wasm). Redirigirlo a cualquier dirección + establecer memory0_size (+0x20) enorme → R/W arbitraria de 64 bits. |
| 4 | Encontrar el espacio de código | jump_table_start (+0x38) es un puntero directo de 64 bits hacia el espacio de código externo (fuera de la cage de 4 GB). |
| 5 | Escribir shellcode | En esta compilación la región de la jump table es RWX (V8 parchea las entradas en tiempo de ejecución): escribe el shellcode de 52 bytes en el hueco en jt+0x100. |
| 6 | Reapuntar slot | El slot de func0 en la jump table es un e9 <rel32> de 5 bytes (destino = slot + 5 + rel32). Establecer rel32 → jt+0x100. |
| 7 | Disparar dispatch | inst.exports.r(0) → JSToWasmWrapper despacha vía el slot 0 → el shellcode se ejecuta. |
La mayoría de los PoCs públicos de CVE-2024-4947 apuntan al d8 con sandbox o a un renderer real de Chrome
(v8_enable_sandbox=true) y se detienen en la primitiva de confusión de tipos / OOB. Este PoC
apunta a un d8 sin sandbox y lleva la cadena hasta la ejecución de código. Las
diferencias que importan:
| Dimensión | Enfoque común de PoCs públicos | Este PoC |
|---|---|---|
| Compilación objetivo | d8 con sandbox / renderer de Chrome | d8 con v8_enable_sandbox=false — los punteros de confianza son directos, el espacio de código externo está activado |
| Puente R/W de 64 bits | sobrescribir JSTypedArray.external_pointer | Ese camino se estrella al leer el rango de código (verificado empíricamente); en su lugar sobrescribir memory0_start y usar load/store wasm directo |
| Ruta final de ejecución de código | el espacio de código es W^X → JIT-spray + redirección indirecta | la región de la jump table es RWX → escritura directa de shellcode + parche del slot e9 rel32 |
| Formato del slot de la jump table | la documentación suele asumir movabs rax, imm64; jmp rax (12 bytes) | medido: e9 <rel32> de 5 bytes, destino = slot + 5 + rel32 |
| Offset de campos | layout de compilación con sandbox | WasmTrustedInstanceData sin sandbox: jump_table_start@+0x38, memory0_start@+0x18, memory0_size@+0x20; WasmInstanceObject.trusted_data@+0x0c |
| Salida limpia | — | d8 es multihilo: debe usarse exit_group (231), no exit (60), o el proceso se cuelga |
Ver docs/walkthrough.md para el informe técnico completo, incluyendo los hallazgos empíricos detrás de cada fila.
.
├── 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
El exploit importa Module.mjs (el objeto vulnerable de espacio de nombres de módulo) desde su propio
directorio, así que mantén los dos archivos juntos (o ajusta la ruta de import).
git y ~10 GB de disco libredepot_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
La salida esperada termina con:
CVE-2024-4947-PWNED
y el proceso sale con estado 0.
--allow-natives-syntaxes necesario para las llamadas de runtime%PrepareFunctionForOptimization/%OptimizeMaglevOnNextCall. Por eso el PoC no puede ejecutarse en un navegador real: es una prueba de concepto del shell de d8 por diseño.
Las piezas genuinamente reutilizables (y nada obvias) para la investigación de explotación de V8:
JSTypedArray.external_pointer
falla en una región objetivo, redirige WasmTrustedInstanceData.memory0_start
en su lugar. Los byte loads/stores de wasm compilado no llevan comprobación de límites por software; el manejador
de trampas por señal convierte solo fallos de guard page, así que las regiones mapeadas pero no escribibles crashean
en lugar de atrapar.try/catch; una trampa wasm de "out of bounds" significa
una guard page (sin mapear), un SIGSEGV real significa mapeada pero RX.e9 rel32 — salto relativo de 5 bytes, no la forma movabs que asumen
muchas publicaciones. Verificado con el desensamblado de --print-wasm-code.Este repositorio se proporciona únicamente con fines educativos y de investigación en seguridad defensiva. Demuestra la explotación de una vulnerabilidad parcheada contra una compilación de V8 solo para desarrollo. No es un arma contra Chrome moderno, no evade el sandbox de V8 y requiere el flag no predeterminado --allow-natives-syntax. El autor no es responsable de ningún mal uso.
La primitiva de disparo (map falso NAME_DICTIONARY_TYPE + escritura de hash con WeakRef) sigue los análisis públicos del exploit de Lazarus in-the-wild publicados después del parche, incluidos los informes de Google y Exodus Intelligence sobre CVE-2024-4947. Las etapas posteriores (sondeo de la jump table, puente, parcheo del slot) se derivaron empíricamente contra esta compilación específica durante este trabajo. El concepto de respaldo de JIT-spray se basa en técnicas públicas de explotación de V8 (p. ej., el patrón make_array de CVE-2024-5830).
MIT — ver LICENSE.