Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
datadome-vm — Ingénierie inverse de la nouvelle VM Datadome 🔥 | Kitploit
Outils/GitHubGitHub/xkiian/datadome-vm
Analyse Dynamique (Sandboxing)Rétro-ingénierieAnalyse de BinairesApprentissage et ÉducationAnti-BotContournement de CAPTCHA
GitHubxkiian/datadome-vm

datadome-vm

Ingénierie inverse de la nouvelle VM Datadome 🔥

Voir le dépôt
12519il y a 6 moisVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

Analyse de la VM DataDome

Résumé

Ce dépôt documente la première version publique de la machine virtuelle (VM) JavaScript intégrée au navigateur de DataDome, utilisée dans leur flux CAPTCHA/interstitiel. Cette analyse couvre :

  • Les mécanismes de chargement et de décodage du bytecode
  • La disposition mémoire et l'architecture de la VM
  • Un désassembleur de preuve de concept
  • Les notes d'analyse du flux de contrôle

Remarque : Ce dépôt ne couvre qu'une seule version (statique) de la VM et est destiné à des fins de recherche et d'analyse en sécurité. Il n'inclut pas de solveurs dynamiques ni d'implémentations de solveurs en production.

Contexte

Le 14 janvier 2026, DataDome a commencé à intégrer un nouveau composant basé sur une VM dans leur tag client.

Déobfuscation

Le code de la VM a été extrait du défi captcha dans vm.js (disponible dans ce dépôt).

La première étape a consisté à désobfusquer le script :

L'obfuscation est simple : évaluer chaque variable et la remplacer par sa valeur réelle. Un script de désobfuscation est disponible dans deobf.js.

Analyse initiale

L'exécution du code désobfusqué (out.js) dans DevTools révèle la sortie attendue de la VM :

La sortie est un objet JSON contenant deux nombres et une chaîne. Plongeons maintenant dans l'implémentation réelle de la VM.

Décodage du bytecode

Au début de la fonction Q.exports, on peut voir comment le bytecode est décodé :

  1. La chaîne d'entrée est décodée en base64
  2. Un tableau de longueur 129 263 est créé
  3. Chaque index est vérifié par rapport à une plage spécifique :
    • Si l'index se trouve dans la plage, la valeur est décodée
    • Sinon, un nombre aléatoire est renvoyé (via B(), un générateur pseudo-aléatoire) -> D contient le bytecode décodé avec un certain « bruit » aléatoire

Architecture de la VM

En descendant, on trouve le point d'entrée de la VM : une fonction avec deux paramètres A (le bytecode) et Q (un dictionnaire vide utilisé pour la gestion des erreurs).

Disposition mémoire

L'aspect le plus intéressant de cette VM est son architecture : tout vit dans un seul tableau (A). Ce tableau contient :

  • La pile
  • Les registres
  • Les opcodes
  • Le bytecode lui-même
  • Le pointeur d'instruction

Cette conception reflète une architecture informatique réelle avec des régions mémoire distinctes. L'étape suivante consiste à cartographier chaque décalage pour comprendre ce qui est stocké où :

root@kitploit:~
var stack_pointer = 4593
var instruction_pointer = 4635
var frame_base_pointer = 4674
var last_result = 4633
var exit_flag = 4656
var current_opcode_handler = 4685
var current_opcode_id = 4675
var stack_offset = 124482
var vm_start = 5258

Avec ces décalages cartographiés, la structure de la VM devient claire.

Fonctions auxiliaires

La VM commence par un ensemble de fonctions auxiliaires qui gèrent :

  • La lecture de valeurs typées depuis la pile
  • Le déplacement de données entre la pile et les « registres »

Initialisation de la VM

Après les fonctions auxiliaires, la VM initialise les valeurs de base :

  • Tous les pointeurs (pile, instruction, base de cadre)
  • Le drapeau de sortie
  • Le registre du dernier résultat

En dessous de l'initialisation se trouvent tous les gestionnaires d'instructions.

La boucle de répartition

Le répartiteur est la boucle principale de la VM qui s'exécute jusqu'à ce que `exit_flag` soit défini :
  • I représente l'instruction courante
  • P est le décalage réel dans le tableau (en tenant compte de l'obfuscation)
  • La boucle définit l'instruction courante sur current_opcode_handler et met à jour current_opcode_id

Implémentation des opcodes

Fonctionnement des opcodes

Voici un exemple basique d'un gestionnaire d'opcode :

  1. Récupère une valeur immédiate depuis le bytecode
  2. Récupère la valeur au sommet de la pile
  3. Effectue une opération (par exemple %= ou ^=)
  4. Appelle la fonction fetch() à la fin

Opcodes intéressants

Opcode 4919 : Création de fonction/fermeture

Un des opcodes les plus complexes crée des fermetures/fonctions :

root@kitploit:~
A[4919] = function () {
    var Q = readUint8();  // Nombre d'arguments attendus
    var B = [];
    for (var E = readUint8(), D = 0; D < E; D++) {
        var g = readUint8();
        var a = A[A[frame_base_pointer] + g];
        B.push(a);  // Capture des variables depuis la portée courante
    }
    var h = A[instruction_pointer] + 3;  // Sauvegarde de l'adresse du corps de la fonction
    A[A[stack_pointer]++] = function (E) {
        // Configuration d'un nouveau cadre de pile lors de l'appel
        var e = A[stack_pointer] - E;
        while (E < Q) {
            A[e + E++] = undefined;  // Remplir les arguments manquants avec undefined
        }
        A[stack_pointer] = e + Q;
        for (var D = 0; D < B.length; D++) {
            var g = B[D];
            A[A[stack_pointer]++] = g;  // Pousser les variables capturées
        }
        A[e - 2] = A[frame_base_pointer];  // Sauvegarder l'ancien pointeur de cadre
        A[e - 1] = A[instruction_pointer];  // Sauvegarder l'adresse de retour
        A[frame_base_pointer] = e;
        A[instruction_pointer] = h;  // Sauter au corps de la fonction
    };
    fetch();
};

Cet opcode :

  1. Lit le nombre d'arguments attendus
  2. Capture les variables de la portée courante (fermeture)
  3. Crée une fonction qui configure un nouveau cadre de pile avec les conventions d'appel appropriées
  4. Gère les arguments manquants en les remplissant avec undefined
  5. Sauvegarde l'adresse de retour et le pointeur de cadre pour des retours corrects

Opcode 5003 : Appel de fonction dynamique

Cet opcode crée un wrapper pour les appels de fonction qui gère à la fois les appels réguliers et les appels constructeurs :

root@kitploit:~
A[5003] = function () {
    var Q = A[--A[stack_pointer]];  // POP fonction
    var B = A[--A[stack_pointer]];  // POP contexte 'this'

    function E(e) {  // e = nombre d'arguments
        var D = A[stack_pointer];
        var g = A.slice(D - e, D);  // Récupérer les arguments depuis la pile
        if (this instanceof E) {
            // Appel constructeur (new E(...))
            g.unshift(null);
            var h = Function.prototype.bind.apply(Q, g);
            A[stack_pointer] -= e;
            try {
                a = new h();
            } catch (A) {
                a = A.message;
            }
            A[A[stack_pointer]++] = a;
        } else {
            // Appel de fonction régulier
            var t;
            try {
                t = Q.apply(B, g);
            } catch (A) {
                t = A.message;
            }
            A[stack_pointer] -= e + 2;
            A[A[stack_pointer]++] = t;
        }
    }

    A[A[stack_pointer]++] = E;
    fetch();
};

Cet opcode :

  1. Dépile la fonction et le contexte de la pile
  2. Crée un wrapper qui peut être appelé avec des arguments
  3. Détecte s'il s'agit d'un appel constructeur (new) ou d'un appel régulier
  4. Applique la fonction avec le contexte approprié et la gestion des erreurs
  5. Pousse le résultat sur la pile

Opcode 4961 : Construction de littéral objet

root@kitploit:~
A[4961] = function () {
    var Q = {};
    for (var E = readUint16(), e = 0; e < E; e++) {
        var D = A[--A[stack_pointer]];  // Premier POP
        var g = A[--A[stack_pointer]];  // Deuxième POP
        Q[D] = g;
    }
    A[A[stack_pointer]++] = Q;
    fetch();
};

Cet opcode construit des littéraux objet en :

  1. Lisant le nombre de paires propriété depuis le bytecode
  2. Dépilant les paires de la pile (le premier dépilé devient la clé)
  3. Construisant un objet : objet[premierDepilé] = deuxièmeDepilé
  4. Poussant l'objet résultant sur la pile

La fonction Fetch

Chaque instruction se termine par l'appel de fetch(), qui prépare l'instruction suivante :

root@kitploit:~
function fetch() {
    var Q = A[instruction_pointer];
    var B = A[vm_start + Q];
    A[instruction_pointer] = Q + 1;
    var E = A[4783 + B];
    A[current_opcode_handler] = E;
    A[current_opcode_id] = B;
}

Cette fonction :

  1. Lit le pointeur d'instruction
  2. Récupère le prochain opcode depuis le bytecode
  3. Incrémente le pointeur d'instruction
  4. Cherche le gestionnaire d'opcode
  5. Met à jour current_opcode_handler et current_opcode_id

Cela reflète la logique du répartiteur, créant un cycle fetch-decode-exécuter typique des architectures de VM.

Implémentation du désassembleur

Pour faciliter l'analyse, un désassembleur de preuve de concept (disasm.js) a été développé pour convertir le bytecode de la VM en assembleur lisible par l'homme.

Approche

Le désassembleur fonctionne en deux passes :

Passe 1 : Découverte des labels

La première passe scanne le bytecode pour identifier toutes les cibles de saut. Cela inclut :

  • Les sauts avant et arrière (JMP_FWD, JMP_BACK)
  • Les sauts conditionnels (JZ, JNZ_KEEP, JZ_KEEP)
  • Les limites des fermetures (corps de fonctions et leurs points de fin)

Chaque adresse cible est marquée avec un label (par exemple L_0042) pour faciliter le suivi du flux de contrôle.

Passe 2 : Désassemblage

La deuxième passe convertit chaque instruction en sortie pseudo-assembleur :

root@kitploit:~
000042:  fa 00 0a           PUSH_IMM 10
000045:  19 00 19           PUSH_REG 25
000048:  eb                 ADD

Chaque ligne comprend :

  • Adresse : Décalage hexadécimal dans le bytecode
  • Octets bruts : Les octets réels constituant l'instruction (utile pour la vérification) et j'sais pas, ça a l'air badass
  • Opcode : Nom mnémotechnique de l'instruction
  • Arguments : Opérandes décodés (numéros de registre, valeurs immédiates, cibles de saut)

Décodage des valeurs

L'un des aspects les plus complexes est le décodage des valeurs immédiates intégrées dans le bytecode. La VM utilise des marqueurs de type pour indiquer comment interpréter les octets suivants :

Types simples (aucune donnée supplémentaire) :

  • 0x28 → true
  • 0x7D → false
  • 0x4C → null
  • 0x3D → undefined

Petits entiers (0-127) : Encodés avec le bit de poids fort

  • 0x85 → 5 (0x80 | 5)

Chaînes : Encodées par XOR et terminées par null

  • Chaînes ASCII : marqueur 0x67, clé XOR commençant à 183
  • Chaînes UTF-8 : marqueur 0x27, clé XOR commençant à 46

Types numériques :

  • 8 bits signé : 0x6F + 1 octet
  • 16 bits signé : 0x61 + 2 octets (big-endian)
  • 24 bits signé : 0x65 + 3 octets (big-endian)
  • 32 bits signé : 0x54 + 4 octets (big-endian)
  • Double IEEE 754 : 0x05 + 8 octets

L'encodage XOR pour les chaînes est simple mais empêche une inspection occasionnelle :

root@kitploit:~
let str = '';
let xorKey = 183;  // Clé initiale pour les chaînes ASCII
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
    str += String.fromCharCode(ch);
}

Opcodes spéciaux

Certains opcodes nécessitent un traitement personnalisé :

CLOSURE (opcode 136) : Crée des fonctions/fermetures avec des variables capturées

root@kitploit:~
Format : CLOSURE locales, nombre_captures, [indices_capture...], décalage_saut

Le décalage de saut pointe après le corps de la fonction, permettant à la VM de sauter par-dessus la définition de fonction lors de l'exécution linéaire.

PUSH_MULTI_IMM (opcode 96) : Pousse plusieurs valeurs à la fois

root@kitploit:~
Format : PUSH_MULTI_IMM nombre, val1, val2, ...

Utilisation

root@kitploit:~
# Désassembler depuis un fichier
node disasm.js bytecode.txt

Exemple de sortie

root@kitploit:~
; Désassemblage VM DataDome
; Taille du bytecode : 5428 octets
; Constantes VM : VM_START=5258, OPCODE_BASE=4783

000000:  88 01 00 04        CLOSURE locals=1, captures=[0, 4], corps=L_0006, fin=L_0a3f
L_0006:
000006:  fa 67 ...          PUSH_IMM "window"
00001f:  2b                 PUSH_WINDOW
000020:  02                 SET
000021:  fa 67 ...          PUSH_IMM "navigator"
00003a:  19 00 00           PUSH_REG 0
00003d:  fa 67 ...          PUSH_IMM "navigator"
000056:  ee                 GET
000057:  02                 SET
...

Ce format de sortie permet de :

  • Tracer le flux d'exécution en suivant les labels de saut
  • Identifier les limites de fonctions via les opcodes CLOSURE
  • Voir exactement quelles valeurs sont poussées et manipulées
  • Recouper avec l'implémentation réelle de la VM

Remarques

Je suis encore assez novice en matière de VM, donc prenez tout avec des pincettes. L'IA a été utilisée pour aider à documenter le code et à rédiger certaines parties de ce readme (parce que la rédaction de documentation est pénible).

Avertissement

Ceci est purement à des fins éducatives / de recherche en sécurité. Aucun solveur ni contournement inclus – juste documenter le fonctionnement de la VM parce que c'est vraiment intéressant.

DataDome : si vous lisez ceci, bonjour !!! C'est juste moi qui essaie d'obtenir une bourse 🙏. Ne me poursuivez pas en justice, je suis fauché comme pas possible. Si vous avez un problème avec ce dépôt, faites-le moi savoir et on peut en discuter :)

Désolé pour ceux qui pensaient que j'allais parler de l'intérieur de la VM... ça n'arrivera pas

NE ME DM PAS POUR ME DEMANDER UNE API DATADOME, JE NE VOUS AIDERAI PAS

Télécharger l’outil