Développons en Java
v 2.40   Copyright (C) 1999-2023 .   
66. La JVM HotSpot dans un conteneur Docker 68. Programmation orientée aspects (AOP) Imprimer Index Index avec sommaire Télécharger le PDF

 

67. La décompilation et l'obfuscation

 

chapitre    6 7

 

Niveau : niveau 5 Confirmé 

 

Le compilateur transforme un fichier source en fichier de classe contenant du bytecode. Ce bytecode est ensuite lu et interprété par la JVM.

La décompilation consiste à générer du code source à partir du bytecode pour effectuer un reverse engineering. Un des outils pionniers dans cette activité est Mocha qui a fait couler beaucoup d'encre.

L'obfuscation consiste à rendre le résultat d'une décompilation difficilement lisible voire impossible.

Ce chapitre contient plusieurs sections :

 

67.1. Décompiler du bytecode

La décompilation consiste à produire un fichier source Java à partir d'un fichier de classe contenant du bytecode. C'est l'opération inverse de la compilation. Ce processus est possible car le bytecode est standardisé et parfaitement documenté.

stop Attention : ce processus est généralement prohibé pour du code dont on n'est pas l'auteur ou qui n'est pas open source. Avant de réaliser une décompilation, il est important de se renseigner sur la licence du code qui va subir cette opération afin de ne pas enfreindre la licence d'utilisation.

La décompilation est possible parce que la compilation du code source ne produit pas du code machine binaire mais produit du bytecode qui est un langage indépendant de toute plate-forme. Lors de son exécution, le bytecode peut être interprété ou compilé en code machine. Le format du bytecode est assez proche du code source, ce qui permet de réaliser une décompilation relativement facilement notamment pour ce qui concerne la logique des traitements.

Il existe plusieurs outils pour décompiler du bytecode :

Outils

Url

JReversePro

http://jrevpro.sourceforge.net/

Jad (the fast Java Decompiler)

http://www.kpdus.com/jad.html

IdeaJad (utilise Jad)

https://www.tagtraum.com/ideajad.html

JODE

http://jode.sourceforge.net/

 

67.1.1. JAD : the fast Java Decompiler

Jad est un décompilateur gratuit pour un usage non commercial ou personnel qui est particulièrement efficace et véloce car il est écrit en C++.

Il faut télécharger le fichier jadnt158.zip à l'url http://www.kpdus.com/jad.html et décompresser l'archive dans un répertoire du système. Le plus simple est d'ajouter ce répertoire à la variable Path du système.

La classe ci-dessous est utilisée comme exemple :

Exemple :
package fr.jmdoudoux.dej;

public class MaClasse {

  /**
   * @param args
   */
  public static void main(
      String[] args) {
    System.out.println("Bonjour");
  }

}

Exécuter jad en lui passant en paramètre le nom du fichier .class à décompiler.

Exemple :
C:\Documents and Settings\jmd\workspace\Tests\bin\com\jmdoudoux\test>jad MaClass
e.class
Parsing MaClasse.class... Generating MaClasse.jad

L'exécution produit un fichier MaClasse.jad.

Exemple :
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   MaClasse.java

package fr.jmdoudoux.dej;

import java.io.PrintStream;

public class MaClasse
{

    public MaClasse()
    {
    }

    public static void main(String args[])
    {
        System.out.println("Bonjour");
    }
}

 

67.1.2. La mise en oeuvre et les limites de la décompilation

Cette section va utiliser la classe ci-dessous :

Exemple :
package fr.jmdoudoux.dej.decompile;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Classe de test 
 *
 */
public class MaClasse {

  private String nom;
  protected String prenom;
  public Date dateNaissance;
  public List commandes = new ArrayList();
  
  public static void main(
      String[] args) {
    MaClasse maClasse = new MaClasse("nom1","prenom1",new Date());
    maClasse.ajouterCommande("commande 1");
    maClasse.ajouterCommande("commande 2");
    System.out.println(maClasse);
  }

  /**
   * Constructeur
   * @param nom
   * @param prenom
   * @param dateNaissance
   */
  public MaClasse(String nom, String prenom, Date dateNaissance) {
    super();
    this.nom = nom;
    this.prenom = prenom;
    this.dateNaissance = dateNaissance;
  }

  /**
   * Ajouter une commande
   * @param libelle libelle de la commande
   */
  public void ajouterCommande(String libelle) {
    commandes.add(libelle);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder("");
    sb.append("Nom : ");
    sb.append(nom);
    sb.append("\n");
    sb.append("Prenom : ");
    sb.append(prenom);
    sb.append("\n");
    sb.append("Date de naissance : ");
    sb.append(dateNaissance);
    sb.append("\n");
    sb.append("Commandes :\n");
    for(Object commande : commandes) {
      sb.append("  ");
      sb.append(commande);
      sb.append("\n");
    }
    
    return sb.toString();
  }  
}

Exemple : décompilation avec jad

Exemple :
C:\java\tests>javac com/jmdoudoux/test/decompile/MaClasse.java
Note: com\jmdoudoux\test\decompile\MaClasse.java uses unchecked or unsafe operat
ions.
Note: Recompile with -Xlint:unchecked for details.

C:\java\tests>cd com/jmdoudoux/test/decompile

C:\java\tests\com\jmdoudoux\test\decompile>dir
 Volume in drive C has no label.
 Volume Serial Number is 1F23-7A9

 Directory of C:\java\tests\com\jmdoudoux\test\decompile

01/04/2008  10:15    <DIR>          .
01/04/2008  10:15    <DIR>          ..
01/04/2008  10:15             1 755 MaClasse.class
01/04/2008  09:58             1 545 MaClasse.java
               2 File(s)          3 300 bytes
               2 Dir(s)  57 852 260 352 bytes free

C:\java\tests\com\jmdoudoux\test\decompile>jad MaClasse.class
Parsing MaClasse.class... Generating MaClasse.jad

Exemple : le fichier MaClasse.jad généré
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   MaClasse.java

package fr.jmdoudoux.dej.decompile;

import java.io.PrintStream;
import java.util.*;

public class MaClasse
{

    public static void main(String args[])
    {
        MaClasse maclasse = new MaClasse("nom1", "prenom1", new Date());
        maclasse.ajouterCommande("commande 1");
        maclasse.ajouterCommande("commande 2");
        System.out.println(maclasse);
    }

    public MaClasse(String s, String s1, Date date)
    {
        commandes = new ArrayList();
        nom = s;
        prenom = s1;
        dateNaissance = date;
    }

    public void ajouterCommande(String s)
    {
        commandes.add(s);
    }

    public String toString()
    {
        StringBuilder stringbuilder = new StringBuilder("");
        stringbuilder.append("Nom : ");
        stringbuilder.append(nom);
        stringbuilder.append("\n");
        stringbuilder.append("Prenom : ");
        stringbuilder.append(prenom);
        stringbuilder.append("\n");
        stringbuilder.append("Date de naissance : ");
        stringbuilder.append(dateNaissance);
        stringbuilder.append("\n");
        stringbuilder.append("Commandes :\n");
        for(Iterator iterator = commandes.iterator(); 
          iterator.hasNext(); stringbuilder.append("\n"))
        {
            Object obj = iterator.next();
            stringbuilder.append("  ");
            stringbuilder.append(obj);
        }

        return stringbuilder.toString();
    }

    private String nom;
    protected String prenom;
    public Date dateNaissance;
    public List commandes;
}

Le code décompilé est similaire au code source original exceptés :

Les fonctionnalités de Java 5 sont rarement décompilées à l'identique de l'original car ces fonctionnalités sont des raccourcis syntaxiques qui sont traités par le compilateur pour générer du code compatible avec les versions précédentes (l'annotation @Override est absente, la boucle for est étendue). La décompilation restitue le code tel qu'il a été généré par le compilateur à partir du bytecode : c'est notamment le cas dans l'exemple de la boucle for.

Si les informations de débogage sont incluses dans le bytecode lors de la compilation, alors le résultat de la décompilation est plus proche du code source original notamment la décompilation pourra restituer le nom des variables locales et des paramètres des méthodes. Pour demander l'ajout des informations de débogage, il faut utiliser l'option -g du compilateur.

Exemple :
C:\java\tests>javac -g com/jmdoudoux/test/decompile/MaClasse.java
Note: com\jmdoudoux\test\decompile\MaClasse.java uses unchecked or unsafe operat
ions.
Note: Recompile with -Xlint:unchecked for details.

C:\java\tests>cd com/jmdoudoux/test/decompile

C:\java\tests\com\jmdoudoux\test\decompile>jad MaClasse.class
Parsing MaClasse.class...Overwrite MaClasse.jad [y/n/a/s] ? y
 Generating MaClasse.jad

Exemple : le fichier MaClass.jad généré
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   MaClasse.java

package fr.jmdoudoux.dej.decompile;

import java.io.PrintStream;
import java.util.*;

public class MaClasse
{

    public static void main(String args[])
    {
        MaClasse maClasse = new MaClasse("nom1", "prenom1", new Date());
        maClasse.ajouterCommande("commande 1");
        maClasse.ajouterCommande("commande 2");
        System.out.println(maClasse);
    }

    public MaClasse(String nom, String prenom, Date dateNaissance)
    {
        commandes = new ArrayList();
        this.nom = nom;
        this.prenom = prenom;
        this.dateNaissance = dateNaissance;
    }

    public void ajouterCommande(String libelle)
    {
        commandes.add(libelle);
    }

    public String toString()
    {
        StringBuilder sb = new StringBuilder("");
        sb.append("Nom : ");
        sb.append(nom);
        sb.append("\n");
        sb.append("Prenom : ");
        sb.append(prenom);
        sb.append("\n");
        sb.append("Date de naissance : ");
        sb.append(dateNaissance);
        sb.append("\n");
        sb.append("Commandes :\n");
        for(Iterator i$ = commandes.iterator(); i$.hasNext(); sb.append("\n"))
        {
            Object commande = i$.next();
            sb.append("  ");
            sb.append(commande);
        }

        return sb.toString();
    }

    private String nom;
    protected String prenom;
    public Date dateNaissance;
    public List commandes;
}

 

67.2. Obfusquer le bytecode

Pour diverses raisons, il n'est pas toujours souhaitable de proposer le code source ou de permettre son obtention grâce à une décompilation, notamment pour protéger des droits sur la propriété intellectuelle.

Il existe plusieurs outils pour obfusquer le bytecode produit par le compilateur. Plusieurs outils open source ou gratuits sont utilisables pour réaliser ce type d'opérations.

Outils

Url

ProGuard

https://www.guardsquare.com/proguard

RetroGuard (Plusieurs licences dont une open source)

http://www.retrologic.com/

yGuard

https://www.yworks.com/products/yguard

JavaGuard (Plus d'évolution depuis 2002)

https://sourceforge.net/projects/javaguard/

jarg (Plus d'évolution depuis 2003)

http://jarg.sourceforge.net/

JODE (Plus d'évolution depuis 2004)

http://jode.sourceforge.net/


Il existe aussi plusieurs outils commerciaux dont un des plus puissants est Klassmaster de Zelix (http://www.zelix.com/klassmaster/)

 

67.2.1. Le mode de fonctionnement de l'obfuscation

L'obfuscation rend parfois la décompilation impossible ou le code source produit non compilable mais plus généralement elle rend le code source issu de la décompilation très peut lisible et donc difficilement compréhensible.

L'obfuscation consiste donc à transformer le bytecode pour le rendre le moins compréhensible par un humain suite à un processus de décompilation.

Il n'existe pas de standard concernant l'obfuscation et chaque outil propose ses propres mécanismes pour obtenir un niveau de protection plus ou moins élevé. Ces mécanismes peuvent inclure entre autres  :

Cette transformation doit cependant garantir que le bytecode modifié est toujours valide et surtout que les fonctionnalités soient toujours les mêmes.

L'outil d'obfuscation charge le fichier .class, analyse la structure et le bytecode, applique les transformations et sauvegarde le résultat dans un nouveau fichier .class qui est différent de l'original mais qui doit proposer exactement les mêmes fonctionnalités.

Il est possible que l'obfuscation rende le résultat d'une décompilation non compilable grâce à l'exploitation des spécifications de Java. Une des techniques consiste à renommer des entités pour les rendre ambigües à la compilation. Au chargement d'un fichier .class le bytecode est vérifié mais certaines vérifications ne sont faites que par le compilateur et ne sont pas reproduites au chargement de la classe. Ainsi le bytecode obfusqué est exécuté dans la JVM mais le résultat d'une décompilation ne se recompile pas.

La plupart des outils d'obfuscation réalisent durant leur traitement une opération de shrinking qui consiste à supprimer les portions de code inutilisées : ceci permet de réduire la taille du bytecode. Certains outils d'obfuscation proposent aussi d'optimiser le bytecode.

L'opération d'obfuscation rend moins facile l'exploitation des piles d'appels des exceptions. La plupart des outils d'obfuscation fournissent une solution pour restituer la pile d'appels telle qu'elle serait affichée avec le code non obfusqué.

 

67.2.2. Un exemple de mise en oeuvre avec ProGuard

ProGuard est un outil open source sous licence GPL écrit en Java qui permet d'effectuer plusieurs opérations sur une application packagée :

Pour lancer Proguard en ligne de commande, il faut exécuter la commande

java -jar proguard.jar [options ...]

Le fichier proguard.jar se trouve dans le sous-répertoire lib de ProGuard.

Pour faciliter la gestion des options, il est possible de les regrouper dans un fichier de configuration. Ce fichier de configuration peut facilement être créé avec l'interface graphique fournie par ProGuard (proguardgui).

Exemple partiel du fichier config.pro :
-injars 'C:\java\test.jar'
-outjars 'C:\java\test.jar'

-libraryjars 'C:\Program Files\Java\jre1.6.0_05\lib\rt.jar'

# Keep - Applications. Keep all application classes, along with their 'main'
# methods.
-keepclasseswithmembers public class * {
    public static void main(java.lang.String[]);
}

# Keep - Library. Keep all public and protected classes, fields, and methods.
-keep public class * {
    public protected <fields>;
    public protected <methods>;
}
...

Pour lancer l'application avec un fichier de configuration, il suffit de le préciser en paramètre précédé d'un caractère @.

Exemple :
java -jar proguard.jar @config.pro

Proguard peut être utilisé avec une interface graphique. Pour lancer cette interface graphique, il faut saisir dans le répertoire lib de ProGuard la commande :

Exemple :
C:\java\proguard4.2\lib>java -jar proguardgui.jar

Pour utiliser ProGuard, il faut packager les fichiers .class dans une archive (de type jar, war, ...)

Exemple :
C:\java\tests>jar -cvfm test.jar manifest.mf com
manifest ajout�
ajout : com/ (entr�e = 0) (sortie = 0) (0% stock�)
ajout : com/jmdoudoux/ (entr�e = 0) (sortie = 0) (0% stock�)
ajout : com/jmdoudoux/test/ (entr�e = 0) (sortie = 0) (0% stock�)
ajout : com/jmdoudoux/test/decompile/ (entr�e = 0) (sortie = 0) (0% stock�)
ajout : com/jmdoudoux/test/decompile/MaClasse.class (entr�e = 1755) (sortie = 97
1) (44% compress�s)
ajout : com/jmdoudoux/test/decompile/MaClasse.java (entr�e = 1545) (sortie = 535
) (65% compress�s)

Cliquez sur le bouton « Input/ouput », puis sur le bouton « Add input... » et sélectionnez le fichier test.jar précédemment créé. Cliquez sur le bouton « Add Output » et sélectionnez le fichier test.jar.

Cliquez sur le bouton « Process » puis sur le bouton « Process ! »

Résultat de l'exécution :
C:\java\tests>dir
 Volume in drive C has no label.
 Volume Serial Number is 1F23-7A9

 Directory of C:\java\tests

01/04/2008  15:31    <DIR>          .
01/04/2008  15:31    <DIR>          ..
31/03/2008  10:05    <DIR>          com
01/04/2008  15:29                51 manifest.mf
01/04/2008  15:31             2 680 test.jar
               3 File(s)          2 806 bytes
               3 Dir(s)  57 784 393 728 bytes free

C:\java\tests>dir
 Volume in drive C has no label.
 Volume Serial Number is 1F23-7A9

 Directory of C:\java\tests

01/04/2008  15:31    <DIR>          .
01/04/2008  15:31    <DIR>          ..
31/03/2008  10:05    <DIR>          com
01/04/2008  15:29                51 manifest.mf
01/04/2008  15:47             1 954 test.jar
               3 File(s)          2 080 bytes
               3 Dir(s)  57 783 062 528 bytes free

Pour vérifier le travail effectué par ProGuard, il faut décompiler le fichier MaClass.class obfusqué.

Exemple :
C:\java\tests>mkdir temp

C:\java\tests>copy test.jar temp
        1 file(s) copied.

C:\java\tests>cd temp

C:\java\tests\temp>dir
 Volume in drive C has no label.
 Volume Serial Number is 1F23-7A9

 Directory of C:\java\tests\temp

01/04/2008  15:49    <DIR>          .
01/04/2008  15:49    <DIR>          ..
01/04/2008  15:47             1 954 test.jar
               1 File(s)          1 954 bytes
               2 Dir(s)  57 783 058 432 bytes free

C:\java\tests\temp>jar -xvf test.jar
d�compress�e: META-INF/MANIFEST.MF
d�compress�e: com/jmdoudoux/test/decompile/MaClasse.class
d�compress�e: com/jmdoudoux/test/decompile/MaClasse.java

C:\java\tests\temp>cd com/jmdoudoux/test/decompile

C:\java\tests\temp\com\jmdoudoux\test\decompile>jad MaClasse.class
Parsing MaClasse.class... Generating MaClasse.jad

Exemple :
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 

package fr.jmdoudoux.dej.decompile;

import java.io.PrintStream;
import java.util.*;

public class MaClasse
{

    public static void main(String args[])
    {
        (args = new MaClasse("nom1", "prenom1", new Date())).a("commande 1");
        args.a("commande 2");
        System.out.println(args);
    }

    private MaClasse(String s, String s1, Date date)
    {
        d = new ArrayList();
        a = s;
        b = s1;
        c = date;
    }

    private void a(String s)
    {
        d.add(s);
    }

    public final String toString()
    {
        StringBuilder stringbuilder;
        (stringbuilder = new StringBuilder("")).append("Nom : ");
        stringbuilder.append(a);
        stringbuilder.append("\n");
        stringbuilder.append("Prenom : ");
        stringbuilder.append(b);
        stringbuilder.append("\n");
        stringbuilder.append("Date de naissance : ");
        stringbuilder.append(c);
        stringbuilder.append("\n");
        stringbuilder.append("Commandes :\n");
        for(this = d.iterator(); hasNext(); stringbuilder.append("\n"))
        {
            Object obj = next();
            stringbuilder.append("  ");
            stringbuilder.append(obj);
        }

        return stringbuilder.toString();
    }

    private String a;
    private String b;
    private Date c;
    private List d;
}

 

67.2.3. Les problèmes possibles lors de l'obfuscation

L'obfuscation rend le traitement des bugs d'exploitation beaucoup plus difficile. Par exemple, un moyen efficace de comprendre et isoler un problème est d'utiliser la pile d'appels (stacktrace) d'une exception qui contient les appels des différentes méthodes. Si le nom des méthodes a été modifié, la pile d'appels devient beaucoup plus difficile à exploiter vis-à-vis du code source. La pile d'appels peut aussi être plus efficace si elle exploite les informations de débogage. Hors généralement, ces informations sont supprimées lors de l'obfuscation.

L'obfuscation doit garantir que le bytecode obfusqué propose les mêmes fonctionnalités que le bytecode initial. Cependant les transformations réalisées par les outils d'obfuscation peuvent parfois avoir des effets de bords importants notamment avec certaines technologies de Java :

 

67.2.4. L'utilisation d'un ClassLoader dédié

Pour rendre la décompilation plus difficile, il est possible d'encoder les fichiers .class avec un algorithme de cryptage et d'utiliser un ClassLoader dédié qui va décrypter ces fichiers avant de les charger en mémoire.

Ainsi, les fichiers .class ne peuvent plus être décompilés puisque le bytecode est illisible. Cependant cette technique est loin d'être infaillible car il suffit de décompiler le ClassLoader pour obtenir l'algorithme de décryptage et de l'utiliser pour décrypter les fichiers .class qui pourront ainsi être décompilés.

 


66. La JVM HotSpot dans un conteneur Docker 68. Programmation orientée aspects (AOP) Imprimer Index Index avec sommaire Télécharger le PDF    
Développons en Java
v 2.40   Copyright (C) 1999-2023 .