Шифруются? Вытаскиваем байткод из JVM

в 10:11, , рубрики: bytecode, java, метки: , ,

Привет. Я пишу на языке Java, занимаюсь, преимущественно, работой с серверами MMORPG игр. Серверные «сборки» выпускаются многими командами, работающими в этой сфере. Некоторые платно, а некоторые и бесплатно распространяют свои творения. С некоторых пор, стала популярна практика помимо систем лицензирования шифровать свой продукт. И, как водится, с этого все и началось, мною овладел интерес (чисто академический), как же это можно обойти.

Как это происходит

Для шифрования ядра сервера (jar) использовался потоковый алгоритм RC4, обрабатывая уже скомпилированные классы таким образом, что бы закриптованным оставалось только тело метода. Пытаясь декомпилировать класс мы получали либо неожиданное завершение самого декомпилятора, либо вот это:

image

Расшифровка всего этого добра осуществляется при помощи нативной библиотеки. (Да-да, кросплатформенность. Но продукт чаще всего используется на *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, а пакетами и еще пара приятных фишек, реализацию которых вы можете посмотреть в исходном коде.

image
image

Ссылка на исходники.

Автор: IOException

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js