This is the article on the 17th day of "Java Advent Calendar 2016".
Some say
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.
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();
}
}
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();
}
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;
}
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);
}
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.
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();
}
}
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