Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

indyとASMを使ってコードを難読化できるんだって

そういえばJavaOne 2015で「Protecting Java Bytecode from Hackers with the InvokeDynamic Instruction 」というセッションに出ておもしろかったことを思い出しました。

そこでデモ用のものが紹介されていたので、今更ながら試してみました。

github.com

これは、クラスファイルを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の改善でパフォーマンスが向上する余地もありそうです。