Skip to content
KitploitKITPLOIT
OutilsExploitsBlog
Log in
Soumettre
OutilsExploitsBlog
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
1251961il y a 7 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ù :

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 :

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 :

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;
        }
    }
Télécharger l’outil