mar.
2010
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.
package net.thecodersbreakfast.quiz35 public class OuterClass { private String message = "Hello World"; private class InnerClass { private String getMessage() { return message; } } }
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 :
public static void main(String[] args) { dumpMethods(OuterClass.class); } private static void dumpMethods(Class<?> clazz) { Method[] methods = clazz.getDeclaredMethods(); for (Method m : methods) { System.out.print(m.isSynthetic() ? "[SYNTHETIC] " : ""); ClassAnalyzer.dumpModifiers(m.getModifiers()); System.out.print(m.getName()); } } private static void dumpModifiers(int mods) { int[] modifiers = new int[] { Modifier.PUBLIC, Modifier.PROTECTED, Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL, Modifier.TRANSIENT, Modifier.ABSTRACT, Modifier.VOLATILE, Modifier.SYNCHRONIZED, Modifier.NATIVE, Modifier.STRICT, Modifier.INTERFACE }; String[] modifierNames = new String[] { "public", "protected", "private", "static", "final", "transient", "abstract", "volatile", "synchronized", "native", "strictfp", "interface", }; for (int i = 0; i < modifiers.length; i++) { if ((mods & modifiers[i]) == modifiers[i]) { System.out.print(modifierNames[i] + " "); } } }
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 :
package net.thecodersbreakfast.quiz35 public class Malware { public static void main(String[] args) { OuterClass oc = new OuterClass(); try { Method method = oc.getClass().getDeclaredMethod("access$000", OuterClass.class); System.out.println(method.invoke(null, oc)); } catch (Exception e) { e.printStackTrace(); } } }
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
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));
}
@@
évidemment j'ai été trop vite et j'ai court-circuité les règles de visibilité des champs de la classe.
En effet, l'utilisation de
setAccessible()
est prohibée, sinon c'est trop facile :)Mais à part ça, tout le reste est autorisé.
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 {
@@
A part que cette méthode s'appelle "access$000" et non "access$0", chapeau, bgiraudou !
Chez moi cette méthode s'appelle bien access$0.
Cela doit dépendre du compilateur, tu utilises la VM IBM HollyDays ?
Merci pour cette info.
É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".
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 (method != null) {
}
@@
Génial, je n'avais effectivement pas pensé à cette subtilité...
A noter que la méthode en question est static :-)