[JAVA] A story about trying hard to decompile JAR files

This is the article on the 17th day of "Java Advent Calendar 2016".

Introduction

The beginning of things

Some say

I thought like this

It should be started from the loader that encrypts the Java class file, decrypts it, and dynamically loads it. Since the class file itself is encrypted, you should not be able to display the code just by putting it in the decompiler obediently.

I tried something like this

Generate a JAR file containing encrypted class files

class file encryption

Appropriately like this

private Key key;// KeyGenerator#generateKey()Appropriately

void encrypt(File file) {
    byte[] inByte = null;
    try {
        inByte = FileUtils.readFileToByteArray(file);

        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] encrypted = cipher.doFinal(inByte);

        FileUtils.writeByteArrayToFile(file, encrypted);
        decryptTest(file, key, inByte);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (NoSuchPaddingException e) {
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        e.printStackTrace();
    } catch (BadPaddingException e) {
        e.printStackTrace();
    } catch (IllegalBlockSizeException e) {
        e.printStackTrace();
    }
}

Rewriting mainClass

Spigot reads the main class of the plugin from the config file (plugin.yml) and rewrites it to that of the loader class

Yaml yaml = new Yaml();
try {
        String str = FileUtils.readFileToString(new File(appTmpDir, "plugin.yml"), "utf-8");
        Map map = yaml.loadAs(str, Map.class);
        map.put("main", getPackageName(getMainClass()) + ".PluginLoader");
        str = yaml.dumpAsMap(map);
        FileUtils.writeStringToFile(new File(appTmpDir, "plugin.yml"), str, "utf-8");
    } catch (IOException e) {
        e.printStackTrace();
}

Decryption & dynamic loading of class files

Decryption

From file to byte string

private byte[] read(InputStream inputStream) {
    byte[] buf = new byte[1024];
    int len;
    BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

    try {
        while ((len = bufferedInputStream.read(buf)) > 0) {
            byteArrayOutputStream.write(buf, 0, len);
        }
        inputStream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return byteArrayOutputStream.toByteArray();
}

Decryption (Key is read by ObjectInputStream)

private byte[] decrypt(byte[] bytes, Key key) {
    byte[] inByte = null;
    try {

        if (key == null) throw new IllegalArgumentException("Key is null.");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, key);
        inByte = cipher.doFinal(bytes);

    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (NoSuchPaddingException e) {
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        e.printStackTrace();
    } catch (BadPaddingException e) {
        e.printStackTrace();
    } catch (IllegalBlockSizeException e) {
        e.printStackTrace();
    }
    return inByte;
}

Dynamic loading

Read a byte string nicely using reflection ClassLoader's defineClass0 method is important

private void loadClass(byte[] bytes, String name) throws ClassFormatError {
    try {
        String packageName = name.replaceAll("/", ".").substring(0, name.length() - 6);
        Method define0Method = ClassLoader.class.getDeclaredMethod("defineClass0", new Class[]{String.class, byte[].class, int.class, int.class, ProtectionDomain.class});
        define0Method.setAccessible(true);
        Class loadedClass = (Class) define0Method.invoke(getClassLoader(), packageName, bytes, 0, bytes.length, null);
        if (packageName.equals(mainClassName)) {
            this.mainClass = loadedClass;
        }
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        Throwable cause = e.getCause();
        if (cause instanceof ClassFormatError) {
            throw (ClassFormatError) cause;
        }
    }
    stub.add(name);
}

This happens

Since each plugin is managed in the server by package name + class name, if there are two or more difficult-to-decompile plugins due to conflict between loader package name and class name, the second and subsequent plugins will be loaded. Fail. There is no problem when distributing as a stand-alone Java program.

solution

The JAR difficult decompile program (provisional) has the source code of the loader in the main body, and the package name is dynamically changed so that it is compiled each time. The argument of getTask was decided by trial and error, and it should be working normally.

private void addLoader(String packageName, File target, File bukkitJar) {
    try {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        String pluginLoaderJava = FileUtils.readFileToString(new File(ClassLoader.getSystemResource("PluginLoader.java").getFile()), "utf-8");
        JavaFileObject file = new JavaSourceFromString("PluginLoader", pluginLoaderJava.replace("{{package}}", "package " + packageName + ";"));

        String[] compileOptions = new String[]{"-d", target.getAbsolutePath(), "-classpath", bukkitJar.getAbsolutePath()};
        Iterable<String> compilationOption = Arrays.asList(compileOptions);
        Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(file);

        JavaCompiler.CompilationTask task = compiler.getTask(
                null,
                null,
                null,
                compilationOption,
                null,
                compilationUnits);

        task.call();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

What I don't like

The point that the JDK is required to compile the loader at the time of decompilation. (Is it possible to do without JDK by directly modifying the class file?) Even though big companies are open source, what about hiding the source code in the first place?

Recommended Posts

A story about trying hard to decompile JAR files
A story about trying to operate JAVA File
A story about trying to get along with Mockito
Add files to jar files
Introduction to JAR files
A story about reducing memory consumption to 1/100 with find_in_batches
A story about PKIX path building failed when trying to deploy to tomcat with Jenkins
A story that I had a hard time trying to build PHP 7.4 on GCE's CentOS 8
A story about misunderstanding how to use java scanner (memo)
A funny story stuck in a mess when trying to import fx-clj
How to decompile java class files
A story about making a calculator to calculate the shell mound rate
[Rails] I learned about migration files! (Adding a column to the table)
A story about changing jobs from a Christian minister (apprentice) to a web engineer
A story about converting character codes from UTF-8 to Shift-jis in Ruby
A story about sending a pull request to MinGW to update the libgr version
A story addicted to JDBC Template placeholders
A note about adding Junit 4 to Android Studio
A story addicted to EntityNotFoundException of getOne of JpaRepository
A story about Java 11 support for Web services
A story about the JDK in the Java 11 era
A story that took time to establish a connection
The story that docker had a hard time
A story about a very useful Ruby Struct class
A story about Apache Wicket and atomic design
A story about making a Builder that inherits the Builder
A story about a new engineer reading a passion programmer
A story about creating a service that proposes improvements to a website using a machine learning API
A story that did not work when trying to handle events in Notification Center