
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.