Face à un SDK de signature sorti de webpack, tout ressemble vite à du bruit : des variables nommées a ou _0x3f2b, un flot de contrôle aplati et des chaînes cachées dans un tableau d’indices. L’objectif, lui, ne change pas : reconstruire une implémentation autonome produisant exactement les mêmes octets que l’original.
Le réflexe actuel consiste à coller tout le bundle dans un chat et à demander : « que fait ce code ? ». C’est généralement là que le travail déraille. Le modèle renvoie une explication convaincante, l’implémentation semble logique, puis la sortie ne correspond pas à la cible — parfois pas même d’un seul octet.
Le problème n’est pas la faiblesse du modèle, mais une mauvaise répartition des rôles. En désobfuscation, un LLM excelle à reconnaître des motifs et formuler des hypothèses, mais il est mauvais pour établir des faits. Il peut repérer le squelette d’une primitive cryptographique sous une montagne d’opérations binaires ; il ne peut pas vous garantir qu’il l’a correctement identifiée. Cette validation relève de vos tests.
Voici donc un workflow en quatre étapes construit autour de cette séparation, suivi de trois tâches que je ne délègue jamais au modèle.
Étape 1 : découpez mécaniquement, sans LLM
Le premier réflexe est souvent : « la fenêtre de contexte est immense, autant tout envoyer ». Ne le faites pas, pour deux raisons.
D’abord, c’est du gaspillage. Un bundle obfusqué contient en grande partie des polyfills, des shims d’exécution et des modules métier sans rapport avec la cible. Vous payez pour les injecter dans le contexte et obtenez en retour une attention diluée.
Ensuite, et c’est le point essentiel : plus le contexte est vaste, plus une hallucination a d’endroits où s’ancrer. Le modèle peut relier des éléments provenant de deux modules sans rapport et vous livrer une conclusion cohérente en apparence, mais totalement absente du code. Ce type d’erreur est bien plus difficile à détecter qu’une absurdité évidente.
Le découpage est une tâche déterministe. Automatisez-la :
Utilisez un outil AST (
@babel/parser,acorn) pour séparer le bundle en modules et fonctions, puis indexez-les par portée ;Extrayez tous les littéraux numériques et toutes les constantes texte, en les regroupant selon leur fréquence et leur largeur en bits ;
Construisez le graphe d’appels et marquez les nœuds dont le degré entrant est 0, c’est-à-dire les points d’entrée, ainsi que ceux dont le degré sortant est 0, les primitives terminales ;
Repérez les fonctions où les opérations binaires sont anormalement denses : des
^,>>>,<<et&concentrés dans une même fonction signalent souvent le cœur algorithmique.
Ce sont précisément ces primitives terminales qu’il faut montrer au modèle. Elles font souvent quelques dizaines de lignes, dépendent peu de l’état des couches supérieures et présentent des entrées et sorties nettes. Une fonction terminale de 40 lignes accompagnée des constantes qu’elle utilise est un niveau de granularité que le modèle traite de façon fiable.
À l’issue de cette étape, vous devez disposer d’une « liste de primitives candidates » : chaque entrée contient le corps de la fonction, les constantes auxquelles elle fait référence et les emplacements depuis lesquels elle est appelée. Toutes les requêtes suivantes portent sur une seule entrée de cette liste, une à la fois.
Étape 2 : utilisez le modèle pour reconnaître la famille d’algorithmes
C’est ici que le modèle devient réellement difficile à remplacer.
Les algorithmes de cryptographie et d’encodage ont des signatures très marquées : certaines constantes, des combinaisons précises de décalages, des structures de boucles caractéristiques. Un humain les reconnaît grâce à l’expérience ; un modèle a vu les implémentations publiques et repère naturellement ces indices.
Quelques exemples publics permettent de voir à quoi ressemble une telle signature :
0x811c9dc5et0x01000193sont respectivement la base de décalage et le nombre premier du hachage FNV-1a 32 bits — les valeurs normalisées publiées sur la page de référence FNV maintenue par son co-auteur Landon Curt Noll, où elles apparaissent en décimal : 2166136261 et 16777619 ;0x61707865, 0x3320646e, 0x79622d32, 0x6b206574sont les mots little-endian de la chaîne ASCII"expand 32-byte k": les constantes de l’état initial de ChaCha20, listées dans la RFC 8439 §2.3 ;une séquence du type
a += b; d ^= a; d = rotl(d, 16); c += d; b ^= c; b = rotl(b, 12);, avec les quatre rotations 16/12/8/7, signe un quarter round ChaCha20 ; Salsa20, son proche parent, utilise 7/9/13/18, et ces quatre nombres suffisent à distinguer les deux ;une table de 64 constantes commençant par
0xd76aa478correspond à la table T de MD5 (RFC 1321 §3.4) ;une table de 256 octets débutant par
0x63, 0x7c, 0x77, 0x7best la S-box AES (FIPS 197, table 4) ;0xEDB88320est le polynôme CRC-32 réfléchi, celui employé dans la spécification gzip, RFC 1952.
Tout se joue dans la formulation de la demande. Avec « que fait ce code ? », vous obtiendrez de la prose. Ce qu’il vous faut, c’est un jugement structuré et vérifiable. Le prompt doit donc imposer trois éléments : candidats, éléments probants et contre-arguments.
Below is a leaf function extracted from an obfuscated bundle, together with
every numeric constant it references.
<function>
{{function body}}
</function>
<constants>
{{constant list, with locations}}
</constants>
Answer in the following structure. Do not write prose.
1. Candidate algorithm families (at most 3, ranked by likelihood)
For each: the name, and which class it belongs to
(hash / stream cipher / block cipher / encoding / compression / checksum)
2. Supporting evidence
Every piece of evidence must point to a specific constant value or a
specific line above. "Structurally similar" and other unverifiable
phrasing is not allowed.
3. Counter-evidence and deviation points
If this were the standard implementation of that algorithm, what should
appear here but doesn't? What appears that a standard implementation
would never contain? Are these deviations a "variant," or "I got it wrong"?
4. The minimal test to decide this hypothesis
Give 3 concrete inputs and, if the hypothesis holds, the shape of the
output each should produce. Include boundary cases.
La troisième partie est celle qui donne tout son intérêt à ce prompt. Les écarts avec l’implémentation standard indiquent exactement les endroits où le code a été modifié : alphabet personnalisé, constante remplacée, nombre de tours modifié. Ce sont aussi les seules portions sur lesquelles vous aurez véritablement à travailler lors de la réécriture ; le reste peut être repris depuis l’implémentation publique. La manière de faire émerger chaque écart est au cœur de l’article sur l’identification par empreinte algorithmique.
La quatrième partie transforme directement le jugement du modèle en cas de test pour l’étape suivante, ce qui évite un aller-retour supplémentaire.
Cette étape rencontre souvent des encodages non standard. Là encore, le diagnostic est mécanique : un alphabet de 65 caractères — 64 symboles et un caractère de remplissage —, des groupes de 6 bits et une longueur de sortie ceil(n/3)*4 indiquent la famille Base64 ; si l’alphabet est réordonné, la table est personnalisée. De même, un dictionnaire commençant à 256 et grandissant, avec une largeur de code qui augmente à mesure qu’il se remplit, correspond à LZW. Ces éléments se vérifient à partir des seules relations entre longueurs d’entrée et de sortie, sans lire une ligne de code.
Étape 3 : transformez l’hypothèse en test différentiel
Le modèle vous a fourni une hypothèse. Tant que vous n’avez pas écrit le test, elle reste une simple phrase.
Il n’existe pas de raccourci, et c’est le seul verrou du workflow contre les hallucinations — la raison pour laquelle c’est le seul est détaillée dans l’article sur les tests différentiels. Traitez l’implémentation d’origine comme une boîte noire et comparez votre réécriture avec elle, cas par cas :
# Differential-test skeleton: original as black box, rewrite compared case by case
CASES = [
b"", # empty input: exposes initial state and padding logic
b"\x00", # single zero byte
b"\xff", # single high byte: checks sign-bit handling
b"a" * 63, # one below the block boundary
b"a" * 64, # exactly one block
b"a" * 65, # one above: checks padding and carry
bytes(range(256)), # full byte coverage: checks alphabet mapping
]
for case in CASES:
assert rewritten(case) == blackbox(case), case.hex()
Quelques règles pratiques :
Les franchissements de seuil sont les plus informatifs. Les algorithmes par blocs exposent plus facilement leur logique de padding à 64n et 64n±1. Si un seul cas échoue dans toute la série, la longueur de son entrée vous indique directement quelle couche est en cause.
Exécutez deux fois la même entrée. Si le résultat diffère, l’implémentation intègre une valeur aléatoire ou un horodatage. Il faut alors trouver le point d’injection et permettre de le surcharger depuis l’extérieur ; sinon, toute comparaison différentielle devient impossible. C’est le blocage le plus courant lors de la réécriture d’une logique de signature : l’algorithme n’est pas faux, sa source d’entropie n’a simplement pas été isolée.
Démontez les couches au lieu de comparer l’ensemble. Validez d’abord le hachage interne, puis l’encodage une fois celui-ci correct, et enfin la couche d’assemblage. Quand la sortie complète diverge, vous ignorez où chercher ; une fois les couches séparées, la première qui échoue est celle qui pose problème.
Conservez vos vecteurs fixes comme fixture. Une table entrée connue → sortie connue vous permet de répondre rapidement à la question : « est-ce mon code qui est faux, ou l’amont a-t-il changé ? » après une mise à jour. La valeur de cette fixture ne fait qu’augmenter avec le temps.
À ce stade, le modèle peut générer des cas de test et expliquer les écarts, mais il ne doit pas décider du vrai et du faux. Cette décision revient à assert.
Étape 4 : déployez en descendant les couches de transport
Lorsque toutes les hypothèses passent les tests, il reste à produire du code durablement exploitable. L’ordre de priorité est clair : plus vous descendez dans cette liste, plus il faut se battre pour remonter.
Une réécriture native dans votre langage cible. Elle est complètement découplée du runtime d’origine et ne dépend que de la bibliothèque standard. C’est la seule forme sans processus supplémentaire, sans dépendance externe et avec une intégration CI propre.
Un moteur JS local exécutant un fragment minimal. Certaines logiques sont trop coûteuses à purifier à court terme ; dans ce cas, conservez une petite portion du JavaScript original et exécutez-la dans un Node/V8 local. Attention : le contexte d’un moteur JS n’est pas thread-safe ; l’appel d’un contexte compilé depuis plusieurs threads doit être verrouillé :
class Signer:
def __init__(self, source: Path) -> None:
self._context = execjs.get("Node").compile(source.read_text())
self._lock = threading.Lock() # V8 context is not thread-safe
def call(self, fn: str, *args):
with self._lock:
return self._context.call(fn, *args)
Un pont passif vers le navigateur. Lorsque l’état recherché ne peut être obtenu que depuis le runtime d’une page réelle, le navigateur reste la seule solution pour le moment. C’est une solution temporaire : signalez-la clairement dans les notes d’interface et ne la laissez jamais devenir l’implémentation par défaut.
Cet ordre de repli mérite d’être inscrit dans les conventions du projet. À chaque niveau inférieur, la surface de dépendances, les modes de défaillance et le coût de déploiement augmentent d’un ordre de grandeur : le niveau 1 est une fonction pure ; le niveau 3 est un processus externe qui exige qu’une personne maintienne une session active. Défendez le niveau 1 par défaut, et vous éviterez la majeure partie du coût à long terme qu’un « faisons-le juste marcher » accumule silencieusement. La façon de délimiter ces trois niveaux et d’empêcher le pont passif de s’emballer est présentée dans l’article sur l’échelle de purification.
Un ordre de grandeur : un SDK de signature obfusqué, passé par les quatre étapes, aboutit à une implémentation autonome de moins de 600 lignes ne dépendant que du module crypto intégré au runtime. Ce taux de compression ne vient pas d’une « compréhension » du fichier original par le modèle : une fois la famille algorithmique correctement identifiée, l’immense majorité du code peut être reprise directement depuis l’implémentation publique.
Le bon modèle à chaque étape
Les quatre étapes ne demandent pas du tout les mêmes capacités. Utiliser un seul modèle pour l’ensemble fait perdre soit de l’argent, soit de la précision :
Étape | Capacité réellement nécessaire | Mon choix | model id |
|---|---|---|---|
Cartographie structurelle après découpage | Contexte long, capable de lire le graphe d’appels d’un module entier en une passe | Kimi K3 |
|
Identification de famille algorithmique et contre-arguments | Raisonnement solide ; détecte les écarts et remet ses propres conclusions en cause | Claude Opus 5 |
|
Renommage massif des symboles, ajout de commentaires | Économique, capable de lancer des centaines d’appels avec une forte concurrence | Claude Sonnet 5 |
|
Attribution des différences, pour lire un diff après l’échec d’un test | Raisonnement intermédiaire ; explique les écarts à partir d’octets précis | GPT-5.6 Sol |
|
L’étape 2 mérite une attention particulière. L’identification de famille algorithmique est la seule étape pour laquelle changer de modèle modifie visiblement le résultat, car elle mesure exactement deux choses : « combien d’implémentations publiques avez-vous vues ? » et « êtes-vous prêt à contester votre propre conclusion ? ». Sur une même fonction terminale, un modèle moins capable donnera une réponse erronée avec assurance ; un modèle plus solide éliminera lui-même un candidat dans la section des contre-arguments.
Ne me croyez pas sur parole : testez-le. Le protocole est simple :
Choisissez 3 fonctions terminales de votre propre bundle obfusqué, dont au moins 1 dont vous connaissez déjà la réponse, pour servir de contrôle.
Avec le prompt en trois parties de l’étape 2, envoyez la même entrée séparément à
claude-opus-5etgpt-5.6-sol.Ne regardez que deux critères : a-t-il identifié la bonne famille candidate ; et la section des contre-arguments conteste-t-elle vraiment la réponse, ou reformule-t-elle simplement le candidat avec d’autres mots ?
La qualité de la section des contre-arguments est votre critère de sélection : elle détermine directement combien de tests inutiles vous écrirez à l’étape 3.
Les étapes 3 et 4 nécessitent à peine une sélection de modèle : n’importe quel modèle fonctionnel convient. L’étape 1 demande simplement un modèle à contexte long, afin de vous éviter d’écrire votre propre mécanisme de récupération par fragments.
Le vrai frein n’est pas le choix du modèle, mais son coût de bascule
Quatre modèles répartis chez trois fournisseurs, cela signifie trois SDK, trois mécanismes d’authentification et trois formats d’erreurs. Réécrire votre client pour économiser un peu d’argent n’en vaut pas la peine — et c’est précisément pour cela que la plupart des gens finissent par employer un seul modèle pour tout.
AIReiter élimine cette couche : une clé, une interface compatible OpenAI, les quatre modèles derrière, et le changement se résume à modifier le champ model dans le corps de la requête.
# Algorithm-family ID: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<Stage 2 prompt + leaf function + constants>"}]
}'
# Bulk symbol renaming: change the model field, leave everything else
# "model": "claude-sonnet-5"
Si vous utilisez déjà le SDK OpenAI, pointez base_url vers https://aireiter.com/api/v1 sans rien changer d’autre. Avec le SDK Anthropic, appelez POST /api/v1/messages en utilisant la même clé.
Côté prix, les modèles Claude bénéficient de 30 % de réduction sur le tarif catalogue et les modèles GPT sont à moitié prix. Dans ce workflow, c’est plus important qu’il n’y paraît : le renommage massif des symboles de l’étape 3 atteint facilement plusieurs centaines d’appels, tandis que le passage à contexte long de l’étape 1 représente quelques centaines de milliers de tokens pour une seule entrée. Ces deux postes concentrent l’essentiel du coût, et la réduction s’applique précisément à la partie la plus chère.
Essayer sans inscription — lancez d’abord quelques prompts à la main, comparez les sections de contre-arguments des deux modèles, puis automatisez seulement après avoir arrêté votre choix.
Trois choses à ne pas déléguer au modèle
Premièrement : ne lui demandez pas de produire directement l’implémentation finale. Si vous demandez une « réécriture complète », vous obtiendrez du code qui semble achevé et s’exécute, mais comporte des écarts subtils. Et vous ne saurez pas les localiser, puisque ce code n’aura pas été vérifié couche par couche par vos soins. La bonne approche consiste à demander une hypothèse pour chaque primitive, à les valider une par une, puis à assembler vous-même le résultat. C’est plus lent, mais vous savez pourquoi chaque ligne existe.
Deuxièmement : ne le laissez pas décider si le résultat est correct. « Peux-tu confirmer que cette implémentation est juste ? » est une question sans issue : le modèle a tendance à vous donner raison. Le test différentiel est la seule autorité qui distingue le vrai du faux. Le modèle dit que c’est correct mais le test échoue ? Le test l’emporte. Le modèle dit que c’est incorrect mais tous les tests passent ? Le test l’emporte aussi.
Troisièmement : ne lui confiez pas la décision de conformité. Le droit de manipuler la cible, de publier vos conclusions ou d’utiliser les données obtenues dépend de votre juridiction, des conditions d’utilisation de la cible et de votre objectif précis. Le modèle n’a aucune base factuelle pour trancher ; sa réponse ne fait qu’imiter le ton des avertissements qu’il a déjà vus. Cette décision vous appartient, ou revient à votre véritable conseil juridique.
Pour conclure
La place du modèle dans ce type de travail est précise : c’est un reconnaisseur de motifs ayant vu les implémentations publiques des algorithmes et capable de vous proposer des hypothèses candidates en quelques secondes. Ce n’est pas une source de vérité, et ce n’est pas un outil de validation.
L’ossature du workflow n’a rien à voir avec l’IA : découpage mécanique, hypothèse, vérification, purification. Ces quatre étapes constituaient déjà le processus avant les modèles. Ce que le modèle compresse, c’est l’étape d’« hypothèse », qui passe de plusieurs jours à parcourir des références à quelques minutes. Les trois autres étapes coûtent exactement ce qu’elles ont toujours coûté.
Une fois les appels au modèle figés dans des scripts pour ces quatre étapes, l’ensemble du workflow devient rapide. Il ne reste alors qu’une friction : le changement de modèle. C’est un problème d’infrastructure, résolu en choisissant votre modèle derrière une interface unifiée.
