Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Java Quiz #35

Depuis une classe tierce, comment afficher la valeur du champ "message" de la classe ci-dessous ?
Il est naturellement interdit de court-circuiter les règles de visibilité des champs de la classe.

  1. package net.thecodersbreakfast.quiz35
  2. public class OuterClass {
  3.  
  4. private String message = "Hello World";
  5.  
  6. private class InnerClass {
  7. private String getMessage() {
  8. return message;
  9. }
  10. }
  11. }

Réponse : Il faut passer par l'appel d'une méthode synthétique.

Quelques explications

Notre exemple de code comprend deux classes : la classe OuterClass, et une classe InnerClass. A la compilation, le compilateur génère donc deux fichiers .class, OuterClass.class et OuterClass$InnerClass.class. Le fait que InnerClass soit une classe interne n'a aucune importance, elle est traitée par Java comme une classe "normale" - et c'est justement ce qui va poser problème.

En effet, le champ "message" d'OuterClass est déclaré "private", et ne devrait donc pas être visible depuis InnerClass. Pour rétablir l'accès à la variable, le compilateur doit donc tricher un peu, et génère une méthode dite synthétique.

Détecter la méthode synthétique

Pour le vérifier, on peut décompiler la classe OuterClass :

javap -classpath . -c -private -s OuterClass

public OuterClass();
  Signature: ()V
  Code:
   0:	aload_0
   1:	invokespecial	#2; //Method java/lang/Object."<init>":()V
   4:	aload_0
   5:	ldc	#3; //String Hello World
   7:	putfield	#1; //Field message:Ljava/lang/String;
   10:	return

static java.lang.String access$000(issue002.OuterClass);
  Signature: (Lissue002/OuterClass;)Ljava/lang/String;
  Code:
   0:	aload_0
   1:	getfield	#1; //Field message:Ljava/lang/String;
   4:	areturn

}

La méthode synthétique est visible à la ligne 13 : static java.lang.String access$000(issue002.OuterClass);. Notez que le nom peut varier selon les compilateurs, celui d'Eclipse n'utilise par exemple qu'un unique chifre dans le nom de la méthode (access$0).

On peut également utiliser la réflexion :

  1. public static void main(String[] args) {
  2. dumpMethods(OuterClass.class);
  3. }
  4.  
  5. private static void dumpMethods(Class<?> clazz) {
  6. Method[] methods = clazz.getDeclaredMethods();
  7. for (Method m : methods) {
  8. System.out.print(m.isSynthetic() ? "[SYNTHETIC] " : "");
  9. ClassAnalyzer.dumpModifiers(m.getModifiers());
  10. System.out.print(m.getName());
  11. }
  12. }
  13.  
  14. private static void dumpModifiers(int mods) {
  15. int[] modifiers = new int[] {
  16. Modifier.PUBLIC, Modifier.PROTECTED, Modifier.PRIVATE, Modifier.STATIC,
  17. Modifier.FINAL, Modifier.TRANSIENT, Modifier.ABSTRACT, Modifier.VOLATILE,
  18. Modifier.SYNCHRONIZED, Modifier.NATIVE, Modifier.STRICT, Modifier.INTERFACE };
  19. String[] modifierNames = new String[] {
  20. "public", "protected", "private", "static",
  21. "final", "transient", "abstract", "volatile",
  22. "synchronized", "native", "strictfp", "interface", };
  23. for (int i = 0; i < modifiers.length; i++) {
  24. if ((mods & modifiers[i]) == modifiers[i]) {
  25. System.out.print(modifierNames[i] + " ");
  26. }
  27. }
  28. }

On obtient alors :

[SYNTHETIC]static access$000

Comment en tirer parti

Mais revenons au quiz.
Nous voyons que la méthode générée est statique et de visibilité "packaged". Il suffit donc de se placer dans le même package que la classe cible pour pouvoir l'appeler par réflxeion, et ainsi avoir accès au champ privé d'OuterClass :

  1. package net.thecodersbreakfast.quiz35
  2. public class Malware {
  3. public static void main(String[] args) {
  4. OuterClass oc = new OuterClass();
  5. try {
  6. Method method = oc.getClass().getDeclaredMethod("access$000", OuterClass.class);
  7. System.out.println(method.invoke(null, oc));
  8. } catch (Exception e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. }

La ligne 7 affiche bien le contenu du champ privé de OuterClass :

Hello World

Ce qui est inquiétant, c'est qu'un hacker moins bien intentionné pourrait en tirer parti pour des fins moins avouables... et il n'est malheureusement pas possible de "sceller" un package pour éviter l'ajout de classes a posteriori (à part java.* et javax.*).


Commentaires

1. Le lundi 15 mars 2010, 10:22 par Pierre Templier

On peut utiliser la réflexion et la méthode setAccessible() de la classe Field

@@
public static void main(String args) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
OuterClass outerClass = new OuterClass();
Field field = outerClass.getClass().getDeclaredField("message");
field.setAccessible(true);
System.out.println(field.get(outerClass));
}
@@

2. Le lundi 15 mars 2010, 10:27 par Pierre Templier

évidemment j'ai été trop vite et j'ai court-circuité les règles de visibilité des champs de la classe.

3. Le lundi 15 mars 2010, 10:39 par Olivier Croisier

En effet, l'utilisation de setAccessible() est prohibée, sinon c'est trop facile :)
Mais à part ça, tout le reste est autorisé.

4. Le lundi 15 mars 2010, 10:42 par bgiraudou

Le champ message étant privé et la classe interne accédant à ce champ, le compilateur génère une méthode nommée access$0, il suffit donc d'invoquer cette méthode :
@@
try {

     Method m = OuterClass.class.getDeclaredMethod("access$0", OuterClass.class);
     OuterClass outerClass = new OuterClass();
     System.out.println(m.invoke(outerClass, outerClass));
   }
   catch (Exception e) {
     e.printStackTrace();
   }

@@

5. Le lundi 15 mars 2010, 16:21 par HollyDays

A part que cette méthode s'appelle "access$000" et non "access$0", chapeau, bgiraudou !

6. Le lundi 15 mars 2010, 16:24 par Olivier Croisier

Chez moi cette méthode s'appelle bien access$0.
Cela doit dépendre du compilateur, tu utilises la VM IBM HollyDays ?

7. Le lundi 15 mars 2010, 22:34 par Eric Reboisson

Merci pour cette info.

8. Le mardi 16 mars 2010, 00:17 par HollyDays

Étonnant. Ce matin, j'ai fait le test avec le javac standard du JDK. En l'occurrence, aussi bien v1.6.0_18 et v1.5.0_04 m'ont donné ce résultat.
Je viens de refaire le test sur mon propre PC avec javac v1.6.0_17 et cela me donne encore le même résultat : "access$000".

9. Le mardi 16 mars 2010, 08:41 par bgiraudou

De mon côté la méthode est bien nommée "access$0" mais j'utilise Eclipse qui a son propre compilateur je crois. A priori il n'y a aucune règle quant au nommage de ce type de méthodes, il est simplement conseillé de l'appeler "access$N" où N est un nombre décimal (source).

Pour être certain du résultat sans se baser sur le nom de la méthode il est possible d'utiliser la méthode isSynthetic() de la classe Method :
@@
Method method = null;
int i = 0;
while (method == null && i < OuterClass.class.getDeclaredMethods().length) {

   if (OuterClass.class.getDeclaredMethods()[i].isSynthetic()) {
       method = OuterClass.class.getDeclaredMethods()[i];
   }
   i++;

}

if (method != null) {

   try {
       System.out.println(method.invoke(null, new OuterClass()));
   }
   catch (Exception e) {
       e.printStackTrace();
   }

}
@@

10. Le mardi 16 mars 2010, 10:02 par Piwaï

Génial, je n'avais effectivement pas pensé à cette subtilité...

A noter que la méthode en question est static :-)

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.