
Prova de conceito educacional para CVE-2024-4947, uma confusão de tipos no V8 Maglev, demonstrando uma cadeia completa desde o gatilho até a execução arbitrária de código em um build d8 sem sandbox.
Uma cadeia de exploração completa e autossuficiente para CVE-2024-4947 (confusão de
tipos no Maglev do V8), do gatilho inicial de confusão de tipos até a execução
arbitrária de código. O PoC executa contra um build d8 sem sandbox e imprime
CVE-2024-4947-PWNED em stdout como prova de execução de código.
⚠️ Este é um PoC de pesquisa / educacional para uma vulnerabilidade pública e já corrigida. Ele tem como alvo um build de desenvolvimento do shell V8 (
d8 --allow-natives-syntax) — não um navegador Chrome real. Consulte 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
O shellcode de 52 bytes é write(1, "CVE-2024-4947-PWNED\n", 20); exit_group(0).
CVE-2024-4947 é uma confusão de tipos no compilador JIT Maglev do V8, corrigida no
Chrome 125.0.6422.60 (commit b3c01ac1e60a). Foi explorada in the wild (em ataques
reais) pelo grupo APT Lazarus. Quando o Maglev compila um store para um objeto de
namespace de módulo (JSModuleNamespace), ele usa um AccessInfo incorreto — o store é
compilado como um mov [[obj + 4], rax] simples que grava um valor controlado no
campo map de um objeto adjacente, em vez de passar pelo caminho correto de
property-store.
A exploração prossegue da seguinte forma:
NAME_DICTIONARY_TYPE (0xB2), o
que altera onde o V8 armazena o hash do objeto.new WeakRef(...) para que a gravação do hash caia no slot de length de um
objeto adjacente → acesso out-of-bounds.A partir daí temos o kit clássico do V8: addrOf/fakeObj e, então, leitura/gravação
arbitrária dentro da cage (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
| # | Estágio | Detalhe |
|---|---|---|
| 1 | Gatilho (trigger) | opt() grava um map fake por meio do store confuso; new WeakRef(m) faz OOB em corruptArray.length. |
| 2 | Engine | addrOf/fakeObj; corrompe o length de doubleArray → R/W arbitrário de 4 bytes em qualquer endereço da cage alinhado a 4 bytes. |
| 3 | Ponte de 64 bits | WasmTrustedInstanceData.memory0_start (+0x18) é um ponteiro cru de 64 bits usado pelo wasm compilado nos i32.load8_u/i32.store8 sem verificação de limites por software (fora do intervalo atinge uma guard page → SIGSEGV → trap do wasm). Redirecione-o para qualquer endereço + defina memory0_size (+0x20) como enorme → R/W arbitrário de 64 bits. |
| 4 | Encontrar o code space | jump_table_start (+0x38) é um ponteiro cru de 64 bits para o external code space (fora da cage de 4 GB). |
| 5 | Gravar shellcode | Neste build, a região da jump table é RWX (o V8 aplica patches nas entradas em tempo de execução): grave o shellcode de 52 bytes no slack (espaço livre) em jt+0x100. |
| 6 | Reapontar o slot | O slot da jump table de func0 é um e9 <rel32> de 5 bytes (target = slot + 5 + rel32). Defina rel32 → jt+0x100. |
| 7 | Acionar o dispatch | inst.exports.r(0) → JSToWasmWrapper faz o dispatch pelo slot 0 → o shellcode é executado. |
A maioria dos PoCs públicos da CVE-2024-4947 tem como alvo o d8 com sandbox ou um
renderer real do Chrome (v8_enable_sandbox=true) e para na primitiva de confusão de
tipos / OOB. Este PoC tem como alvo um d8 sem sandbox e leva a cadeia até a execução de
código. As diferenças que importam:
| Dimensão | Abordagem comum dos PoCs públicos | Este PoC |
|---|---|---|
| Build alvo | d8 com sandbox / renderer do Chrome | d8 com v8_enable_sandbox=false — ponteiros trusted são diretos, external code space ativo |
| Ponte de R/W de 64 bits | sobrescrever JSTypedArray.external_pointer | Esse caminho crasha ao ler o code range (verificado empiricamente); em vez disso, sobrescreva memory0_start e use loads/stores wasm crus |
| Rota final de execução de código | code space é W^X → JIT-spray + redirecionamento indireto | a região da jump table é RWX → gravação direta de shellcode + patch de slot e9 rel32 |
| Formato do slot da jump table | a documentação normalmente assume movabs rax, imm64; jmp rax (12 bytes) | medido: e9 <rel32> de 5 bytes, target = slot + 5 + rel32 |
| Offsets de campos | layout de build com sandbox | WasmTrustedInstanceData sem sandbox: jump_table_start@+0x38, memory0_start@+0x18, memory0_size@+0x20; WasmInstanceObject.trusted_data@+0x0c |
| Saída limpa | — | o d8 é multithread: é preciso usar exit_group (231), e não exit (60), senão o processo trava |
Consulte docs/walkthrough.md para o write-up técnico completo, incluindo os achados empíricos por trás de cada linha.
.
├── 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
O exploit importa Module.mjs (o objeto de namespace de módulo vulnerável) do seu próprio
diretório; portanto, mantenha os dois arquivos juntos (ou ajuste o caminho do import).
git e ~10 GB de espaço livre em 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
A saída esperada termina com:
CVE-2024-4947-PWNED
e o processo encerra com status 0.
--allow-natives-syntaxé necessário para as chamadas de runtime%PrepareFunctionForOptimization/%OptimizeMaglevOnNextCall. É por isso que o PoC não pode ser executado em um navegador real — é, por design, uma prova de conceito no shell d8.
As partes genuinamente reutilizáveis (e não óbvias) para pesquisa de exploração do V8:
JSTypedArray.external_pointer falha em uma região alvo,
redirecione WasmTrustedInstanceData.memory0_start em vez disso. Os loads/stores de
bytes do wasm compilado não têm verificação de limites por software; o signal trap
handler converte apenas faltas de guard page, portanto regiões mapeadas mas não
graváveis crasham em vez de gerar trap.try/catch; um trap de "out of
bounds" do wasm significa guard page (não mapeada); um SIGSEGV real significa mapeada
mas RX.e9 rel32 — salto relativo de 5 bytes, e não a forma movabs
assumida por muitos write-ups. Verificado por meio da desmontagem de
--print-wasm-code.Este repositório é fornecido apenas para fins de pesquisa educacional e de segurança
defensiva. Ele demonstra a exploração de uma vulnerabilidade já corrigida (patched)
contra um build do V8 exclusivo para desenvolvimento. Não é uma arma contra o
Chrome moderno, não contorna o sandbox do V8 e requer o flag não padrão
--allow-natives-syntax. O autor não se responsabiliza por qualquer uso indevido.
A primitiva de gatilho (map fake NAME_DICTIONARY_TYPE + gravação de hash via
WeakRef) segue as análises públicas do exploit in-the-wild da Lazarus publicadas após o
patch — incluindo os write-ups da Google e da Exodus Intelligence sobre a CVE-2024-4947.
Os estágios posteriores (sondagem da jump table, ponte, patch de slot) foram derivados
empiricamente contra este build específico durante este trabalho. O conceito de fallback
por JIT-spray baseia-se em técnicas públicas de exploração do V8 (por exemplo, o padrão
make_array da CVE-2024-5830).
MIT — consulte LICENSE.