Un exécutable Visual Basic 6 a été produit de l'une de deux façons, et le résultat d'une décompilation en dépend entièrement. Le binaire porte la réponse : il n'y a rien à deviner.
MSVBVM60.dll. Instructions typées, de haut niveau.Le mode de compilation est écrit dans le fichier
VB6 propose deux cibles, réglables dans les propriétés du projet, onglet Compile. Le mode retenu ne se déduit pas après coup : il occupe un champ dédié, et un seul test suffit à le lire.
Le Code Natif est le réglage par défaut du compilateur, et c'est de loin ce qu'on rencontre sur les applications réellement distribuées. La version gratuite de VBReFormer lit ce champ et affiche le mode dès l'ouverture du fichier : c'est la façon la plus rapide de savoir à quoi s'attendre avant d'aller plus loin.
Ce que rend chaque mode
P-Code
Chaque instruction du bytecode correspond à une opération du langage. La structure du code revient presque intégralement : boucles, conditions, appels, opérations sur chaînes et sur Variant se retrouvent sous leur forme d'origine.
La reconstruction est largement mécanique, parce que l'instruction disait explicitement ce qu'elle faisait.
Code Natif
Les types ne sont plus déclarés : ils se déduisent de la façon dont les valeurs sont manipulées. Le runtime fournit l'essentiel des indications, un programme VB6 lui déléguant presque tout ce qui n'est pas de l'arithmétique élémentaire.
Le résultat est plus inégal d'une procédure à l'autre, et c'est le mode qui concerne la grande majorité des binaires qu'on nous soumet.
Ces écarts se mesurent. Voici les taux de traduction en construction VB6 nommée, chacun avec son dénominateur :
Mesuré sur 207 programmes VB6, 125 compilés en code natif et 82 en P-Code. Le détail nom par nom est publié.
À quoi ressemble le résultat
Voici une sortie réelle, tirée de notre étude de cas sur un CrackMe VB6. Un seul extrait montre simultanément ce que le binaire a conservé et ce qu'il a perdu.
Private Sub Command1_Click()
var_pv10 = Text1.Text
var_pv13 = Date$ & " " & Time$
For var_pv14 = 1 To Len(var_pv13) Step 1
If IsNumeric(Mid$(var_pv13, CLng(var_pv14), 1)) Then
var_pv15 = Asc(Mid$(var_pv13, CLng(var_pv14), 1))
If var_pv14 <= Len(var_pv10) Then
var_pv16 = Str(Asc(Mid$(var_pv10, CLng(var_pv14), 1)))
var_pv16 = Right$(var_pv16, 1)
End If
var_pv18 = var_pv18 & Chr$(CLng(var_pv15 + 17 + var_pv16))
End If
Next var_pv14
En cyan, ce qui a été retrouvé dans le binaire : le nom du contrôle et de son gestionnaire d'événement (Command1_Click), le nom du contrôle et sa propriété (Text1.Text), et les fonctions du runtime rendues sous leur nom VB6 d'origine — Len, IsNumeric, Mid$, Asc, Right$, Chr$.
En rouge, ce qui a été perdu : les noms des variables locales. Le décompilateur leur attribue un identifiant dérivé de leur emplacement, faute de mieux.
L'algorithme est intégralement lisible : la clé de validation se construit à partir du texte saisi, de la date et de l'heure. Aucun nom de variable locale n'est nécessaire pour le comprendre — mais il faudra les renommer avant de recompiler.
Ce qui survit, ce qui disparaît
Cette répartition ne dépend pas du mode de compilation. Elle est une propriété du format.
Conservé dans le binaire
- Nom du projet, de l'exécutable, du fichier d'aide
- Noms des formulaires, modules et classes
- Noms des contrôles et leurs propriétés
- Noms des procédures publiques
- Noms des paramètres des membres publics
- Noms des variables membres publiques
- Nom et bibliothèque des appels d'API
Declare - Interfaces
Implements, variablesWithEvents
Absent du binaire
- Noms des variables locales
- Noms des procédures et variables privées
- Types déclarés des locales et paramètres
- Constantes et énumérations, repliées sur leur valeur
- Noms des membres d'un
Type - Types et noms des paramètres d'un
Declare - Commentaires et mise en forme
C'est cette colonne de gauche qui fait la différence entre un projet navigable et un tas de fonctions anonymes. Un formulaire retrouve son nom, ses contrôles retrouvent les leurs, et les procédures publiques restent identifiables — ce qui suffit le plus souvent à se réapproprier une application.
Le test qui induit en erreur
Une méthode circule pour deviner le mode sans lire le champ prévu : examiner la table des méthodes et vérifier si ses entrées ressemblent à des adresses de code valides. Elle échoue précisément là où elle devrait servir.
Sur un binaire natif, cette table contient des pointeurs installés par le runtime, qui ont toute l'apparence d'adresses correctes. Les suivre revient à décoder du code machine comme s'il s'agissait de bytecode, et produit des centaines d'entrées qui ne correspondent à rien.
C'est pourquoi un binaire natif soumis à un outil conçu pour le P-Code ne renvoie pas une erreur franche, mais un résultat abondant et faux — le mode d'échec le plus coûteux, puisqu'il faut du temps pour s'en apercevoir.
En pratique
Identifiez le mode avant toute autre chose : il détermine ce qui est atteignable. En P-Code, attendez-vous à retrouver la structure du code presque telle quelle. En Code Natif, attendez-vous à un code reconstruit, exact dans ses appels et ses opérations, avec des noms de variables locales à réattribuer.
Dans les deux cas l'interface se récupère intégralement — formulaires, contrôles, propriétés, ressources — parce qu'elle est décrite par des données et non par du code. C'est souvent ce qui est réellement recherché quand on reprend une application dont le code source est perdu.