JavaSec Execution
JavaSec Execution
Projet: JAVASEC
1 Introduction 5
1.1 Objet du document . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 Présentation du contexte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Organisation du document . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2 Glossaire, Acronymes 7
- Page 2-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
- Page 3-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
6.8.2 Implémentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.8.3 Version du langage Java supporté . . . . . . . . . . . . . . . . . . . . 53
6.8.4 Evolution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.9 Autres implémentations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
6.10 Tableau de synthèse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
8 Problématique de la décompilation 60
8.1 Décompilation de bytecode . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8.2 Techniques d’offuscation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
8.2.1 Offuscation de la présentation du bytecode . . . . . . . . . . . . . . . 64
8.2.1.1 Renommage . . . . . . . . . . . . . . . . . . . . . . . . . . 64
8.2.1.2 Renommage avec surcharge . . . . . . . . . . . . . . . . . . 65
8.2.1.3 Suppression des informations de débogage . . . . . . . . . . 66
8.2.2 Offuscation des données . . . . . . . . . . . . . . . . . . . . . . . . . 66
8.2.2.1 Chiffrement des chaînes littérales . . . . . . . . . . . . . . . 66
8.2.3 Offuscation du flot de contrôle . . . . . . . . . . . . . . . . . . . . . . 67
8.2.3.1 Modification des sauts conditionnels . . . . . . . . . . . . . 67
8.2.3.2 Mise à plat de la hiérarchie des classes . . . . . . . . . . . . 67
8.2.4 Offuscations/transformations préventives . . . . . . . . . . . . . . . . 68
8.3 Analyse des produits d’offuscation . . . . . . . . . . . . . . . . . . . . . . . . 68
8.3.1 Résultats de l’analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
8.3.2 Démarche d’utilisation et limitations . . . . . . . . . . . . . . . . . . . 70
8.4 Conclusion sur la décompilation . . . . . . . . . . . . . . . . . . . . . . . . . 71
- Page 4-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
1 INTRODUCTION
Ce document est réalisé dans le cadre du Projet JAVASEC, relatif au marché 2008.01801.00
.2.12.075.01 notifié le 23/12/2008. Il correspond au livrable technique contractuel émis au titre
du poste 1 : rapport sur les modèles d’exécution Java en version finale (identifiant 1.2.2 dans le
CCTP).
Il présente un état de l’art des modèles d’exécution Java, notamment pour étudier la préser-
vation des propriétés intrinsèques du langage Java.
Java est un langage de programmation orienté objet développé par Sun. En plus d’un langage
de programmation, Java fournit également une très riche bibliothèque de classes pour tous les
domaines d’application de l’informatique, d’Internet aux bases de données relationnelles, des
cartes à puces et téléphones portables aux superordinateurs. Java présente des caractéristiques
très intéressantes qui en font une plate-forme de développement constituant l’innovation la plus
intéressante apparue ces dernières années.
- Page 5-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
- Page 6-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
2 GLOSSAIRE, ACRONYMES
Acronyme Définition
API Application Programming Interface : interface de programmation
AOT Compilation Ahead Of Time : compilation en avance de phase
CAP Converter Applet
CLDC Connected Limited Device Configuration
FPGA Field-Programmable Gate Array : circuit intégré logique qui peut être
reprogrammé après sa fabrication
JCP Java Community Process ou Java CertPath (suivant le contexte)
JCVM JavaCard Virtual Machine
JDK Java Development Kit : environnement de développement Java
JIT Compilation Just In Time : compilation à la volée
JNI Java Native Interface : interface de programmation pour l’intégration
des fonctions natives
JRE Java Runtime Environment : environnement d’exécution Java
JSR Java Specification Request
JVM Java Virtual Machine : machine virtuelle Java
- Page 7-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
La spécification officielle de la JVM [15] ne spécifie pas pour autant un modèle d’exécution
précis pour cette machine virtuelle :
[...] the Java virtual machine does not assume any particular implementation tech-
nology, host hardware, or host operating system. It is not inherently interpreted, but
can just as well be implemented by compiling its instruction set to that of a silicon
CPU. It may also be implemented in microcode or directly in silicon. [..]
To implement the Java virtual machine correctly, you need only be able to read
the class file format and correctly perform the operations specified therein. Im-
plementation details that are not part of the Java virtual machine’s specification
would unnecessarily constrain the creativity of implementors. For example, the me-
mory layout of run-time data areas, the garbage-collection algorithm used, and
any internal optimization of the Java virtual machine instructions (for example,
translating them into machine code) are left to the discretion of the implementor.
La spécification laisse ainsi le choix du moyen d’implémentation mis en œuvre pour exé-
cuter un programme au format bytecode Java, tant que ce modèle respecte la spécification offi-
cielle. Les seuls points imposés par la spécification sont :
– le format des fichiers .class et du bytecode ;
– le chargement, l’édition des liens et l’initialisation des classes ;
– les vérifications réalisées par le vérificateur de bytecode sur les fichiers class ;
– le comportement attendu pour l’exécution d’un programme bytecode Java (la séman-
tique du bytecode, via la spécification d’une machine abstraite à pile) ;
– la gestion des exceptions ;
- Page 8-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
– la gestion des accès concurrents et des threads (la spécification n’imposant aucune
manière spécifique d’implémenter les threads, la gestion des threads ne recouvre l’im-
plémentation de la classe des threads dans la bibliothèque standard).
La spécification mentionne également d’autres services que la JVM doit implémenter, mais
pour lesquels elle fournit peu de détails :
– la gestion automatique des objets créés sur le tas par un mécanisme de glaneur de
cellules (garbage collector). Un objet ne doit jamais être désalloué explicitement ;
– l’interface avec des classes de la bibliothèque standard pour implémenter certains
services (par exemple, le chargement de classes, l’introspection ou la gestion des
threads) ;
– les étapes de finalisation, de déchargement de classes et d’arrêt de la JVM.
Certains services, considérés comme standard, ne sont pas décrits dans la spécification de la
JVM [15], mais dans d’autres documents :
– l’interface JNI permettant au code Java d’appeler des fonctions implémentées en
C/C++ (et inversement d’initialiser une instance de la JVM depuis une application
écrite en C/C++) ;
– l’interface JVMTI qui permet de contrôler la JVM et le flux d’exécution d’une appli-
cation Java à des fins de mise au point.
Etant données les libertés offertes par la spécification de la JVM, notamment en ce qui
concerne l’exécution du bytecode, les implémentations de la plate-forme d’exécution Java (JRE)
diffèrent sensiblement. Ces implémentations s’appuient sur un format d’entrée (le bytecode,
les fichiers class) et une sémantique définie précisément. En revanche, différentes stratégies
ou modèles d’exécution peuvent être adoptés privilégiant la vitesse d’exécution, la portabilité,
l’empreinte mémoire, etc. Ce document différencie trois grandes stratégies :
1. l’exécution à l’aide d’un émulateur ;
2. la compilation native statique ;
3. l’utilisation d’un processeur (ou d’un co-processeur Java) capable d’exécuter direc-
tement les instructions du bytecode.
Le terme JVM est souvent utilisé dans la littérature pour désigner des éléments différents.
Dans la spécification de Sun [15], le terme désigne la spécification d’une machine à pile abs-
traite chargée d’exécuter le bytecode. Le terme est aussi généralement utilisé pour désigner une
implémentation à l’aide d’un émulateur de cette spécification. Il est également parfois utilisé à
tort pour désigner l’environnement d’exécution (JRE) qui comprend, outre l’implémentation de
la JVM, une implémentation de la bibliothèque standard Java. En toute rigueur, les différents
modèles d’exécution doivent, pour assurer la compatibilité avec le modèle d’exécution de Java,
implémenter la machine virtuelle Java.
Le premier modèle correspond à une implémentation littérale de la JVM sous forme logi-
cielle. Il s’agit d’un module exécutable (généralement une bibliothèque partagée) implémentant
différents services dont l’exécution du bytecode, souvent à l’aide d’un interpréteur. Un compi-
lateur « à la volée » (JIT) peut également être utilisé conjointement à l’interpréteur ou pour le
remplacer. L’implémentation de la JVM joue alors pleinement son rôle d’interface entre l’appli-
cation distribuée sous forme de bytecode et la plate-forme d’exécution native (nous supposons
- Page 9-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
pour ce type de modèle d’exécution que le processeur utilisé est un processeur générique qui ne
peut exécuter le bytecode nativement). Il s’agit du modèle d’exécution le plus couramment uti-
lisé, notamment par l’implémentation de référence de Sun du JRE. Il est donc considéré comme
le modèle d’exécution standard.
Le second modèle repose sur une étape de compilation réalisée avant l’exécution (AOT,
Ahead Of Time) de l’application et qui fournit un code natif pour l’ensemble des méthodes
exécutées par l’application. Le code des bibliothèques Java utilisées par l’application est intégré
à l’exécutable ou fourni sous la forme de bibliothèques natives partagées. La compilation peut
être exécutée en deux étapes (Java vers bytecode, puis bytecode vers code natif) ou directement
depuis le code source Java vers le code natif. Ce mode d’exécution permet ainsi de distribuer
les applications sous la forme d’un exécutable natif autonome. Il n’est alors plus nécessaire
d’installer un environnement d’exécution Java, mais ce modèle d’exécution s’écarte alors de la
spécification de la JVM (qui impose notamment de distribuer les applications Java sous forme
de bytecode).
Le dernier modèle s’appuie sur une implémentation (souvent partielle) de la JVM sous
forme matérielle. Il s’agit généralement de processeurs ou de co-processeurs dont le jeu d’ins-
tructions implémente (généralement en partie) le jeu d’instructions de la JVM (les instruc-
tions du bytecode). Le bytecode ne comportant pas d’instructions permettant d’accéder aux
ressources natives (par exemple, pour la gestion des entrées/sorties), ce type de processeur com-
prend également d’autres instructions permettant d’effectuer ces accès ou utilise conjointement
un processeur « générique ». De plus, certains services de la JVM (par exemple, la gestion de
la mémoire ou le chargement de classes) étant difficilement implémentables sous forme ma-
térielle, ce modèle d’exécution s’appuie également sur une implémentation logicielle (généra-
lement désignée sous le terme de JVM) qui fournit ces services et utilise le processeur dédié
pour l’exécution du bytecode. Ce mode d’exécution reste aujourd’hui peu utilisé en raison du
coût d’implémentation d’une plate-forme matérielle spécifique. Principalement employé pour
des raisons d’optimisation, il est surtout mis en œuvre pour des applications embarquées et
supporte parfois un sous-ensemble du langage Java et de la bibliothèque standard.
Cette étude identifie donc les différentes stratégies du modèle d’exécution de la JVM. Ces
stratégies ont été regroupées selon trois grandes catégories, mais l’étude a fait apparaître que
la frontière entre ces différentes catégories était parfois mince : un émulateur peut utiliser des
techniques de compilation native pour optimiser l’exécution du bytecode, un compilateur sta-
tique peut embarquer un interpréteur dans l’exécutable, une architecture à base de processeurs
Java peut nécessiter une implémentation logicielle de la JVM pour assurer certains services, etc.
- Page 10-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Nous abordons dans cette section le modèle d’exécution standard de Java. Celui-ci repose
sur l’utilisation d’un émulateur qui implémente la JVM. Il s’agit de la stratégie d’implémenta-
tion la plus répandue (les principaux environnements d’exécution Java, dont l’implémentation
de Sun, reposent sur cette stratégie) et la plus mature (les premiers environnements d’exécution
Java, développés dans les années 1990, implémentaient cette stratégie). La spécification de la
JVM n’impose aucune architecture précise en ce qui concerne son implémentation. Toutefois,
il est possible de regrouper les services implémentés par un émulateur Java suivant différents
blocs fonctionnels :
– le cœur de l’émulateur : il regroupe notamment les fonctions de gestion du démarrage
et de l’arrêt de l’émulateur, le mécanisme de chargement et d’initialisation des classes,
le mécanisme d’interface avec le code natif (JNI) ainsi que différents mécanismes
utilitaires (JVMTI, audit, etc.) ;
– le vérificateur de bytecode (Bytecode Verifier) : élément clé de la sécurité des appli-
cations Java, il vérifie l’intégrité des fichiers class (et notamment leur typage) avant
leur exécution ;
– le gestionnaire des fils d’exécutions : il assure la gestion (et notamment l’ordonnan-
cement) des différents threads de la JVM (dont les threads Java imposés par la spé-
cification du langage et de la JVM). Il assure également la synchronisation entre ces
threads ;
– le système d’exécution : il est chargé d’exécuter le bytecode des classes chargées.
Il implémente généralement un interpréteur, mais peut également avoir recours à un
compilateur natif pour effectuer des optimisations ;
– le système de gestion de la mémoire : il est chargé de l’allocation et de la désallo-
cation dynamique de la mémoire de l’émulateur. Il comprend notamment un système
de représentation des classes en mémoire ainsi qu’un système de gestion des objets
références (instances de classes et tableaux Java). La spécification de la JVM impose
qu’une application Java ne puisse désallouer explicitement les objets. Le système de
gestion de la mémoire comprend donc obligatoirement un mécanisme de glaneur de
cellules (garbage collector) permettant de désallouer automatiquement les objets qui
ne sont plus référencés ;
– l’interface avec la plate-forme native : ce bloc fonctionnel comprend notamment l’in-
terface avec les mécanismes de l’OS natif (gestion des threads, des signaux, des en-
trées/sorties, etc.). Il peut également comporter (si l’émulateur est destiné à fonction-
ner sur différentes architectures matérielles) une couche d’abstraction de l’architec-
ture matérielle.
Cette étude porte sur la préservation des propriétés de sécurité identifiées dans l’étude du
langage Java [6] en fonction des différentes stratégies d’implémentation de la JVM. Ce do-
cument se focalise donc sur les principaux mécanismes de sécurité implémentés au sein d’un
émulateur Java, ainsi que sur les mécanismes ayant un impact fort sur la sécurité des applica-
tions Java exécutées tout en s’appuyant sur l’architecture décrite précédemment. Les principes
- Page 11-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
de la compilation Java vers bytecode sont rappelés en section 4.1, puis les différents blocs fonc-
tionnels d’un émulateur implémentant la JVM sont analysés :
1. La section 4.2 présente les mécanismes de démarrage des applications Java et de char-
gement de classes. Ces mécanismes ont principalement un impact sur la disponibilité
et l’intégrité du code de l’application sous forme de fichiers class.
2. La section 4.3 aborde le mécanisme de vérification de bytecode. Il s’agit du principal
mécanisme de sécurité de la JVM qui assure la correction des applications distribuées
sous forme de bytecode.
3. La section 4.4 porte sur l’exécution du bytecode. Les mécanismes du système d’exé-
cution doivent en effet implémenter certaines vérifications dynamiques qui permettent
d’assurer la confidentialité et l’intégrité des données et du code de la plate-forme
Java (en s’assurant notamment de l’absence de débordement de tampon). De plus,
les mécanismes d’exécutions reposant sur une étape de compilation doivent assurer
l’intégrité du code natif produit.
4. La section 4.5 expose les différentes stratégies d’implémentation des threads Java
ainsi que la problématique de synchronisation. Le mécanisme de gestion des threads
assure en effet la disponibilité des différents threads.
5. La section 4.6 présente le mécanisme de gestion de la mémoire et notamment le
glaneur de cellules. Ce mécanisme possède un impact sur l’intégrité des données (et
sur la confidentialité des données au travers de la problématique de rémanence des
données confidentielles).
6. La section 4.7 évoque la problématique générale de l’intégration de l’émulateur avec
les mécanismes de l’OS. Celui-ci joue notamment un rôle important dans le confine-
ment entre les différentes applications Java.
Pour chacun des blocs fonctionnels étudiés, le fonctionnement des principaux mécanismes
est rappelé brièvement, ainsi que les propriétés de sécurité assurées. Lorsque différentes im-
plémentations partagent certaines stratégies d’implémentation d’un même mécanisme, celles-ci
sont également rappelées et comparées en termes d’impact sur la sécurité, de maturité et de
complexité 1 . Cette étude précise les « points sensibles » qui, s’ils font l’objet d’un défaut de
conception ou d’implémentation, remettent en cause la sécurité de l’application. Ces points
doivent donc faire l’objet d’une attention particulière, par exemple lors de l’évaluation d’un
émulateur Java. Cette étude précise également les mécanismes ou stratégies d’implémentation
qu’il est souhaitable d’intégrer à une implémentation d’une JVM de confiance et inversement,
ceux qu’il convient d’éviter (ou de restreindre). Dans la plupart des cas, il s’agit d’établir un
compromis entre le niveau de sécurité souhaité et l’optimisation du temps d’exécution.
1. En revanche, les détails propres à chaque implémentation ne sont pas détaillés. Ils pourront être abordés
dans le rapport sur l’étude et la comparaison des JVM [5].
- Page 12-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Les classes chargées par une machine virtuelle Java sont soumises à un processus de vé-
rification. Cette vérification garantit que la représentation binaire d’une classe ou d’une in-
terface est « structurellement correcte » (voir [5]). Si la vérification échoue, une exception
VerifyError est levée. La réalisation de ce processus de vérification lors du chargement de
classes permet d’améliorer les performances de l’exécution en libérant l’interpréteur des tests
dynamiques correspondants (par exemple, l’absence de débordement des piles d’opérandes).
Une machine virtuelle Java dispose de deux stratégies pour vérifier une classe. Les deux types
de vérifications sont l’inférence et la vérification (au sens strict du terme) par stackmaps. Dans
le premier cas, la machine virtuelle calcule entièrement l’information nécessaire à la vérification
de la conformité du bytecode. Dans le second cas, une information pré-calculée (typiquement
par le compilateur) est incluse dans le fichier (approche Proof Carrying Code). La présence
de cette information, appelée table de stackmaps, permet d’accélerer fortement le processus
- Page 13-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
de vérification. Selon la spécification de Sun, un fichier class dont le numéro de version est
strictement inférieur à 50.0 doit être vérifié uniquement par inférence (un tel fichier ne contient
normalement pas de stackmaps). Un fichier dont le numéro de version est strictement supérieur
à 50.0 doit être vérifié uniquement à l’aide des stackmaps. Le cas des fichiers 50.0 est un peu
particulier. La vérification à base de stackmaps doit être utilisée. Cependant, en cas d’échec de
cette dernière, la vérification par inférence peut être utilisée. Ces deux mécanismes sont pré-
sentés dans [6, 5]. Les propriétés assurées par le vérificateur de bytecode sont déjà présentées
dans [6].
Dans cette partie nous présentons les différentes techniques d’exécution de programme by-
tecode. Toutes s’appuient sur des optimisations plus ou moins complexes. Nous présentons les
optimisations principales et leurs risques en termes de sécurité.
4.4.1 Interprétation
Chaque instruction bytecode est décodée puis exécutée. L’allocation dynamique de mé-
moire, la libération automatique et la gestion des threads sont gérées par des appels à des
services de la machine virtuelle. Cette approche a l’avantage de suivre directement la spéci-
fication officielle. Néanmoins, elle souffre d’un défaut de taille : la lenteur de l’exécution des
programmes.
– mode aiguillage (ou switch) : c’est le mode d’interprétation le plus proche de la spé-
cification. Dans ce mode, les instructions bytecode d’une méthode sont analysées une
à une et sont décodées à l’aide d’une opération conditionnelle de type switch/case,
qui redirige vers un traitant spécifique. Ce traitant est ensuite exécuté. Ce mode d’in-
terprétation, notamment utilisé dans JamVM et HotSpot (le C++ interpreter), a pour
2. L’identification de la classe principale n’est pas précisée dans la spécification de la JVM.
- Page 14-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
- Page 15-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
une pile gérée de manière ad hoc au-dessus du tas natif. La spécification laisse le libre choix
quant au mode d’implémentation de la représentation de la pile. Une optimisation possible pour
la pile d’opérandes consiste à mettre en œuvre un cache pour le haut de la pile. Il s’agit d’une
structure très facilement accessible dans laquelle sont placées les opérandes manipulées par des
traitants réalisant des opérations internes ou combinées. Un point important concerne la bonne
synchronisation du cache de pile et de la pile elle-même.
Afin d’accélérer l’exécution du bytecode, il est tentant de compiler les instructions bytecode
en code machine pour la plate-forme sous-jacente, puis d’exécuter directement ce code compilé.
Réaliser cette compilation avant l’exécution (compilation « statique ») ou Ahead-of-Time, c.f.
section 5) pose un problème de portabilité dans le modèle Java où les programmes ont vocation à
être distribués : un code natif dépend de la plate-forme d’exécution et les vérifications statiques
assurées par le vérificateur de bytecode sont très difficiles à réaliser sur ce format de programme.
De plus, le langage Java est un langage « dynamique » : certaines classes à exécuter ne sont
réellement connues qu’à l’exécution du programme, se trouvent parfois sur un réseau distant
et ne peuvent donc pas être compilées avant l’exécution. Ce problème a motivé l’utilisation
de la compilation « dynamique » de type Just In Time Compiler : la compilation a lieu au fur
et à mesure de l’exécution. La première méthode à exécuter est compilée en code natif puis
exécutée. Lors d’un appel de méthode, la classe correspondante est chargée (si ce n’est pas
encore le cas), puis la méthode appelée est compilée et l’exécution continue ainsi. Là encore,
l’allocation dynamique de mémoire, la libération automatique et la gestion des threads sont
gérées par des appels à des services de la JVM.
Pour l’utilisateur, tout se passe comme si le programme Java, distribué sous forme de fichiers
class, était interprété. En particulier, un programme est d’abord vérifié par le vérificateur de
bytecode avant d’être compilé dynamiquement. Il reste à la charge du compilateur dynamique
d’insérer les vérifications dynamiques nécessaires pour toutes les vérifications de typage qui
ne sont pas du ressort du vérificateur de bytecode telles que la vérification des accès dans les
bornes de tableaux, le déréférencement de pointeurs nuls, etc. (voir la section 5.4 de [6]).
- Page 16-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Le surcoût de temps d’exécution lié au temps d’optimisation est une limitation intrinsèque
de l’approche JIT qui compile dynamiquement (et optimise légèrement) toutes les méthodes
appelées durant l’exécution. Les JVM actuels (comme Hotspot de Sun) suivent de ce fait une
approche plus hybride. Durant l’exécution, les premières méthodes ne sont pas optimisées, voir
pas compilées du tout. Cela assure un démarrage relativement rapide. Au cours de l’exécution,
la plate-forme pourra décider qu’une méthode ou une portion de méthode nécessite une com-
pilation native et une optimisation avancée, car cette portion semble être fréquemment utilisée.
En fonction du degré d’importance estimée pour cette portion, le compilateur lance une optimi-
sation plus ou moins poussée en faisant le pari que le temps « perdu » à optimiser sera ensuite
gagné par l’accélération de cette portion critique. La détection des « points chauds » nécessite
généralement de compter le nombre de passage dans les méthodes, voir dans les têtes de boucle.
Le passage incessant entre mode interprété et mode compilé est a priori complexe, mais il est
de toute façon nécessaire à tout interpréteur pour appeler du code natif.
La technique de détection des points chauds a un impact faible sur la sécurité. Une mau-
vaise détection ne fait en général que ralentir l’exécution. Elle n’a d’impact réel que si le JIT
possède des vulnérabilités. En revanche, les optimisations réalisées par le JIT peuvent quant à
elles modifier significativement le comportement attendu en cas d’erreurs de conception de ces
optimisations. Si le JIT présente une telle erreur, il est a priori possible pour un attaquant de
« profiler » son code malveillant de manière à ce qu’il soit compilé et optimisé par le JIT, ce qui
lui permet d’exploiter la vulnérabilité en question.
Nous listons maintenant les principaux aspects du langage Java qui font généralement l’objet
d’optimisations :
– le dépliage des méthodes qui consiste à remplacer un appel de méthode par le code
correspondant ;
– l’optimisation des vérifications dynamiques intrinsèques au langage ;
– la suppression des verrous inutiles pour la synchronisation.
- Page 17-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Ensuite, nous discuterons des annotations utilisées par certaines techniques de JIT pour accélé-
rer leurs recherches d’optimisations. Enfin, nous terminerons cette partie en présentant une liste
non-exhaustive d’optimisations classiques (non spécifiques aux langages orientés objet).
L’exemple suivant montre un cas où il est facile de prévoir que l’appel virtuel m() (noté
A.m() au niveau bytecode) se fera sur un objet de classe A.
A a = new A ();
a.m ();
Dans ce deuxième exemple, il est beaucoup plus difficile de prévoir quelle méthode sera effec-
tivement appelée à cause de l’utilisation du polymorphisme objet.
void m( Object x) {
...
x. toString ();
...
}
La méthode réellement appelée n’est généralement connue qu’à l’exécution. Néanmoins, les
expériences montrent que la plupart des appels virtuels d’un programme n’ont pour cible qu’une
seule méthode. Les compilateurs mettent donc en œuvre de nombreux efforts pour réussir à
« dévirtualiser » ces appels, c’est-à-dire prédire quelle méthode sera effectivement appelée. Le
compilateur Hotspot de Sun utilise même parfois une prédiction optimiste : lorsqu’un appel
virtuel est rencontré à l’exécution, la méthode effectivement appelée (connue à l’exécution) est
dépliée en espérant qu’au prochain passage sur cet appel, la même méthode sera appelée. Si le
prochain appel virtuel au même point de programme appelle effectivement une autre méthode,
le dépliage précédent est annulé. Un tel compilateur doit donc être capable de défaire certaines
de ses optimisations.
- Page 18-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Un dépliage incorrect de méthode peut avoir des conséquences indirectes sur la sécurité d’un
programme, puisqu’il peut amener à l’appel d’une mauvaise méthode. Par exemple, une classe
A peut avoir redéfini une méthode clone() qui assure une copie sécurisée des instances. Si le
compilateur utilise une optimisation de dépliage incorrecte, il se peut qu’il copie un objet de
classe A avec la méthode définie par défaut dans la classe java.lang.Object au lieu d’utiliser
la méthode clone() définie dans A.
Les propriétés intrinsèques du langage Java reposent en partie sur des vérifications dyna-
miques (pas d’accès en dehors des bornes des tableaux, par exemple). Lorsqu’une de ces véri-
fications échoue, la plate-forme d’exécution doit lancer une exception adéquate qui peut éven-
tuellement être rattrapée. Le compilateur doit assurer que les variables locales sont disponibles
au point de rattrapage. Il doit aussi assurer que les exceptions soient lancées dans le même ordre
que celui imposé par une exécution standard. Cela gêne particulièrement les optimisations qui
cherchent à réordonner le code.
le compilateur va chercher à insérer un test 0<=i && i+1 < t.length avant l’entrée de la
boucle. Il lui faut pour cela détecter que i ne varie pas dans la boucle et que t désigne toujours
un tableau de même taille. Il lui faut aussi s’assurer que la portion ... ne lance pas d’exception,
sans quoi une inversion dans l’ordre des exceptions lancées pourrait se produire. Cette inversion
peut mener au masquage d’une exception de sécurité.
Une dernière vérification dynamique qui fait généralement l’objet de nombreuses optimi-
sations est le test de sous-typage. Cette opération clé peut être intensivement utilisée durant
l’exécution d’un programme (pour les opérations de cast, les affectations d’éléments de ta-
bleaux ou bien les manipulations d’interfaces). Les techniques d’optimisations généralement
utilisées essayent de tabuler l’opération (mais la mise à jour de la tabulation 3 est nécessaire
lors d’un chargement dynamique de classes), ou bien de mémoriser les résultats des tests pour
épargner les tests ultérieurs. Il s’agit d’une opération critique, car la relation de sous-typage est
un socle de base pour la sécurité d’un programme Java.
3. Dans le cadre de la programmation dynamique, la tabulation consiste à mémoriser toutes les entrées/sorties
possibles pour une opération dans une table.
- Page 19-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
4.4.4.3 Synchronisation
Les méthodes et les blocs synchronisés permettent d’assurer l’atomicité de l’ensemble des
opérations d’une région. Ce mécanisme occasionne néanmoins une prise de verrous coûteuse
à l’exécution. La réutilisabilité du code Java multiplie cependant les cas où la prise de verrou
est inutile. Pour assurer une bonne synchronisation dans tous les cas, de nombreuses classes de
la bibliothèque standard multiplient les prises de verrous. Lorsque les objets verrouillés ne sont
pas partagés entre threads, le mécanisme est cependant inutile. Ce cas se produit souvent en
pratique.
Des analyses « d’échappement » sont généralement utilisées (par exemple dans la JVM
Hotspot de Sun) pour essayer de détecter les verrous inutiles. Le but est alors d’assurer qu’un
objet reste local au thread courant. Dans l’exemple suivant le vecteur v n’échappe pas à la
méthode m et les verrous des méthodes add de la classe Vector sont inutiles.
public String m () {
Vector v = new Vector ();
v. add ("1" );
v. add ("2" );
v. add ("3" );
v. add ("4" );
v. add ("5" );
return v. toString ();
}
Une erreur de conception dans une optimisation de cette nature pourrait provoquer une mauvaise
synchronisation d’un programme avec deux conséquences majeures :
– tout d’abord la perte des invariants internes d’une classe (comme Vector) dont les
invariants internes sont normalement assurés par des mises à jour en région atomique ;
– mais aussi l’apparition de data races (voir [6]) qui peuvent faire perdre la cohérence
séquentielle d’un programme.
Dans le premier cas, on considère le problème de l’atomicité des modifications dans une
classe. Par exemple, si une classe contient deux champs dont la valeur du second dépend tou-
jours de la valeur du premier champ et si on ne synchronise pas la modification de ces deux
champs au sein de la classe, il serait alors possible à partir d’un autre thread d’accéder à ces
valeurs et de n’y voir aucune dépendance.
Le second cas porte sur l’apparition de data races. Dans ce cas, il ne s’agit plus de « cas-
ser » les invariants d’une classe, mais l’absence de synchronisation sur certaines opérations peut
compromettre l’ordre séquentiel du programme (voir la section 6.1.1.4 de [6] sur le réordon-
nancement des instructions par le compilateur).
- Page 20-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Pour réduire le temps passé à chercher des optimisations sans réduire pour autant la qualité
de ces optimisations, certaines approches [9] proposent de réaliser des optimisations avant l’exé-
cution et de transmettre des annotations pour guider le compilateur dynamique. Une annotation
dans un fichier .class peut ainsi indiquer qu’un certain accès au tableau ne nécessite pas de
vérification dynamique. Si certaines informations assurées par annotation ont un impact faible
sur la sécurité, d’autres peuvent être extrêmement dangereuses. On trouve dans la première
catégorie, les annotations chargées d’aider la détection des points chauds. Une mauvaise anno-
tation insérée par un attaquant ne peut ici mener qu’à une perte de performance 4 . L’exemple
de l’accès au tableau rentre lui dans la deuxième catégorie, celle qui affecte la sémantique d’un
programme. Des annotations malicieuses peuvent ici compromettre les nombreuses propriétés
du langage Java qui doivent être assurées dynamiquement. La seule alternative sûre est de ne
pas croire « sur parole » de telles annotations, mais de les faire vérifier par le JIT. La technique
se rapproche des techniques de code porteur de preuve (elle est présentée dans [6]) et est déjà
utilisée par le vérificateur de bytecode pour le typage standard. À noter que la généralisation de
l’approche code porteur de preuve a aussi l’avantage de réduire la complexité des mécanismes
d’optimisation sur lesquels peut reposer la correction d’une machine virtuelle optimisée, car les
vérificateurs d’annotations sont généralement de nature plus simple que les outils qui génèrent
ces annotations.
Dans les paragraphes précédents, nous n’avons mentionné que les optimisations spécifiques
aux langages orientés objet. Voici une liste non-exhaustive d’optimisations qui sont appliquées
non seulement à Java, mais également à d’autres langages non orientés objet :
– l’élimination de code mort ;
– le réordonnancement des instructions. Des instructions peuvent être réordonnancées
pour pouvoir utiliser au mieux le pipeline du processeur, s’il n’y a pas de dépendance
entre elles. Ce réordonnancement peut occasionner des « surprises », si les instructions
peuvent lever des exceptions ;
– la propagation de constantes. Cela consiste à propager la valeur d’une variable lorsque
cette valeur est une constante. Ceci permet de simplifier les calculs et/ou d’utiliser
moins de registres ;
– l’extraction des boucles de calculs invariants. Si, dans une boucle, la valeur d’un calcul
ne change pas d’une itération à l’autre, alors il peut être extrait de la boucle ;
– la propagation de copies. Cela consiste à supprimer les instructions qui copient la
valeur d’une variable/registre dans une/un autre. Cette optimisation peut être effectuée
lors de l’allocation de registres.
4. Si la perte de performance est trop importante, cela peut éventuellement mener à une forme de déni de
service.
- Page 21-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
La frontière entre compilation statique et dynamique est souvent mince. La compilation AOT
est parfois utilisée par un émulateur pour optimiser un programme juste avant l’exécution. Le
compilateur effectue une résolution statique, compilant ainsi la quasi totalité de l’application.
Concernant les problèmes de portabilité, la distribution est toujours effectuée sous forme de
bytecode et le vérificateur de bytecode peut remplir son rôle standard.
L’utilisation d’un compilateur (JIT ou AOT) suppose la production d’un code natif exécu-
table qui doit être stocké (au moins temporairement) puis exécuté. La problématique majeure
en termes de sécurité consiste à assurer l’intégrité de ce code exécutable entre l’étape de compi-
lation et son exécution. Une modification malicieuse permettrait de contourner les vérifications
effectuées par le vérificateur de bytecode. Assurer cette intégrité est plus ou moins complexe
suivant la persistance de ce code. En effet, certains compilateurs peuvent mettre en cache le
code compilé pour limiter le nombre de compilation. Ce cache peut même, dans certains cas,
être partagé entre plusieurs instances de l’émulateur. Se pose alors le problème de la taille de la
base de confiance et du cloisonnement entre différentes instances de l’émulateur.
Remarque 1
L’intégrité du bytecode doit être garantie après le chargement et la vérification par le véri-
ficateur de bytecode, car le vérificateur de bytecode ne sera pas rappelé si le bytecode est
modifié après son passage.
- Page 22-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
4.5.1 Threads OS
Les threads sont un concept intéressant qui existe aussi bien dans le monde Java (il s’agit
d’une exigence de la spécification du langage et de la JVM) que dans la plupart des OS sous-
jacents. On parle alors respectivement de threads Java et de threads OS.
Sous UNIX, les threads OS sont définis par la norme POSIX.1c, Threads extensions, qui est
également un standard IEEE (IEEE Std 1003.1c-1995).
Sous Linux plus particulièrement, les threads OS sont implémentés par la bibliothèque
NPTL (Native Posix Thread Library).
4.5.2 Implémentation des threads Java par la JVM sans utilisation des threads OS
Une approche utilisée par les anciennes implémentations de la JVM (y compris celles de
Sun en version 1.0), était de ne pas utiliser les threads OS pour implémenter les threads JVM.
Cette approche est appelée Green thread. L’avantage principal concerne la portabilité de l’ému-
lateur qui peut notamment être exécuté sur des OS qui n’implémentent pas de mécanismes de
thread. En ne dépendant pas des threads OS, on assure également que l’implémentation de la
JVM aura rigoureusement le même comportement quel que soit l’OS sous-jacent utilisé. Cette
approche va généralement de pair (pour des raisons de facilité d’implémentation) avec une ges-
tion collaborative des threads Java. Cela signifie que chaque thread doit collaborer avec les
autres threads afin de leur passer la main périodiquement. Si un thread ne le fait pas correcte-
ment, il peut monopoliser l’accès au CPU au détriment des autres threads.
- Page 23-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
4.5.3 Implémentation des threads Java par la JVM avec utilisation des threads OS
L’approche utilisée par la plupart des implémentations des JVM aujourd’hui repose sur les
threads OS pour implémenter les threads Java.
Il est intéressant de noter qu’en plus d’utiliser les threads OS pour l’implémentation des
threads Java, une implémentation de la JVM peut être amenée à instancier d’autres threads OS
pour ses besoins propres. En particulier :
– le glaneur de cellules peut être exécuté concurentiellement dans un thread à part ;
– pour les implémentations qui supportent le JIT (cf section 4.4.2), la JVM peut instan-
cier un ou plusieurs threads de compilation.
Une part importante de la tâche d’ordonnancement de l’implémentation de la JVM consiste
à synchroniser les threads applicatifs avec les threads utilisés pour le fonctionnement interne
de la JVM. En effet, certains mécanismes (glaneur de cellules, compilateur, etc.) nécessitent
parfois que l’ensemble des threads Java de l’application soient stoppés afin de s’assurer que
l’état de l’application n’évolue pas.
5. Il est envisageable pour une JVM de faire du « recyclage » de threads, ou alors de préallouer des threads par
avance, ce qui pourrait amener à une situation où il y a plus de threads OS que de threads Java
- Page 24-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Dans ce cas, la JVM « reprend la main » sur les threads Java en leur envoyant un signal
UNIX d’interruption, typiquement retourné par la fonction pthread_kill().
Ces primitives ne sont pas basées sur des instructions bytecode, mais sont implémentées en
Java pour la plus grosse part, avec des appels à des fonctions natives exportées dans la classe
sun.misc.Unsafe pour ce qu’il est impossible de traiter en Java.
- Page 25-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
4.5.6 Conclusion
La liberté laissée à une implémentation peut paraître grande en ce qui concerne les choix
de conception pour l’implémentation de la programmation concurrente, cependant en pratique
l’architecture présentée ici est celle utilisée par les différentes JVM.
Il existe des appels système de l’OS qui permettent de préciser que certaines pages mémoire
ne doivent pas être utilisées par le mécanisme de pagination (ou swap), c’est-à-dire être écrites
sur le disque dur lors de la saturation de la mémoire vive. Un tel mécanisme n’est clairement
pas souhaitable si l’application manipule des données sensibles telles que du matériel cryptogra-
phique. Ces données ne doivent en effet pas se retrouver sur un support de stockage de masse,
pour des raisons évidentes de confidentialité. Les appels système en question ne sont cependant
pas accessibles à l’intérieur d’une application Java. Or, il serait pertinent de pouvoir les utiliser
au sein du processus du garbage collector afin d’obtenir une gestion sécurisée du tas.
- Page 26-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
– La pile d’appels d’un thread contient des frames qui interviennent dans la gestion des
variables locales, de la pile d’opérandes et des appels et retours de méthodes (voir
[6], chapitre 5). La spécification autorise les piles d’appels à être allouées dans le tas
(voir ci-dessous). Leurs tailles peuvent être fixes ou variables. Selon la spécification
de la JVM, dans le cas de piles de taille fixe, les piles des différents threads n’ont
pas nécessairement la même taille, mais rien n’indique qui décide de la taille. Dans
le cas de piles de taille variable, l’utilisateur peut spécifier des tailles minimales et
maximales. Le support des méthodes natives suppose également la présence d’une
zone mémoire pour l’allocation des piles natives.
– Le tas est la zone mémoire, créée au démarrage de la machine virtuelle, où les ins-
tances de classe et les tableaux sont alloués. L’espace utilisé pour l’allocation d’objets
est récupéré par un garbage collector dont le type n’est pas imposé par la spécifica-
tion. La taille de cette zone mémoire peut être fixe ou variable et n’est pas néces-
sairement contiguë. L’utilisateur peut spécifier la taille de cette zone ou des valeurs
minimales et maximales si elle est variable ;
– La zone de méthodes est la zone mémoire créée au démarrage de la machine virtuelle.
Cette zone fait partie du tas, mais la gestion automatique de la mémoire sur cette
zone est optionnelle. Comme le tas, sa taille peut être fixe ou variable et spécifiée par
l’utilisateur. Cette zone mémoire contient les structures associées aux classes :
– le constant pool, alloué dans la zone de méthodes, contient différentes constantes
connues à la compilation (voir [6], chapitre 5),
– la description (les signatures) des champs et méthodes,
– le code des constructeurs et des méthodes.
- Page 27-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
F IGURE 1 – Références
Le rôle d’un garbage collector est de détecter parmi les blocs mémoire alloués par le pro-
gramme ceux qui sont devenus inaccessibles. En Java, un bloc mémoire (ou objet en l’occu-
rence) est accessible si :
– la pile d’appels d’un thread, ou « racine » (root), contient une référence vers cet objet
au travers d’une variable locale ou d’une pile d’opérandes ;
– un des champs d’un objet accessible pointe vers cet objet.
Plusieurs stratégies peuvent être utilisées pour l’implémentation d’un garbage collector.
Les deux principales familles sont le comptage de références (Reference Counting Collectors,
paragraphe 4.6.2.1) et l’exploration (Tracing Collectors, paragraphe 4.6.2.2). Le choix d’une
stratégie dépend fortement du cahier des charges de l’application utilisant la gestion automa-
tique de la mémoire. Un garbage collector peut être exact ou conservatif. Dans le premier
cas, les références sont parfaitement identifiées et tous les blocs de mémoire non accessibles
peuvent être libérés. Dans le second cas, il peut y avoir confusion entre les valeurs de type pri-
mitifs et les références. S’il n’est pas possible de distinguer ces deux types de valeurs, alors les
premières seront interprétées par un garbage collector conservatif comme des références, ce
qui peut conduire à considérer comme accessible un bloc mémoire qui ne l’est pas.
- Page 28-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
par le premier. L’instrumentation de chaque instruction permet de libérer un objet dès que celui-
ci devient inaccessible. Le coût de la gestion automatique de la mémoire est ainsi réparti dans
le temps ce qui peut être crucial pour certaines applications (par exemple pour les applications
temps réel). En revanche, la définition même du comptage de référence pose un problème pour
la gestion des données cycliques. Ce problème est illustré par les figures 2(c) et 2(d) où un
cycle est créé entre les objets x.f et y. Bien que ces objets deviennent inaccessibles (figure
2(d)) depuis la racine, leurs compteurs ont la valeur 1 ce qui empêche leur désallocation.
Remarque 2
Le problème des données circulaires peut être contourné en combinant le comptage de réfé-
rences avec un autre algorithme. Dans ce cas, ce dernier sera appelé occasionnellement pour
pallier au problème. C’est par exemple le cas pour le langage Python.
4.6.2.2 Exploration
- Page 29-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Mark and Sweep L’algorithme Mark and Sweep, constitué de deux phases, est la forme la
plus simple de gestion automatique de la mémoire par exploration. La première phase (Mark)
marque les références découvertes par l’exploration (marquage direct du bloc mémoire dans
le tas). Ces éléments sont marqués au cours d’un parcours des éléments accessibles à partir
de la racine (figure 3(b)). La seconde phase (Sweep) parcourt l’intégralité du tas et libère les
blocs mémoire non-marqués (figure 3(c)). Le principal inconvénient de cet algorithme réside
dans le parcours intégral du tas effectué lors la seconde étape. Même si peu de blocs mémoire
sont effectivement utilisés par le programme, le temps d’exécution est proportionnel à l’espace
réservé pour le tas.
(c) Sweep
+ Technique simple,
+ Technique peu intrusive,
+ Gère les structures de données cycliques,
- Parcours complet du tas (en particulier des objets inaccessibles).
Copying L’algorithme Copying permet d’éviter le parcours intégral du tas réalisé par le
Mark and Sweep. Il ne comporte qu’une phase, mais nécessite en revanche l’utilisation de deux
tas : un tas actif et un tas inactif. Comme précédemment, un parcours du tas (actif) à partir des
racines est effectué pour découvrir les éléments accessibles (figure 4(a)). Au fur et à mesure que
les éléments sont découverts, ils sont recopiés dans le tas inactif. Une fois le parcours achevé,
les rôles des deux tas sont inversés (figure 4(b)). Le déplacement en mémoire des objets sup-
pose une mise à jour des adresses (dans les racines et dans les objets). Ceci peut être facilement
réalisé en maintenant une table de correspondance nouvelle/ancienne adresse ou en utilisant la
table d’indirection utilisée pour implémenter les références si elle existe. Outre l’absence de la
deuxième phase coûteuse du Mark and Sweep, cette approche permet d’éviter la fragmentation
du tas (les blocs mémoires sont directement compactés dans le tas inactif). En particulier, l’allo-
cation devient beaucoup plus efficace, puisque le calcul de l’adresse du prochain bloc mémoire
libre est immédiat.
- Page 30-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
F IGURE 4 – Copying
+ Technique simple,
+ Bonnes performances,
+ Compactage du tas, pas de fragmentation et augmentation des performances de l’al-
location,
- Doublement de la taille du tas.
Mark and Compact L’algorithme Mark and Compact représente un compromis entre le
Mark and Sweep et le Copying. La première phase (Mark) est identique à celle du Mark and
Sweep. La seconde phase compacte les blocs mémoire marqués au début du tas.
(c) Mark
- Page 31-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Comme dans l’algorithme Copying, les références doivent être mises à jour.
4.6.2.3 Optimisations
Generational garbage collector Les garbage collectors générationnels s’appuient sur l’ob-
servation que toutes les références n’ont pas la même durée de vie. La plupart des objets ont
une durée de vie très courte et seul un petit nombre ont une durée de vie très longue. Dans un
- Page 32-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
garbage collector générationnel, le tas est divisé en plusieurs parties, chacune correspondant
intuitivement à l’âge des objets qu’elle contient. Le cas de figure le plus simple comporte deux
générations : jeune et ancienne. Dans ce cas, un nouvel objet est alloué dans la jeune génération.
Lorsqu’une phase de garbage collection est nécessaire, on essaie d’abord de la réaliser sur la
jeune génération (minor collection). Un objet qui survit à un nombre fixé de minor collections
est promu à l’ancienne génération. Si cette étape n’est pas suffisante, on procède à une phase
de garbage collection sur l’ancienne génération (major collection). Dans la plupart des cas, la
première phase est suffisante, ce qui permet un gain de temps important, puisque des objets
dont la durée de vie est longue seront testés moins souvent. L’implémentation d’un garbage
collector générationnel nécessite de mettre en place des mécanismes de gestion des pointeurs
inter-générations sans quoi une référence de la jeune génération, accessible seulement depuis
une référence de l’ancienne génération (elle-même accessible depuis la racine) pourrait être
libérée à tort.
Parallel garbage collector Un garbage collector parallèle, à ne pas confondre avec concur-
rent, est un garbage collector dont l’algorithme est parallèle. Il n’existe pas de spécification plus
précise de ce type de garbage collector, le cas particulier de celui de HotSpot est présenté dans
[5].
4.6.3 Conclusion
L’impact direct d’un garbage collector sur la sécurité est faible, les risques reposent avant
tout sur la complexité de l’algorithme utilisé et sur son implémentation. Le garbage collector
de HotSpot, par exemple, atteint un degré de complexité important, car il combine plusieurs
approches dont il faut maitriser les interactions. En particulier, l’implémentation d’un garbage
collector parallèle efficace est une tâche très difficile dans laquelle beaucoup d’erreurs peuvent
se glisser. La question des garbage collectors conservatifs ne pose un problème de sécurité que
si l’on se repose sur la gestion automatique de la mémoire pour assurer l’effacement de données
sécurisées, ce qui, de manière générale, est une mauvaise pratique.
- Page 33-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Une autre protection, le bit NX, implémentée dans les processeurs récents, permet d’inter-
dire l’exécution de code au sein d’une page mémoire marquée comme non-exécutable. Cette
protection permet de protéger une application notamment face aux attaques de type déborde-
ment de pile. Une telle protection n’est pas contraignante pour le fonctionnement de la JVM,
quel que soit le mode d’exécution implémenté (interprétation ou JIT).
Les protections mémoire avancées, comme PaX, offrent un niveau de robustesse intéressant
vis-à-vis de nombreuses formes d’attaques. Cependant, ces protections ne sont pas toujours
compatibles avec le fonctionnement des applications qu’elles protègent. C’est notamment le
cas des applications qui réalisent des manipulations non classiques telles que la modification,
pendant l’exécution, du code exécutable. Les implémentations de la JVM effectuant de la com-
pilation « à la volée » ou des formes optimisées d’interprétation (mode inline ou mode template),
sont typiquement concernées par cette incompatibilité. Il est donc nécessaire d’avoir connais-
sance du fonctionnement interne de la JVM utilisée lorsque des protections mémoire avancées
doivent être mises en place.
Au final, il est possible de tirer partie des protections mises en œuvre par le système d’ex-
ploitation. Les mécanismes de protection mémoire sont pour la plupart compatibles avec le
fonctionnement de la JVM. Seule la protection mémoire avancée, de type PaX, peut empêcher,
sous certaines circonstances (mode JIT et/ou mode interprété inline ou template) le fonctionne-
ment de la JVM.
- Page 34-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
5.1 Introduction
Cette section traite du mode d’exécution natif. Il est intéressant de préciser ce que nous
entendons par « mode d’exécution natif », dans le contexte des modèles d’exécution Java. Il
s’agit de la stratégie d’implémentation de la machine virtuelle Java qui repose principalement
sur la compilation statique (c’est-à-dire avant exécution) de l’application. Cette stratégie repose
sur l’utilisation d’un compilateur AOT (Ahead-of-Time) qui est un outil de conversion d’un
bytecode Java en code natif pour le processeur cible. Le compilateur AOT est généralement
utilisé pour fournir un exécutable autonome au format adapté à la machine cible. Ce format sera
en général le même format que celui utilisé pour les programmes C et C++ de la plate-forme :
sous Linux, il s’agit du format ELF.
5.2 Historique
L’approche native a connu son heure de gloire quand le mode interprété était encore d’une
lenteur affligeante, avant l’introduction des techniques de JIT.
6. L’étape de chargement de classes décrite dans la spécification de la JVM étant dynamique, il se peut que
certaines classes ne puissent être déterminées statiquement et nécessitent d’être chargées lors de l’exécution du
programme. Un interpréteur doit donc dans ce cas être embarqué dans l’exécutable pour exécuter les méthodes de
ces classes.
- Page 35-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
De ces solutions, seules GCJ et JET existent encore aujourd’hui 7 , et GCJ est le seul à
supporter des environnements non x86. Les autres ont disparu, ou, pour les projets open source
(Manta Fast Parallel Java, The Timber Compiler), aucune nouvelle version n’est apparue depuis
au moins 5 ans.
5.3 Implémentations
Les deux implémentations encore en course se distinguent par les éléments suivants :
– L’implémentation du projet GNU est avant tout motivée par un aspect idéaliste (four-
nir une implémentation de Java sous licence GPL). En second plan interviennent les
considérations techniques, telles que la plus grande facilité d’intégrer Java dans l’en-
vironnement GNU en passant par le mode natif plutôt que par l’approche utilisant un
émulateur. Le projet GNU met également en avant le fait que la compilation native
rend le code source Java plus portable, car GCJ supporte plus de plates-formes cibles
que ne le supportent les émulateurs du marché.
– L’implémentation de Excelsior JET met en avant la réduction de l’empreinte mémoire
(à la fois en mémoire de masse et en mémoire vive), un temps de chargement réduit
ainsi qu’une optimisation poussée disponible dès le chargement du programme. JET
existe pour Windows et Linux.
7. Certaines des pages citées ci-dessus ne font plus directement référence aux compilateurs sur leur page prin-
cipale, mais des informations sont encore disponibles sur ces sites.
- Page 36-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
On décrit ici une architecture de référence d’un environnement d’exécution natif. On peut
en effet parler ici d’implémentation de référence, car il s’agit de celle respectant au mieux la
spécification de la JVM. Elle est supportée aussi bien par JET que GCJ.
Une autre solution architecturale permettrait de passer directement du code Java au code
natif, sans passer par une représentation intermédiaire de type bytecode. Cette solution, bien
que techniquement possible, pose la question de la transposition des vérifications statiques ef-
fectuées normalement sur le bytecode vers des vérifications effectuées durant le processus de
compilation (donc à partir d’une représentation de plus haut niveau). En effet, la spécification
- Page 37-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
de la JVM précise les vérifications à effectuer au niveau du bytecode : il existe donc une spé-
cification assez précise du vérificateur de bytecode. Comment réaliser la transposition de ces
vérifications dans une architecture où le bytecode n’existe plus en tant que tel ?
- Page 38-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
– il passe par une étape intermédiaire de transformation du code Java en C, avant com-
pilation par le compilateur C GCC ;
– il est plus riche en fonctionnalités (support d’un interpréteur en sus de la compilation
native par exemple).
Ces optimisations agressives sont en revanche très coûteuses en temps CPU. Elles sont
similaires aux méthodes d’optimisation des compilateurs C ou C++, où un temps de compilation
de plusieurs minutes, voire plusieurs heures pour des applications conséquentes, est considéré
comme « acceptable ».
Par conséquent, pour les implémentations AOT, une fois qu’une classe (ou un JAR) est
convertie au format natif, cette dernière est en général stockée sur disque dans une base de
données, afin de pouvoir être rechargée plus tard.
Ceci a un effet de bord intéressant d’un point de vue sécurité. En effet, une JVM tradition-
nelle se protège des attaques par injection de bytecode non conforme en passant le vérificateur
de bytecode sur le bytecode avant de l’exécuter. Dans notre cas, le vérificateur de bytecode sera
forcément passé avant la compilation AOT. Ce qui veut dire que l’intégrité du code machine
stocké dans la base de données doit être assurée. Ceci est une contrainte supplémentaire par
rapport au modèle d’exécution purement interprété, ou à un JIT qui ne conserve pas le code
machine généré d’une exécution sur l’autre.
La problématique de gestion des threads après compilation native est globalement similaire
à celle rencontrée dans un environnement d’exécution par émulateur. On pourra se reporter à la
section 4.5 pour plus de détails.
- Page 39-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Dans le cas d’un modèle utilisant uniquement la compilation AOT, l’ensemble des classes
doit être connu et compilé avant l’exécution de l’application. Il y a alors un choix architectural
à faire :
– soit le modèle ne supporte pas le chargement dynamique ce qui constitue une dévia-
tion par rapport au modèle d’exécution JVM (le chargement dynamique de bytecode à
l’exécution n’est plus supporté et la distribution des applications n’est plus effectuée
sous forme de fichier class).
– soit un interpréteur Java (ou un JIT) est intégré ou lié à l’exécutable natif. Le modèle
se rapproche alors de la stratégie par utilisation d’un émulateur.
On voit ici qu’il n’y a pas de solution « idéale » pour la problématique du chargement de
classes. Soit on inclut dans le binaire de l’application un interpréteur, ce qui est une solution
relativement lourde et complexe, soit on supprime une fonctionnalité phare de la plate-forme
Java.
- Page 40-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Par conséquent, le modèle d’exécution natif ne permet pas d’avoir un réel exécutable auto-
nome, puisqu’il est accompagné d’un runtime qui peut être de taille conséquente.
Au delà de l’utilisation de compilateurs AOT et JIT pour passer en mode natif, il est intéres-
sant de noter que l’amélioration des performances d’une plate-forme d’exécution Java repose en
partie sur la qualité d’implémentation de la bibliothèque standard (seules les « couches hautes »
de la bibliothèque, c’est-à-dire l’API, étant standardisées). Une stratégie d’optimisation consiste
alors à recourir massivement à l’interface JNI et à implémenter en C ou C++ les « couches
basses » de la bibliothèque. Cette utilisation du code natif présente des avantages (meilleures
performances, meilleure intégration avec le système, notamment en ce qui concerne les primi-
tives graphiques). Elle constitue cependant une déviation importante du modèle, puisque les
propriétés du langage Java ne sont pas assurées pour ce code natif. Celui-ci peut notamment
comporter des vulnérabilités que l’utilisation de Java est censée éviter (typiquement, les débor-
dements de tampon ou buffer overflow). Du point de vue de la sécurité, l’utilisation de JNI dans
l’implémentation de la bibliothèque standard devrait être limitée à l’interface avec les couches
basses de l’OS (typiquement, les appels système) afin de réduire la portion de code natif « non-
Java » utilisé par une application.
5.12 Conclusion
Il est intéressant de noter que le mur de séparation entre mode d’exécution natif et l’utilisa-
tion d’un émulateur s’est considérablement effrité avec le temps. En effet, les JVM performantes
incluent toutes un JIT, qui n’est rien d’autre qu’une compilation native, mais effectuée durant
l’exécution d’un programme Java. De même, une approche qui privilégierait une compilation
complète avant exécution du programme Java (approche AOT), qu’on appelle ici compilation
native, doit inclure un interpréteur ou un JIT afin d’assurer la compatibilité avec les applications
Java qui utilisent le chargement de classes dynamique.
Le modèle d’exécution natif (compilation statique ou AOT) est caractérisé par un manque
flagrant d’étude approfondie sur son mode de fonctionnement détaillé. Cette stratégie d’im-
plémentation souffre d’une distance importante entre l’implémentation et la spécification de
la JVM. Au delà de la conformité à la spécification, se pose le problème de la vérification de
- Page 41-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
cette conformité (notamment en ce qui concerne les aspects liés à la sécurité). Le besoin indus-
triel pour ce type d’approche n’est aujourd’hui plus évident, les performances des émulateurs
s’étant considérablement améliorées par rapport aux premières versions de Java, surtout de-
puis l’arrivée d’une forme de compilation native (JIT). Il est intéressant de noter qu’au delà de
l’utilisation des compilateurs AOT et JIT, l’amélioration des performances d’une plate-forme
d’exécution Java repose en partie sur la qualité d’implémentation de la bibliothèque standard,
seules les « couches hautes » de la bibliothèque (c’est-à-dire l’API) étant standardisées.
Une des raisons qui pourraient pousser à vouloir utiliser le mode d’exécution natif est la
possibilité d’aller beaucoup plus loin dans l’offuscation de code que ce qu’il est possible de faire
en restant dans le monde Java. Il ne faut cependant pas oublier que le passage au mode natif
n’est pas anodin, et qu’il peut y avoir un impact sur les propriétés de sécurité de l’application
ainsi transformée. On pensera en particulier au code machine de l’application transformée qui
peut avoir éventuellement besoin d’être protégé en intégrité, si le mode de transport de celui-ci
permet à un attaquant de l’altérer.
Les implémentations GNU (GCJ) et Excelsior (JET) sont les seules survivantes, et aucune
n’a été véritablement conçue dans une optique du développement d’applications de sécurité.
La raison d’être de GCJ était de fournir une implémentation entièrement libre du langage Java.
Excelsior quant à elle vise plutôt l’aspect performance.
GCJ et Excelsior sont toutes les deux à jour vis-à-vis des derniers standards Java. Cependant,
on peut se poser la question de leur pérennité à long terme. L’intérêt pour GCJ est retombé
depuis qu’OpenJDK (issu de la libération du code source de l’implémentation Sun) est apparu
et que Redhat s’est maintenant détourné de GCJ au profit d’OpenJDK.
Du fait des progrès continus réalisés par le JIT, on peut également se poser la question
de l’intérêt de l’approche d’Excelsior, dans la mesure où les performances, qui hier étaient
clairement en faveur d’Excelsior, sont aujourd’hui à peu près équivalentes à celles d’une JVM
avec un JIT performant.
- Page 42-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Il existe deux approches Java processor différentes pour améliorer l’exécution du bytecode
Java de façon matérielle :
1. l’approche par coprocesseur Java. Cette première méthode consiste à utiliser un
« coprocesseur » Java conjointement au processeur classique. Ce coprocesseur peut
alors traduire le bytecode Java en une suite d’instructions RISC compréhensibles par
l’autre processeur ou alors l’exécuter directement.
2. l’approche par remplacement du processeur principal. Cette autre méthode utilise
des processeurs Java qui viennent remplacer le processeur classique. Ceci implique
que toutes les applications qui s’exécutent doivent être écrites dans un langage com-
pilable vers le bytecode Java. Ce type de processeur est donc particulièrement adapté
au monde de l’embarqué.
L’approche Java processor a été une approche envisagée pour permettre l’exécution de by-
tecode Java sur des plates-formes embarquées disposant de très peu de ressource mémoire et
CPU. À cette époque, l’utilisation d’un émulateur n’était pas envisageable sur ces plates-formes,
du fait de la pénalité en temps d’exécution et en occupation mémoire introduite par l’émulateur.
Ce modèle s’appuie sur une implémentation (souvent partielle) de la JVM sous forme ma-
térielle. Le jeu d’instructions de la JVM (les instructions du bytecode) est généralement im-
plémenté seulement partiellement dans le silicium. Le but est de rendre le processeur capable
d’exécuter directement les instructions Java les plus simples (calculs numériques, opérations
de load et store, tests de branchement), et qui sont aussi celles rencontrées le plus souvent
dans les fichiers class. Le bytecode ne comportant pas d’instructions permettant d’accéder aux
ressources natives (par exemple, pour la gestion des entrées/sorties), ce type de processeur com-
prend également d’autres instructions permettant d’effectuer ces accès ou utilise le processeur
« générique » dans le cas d’un coprocesseur. De plus, certains services de la JVM (par exemple,
la gestion de la mémoire ou le chargement de classes) étant difficilement implémentables sous
forme matérielle, ce modèle d’exécution s’appuie également sur une implémentation logicielle
(généralement désignée sous le terme de JVM) qui fournit ces services et s’appuie sur le pro-
cesseur dédié pour l’exécution du bytecode.
Pour accéder à une implémentation logicielle, les processeurs utilisent généralement le mé-
canisme des « traps » ou interruptions logicielles. Le « trap logiciel » est une interruption clas-
sique, à la différence qu’elle n’est pas prise en compte par le processeur à partir de signaux ex-
ternes provenant d’autres composants, mais induite par l’exécution d’une instruction spécifique
du langage machine. Après l’exécution de la procedure principale de gestion de l’interruption
qui contient le code permettant d’émuler la fonction à exécuter, le processeur pourra reprendre
son exécution normale.
- Page 43-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
La suite de l’étude présente une liste de solutions proposées par des fournisseurs. Il est
important de noter que les détails d’implémentation et d’architecture de chaque processeur Java
ont été ici laissés de côté. En effet, on se heurte dans ce domaine à un problème d’accessibilité
de l’information technique. À la différence des modèles d’exécution par émulateur (JVM) ou
alors de JavaCard, les fonctionnements internes des processeurs Java sont peu documentés. Les
seules documentations mises à disposition par les fondeurs de Java Processors sont plutôt des
informations macroscopiques sous forme de schémas d’architecture. Ce type d’information est
plutôt destiné à des donneurs d’ordre des fabricants de périphériques mobiles.
Par conséquent, l’étude suivante constitue plus une liste des produits existants ou ayant
existé, en précisant leurs caractéristiques principales et leurs limites, qu’une étude poussée du
mode de fonctionnement de chaque solution.
Co-processeurs :
– Jazelle 9 .
Processeurs :
– IM1101(Cjip) 10 .
– picoJava 11 ;
– aJile 12 ;
– JOP 13 ;
– IM3000 14 .
Les informations accessibles sur les différentes architectures possibles, ou sur les différents
processeurs, ne permettent pas de cerner précisément quels sont les gains/pertes au niveau sé-
curité par rapport au modèle d’exécution par émulateur.
9. http://www.arm.com/products/multimedia/java/jazelle.html
10. http://www.imsystech.com/
11. http://www.sun.com/processors/technologies.html
12. http://www.ajile.com/
13. http://www.jopdesign.com/
14. http://www.imsystech.com/
- Page 44-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
6.4 picoJava
Le processeur Java picoJava de Sun est le plus souvent cité dans les documents de recherche.
Il s’agit en réalité d’une spécification utilisée comme référence pour les nouveaux processeurs
Java et comme base de recherche pour l’amélioration de processeurs existants.
6.4.1 Historique
La première version a été introduite en 1997. Le marché visé est celui des systèmes embar-
qués, en offrant un « pur » processeur Java. Une seconde version, picoJava-II, a ensuite vu le
jour en 1999.
6.4.2 Implémentation
Etant donné qu’il s’agit d’une spécification, ce n’est qu’une base vers la création réelle de
processeurs. Sun n’a jamais lui-même produit (de manière industrielle) de processeurs respec-
tant picoJava. Bien que des licences aient été fournies à Fujitsu, IBM, LG Semicon et NEC, ces
sociétés n’ont pas non plus produit de processeurs picoJava.
6.4.4 Evolution
- Page 45-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
6.5 aJile
6.5.1 Historique
Courant 1997, Rockwell Collins a annoncé avoir réalisé le premier processeur Java, le
JEM1. Ce processeur, qui n’était pas basé sur le modèle proposé par Sun, fut créé pour une uti-
lisation en interne. Ajile Systems commercialise ensuite l’aJ-100, basé sur le processeur JEM2
et créé en 1999 par des ingénieurs issus de Rockwell Collins.
6.5.2 Implémentation
- Page 46-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
L’environnement d’exécution Java temps réel d’aJile est implémenté suivant les recomman-
dations CLDC 1.1 (Connected Limited Device Configuration) et CDC 1.1 (Connected Device
Configuration). Le processeur aJ-100 est donc un processeur compatible avec la plate-forme
J2ME.
6.5.4 Evolution
aJile Systems propose une nouvelle génération de son processeur Java, du nom d’aJ-102.
Selon la compagnie, ce nouveau venu traite les instructions Java trois fois plus rapidement que
son prédécesseur. Cette puce tout-en-un intègre diverses unités de traitement (nombres flottants
et chiffrement AES), de multiples contrôleurs (mémoire, USB 2.0, Ethernet à 10/100 Mb/s,
écran LCD, etc.) et un ensemble complet d’entrées/sorties. Le tout est livré avec un système
d’exploitation temps-réel, entièrement écrit en Java.
L’aJ-102 est disponible depuis avril 2009 au tarif de 16 dollars HT. La compagnie prosera
également l’aJ-200 : cette puce sera cadencée à 180 MHz et proposera une unité multimédia
évoluée capable de supporter les formats JPEG, MPEG et H.263.
- Page 47-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
6.6.1 Historique
6.6.2 Implémentation
Le processeur Java IM-1101 embarque une machine virtuelle Java implémentée en micro-
code alors que les processeurs Java de la série IM3000 possèdent une machine virtuelle basée
sur KVM (Kilobyte Virtual Machine) qui est une version plus légère de la JVM de Sun.
Le jeu d’instruction des processeurs de Imsys se limite à 85% du jeu d’instructions stan-
dard du langage Java contre 99% pour aJile et son processeur Aj-100. En effet, les instructions
les plus complexes et les plus rares ne sont pas implémentées, mais exécutées de façon logi-
cielle [16].
Ces processeurs supportent le langage Java, mais également le C/C++ et le langage assem-
bleur.
- Page 48-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Contrairement à aJile, les processeurs de la série IM3000 ne peuvent avoir qu’une seule
machine virtuelle active simultanément. Néanmoins, plusieurs applications Java peuvent être
exécutées en même temps et partager alors le même « tas ».
Sur cette famille de processeurs, l’exécution du Garbage Collector, qui se déroule dans
un thread distinct, peut être réalisée automatiquement lorsque le « tas » est plein, ou de façon
explicite depuis un programme Java ou C.
Processeurs de la série IM3000 : L’API de base de ces processeurs est le Connected Li-
mited Device Configuration (CLDC) version 1.0. Elle contient également un sous-ensemble
du JDK 1.1.8, le package javax.comm, ainsi qu’un ensemble de classes spécifiques à Imsys
permettant aux applications d’accéder aux ressources système.
6.6.4 Evolution
Les processeurs réalisés par Imsys sont toujours disponibles, mais il n’y a pas d’information
disponible sur les futures réalisations de cette société.
6.7 JOP
6.7.1 Historique
Le processeur JOP est en réalité un « cœur logiciel » basé sur l’idée qu’une implémentation
native de la totalité du bytecode Java n’est pas une approche utile.
- Page 49-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
– Juin 2001 : création de JOP3, qui est la version actuelle et est généralement désignée
par le terme JOP.
Le projet JOP (Java Optimized Processor) a été l’objet de la thèse réalisée par Martin Schoe-
berl en 2004 à l’université de Vienne (voir [11, 12] pour plus de détails). Depuis le 24 février
2008, ce projet open source est passé sous la licence GNU General Public Licence version 3.
Un des principaux objectifs de ce projet est de pouvoir prévoir la durée d’exécution du bytecode
Java pour des systèmes temps-réel.
6.7.2 Implémentation
Le processeur JOP possède son propre jeu d’instructions. Ce dernier, appelé « microcode »,
est un jeu d’instructions réduit permettant de traduire une instruction bytecode en une ou plu-
sieurs instructions microcodes. Contrairement aux autres processeurs Java, JOP n’utilise pas le
mécanisme des « traps » (défini dans le paragraphe 6.1) pour émuler le bytecode Java qui n’est
pas directement implémenté en microcode. Le processeur passe donc par une table de corres-
pondances qui contient, pour chaque instruction bytecode, l’adresse de la suite d’instructions
microcode à exécuter. Pour toutes les instructions bytecode n’ayant pas de correspondance en
microcode, l’adresse inscrite dans la table pointe sur une séquence d’instructions qui invoque
une méthode statique. Cette méthode statique, qui est du bytecode Java, est à son tour traduite
en microcode à l’aide de la table de correspondance.
Ce processeur, de part sa petite taille, peut être implanté dans un FPGA standard de type Al-
tera (ACEX 1K50, Cyclone) et Xilinx (Spartan II et Spartan-3). L’implantation du programme
Java est réalisée à l’aide de l’outil JOPizer. Ce dernier réalise la vérification du bytecode, l’édi-
tion des liens, puis la transformation en un fichier .jop. C’est ce fichier qui sera ensuite implanté
sur le FPGA cible.
- Page 50-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Sur les 201 différentes instructions bytecode d’une JVM « classique », JOP en implémente
43 en tant qu’instruction microcode unique, 92 en tant que suite d’instructions microcode, et 41
en Java. Les instructions restantes sont implémentées par des méthodes natives codées en C.
6.7.4 Evolution
Au mois de Juin 2009, les dernières modifications apportées à JOP dataient de Mars 2009 et
des groupes de discussions avec le développeur de JOP, sont actifs (avril 2009). Ce projet n’est
donc pas obsolète et on recense aujourd’hui 11 projets académiques et 4 projets industriels qui
l’utilisent 15 .
6.8 Jazelle
6.8.1 Historique
Jazelle est la technologie mise au point par ARM pour pouvoir exécuter directement du
bytecode Java au niveau matériel. Elle fut intégrée pour la première fois en 2002 dans le proces-
seur ARM926EJ-S. On retrouve également cette technologie dans le processeur ARM1176JZF
(utilisé notamment dans l’iPhone), ainsi que dans tous les processeurs ARM dont la référence
contient la lettre ’J’.
L’atout de Jazelle réside dans son intégration à des processeurs génériques, sachant qu’en
2002 les différents modèles ARM équipaient déjà les trois quarts des terminaux mobiles. Ja-
zelle est une solution moins onéreuse et plus simple à intégrer que les processeurs Java dédiés.
En effet, cette technologie vient directement s’intégrer au cœur des processeurs existants. Elle
ne nécessite donc pas, contrairement aux co-processeurs « indépendants », de modification ma-
térielle et ne remet pas en cause ce que le processeur était capable d’exécuter auparavant. De
plus, cette technologie peut bénéficier du « cache » du processeur dans lequel elle est intégrée
et permet de s’affranchir de l’utilisation du bus de données utilisé normalement pour l’échange
de données entre le processeur et le co-processeur.
15. http://www.jopwiki.com/Projects
- Page 51-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
6.8.2 Implémentation
Jazelle permet d’exécuter directement au niveau matériel environ 90% d’un programme Java
classique.
Les compilateurs Java produisent du bytecode « générique » qui n’est donc pas optimisé pour
une cible spécifique. La technologie Jazelle DBX ne fait de différence entre du code optimisé
ou non-optimisé, elle se contente d’exécuter les séquences de bytecode qu’on lui fournit. ARM
a donc introduit Jazelle Runtime Optimizer (JRO) qui réalise, à l’exécution, de la traduction
Java bytecode vers Java bytecode. JRO déplie (inlining) les méthodes les plus utilisées, limite
les blocages de pipeline 16 , et transforme le bytecode exécuté de façon logicielle en du bytecode
exécutable par le matériel.
La nouvelle machine virtuelle multi-tâches de Jazelle [1] permet d’exécuter plusieurs appli-
cations simultanément. Les limites du tas de chaque application peuvent être ajustées pendant
leur exécution. Lorsqu’une application a un besoin supérieur à ce qui est disponible, l’erreur
out-of-memory n’est pas immédiate. En effet, un composant appelé Application Management
System (AMS), qui gère les quotas, est informé que l’application a un besoin trop grand. L’AMS
peut alors terminer l’application ou l’informer 17 qu’elle doit réduire son utilisation mémoire.
C’est seulement si l’application continue d’essayer d’utiliser plus de mémoire que l’erreur out-
of-memory apparaît.
16. Essentiellement, un pipeline utilise l’unité arithmétique et logique (ALU) pour exécuter plusieurs instruc-
tions en même temps et en différentes étapes. Les conflits dans le pipeline se produisent quand une instruction
dans une étape de le pipeline dépend du résultat d’une autre instruction qui est devant elle dans le pipeline et qui
n’est pas encore totalement exécuté. Ces conflits causent un blocage du pipeline (« Pipeline Stall ») et une perte
de temps pendant lequel le processeur attend la résolution du conflit. Les optimisateurs peuvent ordonnancer et
réorganiser les instructions pour éviter au mieux les blocages du pipeline.
17. Le mécanisme de notification utilisé ne fait pas partie des informations publiquement disponibles.
- Page 52-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
La documentation d’ARM explique que chaque application s’exécute comme si elle était sur
sa propre machine virtuelle et en étant isolée des autres applications. Il est indiqué que ARM
utilise le concept sur les « applications multiples isolées » défini dans le CLDC HotSpotTM
Implementation 2.0, mais aucun détail sur son implémentation n’est disponible.
Jazelle est réalisée suivant la spécification CLDC relative à la plate-forme J2ME. Mais, ce
processeur peut également être utilisé pour accélérer des applications J2SE comme par exemple
lors de l’utilisation du système d’exploitation SavaJe XE.
6.8.4 Evolution
La première version de Jazelle n’était qu’un driver partiel qui améliorait les performances
des processeurs compatibles avec Jazelle DBX (Direct Bytecode eXecution).
Ensuite, les optimisations de la JVM, comme celles effectuées au niveau du garbage col-
lector ou de l’exécution des appels des méthodes, ont doublé les performances de la version 3
de Jazelle tout en diminuant l’utilisation de la mémoire (8 fois moins qu’un simple compilateur
JIT).
- Page 53-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Il existe d’autres processeurs Java qui ont existé ou existent toujours, mais, au sujet desquels,
nous disposons de très peu d’information.
18. http://www.lavacore.com/products_ip.htm
19. http://www.velocitysemi.com/
20. http://www.vivaja.com/
21. http://www.ptsc.com/
22. http://www.informatik.uni-augsburg.de/en/chairs/sik/publications/papers/2007_jtres_
uhr.html
23. http://www.ee.cityu.edu.hk/~hisc/architecture.html
24. http://shap.inf.tu-dresden.de/
- Page 54-
Année de
Fournisseur Activité Type Standard Java
de Java
lancement
aJile Ajile Systems 1999 Ac / I Processeur J2ME - CLDC - CDC
Processeur /
AU-J1000(Espresso) bytecode couvert à
Aurora VLSI 2001 I Co-processeur
100%
AU-J1100 ;AU-
Co-processeur -
J1200(DeCaf)
IM1000/IM3000 IMSYS 1999 Ac / I Processeur J2ME - CLDC
Jazelle ARM 2002 Ac / I Co-processeur J2ME - CLDC / J2SE
J2ME - CLDC
JSTAR/JA108 Nazomi - Ob / I Co-processeur bytecode couvert à
70%
Rapport d’étude sur les modèles d’exécution
F IGURE 13 – Synthèse de l’étude des processeurs Java - Légende : A Académique, I Industriel, Ac Actif, Ob Obsolète
Réf : JAVASEC_NTE_003
- Page 55-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Cette section se veut une introduction au modèle d’exécution des applets Java qui s’exé-
cutent sur des cartes à puces, communément appelées JavaCard. On s’intéresse ici à la spé-
cification JavaCard version 2, qui représente encore la majorité de la base installée, sachant
que la version 3 de la spécification n’est disponible que depuis avril 2008. L’analyse est axée
sur les principales différences entre un environnement d’exécution JavaCard et un environne-
ment d’exécution Java plus classique, tel que celui trouvé dans un environnement Java 2 Micro
Edition (J2ME), Java 2 Standard Edition (J2SE), ou Java 2 Enterprise Edition (J2EE).
Cette analyse reste valable pour JavaCard version 3 dit « Classic Edition » 25 , qui est une
mise à jour mineure de JavaCard version 2.
Le langage Java utilisé pour la programmation des applets Javacard n’est pas le langage
Java standard, mais constitue un sous ensemble strict et cohérent de celui-ci. Ceci veut dire que
l’ensemble des mots-clés et fonctionnalités du langage Java pour l’environnement JavaCard
existent dans J2SE, mais pas l’inverse. Le bytecode Java d’une application JavaCard pourrait
donc en théorie être exécuté dans un environnement J2SE, pourvu que les classes de base de
l’environnement JavaCard soient portées sur cet environnement.
Les fonctionnalités suivantes du langage Java ne sont pas supportées dans l’environnement
JavaCard :
– les types primitifs de grande taille : long, double, float ;
– les types énumérés ;
– les chaînes de caractères et le type caractère ;
– les tableaux de dimension plus grande que 1 (seuls les tableaux uni-dimensionnels
sont supportés) ;
– le chargement de classes à la volée ;
– le Security Manager ;
25. L’autre édition de JavaCard 3, la « Connected Edition » est un portage de la JVM de JavaME CLDC, à
laquelle est adjointe la bibliothèque de classe de JavaCard.
- Page 56-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Une applet commence sa vie en étant installée sur une JavaCard. L’installation provoque
l’exécution de la méthode install de l’applet, qui a alors l’occasion d’initialiser ses structures
de données internes. C’est le seul moment où l’applet est autorisée à faire de l’allocation dyna-
mique de mémoire. Ensuite, l’applet est sollicitée par l’environnement Javacard pour traiter les
requêtes (messages de type APDU) qui lui sont destinés.
La mémoire est finalement libérée, lorsque l’applet est supprimée (désinstallée) de la Java-
Card.
Une applet Java est uniquement passive. Elle reçoit une requête, calcule le résultat, puis en-
voie sa réponse. C’est la JCVM qui est responsable de routage des messages entre les différentes
applets. Une seule applet peut être en cours d’exécution à un instant t.
Le mode d’exécution sur une JavaCard se rapproche d’un mode d’exécution sur machine
virtuelle. En effet, il existe une machine virtuelle appelée JCVM, pour JavaCard Virtual Ma-
chine, permettant d’exécuter du bytecode sur la carte à puce. Il est intéressant de noter que
le bytecode exécuté par cette machine virtuelle n’est pas du bytecode Java standard, mais un
bytecode spécialement adapté pour réduire l’encombrement mémoire.
Ce bytecode Java non standard est obtenu par l’utilisation d’un convertisseur à partir d’un
fichier de bytecode standard. La conversion résulte en la génération d’un fichier CAP, pour
Converter APplet. C’est ce format de fichiers qui est interprété par la JCVM.
- Page 57-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
La vérification de bytecode a bien lieu et est effectuée par le convertisseur bytecode vers
CAP.
- Page 58-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Elle inclut également les mécanismes suivants qui, bien que n’étant pas à proprement parler
des mécanismes de sécurité, facilitent l’écriture d’applications correctes :
– les modificateurs de visibilité 26 ;
– le regroupement de classes dans les packages.
JavaCard apporte en sus les mécanismes suivants :
– support transactionnel et opérations atomiques ;
– le firewall d’applets, qui empêche une applet JavaCard d’aller empiéter sur les autres
applets s’exécutant sur la même carte, sauf si une applet choisit explicitement de
rendre un de ses objets accessible pour les autres applets 27 ;
– l’accès aux capacités cryptographiques de la carte à puce (chiffrement/déchiffrement
avec algorithmes symétriques ou asymétriques, générateurs de nombres aléatoires,
résumés cryptographiques...).
Il est intéressant de noter qu’une fois qu’une applet est transformée au format CAP, le res-
pect de l’intégrité de cette applet avant son chargement sur la JavaCard est primordial, car il
n’y a plus de vérification faite une fois l’applet sur la JavaCard : la JCVM ne contient pas de
vérificateur de bytecode.
La spécification JavaCard ne précise pas quel mécanisme sera utilisé pour vérifier l’intégrité
des applets, celui-ci est laissé à la discrétion de l’organisme qui diffuse les cartes.
Une fois l’applet installée, la protection de l’intégrité du bytecode transformé est assurée par
l’environnement d’exécution final (la JavaCard). L’implication en termes de sécurité est donc
moindre que dans l’environnement d’exécution natif étudié en section 5, où la problématique
est encore présente sur l’environnement de déploiement (de type PC), et qu’on peut considérer
comme moins sûr que la JavaCard.
26. Il est important de noter que même en l’absence de Security Manager, il n’est pas possible de contourner les
modificateurs de visibilité par introspection, ce mécanisme n’étant pas supporté par JavaCard
27. Ce paragraphe se voulant une introduction à JavaCard, le fonctionnement du firewall ne sera pas détaillé ici.
- Page 59-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
8 PROBLÉMATIQUE DE LA DÉCOMPILATION
La compilation d’un code source Java suivant le modèle standard produit un langage inter-
médiaire : le bytecode. Contrairement aux langages tels que C ou C++, l’approche de Java offre
un avantage considérable en termes de portabilité, puisque le bytecode généré est exécutable
par des JVM fonctionnant sur des plates-formes d’exécution natives différentes. Néanmoins,
dans le but de permettre une telle portabilité, le niveau d’abstraction du bytecode généré doit
rester suffisamment élevé. La conséquence directe de cette abstraction concerne la relative si-
milarité du code généré par rapport au code source. Comme nous le verrons plus en détails dans
la section 8.2, cette similarité a un impact direct sur la capacité de protection de Java contre la
rétro-ingénierie.
Or, un développeur peut vouloir protéger son programme Java contre cette menace de rétro-
ingénierie. Trois besoins sont généralement évoqués pour justifier la mise en place de protec-
tions face à une telle menace :
– protéger la propriété intellectuelle et/ou les secrets industriels présents dans les classes
Java ;
– protéger l’application contre la recherche de vulnérabilités. L’objectif est d’empêcher
un attaquant de trouver et d’exploiter une vulnérabilité présente dans le programme
Java ;
– enfouir une clé de chiffrement ou un mot de passe dans le bytecode.
- Page 60-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
À première vue, la compilation de code source Java vers du bytecode n’apporte pas d’opti-
misations ayant comme effet de bord de compliquer la tâche de rétro-ingénierie. Ce n’est pas
le cas, par exemple, de la compilation vers du code natif. Les principaux compilateurs, tels
que GCC, introduisent en effet des optimisations ayant pour effet de modifier l’enchaînement
des instructions (fusion, suppression ou encore réordonnancement d’instructions). Ces optimi-
sations ont pour effet de compliquer de manière importante l’opération de décompilation.
La décompilation du bytecode généré par un compilateur Java est donc un processus rela-
tivement simple, qui produit un code proche du code source original. Par conséquent, le code
généré est compréhensible par un développeur Java. Différents outils sont disponibles depuis
plusieurs années. Ils ne seront pas décrits dans ce document, mais seront simplement cités en
- Page 61-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
référence. Les plus connus sont Jad 28 , DejaVu 29 , SourceAgain 30 et Mocha 31 . De nombreux
autres produits existent et se basent le plus souvent sur Jad.
De manière générale, les produits de décompilation peuvent être classés en deux catégo-
ries (voir [14]). Il y a d’une part ceux qui présupposent que le bytecode a été produit par un
compilateur spécifique. Ces décompilateurs commencent par identifier le compilateur utilisé et
recherchent ensuite les motifs de compilation associés afin de les inverser. La prise en charge
de bytecode offusqué pose généralement problème du fait de la difficulté de trouver des motifs.
Les outils cités dans le paragraphe précédent font partie de cette catégorie. D’autre part, il y a
les décompilateurs qui ne reposent pas sur une telle hypothèse et essaient d’abord d’identifier
la structure du flot de contrôle avant de rechercher des motifs génériques. Ils sont plus aptes à
traiter du bytecode ayant subit des traitements d’optimisation ou d’offuscation. En contrepartie,
ils fournissent un code source Java qui peut diverger de manière notable avec le code source
original, bien que sémantiquement équivalent.
Voici un exemple montrant la capacité de l’outil Jad à décompiler une portion de bytecode
Java, non offusquée, issue du code source suivant :
public class UneClasse
{
public int entierClasse ;
protected String chaineClasse ;
private String chaineClassePrivate ;
28. http://www.kpdus.com/jad.html
29. http://www.isg.de/OEW/Java/
30. http://www.ahpah.com/product.html
31. http://www.brouhaha.com/~eric/software/mocha/
- Page 62-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Cet exemple simple montre à quel point le code généré est semblable au code source ori-
ginal. Les commentaires et les noms d’identifiants locaux ont été respectivement supprimés et
modifiés. La présentation du code est restée la même et apparaît donc facilement compréhen-
sible par un développeur.
- Page 63-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
sections suivantes les techniques d’offuscation qui permettent de compliquer la tâche de décom-
pilation et donc de rétro-analyse. Nous évoquerons également les limites de ces techniques.
La littérature (voir [3], [2] et [13]) identifie différentes approches en ce qui concerne les
techniques d’offuscation en Java. Ces approches se différencient par leur degré de robustesse
vis-à-vis de la menace de rétro-ingénierie et par leur difficulté de mise en œuvre. Il est ainsi
possible de regrouper les techniques d’offuscation suivant quatre catégories :
– l’offuscation de la présentation du bytecode ;
– l’offuscation des données ;
– l’offuscation du flot de contrôle ;
– l’offuscation à base de transformations préventives.
Pour chaque catégorie, les principales techniques mises en œuvre sont décrites. Les limita-
tions de ces techniques sont également présentées.
Remarque 3
Il faut noter que notre étude ne se base que sur l’offuscation de bytecode. Il existe quelques
outils d’offuscation de code source, mais ils ne seront pas présentés ici, car ils ne sont pas
liés à la problématique de protection contre la décompilation.
8.2.1.1 Renommage
La technique de renommage consiste à modifier les noms des classes, des méthodes et des
attributs de manière à ce qu’un humain ne puisse inférer facilement la signification de ces dif-
férents éléments. Il s’agit d’une technique simple qui applique la règle contraire à la bonne pra-
tique de développement qui consiste justement à attribuer des noms explicites à ces différents
éléments. La majorité des outils d’offuscation renomme ces identifiants en les réduisant à un
- Page 64-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
seul caractère. Ceci a comme effet de bord de réduire de manière potentiellement considérable
la taille du bytecode généré.
Exemple :
int monEntier ; => int a;
String maChaine ; => String a2 ;
Double monDouble ; => Double a3 ;
public void maMethode (){} => public void a (){}
Limitation Pour des raisons évidentes de compatibilité, les méthodes et attributs publics
des classes issues d’API publiques doivent être laissés inchangés. Il est donc important de ne
pas offusquer ces méthodes et attributs publics. De plus, il est nécessaire de faire attention à
ne pas renommer des objets utilisés dans le cadre d’une programmation réflexive, c’est-à-dire
reposant sur la découverte automatisée des méthodes et attributs d’une classe (l’introspection).
Les principaux outils d’offuscation proposent ainsi une fonctionnalité pour exclure certains
objets du processus d’offuscation.
Le renommage avec surcharge consiste à renommer les méthodes Java avec un même iden-
tifiant, en tirant parti du mécanisme de surcharge. Cette approche permet d’obtenir plusieurs
méthodes ayant un nom identique ; la JVM se chargeant de les discriminer en fonction de leur
signature.
Exemple :
public int methode1 (){} => public int a (){}
public int methode2 (int val ){} => public int a(int a ){}
public String methode3 ( String str ){} => public String a( String a ){}
Limitation Cette technique effectue également une forme de renommage des éléments
des classes Java. Elle est donc soumise aux mêmes limites que celles évoquées précédemment
(section 8.2.1.1). En outre, certaines JVM acceptent des surcharges « agressives » : deux attri-
buts de type différent, mais avec un même identifiant. Cependant, la plupart des compilateurs
ne supportent pas de telles surcharges (il s’agit en fait d’une déviation entre la spécification
Java et la première version de la spécification du bytecode 32 , la première interdisant ce type de
surcharge mais pas la seconde).
32. Ce problème a été identifié dans la seconde version de la spécification de la JVM. Cependant, il fait seule-
ment l’objet d’une note à la fin du document et on peut s’interroger sur la prise en compte effective de ce point par
les implémentations des JVM.
- Page 65-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Cette technique consiste à modifier et/ou supprimer les informations non nécessaires à
l’exécution du bytecode Java. Plus particulièrement, il peut s’agir de supprimer la structure
LineNumberTable, optionnelle, utilisée par les débogueurs et par la JVM dans l’affichage de
la pile d’appels des méthodes. La suppression de cette structure complique la phase de recons-
truction du code source par les décompilateurs.
Les constantes d’un programme, et en particulier les chaînes de caractères, persistent après
l’étape de compilation. Ces données apparaissent de manière claire dans le bytecode. Leur offus-
cation est donc nécessaire afin de limiter les moyens de rétro-analyse. La principale technique
employée consiste à chiffrer les chaînes littérales.
- Page 66-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Les techniques d’offuscation du flot de contrôle ont pour objectif de rendre plus difficile
la rétro-analyse statique du bytecode et, dans une certaine mesure, la rétro-analyse dynamique
(c’est-à-dire le suivi pas à pas du flot de contrôle pendant l’exécution). Les principaux méca-
nismes mis en œuvre concernent la modification des sauts conditionnels et la mise à plat de la
hiérarchie des classes.
L’analyse statique du bytecode peut être rendue plus complexe en modifiant la structure
du graphe de flot de contrôle. Pour ce faire, les meilleurs outils d’offuscation implémentent
des techniques modifiant le branchement des sauts conditionnels (switch, while, for, if,
do) ou en complexifiant les calculs conditionnels. Ces outils peuvent, par exemple, rajouter des
branches non exécutées et des tests conditionnels supplémentaires.
Limitation Une telle méthode d’offuscation peut avoir un impact potentiel sur les per-
formances de l’application. De ce fait, il est important de ne pas l’appliquer aux algorithmes
critiques en termes de performances sans évaluation a posteriori. Cet effet de bord peut avoir
une incidence sur la capacité d’une application à s’exécuter en temps contraint. En particulier,
il peut s’agir de préserver les propriétés de sécurité d’un programme, notamment la capacité à
résister aux attaques temporelles.
De plus, bien que la modification des sauts conditionnels puisse être considérée comme
efficace contre la rétro-analyse statique, cette technique se montre moins pertinente contre la
rétro-analyse dynamique. Le suivi pas à pas est simplement rendu plus long en temps.
La mise à plat de la hiérarchie des classes a pour objectif de modifier la structure relation-
nelle entre les classes et les packages. Elle met en œuvre des techniques de fusion et de décou-
page de classes afin de produire une structure « à plat ». Cette approche permet de complexifier
- Page 67-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
la compréhension du rôle des classes et des relations hiérarchiques issues des mécanismes d’hé-
ritage et de polymorphisme.
Certains outils mettent en œuvre des techniques d’offuscation visant à tromper un décom-
pilateur spécifique. Par exemple, le fonctionnement de l’outil d’offuscation HoseMocha a pour
unique objectif de rendre défaillant l’opération de décompilation effectuée par Mocha. La tech-
nique utilisée dans ce cas précis consiste à rajouter des instructions non exécutées après les
return des méthodes. Ceci n’a aucun impact lors de l’exécution de l’application, mais sera
fatale pour Mocha qui tentera de les prendre en compte.
Limitation Peu d’outils mettent en œuvre ce type d’offuscation, puisqu’il est trop dépen-
dant du comportant des décompilateurs ou d’une version spécifique d’un décompilateur. Il s’agit
d’une technique essentiellement basée sur l’exploitation d’astuces techniques.
Cette étude présente une analyse des principaux produits d’offuscation du marché, encore
maintenus ou en développement à la date de rédaction du présent document :
– Allatori 33 ;
– DashO 34 ;
– Proguard 35 ;
– RetroGuard 36 ;
33. http://www.allatori.com/features.html
34. http://www.preemptive.com/dasho-java-obfuscator.html
35. http://proguard.sourceforge.net
36. http://www.retrologic.com/retroguard-main.html
- Page 68-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
– SmokeScreen 37 ;
– Zelix Klassmaster 38 .
Le tableau suivant présente, pour chacun des outils identifiés, les types d’offuscation mis en
œuvre et, à titre d’indication, les performances mesurées.
Zelix
Retro Smoke
Produit Allatori DashO Proguard Klass-
Guard Screen
master
Offuscation Renommage x x x x x x
de
présentation Renommage avec
x x x x x
surcharge
Suppression des
informations de x x x x x x
débogage
Offuscation Chiffrement des
x x x x
des données chaînes littérales
Modification des
Offuscation sauts condition- x x x x
du flot de nels
contrôle
Mise à plat de
la hiérarchie des
classes
Transformations préventives x x
Durée d’exécution +2% +1% 0% 0% +1% +1%
Taille du bytecode +3% -35% -32% -27% -5% -24%
Complexité du bytecode +84% +108% 0% 0% +115% +230%
Méthodologie d’analyse Les performances sont présentées suivant trois aspects : la durée
d’exécution, la taille 39 et la complexité du bytecode généré. La complexité est donnée par le
nombre cyclomatique de McCabe 40 qui représente le nombre de chemins indépendants dans le
graphe de flot de contrôle du programme. Un accroissement de 10% de cette complexité (par
exemple, 10 chemins en plus) induit un temps proportionnellement plus important de rétro-
analyse statique. Les tests ayant conduit aux résultats du tableau ont été menés sur du code Java
37. http://www.leesw.com/smokescreen/index.html
38. http://www.zelix.com/klassmaster/features.html
39. On considère ici que le bytecode est vidé de ces informations de débogage avant application des produits.
40. http://en.wikipedia.org/wiki/Cyclomatic_complexity
- Page 69-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
hétérogène : algorithmes, interfaces graphiques, jar multiples. Les données du tableau corres-
pondent au ratio :
valeur après offuscation − valeur avant offuscation
× 100
valeur avant offuscation
Il pourrait être envisagé de réaliser une offuscation multiple (plusieurs passages d’un même
outil ou d’outils différents) dans l’objectif d’augmenter la robustesse du bytecode face à la rétro-
ingénierie. Cet aspect ne semble cependant pas pertinent, notamment lorsqu’il s’agit d’appli-
quer une offuscation multiple des données. En effet, la robustesse est identique quel que soit le
nombre de renommages.
En ce qui concerne les modifications sur le flot de contrôle, la complexité sera un peu plus
importante lors de la seconde passe. Des premiers tests montrent que l’apport limité de résis-
tance dû à une seconde passe n’est pas pertinent face à la menace de rétro-ingénierie. L’essentiel
de l’apport en robustesse est fourni lors de la première passe. En d’autres termes, un attaquant
capable de « casser » une première passe d’offuscation, sera suffisamment compétent pour atta-
quer du bytecode offusqué à deux reprises.
La démarche pour appliquer un programme d’offuscation sur du bytecode Java est présentée
ci-dessous. Cette démarche est relativement similaire quel que soit le programme utilisé.
- Page 70-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
Les outils du marché ont cependant quelques limitations. D’une part, l’ensemble des tech-
niques théoriques d’offuscation n’est pas disponible avec ces outils, notamment les techniques
trop complexes ou ayant un impact trop important sur les performances. De fait, le niveau d’of-
fuscation offert par ces outils s’avère moindre que le niveau de l’état de l’art. D’autre part, le
niveau de configuration des outils est relativement inégal. En effet, certains ne permettent pas
de choisir explicitement les éléments à exclure du processus d’offuscation. Il convient donc de
choisir l’outil en fonction de ses besoins en termes de finesse de configuration. Enfin, ces dif-
férents outils ne supportent pas, pour la plupart, les fichiers qui ne sont pas au format Java. Par
exemple, les fichiers XML ou les fichiers properties, utilisés lors du chargement ou déploiement
d’applications Java, et contenant des références vers les éléments de l’application, peuvent ne
pas être pris en compte lors du processus d’offuscation. Il convient, là aussi, de choisir un ou-
til capable de prendre en compte de telles situations ou, dans le cas contraire, de propager les
renommages d’éléments Java vers les fichiers XML ou fichiers properties.
- Page 71-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
- Page 72-
Rapport d’étude sur les modèles d’exécution Réf : JAVASEC_NTE_003
de Java Version : 1.2
RÉFÉRENCES
[1] Chris Porthouse(ARM) and Dave Butcher(ARM). Multitasking Java on ARM platforms,
2006.
[2] Christian Collberg and Clark Thomborson. Watermarking, Tamper-Proofing, and Obfus-
cation - A Tools for Software Protection. 2002.
[3] Christian Collberg and Clark Thomborson and Douglas Low. A Taxonomy of Obfuscating
Transformations. Technical report, The University of Auckland, New Zealand, 1997.
[4] Consortium JAVASEC. Comparatif des compilateurs. Technical Report Livrable 2.1 dans
le CCTP JAVASEC, Silicom Région Ouest - Amossys - INRIA Rennes Bretagne Atlan-
tique - SGDN, 2009.
[5] Consortium JAVASEC. Comparatif des JVM. Technical Report Livrable 2.2 dans le
CCTP JAVASEC, Silicom Région Ouest - Amossys - INRIA Rennes Bretagne Atlantique
- SGDN, 2009.
[6] Consortium JAVASEC. Rapport sur le langage Java. Technical Report Livrable 1.1 dans le
CCTP JAVASEC, Silicom Région Ouest - Amossys - INRIA Rennes Bretagne Atlantique
- SGDN, 2009.
[7] Imsys Technologies AB. IM1101C Technical Reference Manual, 2008.
[8] Imsys Technologies AB. IM3910 Microcontroller - Datasheet, 2008.
[9] J. Hummel and A. Azevedo and D. Kolson and A. Nicolau. Annotating the Java bytecodes
in support of optimization. Concurrency : Practice and Experience, 9(11), 1997.
[10] James Gosling and Bill Joy and Guy Steele and Gilad Bracha. The Java Language Speci-
fication, Third Edition. Addison-Wesley Longman, Amsterdam, 3 edition, June 2005.
[11] Martin Schoeberl. JOP : A Java Optimized Processor for Embedded Real-Time Systems.
PhD thesis, Vienna University of Technology, 2005.
[12] Martin Schoeberl and Rasmus Pedersen. WCET Analysis for a Java Processor. 2006.
[13] Micheal Batchelder and Laurie Hendren. Obfuscating Java : the most pain for the least
gain . 2006.
[14] Nomair A. Naeem and Laurie Hendren. Programmer-friendly Decompiled Java. Technical
report, The University of McGill, Canada, 2006.
[15] Tim Lindholm and Frank Yellin. Java Virtual Machine Specification. Addison-Wesley
Longman Publishing Co., Inc., Boston, MA, USA, 1999.
[16] Tom R. Halfhill. IMSYS Hedges Bets On Java. Microprocessor Report, 2000.
- Page 73-