AST : désobfuscation de JavaScript
Les outils d’obfuscation JavaScript modernes (comme javascript-obfuscator) produisent du code illisible : variables renommées en _0x3f2a, chaînes encodées en Base64, flux de contrôle aplati. Une lecture directe est inefficace. Passer par l’AST permet de transformer le code de façon structurée plutôt que de l’analyser ligne par ligne.
Ce qu’est un AST
Section intitulée « Ce qu’est un AST »Un Abstract Syntax Tree (arbre syntaxique abstrait) est la représentation structurée d’un programme. Lorsqu’un moteur JavaScript parse du code, il le traduit d’abord en un arbre de nœuds avant de l’exécuter. Chaque nœud correspond à un élément syntaxique : une déclaration de variable, un appel de fonction, un opérateur, une chaîne littérale.
// Code sourceconst x = 1 + 2;Produit un AST de cette forme :
{ "type": "VariableDeclaration", "kind": "const", "declarations": [{ "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "x" }, "init": { "type": "BinaryExpression", "operator": "+", "left": { "type": "NumericLiteral", "value": 1 }, "right": { "type": "NumericLiteral", "value": 2 } } }]}L’intérêt pour la désobfuscation : l’AST expose la structure logique indépendamment du renommage ou des encodages. On peut parcourir l’arbre, identifier les nœuds qui correspondent à des patterns connus (tableau de chaînes décodées, appels de rotation, wrappers inutiles), les transformer, puis régénérer du code source propre.
Explorer un AST en ligne
Section intitulée « Explorer un AST en ligne »Avant d’écrire du code, AST Explorer permet de coller du JavaScript obfusqué et de visualiser l’arbre correspondant en temps réel. C’est le point de départ pour identifier quels types de nœuds cibler.
Outillage : recast vs Babel
Section intitulée « Outillage : recast vs Babel »Deux bibliothèques couvrent la majorité des cas.
recast parse le code en AST, permet de le modifier, puis le régénère en préservant au maximum le formatage original des parties non modifiées. Il est agnostique au parseur et accepte des AST produits par acorn, babel, ou d’autres.
@babel/generator fonctionne avec les AST produits par @babel/parser. Il est plus adapté si le code obfusqué a été généré par un outil basé sur Babel, ou si les transformations utilisent l’API @babel/traverse.
Installation
Section intitulée « Installation »# Option recastnpm install recast
# Option Babelnpm install @babel/core @babel/generator @babel/traverse @babel/parserDésobfuscation avec recast
Section intitulée « Désobfuscation avec recast »const recast = require("recast");const fs = require("fs");
const source = fs.readFileSync("obfuscated.js", "utf8");const ast = recast.parse(source);
// Parcourir l'arbre et appliquer des transformationsrecast.visit(ast, { visitStringLiteral(path) { const { node } = path; // Décoder les chaînes encodées en hex ou unicode if (node.value && /\\x[0-9a-f]{2}/i.test(node.extra?.raw || "")) { // La valeur décodée est déjà dans node.value, rien à faire, recast la restituera proprement } this.traverse(path); }});
const output = recast.print(ast).code;fs.writeFileSync("deobfuscated.js", output);L’avantage de recast : il ne retouche que les nœuds modifiés. Les parties intactes de l’arbre conservent leur formatage d’origine, ce qui facilite la comparaison avec le fichier source.
Désobfuscation avec Babel
Section intitulée « Désobfuscation avec Babel »const parser = require("@babel/parser");const generate = require("@babel/generator").default;const traverse = require("@babel/traverse").default;const fs = require("fs");
const source = fs.readFileSync("obfuscated.js", "utf8");
const ast = parser.parse(source, { sourceType: "module", plugins: ["jsx"]});
traverse(ast, { // Remplacer les expressions binaires calculables à la compilation BinaryExpression(path) { const { node } = path; if ( node.left.type === "NumericLiteral" && node.right.type === "NumericLiteral" ) { const result = eval(`${node.left.value} ${node.operator} ${node.right.value}`); path.replaceWith({ type: "NumericLiteral", value: result }); } },
// Dérouler les appels inutiles à des wrappers identité CallExpression(path) { const { node } = path; if ( node.callee.type === "FunctionExpression" && node.arguments.length === 0 && node.callee.body.body.length === 1 && node.callee.body.body[0].type === "ReturnStatement" ) { path.replaceWith(node.callee.body.body[0].argument); } }});
const { code } = generate(ast, { compact: false });fs.writeFileSync("deobfuscated.js", code);Exécuter avec :
node index.jsPatterns d’obfuscation courants et comment les traiter
Section intitulée « Patterns d’obfuscation courants et comment les traiter »Tableau de chaînes déplacées
Section intitulée « Tableau de chaînes déplacées »L’obfuscateur extrait toutes les chaînes littérales dans un tableau global, puis remplace chaque usage par un appel à une fonction de lookup :
// Code obfusquévar _0x1a2b = ["admin", "password", "/api/login"];function _0x3c4d(index) { return _0x1a2b[index]; }
_0x3c4d(0); // → "admin"_0x3c4d(2); // → "/api/login"Approche AST : identifier le tableau de déclaration, évaluer statiquement les appels de lookup dont l’index est un littéral numérique, et remplacer les nœuds CallExpression par les StringLiteral correspondants.
traverse(ast, { CallExpression(path) { const { node } = path; // Vérifier que c'est un appel au wrapper de lookup if ( node.callee.type === "Identifier" && node.callee.name === "_0x3c4d" && node.arguments.length === 1 && node.arguments[0].type === "NumericLiteral" ) { const index = node.arguments[0].value; const strings = ["admin", "password", "/api/login"]; // extrait au préalable path.replaceWith({ type: "StringLiteral", value: strings[index] }); } }});Rotation du tableau
Section intitulée « Rotation du tableau »Certains outils font tourner le tableau d’un offset pour rendre le lookup moins direct. L’offset est déterminé lors du chargement via une IIFE. Évaluer cette IIFE isolément pour récupérer l’état final du tableau avant d’appliquer le remplacement.
Flux de contrôle aplati
Section intitulée « Flux de contrôle aplati »Le code est transformé en une machine à états avec un switch central :
var _state = "3|1|0|2";var _steps = _state.split("|");var _idx = 0;while (true) { switch (_steps[_idx++]) { case "0": doC(); continue; case "1": doB(); continue; case "2": break; case "3": doA(); continue; } break;}Approche AST : identifier la chaîne d’état, la parser, remettre les cases dans l’ordre défini, et remplacer le while/switch par la séquence linéaire correspondante.
Erreurs de parsing fréquentes
Section intitulée « Erreurs de parsing fréquentes »Un fichier partiellement corrompu ou généré par un outil non standard peut provoquer des erreurs à l’étape de parse. Diagnostics courants :
Unexpected token: le fichier contient une syntaxe que le parseur ne reconnaît pas. Essayer un autre parseur (acornà la place de@babel/parser, ou l’inverse) ou activer des plugins supplémentaires ("estOptionality","v8intrinsic").Maximum call stack size exceeded: l’arbre est très profond (obfuscation par nesting excessif). Implémenter un visiteur itératif plutôt que récursif.- Nœud inconnu : recast échoue si l’AST contient un type de nœud qu’il ne reconnaît pas. Vérifier que le parseur utilisé par recast correspond au format produit.
// Forcer recast à utiliser le parseur babelconst ast = recast.parse(source, { parser: require("recast/parsers/babel")});Ressources complémentaires
Section intitulée « Ressources complémentaires »Le dépôt SteakEnthusiast/Supplementary-AST-Based-Deobfuscation-Materials documente des cas concrets de désobfuscation AST avec le code source des scripts utilisés. Point de départ utile pour adapter des transformations à un obfuscateur spécifique.
À retenir
Section intitulée « À retenir »L’AST transforme la désobfuscation d’un travail de lecture textuelle en un travail de transformation structurée. Identifier les patterns dans l’arbre, écrire un visiteur ciblé, régénérer le code. Chaque couche d’obfuscation correspond à un type de nœud spécifique : les traiter un par un plutôt que de chercher une solution générique.