Accueil > Tutoriels > Modifier le code IL d’un programme .NET

Modifier le code IL d’un programme .NET

Depuis l’arrivée de la plateforme .NET, les programmes réalisés avec Visual Studio ne sont plus compilés directement en code natif. De la même façon que les programmes Java, ils sont d’abord compilés dans un langage intermédiaire, assez proche de l’assembleur (mais en beaucoup plus compréhensible tout de même), nommé IL pour Intermediate Language ou encore CIL pour Common Intermediate Language. C’est un second compilateur qui se charge ensuite de traduire ce code IL en instructions machine.

Bon, je ne vais pas faire tout un article sur le fonctionnement de la plateforme .NET, ce n’est pas trop le but ici. J’aimerai juste montrer un petit exemple pour prouver qu’il peut être utile de s’intéresser un peu à ce code IL pour résoudre facilement certains problèmes, et pour garder en tête aussi que même un développeur débutant peut désassembler, récupérer du code et modifier un programme .NET, simplement en se servant des outils fournis par Microsoft et sans avoir les fichiers source.

Il y a quelques années, une application .NET (version 1.1 à l’époque) dont on m’avait demandé d’assurer la maintenance a eu un souci en production : Alors que le programme fonctionnait bien juste là, il s’est mis soudainement à planter sur une erreur du type IndexOutOfRangeException. La raison : une augmentation de la charge traitée par le programme, qui stockait certaines données non pas dans une collection, mais dans un tableau dont la capacité maximale était définie en dur, comme dans l’exemple ci-dessous que j’utiliserai pour la suite de la démonstration :

public MyClass()
{ 
    myArray = new MyObject[300];
}

 

C’est sûr que ce problème paraît un peu simpliste, mais à l’époque beaucoup de programmeurs ne connaissaient pas encore très bien les objets du .NET Framework et avaient plutôt tendance à penser en C++, et les objets génériques n’existaient pas encore. L’usage des collections n’était donc pas aussi répandu (et bien plus pénibles à implémenter). Si on avait de la chance, on pouvait trouver quelques Hashtable ou encore des ArrayList, mais parfois on avait des mauvaises surprises comme ce fut le cas ici. Pour arranger le tout, le code source correspondant à la version en production n’était plus disponible. Pour corriger le problème et mettre la nouvelle version en production, on aurait donc dû refaire tous les tests unitaires ainsi que les étapes de pré-production et de livraison habituels. Trop long, alors que les utilisateurs commençaient déjà à perdre patience.

J’ai donc proposé une solution temporaire consistant à patcher directement l’exécutable de production. Pour éviter de devoir repasser par une phase de modification du code source et de validation, la modification consistait simplement à passer la limite maximal du tableau incriminé de 300 à 2000, de façon à ce que le programme puisse fonctionner tout en laissant plus de temps pour développer une solution plus propre que l’on pourrait tester tranquillement.

 

Identifier le code à modifier

 

Après avoir récupéré le fichier exécutable, la première étape est de désassembler le programme et d’identifier le code à modifier. Pour ceux qui n’ont pas trop l’habitude du langage IL, le plus simple de commencer par utiliser le programme Reflector. Ce programme permet d’afficher une arborescence des différentes classes et méthodes d’un assembly .NET, ainsi que leur contenu sous forme de code IL, mais aussi de faire du reverse engineering à la volée pour traduire le code IL en C#, VB.NET ou encore en C++ managé, ce qui facilite beaucoup le repérage des instructions à modifier :

.NET Reflector

.NET Reflector

 

Ici rien de bien compliqué, après avoir identifié dans Reflector la classe et la fonction contenant notre code, il suffit juste de basculer le langage sur “IL” et de repérer le chiffre “300” correspondant à la taille du tableau à modifier :

.maxstack 8
L_0000: ldarg.0
L_0001: call instance void [mscorlib]System.Object::.ctor()
L_0006: ldarg.0
L_0007: ldc.i4 300
L_000c: newarr ConsoleApplication1.MyObject
L_0011: stfld class ConsoleApplication1.MyObject[] ConsoleApplication1.MyClass::myArray
L_0016: ret

La ligne 7 semble correspondre à ce que l’on cherche. Même si l’on a quelques difficultés avec les instructions IL, les différentes variables et le nom des objets et classes permettent malgré tout de se repérer assez vite. Attention toutefois à la base numérique utilisée pour l’affichage du nombre (il faut parfois penser à rechercher plutôt une valeur hexadécimale).

 

Extraire le code IL de l’exécutable

 

Pour cela il faut utiliser un des programmes de Microsoft fourni avec le SDK (Software Development Kit) du .NET FrameworK: ILDASM.exe. Suivant la version installée, il peut se situer dans le dossier C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\ ou bien C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin. Ce petit programme permet de faire sensiblement la même chose que Reflector, mais sans les fonctions de reverse-engineering avancées (traduction en C#, VB.NET, C++, liens hypertextes, etc.) avec cependant la possibilité de faire un « dump » (extraction) des sources en IL dans un fichier lisible avec un éditeur texte :

ILDASM

IL DASM

 

Pour cela il suffit d’aller dans le menu “File” puis choisir “Dump”. Laissez ensuite les options par défaut, sauf peut-être pour “Lignes sources” qui ajoute en commentaire le code C# ou autre avant chaque bloc d’instruction IL correspondant, ce qui est quand même bien pratique (attention à la taille du fichier de sortie cependant). ILDASM crée ensuite un fichier avec l’extension “.il” lisible dans un éditeur de texte, ainsi qu’un fichier de ressources “.res”.

 

Modifier le code IL

 

Rien de bien compliqué ici, il suffit d’ouvrir le fichier IL obtenu précédemment avec un éditeur de texte (en évitant le Bloc-notes de Windows car le fichier peut être assez gros), puis de rechercher le code à modifier. Attention ici à ne pas rechercher directement la valeur “300” car toutes les valeurs numériques sont écrites en hexadécimal. Il faut donc se baser sur les informations repérées avec Reflector : numéro de ligne IL, le nom de la fonction, etc. Ou rechercher 0×12c (300 en hexadécimal). Si l’option “Ligne sources” a été activée avant l’extraction dans ILDASM, la recherche sera encore plus simple a effectuer :

Code IL dans Notepad++

Code IL dans Notepad++

 

Une fois l’instruction repérée, il faut donc modifier la valeur pour passer la limite de 300 à 2000. Pour respecter le format du fichier, on mettra plutôt la valeur hexadécimale correspondante, c’est-à-dire 0×7d0, avant de sauvegarder le fichier IL :

//000014:             myArray = new MyObject[300];
IL_0006:  ldarg.0
IL_0007:  ldc.i4     0x12c 0x7d0
IL_000c:  newarr     ConsoleApplication1.MyObject
IL_0011:  stfld      class ConsoleApplication1.MyObject[] ConsoleApplication1.MyClass::myArray
.line 15,15 : 9,10 ''

 

Plus d’infos sur ILDASM ici avec un petit tutorial de Microsoft en prime sur MSDN.

 

Recompiler le programme

 

Pas besoin de Visual Studio pour cette dernière étape. Après avoir utilisé ILDASM.exe pour désassembler le programme en code IL, nous allons en toute logique utiliser le programme ILASM pour ré-assembler ce code. Attention cependant, il ne s’agit plus d’un outil du SDK mais du .NET Framework de base, donc disponible dans le dossier System. Bien que mon exemple utilise le Framework 3.5, le programme ILASM.exe se situe en fait dans le dossier du Framework 2.0 (C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ ), le noyau restant le même pour la version 3.5. Par contre il n’y a pas d’interface graphique cette fois, il faut passer par la ligne de commande. Pas besoin de se compliquer la vie, il faut juste préciser le fichier IL en paramètre et lancer la commande :

ILASM 1

ILASM 2

Pour plus d’informations sur les options du programme ILASM.exe : MSDN

Si tout s’est bien déroulé, le programme génère un nouvel exécutable contenant la modification. Ce qui permet, en l’absence d’une bonne gestion des versions du code source C# (ou autre), de créer un correctif rapidement en étant certain de l’étendue des modifications effectuées. Utile pour gagner un peu de temps pour mettre au point une solution plus propre.

Et voilà, c’est déjà fini ! Globalement cette opération est très rapide a réaliser. Bien plus que la rédaction de cet article en tout cas !

 

Exemple avancé

 

Pour finir j’aimerai montrer un petit bout de code C# très simple qui permet de vérifier un mot de passe :

private bool CheckPassword(string password)
{
    if (password == "a23dk45")
        return true;
    else
    {
        MessageBox.Show("Invalid password");
        return false;
    }
}

Le code, bien que fonctionnel, est volontairement rempli d’erreurs grossières qu’il est facile de mettre en évidence. Pour cela on va s’intéresser ici à ce qui est visible dans un premier temps par l’utilisateur, à savoir le message “Invalid password” et le rechercher dans le fichier “.il” que l’on obtiendrait si l’on utilisait ILDASM sur ce programme :

//000020:         private bool CheckPassword(string password)
//000021:         {
    IL_0000:  nop
//000022:             if (password == "a23dk45")
    IL_0001:  ldarg.1
    IL_0002:  ldstr      "a23dk45"
    IL_0007:  call       bool [mscorlib]System.String::op_Equality(string,
                                                                   string)
    IL_000c:  ldc.i4.0
    IL_000d:  ceq
    IL_000f:  stloc.1
    IL_0010:  ldloc.1
    IL_0011:  brtrue.s   IL_0017

//000023:                 return true;
    IL_0013:  ldc.i4.1
    IL_0014:  stloc.0
    IL_0015:  br.s       IL_0027

//000024:             else
//000025:             {
    IL_0017:  nop
//000026:                 MessageBox.Show("Invalid password");
    IL_0018:  ldstr      "Invalid password"
    IL_001d:  call       valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
    IL_0022:  pop
//000027:                 return false;
    IL_0023:  ldc.i4.0
    IL_0024:  stloc.0
    IL_0025:  br.s       IL_0027

//000028:             }
//000029:         }
    IL_0027:  ldloc.0
    IL_0028:  ret
//000030:         } // end of method Form1::CheckPassword

On retrouve facilement la chaîne à la ligne 18, bien labellisée par ILDASM qui nous simplifie la tâche en insérant le code source C# en commentaire. Grâce à ces indications commentées, on voit rapidement que cette chaîne se situe dans un bloc if..else, et que l’appel à l’instruction MessageBox.Show, qui conduit donc à l’affichage de ce message, se situe dans le bloc “else” qui commence apparemment à la ligne 17. Un peu plus haut, on remarque qu’une instruction fait référence à cette ligne 17 :

IL_0011:  brtrue.s   IL_0017

Le commentaire placé légèrement au-dessus de cette ligne nous renseigne qu’elle se situe en fait dans le bloc d’instructions correspondant au test “if” de vérification de la variable “password”. On a déjà trouvé le bloc de code correspondant à l’échec du test et responsable du message “Invalid password”, il est facile de trouver le début du bloc correspondant au succès du test, situé ligne 13 (le “return true”).

Que se passe-t-il alors si l’on remplace à la ligne 11 l’étiquette IL_0017 (début du code correspondant à l’échec de la vérification du password), par IL_0013 (test du password réussi) ?

IL_0011:  brtrue.s   IL_0017 IL_0013

Si on recompile le fichier “.il” avec le programme ILASM et qu’on exécute le nouveau code, on se rend compte que cette fois-ci le programme accepte n’importe quel mot de passe, et n’affiche plus le message “Invalid password”. On vient en fait tout simplement de shunter la routine de vérification : quel que soit le résultat du test “if” effectué, le code exécuté sera toujours celui normalement réservé au vrai mot de passe. L’instruction brtrue.s est en fait une petite routine qui laisse le code s’exécuter à la ligne suivant si le résultat du test précédent était “vrai”, ou effectue un saut à l’étiquette spécifiée si le test était “faux”. Après notre modification, quel que soit le résultat du test du mot de passe, la prochaine instruction exécutée est celle de située à la ligne suivante.

Il est amusant de noter que Reflector donne une nouvelle traduction C# de ce code IL modifié, ce qui permet de mieux comprendre les conséquences de ce changement :

private bool CheckPassword(string password)
{
    if (password == "a23dk45")
    {
    }
    return true;
    MessageBox.Show("Invalid password");
    return false;
}

Bon en fait, même sans aller jusqu’à recompiler le programme ou modifier le code IL, dans cet exemple on peut aussi simplement relever le mot de passe écrit en dur dans le code pour contourner la sécurité.

 

Conclusion

 

Donc, pour le code sensible, évitez de laisser les chaînes de caractère dans le code sans les crypter. Attention aux noms de variables ou de méthodes trop évidents (comme « CheckPassword » par exemple), et aux tests simplistes. Pour y remédier, il existe aussi certains programmes appelés obsfuscateurs, qui peuvent brouiller ces informations en retravaillant le code IL une fois celui-ci compilé par Visual Studio, de façon à rendre ce type de recherche beaucoup moins évidente. Gardez bien ça à l’esprit si vous devez un jour sécuriser votre code !

Pour ceux qui souhaitent continuer sur le sujet, vous pouvez jeter un œil à cet article : http://msdn.microsoft.com/fr-fr/security/dd776108.aspx

Categories: Tutoriels Tags: , , ,
  1. Pas encore de commentaire
  1. Pas encore de trackbacks