📜 ⬆️ ⬇️

Are they encrypted? We take out baytkod from JVM



Hi, Habr. I write in the Java language, I mainly work with the servers of MMORPG games. Server "assemblies" are produced by many teams working in this field. Some are paid, and some even distribute their creations for free. For some time now, the practice has become popular besides licensing systems to encrypt your product. And, as usual, it all started with this, I was interested in (purely academic), how can this be circumvented.

How does this happen


To encrypt the server core (jar), the RC4 streaming algorithm was used, processing the already compiled classes in such a way that only the method body would remain encrypted. Trying to decompile the class, we received either an unexpected completion of the decompiler itself, or this:

image
')
Decryption of all this stuff is done with the help of the native library. (Yes, cross-platform. But the product is most often used on * nix systems, compiled for win and nix, this is enough). In the classifier, the defineClass method is overridden, it calls the defineClass method in the native library via JNI, the bytecode is passed to it, the decryption takes place, and then the finished class is sent to the JVM. There were several solutions: analyzing the library and then hooking it, recompiling Open-JDK and dragging classes with it. I asked Google if there were any other ways to pull out the decoded bytecode directly from the JVM. As it turned out, there is, and this is where the wonderful java.lang.instrument.Instrumentation class will help us, which is passed when the agent’s attachment has a useful method retransformClasses (Class [] classes). What is he doing? And what we want. Or rather, what the implementation of the ClassFileTransformer interface will want. Here is the only method that should implement the class that will implement this interface:

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException 


Eureka! byte [] classfileBuffer - what you need there! This is already decrypted bytecode class, we can only save it.

 try { className = className.replace("/", File.separator); StringBuilder buf = new StringBuilder(); buf.append(dumpDir); buf.append(File.separatorChar); int index = className.lastIndexOf(File.separatorChar); if (index != -1) buf.append(className.substring(0, index)); String dir = buf.toString(); new File(dir).mkdirs(); String fileName = dumpDir + File.separator + className + ".class"; FileOutputStream fos = new FileOutputStream(fileName); fos.write(classBuf); fos.close(); } catch (Exception exp) { exp.printStackTrace(); } 


Create an object of the transformer class and add it to our Instrumentation at the time of the agent's attachment:

 public static void agentmain(String agentArgs, Instrumentation inst) { inst.addTransformer(new ClassFileTransformerImpl(), true); inst.retransformClasses(inst.getAllLoadedClasses()); //       } 


We have found the leverage, now we need to put everything together and connect to our project. We will load the agent into the target application. It was possible to go a simple way and simply indicate the pid of our application for the attacker, but we want beauty and frustration! Well, let's try to write a program with a GUI that will do everything for us.

To the point!


First, we want to select an application from the list of already running ones, so as not to figure out the pid we need using the standard jdk toolkit.

At first, to get all the pids of running jvm, I used the com.sun.tools.attach.VirtualMachine.list () method, which returned a list of running jvm handles. The method was written into an infinite loop in a separate thread and updated our GUI, displaying the running JVM and process id. But this method is terrible, just shamelessly devoured memory. Yes, and an illogical way, there must also be another method, like with, say, profilers. And I turned to the standard profiler, which is included in the jdk kit, studied it, and found a curious fox box. It was much more practical to use it, and this is what happened.

 import sun.jvmstat.monitor.MonitoredHost; import sun.jvmstat.monitor.event.HostEvent; import sun.jvmstat.monitor.event.HostListener; import sun.jvmstat.monitor.event.VmStatusChangeEvent; public class VMUpdater { public static MonitoredHost MH; public VMUpdater() { try { MH = MonitoredHost.getMonitoredHost("localhost"); MH.addHostListener(new HostListenerAction()); } catch (Exception e) {} } private class HostListenerAction implements HostListener { @Override public void vmStatusChanged(VmStatusChangeEvent vmStatusChangeEvent) {} @Override public void disconnected(HostEvent hostEvent) {} } } 


In the vmStatusChanged method, we implement a GUI update.

So, the goal is selected, we load the agent.

 private static final String PATH = getClass().getProtectionDomain().getCodeSource().getLocation().getPath(); private static VirtualMachine vm; //   pid ,    GUI public static void attach(int pid) throws Exception { vm = VirtualMachine.attach(String.valueOf(pid)); vm.loadAgent(PATH); //  jar c  (  ,  ) AttachedGUI.getInstance().draw(); // GUI } 


What do we need? Ideally, get an exact copy of the jar. We carry out. In the GUI, we specify the jar, which is used in the application and pass this information to the agent, who by then had already provided us with his own communication channel - RMI.
Having received the jar path, we are looking for it, parse the insides, memorize its classes. But not all classes are loaded into memory, you say! And you will be absolutely right. We implement our own classifier, which will forcefully load all the classes from jar and give them to our transformer, which will safely put everything in a handful.

 import java.io.BufferedInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Enumeration; import java.util.Hashtable; import java.util.jar.JarEntry; import java.util.jar.JarFile; public class JarClassLoader extends ClassLoader { private Hashtable<String, Class<?>> classes = new Hashtable<String, Class<?>>(); public JarClassLoader(ClassLoader parent) { super(parent); } @Override public synchronized Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { Class<?> result = null; result = classes.get(className); if(result == null) result = super.findSystemClass(className); if(result == null) result = super.loadClass(className); classes.put(className, result); return result; } public Hashtable<String, Class<?>> loadJar(String jarPath, String dumpPath) throws IOException, ClassNotFoundException { classes.clear(); JarFile jar = new JarFile(jarPath); Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { String name = entries.nextElement().getName(); if(name.endsWith(".class")) { String className = name; if(name.contains(".")) className = name.substring(0, name.lastIndexOf(".")).replace("/", "."); Class<?> c = loadClass(className); if(c != null) classes.put(className, c); } else if(jar.getEntry(name).isDirectory()) { name = slash2sep(name); new File(dumpPath + File.separator + name).mkdirs(); } else { FileOutputStream fos = new FileOutputStream(dumpPath + File.separator + name); BufferedInputStream bis = new BufferedInputStream(jar.getInputStream(jar.getEntry(name))); byte[] data = new byte[(int) jar.getEntry(name).getSize()]; bis.read(data); fos.write(data); bis.close(); fos.close(); } } jar.close(); return classes; } private static String slash2sep(String src) { int i; char[] chDst = new char[src.length()]; String dst; for(i = 0; i < src.length(); i++) { if(src.charAt(i) == '/') chDst[i] = File.separatorChar; else chDst[i] = src.charAt(i); } dst = new String(chDst); return dst; } } 


If we ask, it will archive (all settings are specified in gui, parameters are transferred via rmi).

And yes, to resolve all this disgrace, we need to correct the manifest (even if our agent's main class is called ClassDumperAgent):

 Premain-Class: ClassDumperAgent Agent-Class: ClassDumperAgent Can-Redefine-Classes: true Can-Retransform-Classes: true 


Well, and finally, a couple of screenshots. The ability to pull not all jar was added, but packages and a couple of nice chips, the implementation of which you can see in the source code.

image
image

Link to the source .

Source: https://habr.com/ru/post/164855/


All Articles