Step 4 - T’as la tête plus grosse que ton moteur (v8)

Étape 4 - Hic Jacet Chromium

Description

Thanks to your help, we have been able to forge signatures and chat with other members from the target organization.

More particularly, by impersonating the DEV, we have now established a privileged communication channel with the boss! After a bit of chatting with Emerald we had more details about the mission he was asking for:


EMERALD> I need your help to build my new secret web page. My cousin Bonarium is working on a highly secure browser project. He sent me a brand new Chromium shipped with an additional authentication module. It's still in development, but I want you to test it to make sure he's not scamming me once again.

DEV> Okay, how should I proceed? Do you want me to send you the code directly?

EMERALD> Yes, just send me a POST request containing the webpage contents and I'll try it locally as soon as possible. Hurry up, I need it fast. You can download the material needed here. It contains the code Bonarium added and the binaries you will need.

DEV> Trying to run it right now but I'm not sure which flags to use...

EMERALD> You can't do anything on your own, can you? Bonarium told me to use --no-sandbox --headless=new --disable-gpu to speed up my browsing experience. Don't know what it means, but I hope he's right or I'm definitely firing him this time.


We have been informed that Bonarium is the worst developer ever born. This may very well be our shot to hack the boss' browser and get a reverse shell on his machine. We've never been this close to reaching our goal. Good luck, you got this!

Résolution

Pour cette étape, il est demandé de compromettre l’environnement browser d’Emerald, le boss, qui utilise un chromium sans sandbox (heureusement). De plus, il utilise un module tier d’authentification.

On nous donne les sources du module tier, un installeur de chromium utilisant le module, les symboles (.pdb) associés, assez volumineux (pas inclus dans les fichiers avec cette solution, mais peut-être rendus directement accessibles plus tard).

Le challenge est ainsi clair : compromission d’un navigateur connu basé sur v8, en boîte blanche. Sûrement le challenge le plus dur de l’ensemble, surtout quand on découvre cette pièce logicielle.

Disclaimer : vu la complexité globale de V8, des approximations et des oublis seront présents dans ce write-up, ainsi que des questions ouvertes qui pourraient être résolues par une compréhension plus fine des différents mode d’allocation et des abstractions fournies par le moteur. On se concentre ici sur l’essentiel pour comprendre la résolution de l’étape.

Setup

Avoir un bon setup pour cette étape est cruciale, car on peut prévoir qu’on va avoir besoin de relancer de nombreuses fois l’exploit avant d’avoir quelque chose de fonctionnel.

Un fichier d’installation mini_installer.exe est fourni qui paramètre tout ce qui est nécessaire pour lancer chromium dans C:\Users\[...]\AppData\Local\Chromium\Application\121.0.6167.180.

Le concepteur de l’étape a été plutôt clément, puisqu’il donne le .pdb associé à chromium.dll (particulièrement volumineux : 4.5Go) qui contient tout les symboles qu’on pourrait être amené à résoudre.

Utiliser windbg est donc conseillé (du moins au départ) :

.sympath cache*;C:\Users\[...]\AppData\Local\Chromium\Application\121.0.6167.180
.reload

On peut ensuite accéder aux symboles souhaités, par exemple ceux définis dans le module blink additionnel :

x chrome!blink::Authenticator::Authenticate
0x0007fffa0b52224 chrome!blink::Authenticator::Authenticate

Finalement (cf. post-mortem tout en bas), il est très très très très très fortement conseillé d’installer d8, en suivant les instructions disponibles ici. Au départ rebuté par le temps d’installation, c’est vraiment un impératif pour comprendre la structuration des objets JavaScript qu’on a sous les yeux par la suite.

NOTE: Installez d8, vraiment. Si tous les write-ups le mentionnent, c’est qu’il y a une raison.

Quelques éléments de contexte

La version de chromium fournie est la version 121.0.6167.180 déployée le 12 février 2024. Bien que cela soit sûrement plus difficile que de résoudre le challenge de la manière légitime, on peut toujours chercher des vulnérabilités récentes impactant le composant et divulguée entre la date de release et le moment de résolution du challenge.

Cette piste peut être abandonnée rapidement après avoir jeté un coup d’oeil au code source fourni (et de toute façon ça n’est pas très honnête).

Le module fourni est un module blink, qui est notamment utilisé pour, euh, rendre tout ce qui est affiché par le navigateur (rien de moins) :

Blink is a rendering engine of the web platform. Roughly speaking, Blink implements everything that renders content inside a browser tab

Cela inclut notamment l’exécution de V8 et la possibilité de définir des nouveaux objets JS qui sont utilisables depuis les scripts exécutés.

La documentation d’introduction disponible à l’adresse suivante définit les concepts de base et notamment le concept de Web IDL binding qui permet de faire la liaison entre des objets manipulables en JavaScript et des objets définis en C++ (par exemple, appeler a.b() en JavaScript peut être équivalent à appeler A::b() dans l’objet C++ défini dans a.h). Pour cela, un fichier IDL (interface description language) doit être écrit pour fournir la correspondance.

The syntax of Web IDL is defined in the Web IDL spec. […] is called IDL extended attributes. Some of the IDL extended attributes are defined in the Web IDL spec and others are Blink-specific IDL extended attributes. Except the Blink-specific IDL extended attributes, IDL files should be written in a spec-conformant manner (i.e., just copy and paste from the spec).

Ensuite, l’objet défini doit hériter de ScriptWrappable pour être lié et appelable. C’est le cas ici pour deux objets :

L’objet Authenticator n’expose (malheureusement) pas 2 méthodes publiques AuthenticationData::patch_value et AuthenticationData::restaure_value dans le fichier de définition des interfaces, ce qui va complexifier les choses comme on le verra plus tard.

Basiquement, le workflow supposé pour utiliser le module est le suivant :

Ce qui se passe lors du dernier appel est l’enchaînement suivant :

La fonction patch_value a pour effet :

Le tout en fonction de la valeur de l’attribut authentication_method_ lui aussi paramétré au moment de la construction de l’instance (en fonction de cet attribut, 0, 1, 2, 4 ou 8 octets sont lus / réécrits).

Quant à la fonction restaure_value, elle réalise l’effet inverse, c’est-à-dire qu’elle remet la valeur pointée par saved_value_ dans la case authenticode_[end_of_authenticode_].

La vulnérabilité est donc immédiatement visible : en choisissant bien un offset end_of_authenticode_, on réécrit des données à l’adresse du buffer authenticode_ à laquelle on ajoute la valeur de end_of_authenticode_ qu’on contrôle, sur 8 octets.

Cerise sur le gateau : la fonctionnalité de restaure utilise la valeur _saved state qui est lue depuis l’emplacement à réécrire. Cette valeur n’est pas réinitialisée, et on peut l’afficher lors de l’appel à la méthode get_authentication_data_info depuis l’instance Authenticator, ce qui fournit un beau leak info.

On peut donc facilement générer un premier crash :

var ad = new AuthenticationData(new DataView(new ArrayBuffer(32)), BigInt("0x28"), 3, BigInt("0x6565656463636261"));
var auth = new Authenticator(new DataView(new ArrayBuffer(32)));
var res = auth.Authenticate(ad);
console.log(auth.get_authentication_data_info());

var ad = new AuthenticationData(new DataView(new ArrayBuffer(32)), BigInt("0xaaaaaaaa"), 3, BigInt("0x6565656463636261"));
var auth = new Authenticator(new DataView(new ArrayBuffer(32)));
var res = auth.Authenticate(ad);

Tadaaa, on a fini ! (euh, presque)

Un beau leak info, puis un beau crash

Premières pistes d’exploitation

On nous offre donc une primitive d’écriture et de lecture arbitraire, alors même qu’on n’avait rien demandé. Toutefois, ces primitives sont limitées par les deux aspects suivants :

Bref, alors que la seconde contrainte semble pour le moment non critique, on sait qu’on va devoir dans tous les cas étendre la primitive d’écriture pour avoir une persistance fiable. Exploiter des races conditions n’étant pas mon passe temps favori, j’ai opté pour la piste de recherche consistant à bien comprendre l’ensemble des interactions provoquant l’écriture et la restauration des données pour voir si on ne pouvait pas exploiter ça.

A ce moment, faire des beaux schémas de ce qu’on a comme organisation des objets au moment de l’exploitation de l’écriture arbitraire peut s’avérer très utile. Je n’aime pas du tout la lenteur terrible et l’UI catastrophique de windbg que je n’ai pas voulu trop creuser (principalement vu la lenteur de chargement de tous les symboles à chaque pause j’ai l’impression). Du coup, j’ai préféré utiliser x64dbg qui, même si je n’ai pas réussi à charger le pdb avec, qui est beaucoup plus intuitif et UI-friendly. Voici ce qu’on observe au moment du crash :

Layout mémoire au moment du crash

On peut clairement observer :

Un schéma récapitulatif est donné ci-dessous :

Un objet AuthenticationData et Authenticator déclarés à la suite

Le premier et second pointeurs de chaque objet sont les seuls éléments en lien avec l’aspect d’objet JavaScript des éléments qu’on manipule, il s’agit du pointeur vers le type de l’objet (Map), et du pointeur vers les propriétés (properties) de l’objet. Une description particulièrement détaillée et intéressante est fournie dans un write-up de jhalon pour mieux comprendre ces internals V8. De manière générale, l’ensemble des moteurs JavaScript connus implémentent ce type de description des objets en mémoire. Le concept de forme (shape) est particulièrement intéressant puisqu’il permet de nombreuses optimisations par la suite. Toutefois, la vulnérabilité étant ici triviale, pas besoin de trop s’intéresser à ces mécanismes de stockage interne, car pas besoin d’exploiter de confusion de type ou autre subtilité d’un moteur d’optimisation parmi les nombreux existants (Maglev, TurboFan ou encore Sparkplug pour v8 pour ne citer qu’eux, par exemple).

Revenons-en au principal. Ici, lorsqu’on a demandé lors de la première déclaration d’un AuthenticationDataBigInt(“0x28”), on peut constater qu’on a bien fait fuiter l’adresse du composant à l’offset 0x28 après le début du tableau authenticode_, c’est à dire le pointeur Map (Authenticator). Il s’agit d’un pointeur vers une VTable dans chrome.dll, ce qui est intéressant à noter pour la suite.

Quant aux propriétés de l’objet, elles sont stockées sur le tas, mais à une autre valeur de addr&0xffffffff00000000 que l’objet AuthenticationData étudié, comme on peut également le voir au moment du crash sur la capture précédente.

Point d’attention: Ce concept de (addr&0xffffffff00000000) et la séparation logique de zones du tas des adresses des différents objets JavaScript par leur 32 bits de poids fort constitue le coeur du mécanisme de protection qui sera détaillé plus bas et nommé “pointer compression”.

Ce mécanisme de pointer compression se manifeste d’ailleurs ici pour deux objets stockés dans l’objet Authenticator :

Pointer compression, these are real addresses

Cela ne saute pas aux yeux, mais sur la capture précédente, les 32 bits encadrés en bleu en haut (0x800980B4) correspondent à l’adresse pointée par la flèche de l’objet AuthenticationData (0x438900130168).

(compressed * 2 & 0xffffffff) + bits de poids fort de l’objet courant

(0x800980B4 * 2  & 0xffffffff) + 0x438900000000 = 0x438900130168

Le second objet correspond lui à l’attribut key_ qui stocke la dataview passée en argument lors de la construction de l’authenticator :

DataView (key_) pointée par l’authenticator
(0x80083CC4 * 2  & 0xffffffff) + 0x438900000000 = 0x438900107988

On peut observer à la fois un pointeur (raw cette fois) vers une zone de donnée contenant les données de la vue, et la taille du buffer, bien 0x20 ici. L’idée naturelle qui vient alors est de modifier ce pointeur pour pouvoir avoir une dataview qui pointe sur n’importe quel type de données et puisse servir à une primitive de lecture arbitraire non limitée, voire à une primitive d’écriture complète également ?

Relaxer des contraintes

Continuons la recherche d’une écriture de données stable. On a vu que le mécanisme patch/restaure effectue deux écritures de données l’une après l’autre depuis le même objet authenticated_data_ d’un authenticator. Les deux fonctions se basent sur les attributs end_of_authenticode_ pour obtenir la case à réécrire ou à remettre à son état d’origine. Mais que se passe-t-il si entre les deux appels, le pointeur vers authenticated_data_ est modifié ? Il s’avère que dans ce cas la fonction restaure remet en place des données autres et à un autre offset que lors de la fonction patch !

Très utile, même si de base ça n’est pas contrôlé et provoque le crash du renderer chromium, lorsqu’on cible le pointeur d’un AuthenticationData dans un Authenticator (authenticode_ + 0x38) au moment du patch.

On peut alors chercher à contrôler de la manière suivante :

Désynchronisation de l’objet AuthenticatedData 0x20 octets plus bas

Pour faciliter la suite on veut une fonction write qui soit autonome. Pour ça, on peut réaliser une étape en plus de leak avant de réaliser l’enchaînement décrit. Les opérations réalisées sont donc les suivantes :

NOTE: Presque stable car dans un premier temps seules quelques écritures étaient réalisées. Avec l’augmentation du volume de données à réécrire notamment à la fin de l’exploit, la stabilité de l’ensemble a brusquement chuté. Le problème a été adressé de manière non formelle.

Voici le schéma global qui récapitule une écriture arbitraire :

Layout pour l’écriture arbitraire

Le code qui en résulte (à noter que la variable globale authpivotLeaked doit avoir précédemment été configurée avec les bits de poids fort de la zone de tas hébergeant les AuthenticationData / Authenticator) :

function write8At(targetAddrBigInt, toWriteBigInt) {
    var ab = new ArrayBuffer(32);
    var ad = new AuthenticationData(ab, BigInt(0x10), 3, BigInt("0xdeadbeef"));
    // attacking one (fake for the moment, we do not have leak (we want standalone OK))
    var auth = new Authenticator(new DataView(ab));
    auth.Authenticate(ad); // 0x10 just not to crash but associate the buffer at this stage

    // reference one to leak the buffer for current location, values does not matter
    var auth2 = new Authenticator(new DataView(ab));
    ad.setEndOfAuthenticode(BigInt(0x38));
    var authres = auth2.Authenticate(ad);  // the second one will not fail as below in memory

    var parsedLeak = parse(auth2.get_authentication_data_info());
    var bigIntLeak = BigInt(parsedLeak);
    var highbits = BigInt("0xffffffff00000000") & authpivotLeaked;
    var leakToRealAddr = ((BigInt(2) * bigIntLeak) & BigInt(0xffffffff)) + highbits;
    var targetFinal = targetAddrBigInt - leakToRealAddr - BigInt(0x20 + 0x58*3 + 0x30);  // final 0x30 offset
    if (targetFinal < BigInt(0)) {
        targetFinal += BigInt("0x10000000000000000");
    }

    var arrayToWrite2 = [targetFinal, toWriteBigInt,
                         BigInt(0), BigInt(0x20) + BigInt(0x300000000)];
    var ab2 = new ArrayBuffer(32);
    var charbuf2 = new Uint8Array(ab2);

    for (j=0; j<4; j++) {
        var toBytes = bigIntToBytes(arrayToWrite2[j]);
        for (i=0; i<8; i++) {
            charbuf2[j*8 + i] = toBytes[i];
        }
    }

    // just fall 0x20 further for desync, plus 3x 0x58 for each block inbetween the leaked addr
    var ad2 = new AuthenticationData(ab2, BigInt(0x38), 3, bigIntLeak + BigInt(0x10 + (0x58*3/2)));
    var auth3 = new Authenticator(new DataView(ab2));
    auth3.Authenticate(newad2);
}

Le chemin de croix

A ce stade, on a une écriture persistante arbitraire. Essentiellement deux possibilités s’offrent à nous :

C’est donc sur cette dernière piste que je me suis engagé, déterminé à trouver où pourrait être stockée cette zone quand on déclare une instance WASM. L’avantage est qu’une fois la zone localisée, en théorie il suffit juste d’écrire son shellcode à la place, puis d’appeler la fonction WASM, c’est donc plus “idiomatique” pour ce genre d’exploit.

Voici les étapes du script final :

leakez, leakez, il en restera toujours quelque chose

Pour cette dernière partie, la démarche est sûrement plus intéressante que le résultat, la voici exposée dans les grandes lignes.

Fuiter le contenu de la base du tas

On commence doucement. Le pointeur V8HeapCompressionScheme::base_ peut être récupéré dans la section data de chrome.dll, à offset fixe :

Pointeur V8HeapCompressionScheme::base_

Localiser l’instance WASM

Comme des tableaux ou des objets JavaScript “provenant de l’exécution de scripts”, les instances WASM sont localisées dans la même zone de tas à partir du pointeur V8HeapCompressionScheme::base_ afin de pouvoir appliquer le mécanisme de protection et d’optimisation.

(une partie de la suite est réalisée dans d8 sous Ubuntu, mais c’est les mêmes layouts sous Windows)

gdb ./out.gn/x64.debug/d8
r --allow-natives-syntax
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,43,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;
for(i=0;i<0x1000;i++){f()}
%DebugPrint(f)

var ar = [1, 2, 3];
var obj = {};
%DebugPrint(ar)
%DebugPrint(obj)
// heap base is 0x33e9
0x33e900299e19
0x33e90004c9d1
0x33e90004ca1d

Petite subtilité, dans notre cas les objets AuthenticationData / Authenticator ne sont pas dans la même zone de tas. Je n’ai pas creusé pour comprendre qu’est ce qui implique qu’un objet est placé dans telle ou telle zone (ici vu que c’est un module blink dédié j’imagine que cela provient de là). Il ne semble pas non plus y avoir de lien direct entre les objets qu’on manipule (AuthenticationData et Authenticator) et des objets JavaScript “classiques” dans la zone de tas précédente. Lorsqu’on déclare une dataView qu’on lie à un AuthenticationData, l’objet reste cantonné dans ce tas.

Comme on n’a pas vraiment de pointeur facilement accessible à partir duquel itérer pour identifier ces objets, j’ai pris le parti dans un premier temps d’adopter une démarche empirique, en constatant en local que tous mes tableaux étaient alloués à proximité de l’adresse 0x4c000 ou 0x29000 après la base récupérée plus tôt.

C’est à cette étape que le script de résolution a le plus de chance d’échouer. En effet, l’espace de tas à partir de V8HeapCompressionScheme::base_ n’est pas contigü et il existe des espaces non alloués qui provoquent un accès mémoire invalide si on essaie de fuiter de l’information à ces niveaux.

On peut ainsi chercher un motif après avoir créé un tableau qui contient des constantes et notre instance WASM au milieu :

var ar1 = new Uint8Array(64);
ar1[50] = 0x67;
var ar2 = [0xddddeeaaaaaa, 0xddddeeaaaaab, 0xddddeeaaaaac, 0xddddeeaaaaad, ar1, 0xddddeeaaaaaa, 0xddddeeaaaaaa, 0xddddeeaaaaaa, 0xddddeeaaaaaa, 0xddddeeaaaaaa, 0xddddeeaaaaab, 0xddddeeaaaaaa, 0xddddeeaaaaaa, 0xddddeeaaaaaa, f, 0x88776655];

%DebugPrint(ar2)

On sait alors que si on retrouve une suite de valeurs 0x405555d5bdbbeb42 en mémoire on a retrouvé notre tableau :

struct.pack('d', 0xddddeeaaaaaa).hex()
'405555d5bdbbeb42'
Notre tableau en mémoire
Détail in-memory de la représentation ci-dessus

En prenant l’élément correspondant à l’instance WASM on a réussi à obtenir l’adresse de l’instance une fois le pointeur décompressé :

for i in range(0, len(leak), 4):
    data = leak[i:i+4]
    if u32(data) == 0x20:
        bad = False
        for j in range(16):
            if i+4+j*4+3 < len(leak) and leak[i+4+j*4+3] != 0 or leak[i+4+j*4+2] == 0:
                bad = True
        if not bad:
            if leak[i+4+64:i+4+64+8] == b'\xb1\x07\x00\x00\x40\x55\x55\xd5':
                print("[++++] Array located :), extracting the wasm function")
                wasm_func = u32(content[i+4+56:i+4+56+4])
                print(f"[+] WASM REL is {hex(wasm_func)}")
                pwn_context['wasm_abs'] = wasm_func + pwn_context['heap_base'] - 1
                print(f"[+] WASM ABS is {hex(pwn_context['wasm_abs'])}")

Localiser maintenant la zone RWX

Une fois l’instance WASM localisée, ça n’est pas encore fini. En effet, l’exécution de code depuis cet objet n’est pas immédiate et il y a de multiples déréférencements entre le moment où l’objet est utilisé et où le pointeur vers la zone de code RWX est appelé.

La démarche pour trouver le plus rapidement possible quel est le chemin vers la zone RWX est de mettre un point d’arrêt en lecture sur la zone mémoire associée à l’objet et lorsque le point d’arrêt est atteint (après avoir appelé la fonction) à identifier le moment où un saut relatif à une donnée est réalisé (typiquement un jmp [reg]).

Call relatif calculé à partir du contenu de l’objet WASM

Heureusement, même si le nombre de pointeurs et des zones accédés entre le moment reste notable, le jump n’est pas très loin lorsqu’on est dans la fonction Builtins_CallFunction_ReceiverIsAny qui lit l’attribut en +0xc de WASMInstance pour calculer un offset dans une table de structures.

On peut aussi s’aider des symboles, de WinDBG et du code source de chromium pour retracer les différents éléments qui interviennent dans le calcul de l’adresse finale :

[…]

Beaucoup d’indirections, pas facile à suivre

Le processus fait notamment intervenir une autre zone mémoire pas encore mentionnée relative à la gestion de l’exécution de code sous v8 et à la représentation des objets associés comme les fonctions, les fonctions Jittées, les fonctions WASM, … Je n’ai pas forcément cherché à comprendre non plus les subtilités et complexité des différents mode de résolution des pointeurs de fonction, juste à identifier ces pointeurs. Via un debug pas à pas en lisant le code source en même temps, on retrouve ainsi le pointeur initial dans la section .data de chrome.dll à offset fixe d’un pointeur précédemment obtenu.

Une fois le chemin entièrement constitué, ne reste plus "qu’à"" l’implémenter par leaks successifs.

NOTE : Il existe des chemins plus courts ou des objets plus intéressant à chercher, qui minimiseraient le risque d’échouer lors de la phase de recherche du motif. Malgré tout, ça marche assez régulièrement à distance (un peu moins en local pour une raison inconnue).

La fin

On termine par un shellcode directement mappé dans une zone RWX, pas forcément l’étape la plus dure toutefois j’ai été confronté à deux problèmes :

Finalement, en voulant remettre au propre l’exploit lors de l’écriture de ce write-up, il y avait une telle masse de code à reformater que j’ai cassé des bouts et que l’ensemble ne fonctionne plus … Je le laisse en exemple car aucune des opérations de pwn_context.py n’a changé entre les deux versions même si c’est difficilement lisible.

J’ai laissé une trace complète d’exécution réussie dans fulltrace.txt bien que ça ne présente pas non plus un intérêt énorme.

NOTE: A la réflexion, l’idée de base de dissocier un backend de l’exploit pour faciliter le contrôle de la gestion distante de l’état me parait discutable. L’overhead pour conserver une structure minimale fonctionnelle reste colossal par rapport à “juste” un exploit en JavaScript qui aurait fait tout d’un bloc. Je ne pense pas conserver cette approche pour plus tard malgré ses avantages comme le stockage des traces et des états rencontrés, le fait de pouvoir utiliser un langage avec lequel je suis plus à l’aise, et l’automatisation du processus global d’exploitation.

Aussi, quelques ressources et auteurs qui m’ont énormément aidé lors de la résolution de ce challenge pour savoir vers où chercher :

NOTE : On voit dans beaucoup de write-ups / blogposts les primitives addrof et fakeobj définies pour faciliter les exploitations ultérieures. En effet, lorsque ces deux primitives sont stables, on peut généralement automatiser en JavaScript et de manière plus simple les exploitations. Toutefois, ici vu le pointer compression en place, j’ai préféré ne pas prendre du temps pour implémenter ces primitives étant donné qu’on reste toujours dans notre zone de tas et que cela ne permettra pas d’en sortir a priori.

Analyse post-mortem

Pourquoi ne pas avoir utilisé d8 plus vite ? Le temps passé à faire des aller-retours entre les différentes fenêtres de débogage sans jamais avoir les informations des différents pointeurs entre objets, de devoir parcourir à la main les listes chaînées de propriétés, ou de calculer à la main les adresses véritables une fois les pointeurs décompressés, est monstrueux au regard du temps passer à déboguer une fois d8 en place, malgré son temps de compilation.

On trouve plus rapidement les chaînages pertinents pour identifier les zones RWX depuis un objet WASMInstance, et on peut vérifier en live dans GDB que c’est bien des parcours de pointeurs compressés qui mènent à l’objet recherché. Bref, surtout ne faites pas la même erreur.