Как это происходит
Для шифрования ядра сервера (jar) использовался потоковый алгоритм RC4, обрабатывая уже скомпилированные классы таким образом, что бы закриптованным оставалось только тело метода. Пытаясь декомпилировать класс мы получали либо неожиданное завершение самого декомпилятора, либо вот это:
Расшифровка всего этого добра осуществляется при помощи нативной библиотеки. (Да-да, кросплатформенность. Но продукт чаще всего используется на *nix системах, компилируют под win и nix, этого хватает). В класслоадере переопределяется метод defineClass, он через JNI вызывает метод defineClass в нативной библиотеке, ей передается байткод, происходит расшифровка и далее отдается готовый класс в JVM. Было несколько путей решения: анализ библиотеки и последующий ее хук, перекомпилировать Open-JDK и тащить классы с помощью нее. Я поспрашивал у гугла, нет ли еще способов вытащить уже расшифрованный байткод непосредственно из JVM. Как оказалось, есть, и в этом нам поможет замечательный класс java.lang.instrument.Instrumentation, который передается при аттаче агента имеет полезный метод retransformClasses(Class[] classes). Что он делает? А то, что мы захотим. А точнее то, что захочет имплементация интерфейса ClassFileTransformer. Вот единственный метод, который должен реализовать класс, который будет имплементировать данный интерфейс:
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException
Эврика! byte[] classfileBuffer – то, что там нужно! Это уже расшифрованный байткод класса, нам остается только сохранить его. (код приводить не буду, можно посмотреть весь проект по ссылке ниже).
Создаем объект класса трансформера и добавляем его для нашего Instrumentation в момент аттача агента:
public static void agentmain(String agentArgs, Instrumentation inst) { inst.addTransformer(new ClassFileTransformerImpl(), true); inst.retransformClasses(inst.getAllLoadedClasses()); // и отправляем нашему трансформеру на пользование }
Рычаги воздействия мы нашли, теперь нужно собрать все воедино и подключить к нашему проекту. Будем загружать агента в целевое приложение. Можно было пойти простым путем и просто указывать pid нашего приложения для аттачера, но мы же хотим красоты и изыска! Ну что же, попробуем написать программу с GUI, которая будет делать за нас все сама.
К делу!
Во-первых, мы хотим выбирать приложение из списка уже запущенных, что бы не выяснять нужный нам pid при помощи стандартного инструментария jdk.
Сначала для получения всех pid запущенных jvm я использовал метод com.sun.tools.attach.VirtualMachine.list(), который возвращал список дескрипторов запущенных jvm. Метод запиливался в бесконечный цикл в отдельный поток и обновлял наш GUI, выводя работающие JVM и id процесса. Но этот способ жутко, просто безбожно отъедал память. Да и нелогичный способ, должен же быть еще метод, как у, скажем, профайлеров. И обратился я к стандартному профайлеру, который включен в комплект jdk, поизучал, и нашел любопытный лисенер. Использовать его оказалось гораздо практичнее, и вот что получилось.
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) {} } }
В методе vmStatusChanged реализуем обновление GUI.
Итак, цель выбрана, загружаем агент.
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 }
Что нам нужно? В идеале – получить точную копию jar. Выполняем. В GUI указываем jar, которая используется в приложении и передаем эту информацию агенту, который к тому времени уже предоставил нам свой канал связи – RMI.
Получив путь jar, ищем ее, разбираем внутренности, запоминаем ее классы. Но ведь не все классы загружены в память скажете вы! И будете совершенно правы. Реализуем свой класслоадер, который принудительно загрузит все классы из jar и отдаем их нашему трансформеру, который благополучно все сложит в кучку.
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; } }
Если попросим – заархивирует (настройки все указываются в gui, параметры передаются через rmi).
И да, для разрешения всего этого безобразия нам понадобится подправить манифест (пусть, мейн-класс нашего агента называется ClassDumperAgent):
Premain-Class: ClassDumperAgent Agent-Class: ClassDumperAgent Can-Redefine-Classes: true Can-Retransform-Classes: true
Ну, и напоследок пара скриншотов. Была добавлена возможность вытягивания не всей jar, а пакетами и еще пара приятных фишек, реализацию которых вы можете посмотреть в исходном коде.
Ссылка на исходники.
ссылка на оригинал статьи http://habrahabr.ru/post/164855/
Добавить комментарий