
Preuve de concept pédagogique pour CVE-2024-4947, une confusion de type Maglev de V8, démontrant une chaîne complète allant du déclenchement à l'exécution de code arbitraire sur une version d8 non sandboxée.
Une chaîne d'exploitation complète et autonome pour CVE-2024-4947 (V8 Maglev type
confusion), du déclencheur initial de type confusion jusqu'à l'exécution de code
arbitraire. Le PoC s'exécute contre un build d8 non sandboxé et affiche
CVE-2024-4947-PWNED sur stdout comme preuve d'exécution de code.
⚠️ Ceci est un PoC de recherche / éducatif pour une vulnérabilité corrigée et publique. Il cible un build de développement du shell V8 (
d8 --allow-natives-syntax) — pas un vrai navigateur Chrome. Voir Disclaimer.
$ ./v8-build-nosandbox.sh # build du d8 vulnérable (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
Le shellcode de 52 octets est write(1, "CVE-2024-4947-PWNED\n", 20); exit_group(0).
CVE-2024-4947 est une type confusion dans le compilateur JIT V8 Maglev, corrigée dans
Chrome 125.0.6422.60 (commit b3c01ac1e60a). Elle a été exploitée dans la nature par
le groupe APT Lazarus. Lorsque Maglev compile un stockage vers un objet espace de noms
de module (JSModuleNamespace), il utilise une AccessInfo incorrecte — le stockage est
compilé comme un simple mov [[obj + 4], rax] qui écrit une valeur contrôlée dans le
champ map d'un objet adjacent au lieu de passer par le chemin de stockage de
propriété approprié.
L'exploitation se déroule ainsi :
NAME_DICTIONARY_TYPE (0xB2),
ce qui change l'endroit où V8 stocke le hash de l'objet.new WeakRef(...) pour que l'écriture du hash atterrisse dans le
slot de longueur d'un objet adjacent → accès hors limites.De là, on obtient la boîte à outils V8 classique : addrOf/fakeObj, puis lecture/écriture
arbitraire dans la cage.
Déclencheur CVE-2024-4947 (fausse map NAME_DICTIONARY + écriture de hash WeakRef)
└─► écriture OOB ─► corruption de la longueur du doubleArray
└─► R/W arbitraire 4/8 octets dans la cage (le "moteur")
└─► écrasement de WasmTrustedInstanceData.memory0_start (+0x18)
& memory0_size (+0x20) → énorme
└─► wasm load8_u / store8 = pont R/W 64 bits propre
(aucune vérification des bornes logicielle dans le code compilé)
└─► lecture de jump_table_start (+0x38) — external code space
└─► la région de la jump table est RWX (ce build)
└─► écriture du shellcode dans l'espace libre (jt+0x100)
└─► redirection du slot `e9 rel32` de func0 vers celui-ci
└─► appel de func0 → shellcode → RCE
| # | Étape | Détail |
|---|---|---|
| 1 | Déclencheur | opt() écrit une fausse map via le stockage confus ; new WeakRef(m) fait sortir corruptArray.length des limites. |
| 2 | Moteur | addrOf/fakeObj ; corruption de la longueur de doubleArray → R/W arbitraire 4 octets à n'importe quelle adresse alignée sur 4 dans la cage. |
| 3 | Pont 64 bits | WasmTrustedInstanceData.memory0_start (+0x18) est un pointeur 64 bits brut utilisé par le wasm compilé i32.load8_u/i32.store8 sans vérification logicielle des bornes (hors limites → page de garde → SIGSEGV → trap wasm). Redirigez-le vers n'importe quelle adresse et définissez memory0_size (+0x20) sur une valeur énorme → R/W arbitraire 64 bits. |
| 4 | Trouver l'espace de code | jump_table_start (+0x38) est un pointeur 64 bits brut dans l'espace de code externe (hors de la cage de 4 Go). |
| 5 | Écrire le shellcode | Sur ce build, la région de la jump table est RWX (V8 patche les entrées à l'exécution) : écrivez le shellcode de 52 octets dans l'espace libre à jt+0x100. |
| 6 | Rediriger le slot | Le slot de jump table de func0 est un e9 <rel32> de 5 octets (cible = slot + 5 + rel32). Définissez rel32 → jt+0x100. |
| 7 | Déclencher le dispatch | inst.exports.r(0) → JSToWasmWrapper dispatch via le slot 0 → exécution du shellcode. |
La plupart des PoC publics pour CVE-2024-4947 ciblent le d8 sandboxé ou un vrai
renderer Chrome (v8_enable_sandbox=true) et s'arrêtent à la primitive de type confusion /
OOB. Ce PoC cible un d8 sans sandbox et mène la chaîne jusqu'à l'exécution de code.
Les différences qui comptent :
| Dimension | Approche des PoC publics courants | Ce PoC |
|---|---|---|
| Build cible | d8 sandboxé / renderer Chrome | d8 v8_enable_sandbox=false — les pointeurs de confiance sont directs, l'espace de code externe est activé |
| Pont R/W 64 bits | écrasement de JSTypedArray.external_pointer | Ce chemin crash lors de la lecture de la plage de code (vérifié empiriquement) ; à la place, écrasement de memory0_start et utilisation des load/store wasm bruts |
| Voie finale d'exécution de code | l'espace de code est W^X → JIT-spray + redirection indirecte | la région de la jump table est RWX → écriture directe du shellcode + patch de slot e9 rel32 |
| Format du slot de jump table | les docs supposent souvent movabs rax, imm64; jmp rax (12 octets) | mesuré : e9 <rel32> sur 5 octets, cible = slot + 5 + rel32 |
| Offsets de champs | layout du build sandboxé | WasmTrustedInstanceData sans sandbox : jump_table_start@+0x38, memory0_start@+0x18, memory0_size@+0x20 ; WasmInstanceObject.trusted_data@+0x0c |
| Sortie propre | — | d8 est multi-threadé : il faut utiliser exit_group (231), pas exit (60), sinon le processus se bloque |
Voir docs/walkthrough.md pour la documentation technique complète, y compris les constatations empiriques derrière chaque ligne.
.
├── exploit/
│ ├── Module.mjs # objet espace de noms de module corrompu par le déclencheur
│ ├── exploit_rce.mjs # la chaîne complète (déclencheur → R/W arbitraire → RCE)
│ └── shellcode.S # source assembleur pour le payload de 52 octets
├── build/
│ └── v8-build-nosandbox.sh # build du d8 vulnérable sans sandbox depuis les sources V8
└── docs/
└── walkthrough.md # plongée en profondeur : mécanique du pont, layouts, pièges
L'exploit importe Module.mjs (l'objet espace de noms de module vulnérable) depuis son
propre répertoire, donc gardez les deux fichiers ensemble (ou ajustez le chemin
d'import).
git, et ~10 Go d'espace disque libredepot_tools (git clone https://chromium.googlesource.com/chromium/tools/depot_tools)# 1. récupérer V8 au tag vulnérable (12.4.254.16 est la version pré-correction)
export PATH="$HOME/depot_tools:$PATH"
cd ~/v8w && fetch v8 && cd v8
git checkout 12.4.254.16 # ou le commit juste avant b3c01ac1e60a
# 2. builder d'abord un d8 release normal (nécessaire pour amorcer args.gn), puis :
./v8-build-nosandbox.sh # copie args.gn et ajoute v8_enable_sandbox = false
v8-build-nosandbox.sh utilise ninja -C out.gn/x64.release_nosandbox -j6 d8.
out.gn/x64.release_nosandbox/d8 --allow-natives-syntax --module exploit/exploit_rce.mjs
La sortie attendue se termine par :
CVE-2024-4947-PWNED
et le processus se termine avec le statut 0.
--allow-natives-syntaxest requis pour les appels runtime%PrepareFunctionForOptimization/%OptimizeMaglevOnNextCall. C'est pourquoi le PoC ne peut pas s'exécuter dans un vrai navigateur — c'est une preuve de concept pour le shell d8 par conception.
Les éléments véritablement réutilisables (et non évidents) pour la recherche d'exploitation V8 :
JSTypedArray.external_pointer échoue sur une région cible, redirigez plutôt
WasmTrustedInstanceData.memory0_start. Les chargements/stockages d'octets wasm
compilés ne comportent aucune vérification logicielle des bornes ; le gestionnaire de
trap par signal ne convertit que les défauts de page de garde, donc les régions
mappées mais non inscriptibles crashent plutôt que de déclencher un trap.try/catch ; un trap
wasm « out of bounds » signifie une page de garde (non mappée), un vrai SIGSEGV
signifie mappé mais RX.e9 rel32 — saut relatif de 5 octets, pas la forme movabs
supposée par de nombreux écrits. Vérifié contre le désassemblage
--print-wasm-code.Ce dépôt est fourni uniquement à des fins de recherche éducative et de sécurité
défensive. Il démontre l'exploitation d'une vulnérabilité corrigée contre un build
V8 réservé au développement. Ce n'est pas une arme contre Chrome moderne, ne
contourne pas la sandbox V8, et nécessite le drapeau non défaut
--allow-natives-syntax. L'auteur n'est pas responsable de toute mauvaise utilisation.
La primitive de déclenchement (fausse map NAME_DICTIONARY_TYPE + écriture de hash
WeakRef) suit les analyses publiques de l'exploit Lazarus in-the-wild publiées après la
correction — y compris les écrits de Google et d'Exodus Intelligence sur CVE-2024-4947.
Les étapes ultérieures (sondage de la jump table, pont, patch de slot) ont été dérivées
empiriquement contre ce build spécifique au cours de ce travail. Le concept de repli
JIT-spray s'appuie sur des techniques publiques d'exploitation V8 (par ex. le motif
make_array de CVE-2024-5830).
MIT — voir LICENSE.