
Ingénierie inverse de la nouvelle VM Datadome 🔥
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 :
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.
Le 14 janvier 2026, DataDome a commencé à intégrer un nouveau composant basé sur une VM dans leur tag client.
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.
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.
Au début de la fonction Q.exports, on peut voir comment le bytecode est décodé :
B(), un générateur pseudo-aléatoire)
-> D contient le bytecode décodé avec un certain « bruit » aléatoire
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).
L'aspect le plus intéressant de cette VM est son architecture : tout vit dans un seul tableau (A). Ce tableau contient :
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.
La VM commence par un ensemble de fonctions auxiliaires qui gèrent :
Après les fonctions auxiliaires, la VM initialise les valeurs de base :
En dessous de l'initialisation se trouvent tous les gestionnaires d'instructions.
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 couranteP est le décalage réel dans le tableau (en tenant compte de l'obfuscation)current_opcode_handler et met à jour current_opcode_id
Voici un exemple basique d'un gestionnaire d'opcode :
%= ou ^=)fetch() à la finUn 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 :
undefinedCet 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;
}
}