そういえばJavaOne 2015で「Protecting Java Bytecode from Hackers with the InvokeDynamic Instruction 」というセッションに出ておもしろかったことを思い出しました。
そこでデモ用のものが紹介されていたので、今更ながら試してみました。
これは、クラスファイルを1つ入力にして、難読化したクラスファイルを1つ出力するものです。ここでの難読化というのは、invokevirtual、invokeinterface、invokestaticといったメソッド呼び出しを難読化することに目的を絞っています。
普通のHelloWorld。
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } }
javapします。
$ javap -v HelloWorld ... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return
さきほどのGithubのプロジェクトをcloneしmvn package
するとIndyProtectorDemo-1.0.jarというJARファイルができます。これを使って$ java -jar IndyProtectorDemo-1.0.jar HelloWorld.class HelloWorld2.class
を実行します。
$ javap -v HelloWorld2 ... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #19 // String Hello World 5: invokedynamic #37, 0 // InvokeDynamic #0:"242602059":(Ljava/lang/Object;Ljava/lang/Object;)V 10: return LineNumberTable: line 3: 0 line 4: 10 } BootstrapMethods: 0: #26 invokestatic "LHelloWorld;".bootstrap$0:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object; Method arguments: #27 182 #29 java.io.PrintStream #31 println #33 (Ljava/lang/String;)V
invokevirtualがなくなってinvokedynamicを使うようになりました。BootstrapMethodsも作られています。
このライブラリの中身は、ASMを使ってバイトコードを操作しています。ASMのOpcodesを実装してブートストラップメソッドを生成しています。
public class BootstrapMethodGenerator implements Opcodes { ... }
indyへの置換はASMのMethodVisitorのサブクラスを作って処理しています。
public class MethodIndyProtector extends MethodVisitor implements Opcodes { Handle bootstrapMethodHandle = null; SecureRandom rnd = new SecureRandom(); ... @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { boolean isStatic = (opcode == Opcodes.INVOKESTATIC); String newSig = isStatic ? desc : desc.replace("(", "(Ljava/lang/Object;"); Type origReturnType = Type.getReturnType(newSig); Type[] args = Type.getArgumentTypes(newSig); for (int i = 0; i < args.length; i++) { args[i] = genericType(args[i]); } newSig = Type.getMethodDescriptor(origReturnType, args); switch (opcode) { case INVOKESTATIC: // invokestatic opcode case INVOKEVIRTUAL: // invokevirtual opcode case INVOKEINTERFACE: // invokeinterface opcode mv.visitInvokeDynamicInsn(String.valueOf(rnd.nextInt()), newSig, bootstrapMethodHandle, opcode, owner.replaceAll("/", "."), name, desc); if (origReturnType.getSort() == Type.ARRAY) { mv.visitTypeInsn(Opcodes.CHECKCAST, origReturnType.getInternalName()); } break; default: mv.visitMethodInsn(opcode, owner, name, desc, itf); } }
indyを使って、invoke*の呼び出しを取り除くことができました。これがどのように役立つかは僕には少し思いつきませんが…indyを使っているので、実行速度への影響はあまりないでしょうし、将来的なJVMの改善でパフォーマンスが向上する余地もありそうです。