
Многофункциональный набор инструментов для деобфускации Java
Threadtear — это многофункциональный инструмент для деобфускации Java. Скоро появится поддержка приложений Android (сейчас ведётся разработка конвертера Dalvik в Java).
Подходит для упрощённого анализа кода без излишнего беспокойства об обфускации.
Поддерживаются даже самые дорогие обфускаторы, такие как ZKM или Stringer. Для упрощения отладки включены другие инструменты.
Вставьте номера строк отладки, чтобы лучше понять, откуда возникают исключения, или добавьте .printStackTrace() в блоки try-catch без перекомпиляции кода.
Обратная совместимость больше не проблема, если не используются версиезависимые методы. Анализируйте поток кода в графе, чтобы лучше понимать алгоритмы.

«Исполнение» — это задача, которая выполняется и изменяет все загруженные файлы классов. Существует несколько типов исполнений — от очистки байткода до деобфускации строк. Убедитесь, что они расположены в правильном порядке. Например, исполнения очистки должны выполняться последними, но также могут помочь другим исполнениям, если выполнить их первыми. Когда будете готовы, нажмите кнопку «Run», и они выполнятся по порядку.
Используйте этот инструмент на свой страх и риск. Некоторые исполнения используют реализованные ClassLoader для выполнения кода из JAR-файла. Злоумышленник может подстроить байткод так, чтобы выполнился вредоносный код.
Затронутые исполнения используют класс me.nov.threadtear.asm.vm.VM.
В основном они используются для дешифровки строк или деобфускации ресурсов/доступа, так как гораздо проще выполнять методы дешифровки удалённо.
Threadtear делает всё возможное, чтобы защитить вас от вредоносных вызовов (произвольного выполнения кода), используя собственный SecurityManager, но гарантий нет.
Особенно с деобфускаторами для ZKM или Stringer нужно быть очень осторожным, так как рефлексия должна быть разрешена, иначе они не будут работать.
Если вы обнаружите ACE, пожалуйста, откройте issue. Я постараюсь исправить их как можно скорее.
Сначала выполните gradle build, затем gradle fatJar. В папке builds/libs должен появиться исполняемый JAR-файл. Если вы не хотите скачивать репозиторий, можете использовать последний релиз.
Вы можете легко создать собственное исполнение. Просто расширьте класс me.nov.threadtear.execution.Execution:
public class MyExecution extends Execution {
public MyExecution() {
super(ExecutionCategory.CLEANING /* категория */, "My execution" /* имя */,
"Executes something" /* описание, можно использовать HTML */);
}
/**
* Этот метод вызывается, когда пользователь нажимает кнопку Run
* @return true при успехе, false при неудаче
*/
@Override
public boolean execute(Map<String, Clazz> classes, boolean verbose) {
classes.values().stream().map(c -> c.node).forEach(c -> {
//преобразуйте классы здесь, используя tree-API ASM
});
return false;
}
}
Для загрузки ClassNodes во время выполнения используйте класс me.nov.threadtear.asm.vm.VM и реализуйте me.nov.threadtear.asm.vm.IVMReferenceHandler:
public class MyExecution extends Execution implements IVMReferenceHandler {
public MyExecution() {
super(ExecutionCategory.GENERIC, "My execution", "Loads ClassNodes at runtime");
}
@Override
public boolean execute(Map<String, Clazz> classes, boolean verbose) {
classes.values().stream().map(c -> c.node).forEach(c -> {
VM vm = VM.constructVM(this);
//преобразуйте байткод в java.lang.Class
Class<?> loadedClass = vm.loadClass(c.name.replace('/', '.'), true);
//работайте с этим классом здесь
loadedClass.getMethods()[0].invoke(...);
return true;
});
}
/**
* Будет вызван VM, когда вызывается VM.loadClass
*/
@Override
public ClassNode tryClassLoad(String name) {
//попробуйте найти загружаемый класс в открытом JAR-архиве
return classes.containsKey(name) ? classes.get(name).node : null;
}
}
Используя ConstantTracker (me.nov.threadtear.analysis.stack.ConstantTracker), вы можете анализировать методы и отслеживать не переменные значения стека.
Например, если iconst_0 помещается в стек, само значение не теряется, как в базовом анализаторе ASM, и вы можете использовать его для предсказания дальнейших действий в коде.
public class MyExecution extends Execution implements IConstantReferenceHandler {
public MyExecution() {
super(ExecutionCategory.GENERIC, "My execution", "Performs stack analysis and replaces code.");
}
@Override
public boolean execute(Map<String, Clazz> classes, boolean verbose) {
classes.values().stream().map(c -> c.node).forEach(this::analyzeAndRewrite);
return true;
}
public void analyzeAndRewrite(ClassNode cn) {
cn.methods.forEach(m -> {
// этот анализатор сохраняет известные значения стека, например, полезно для предсказания переходов
Analyzer<ConstantValue> a = new Analyzer<ConstantValue>(new ConstantTracker(this, Access.isStatic(m.access), m.maxLocals, m.desc, new Object[0]));
try {
a.analyze(cn.name, m);
} catch (AnalyzerException e) {
logger.severe("Failed stack analysis in " + cn.name + "." + m.name + ":" + e.getMessage());
return;
}
Frame<ConstantValue>[] frames = a.getFrames();
InsnList rewrittenCode = new InsnList();
Map<LabelNode, LabelNode> labels = Instructions.cloneLabels(m.instructions);
// перезапись инструкций метода
for (int i = 0; i < m.instructions.size(); i++) {
AbstractInsnNode ain = m.instructions.get(i);
Frame<ConstantValue> frame = frames[i];
// заменить / изменить инструкции и т.д.
if (frame.getStackSize() > 0) {
ConstantValue top = frame.getStack(frame.getStackSize() - 1);
if (top.isKnown() && top.isInteger()) {
int knownTopStackValue = top.getInteger();
// используйте известное значение стека для удаления переходов, упрощения кода и т.д.
// if(...) { rewrittenCode.add(...); }
continue;
}
}
rewrittenCode.add(ain.clone(labels));
}
// обновить инструкции и исправить блоки try-catch, локальные переменные и т.д.
Instructions.updateInstructions(m, labels, rewrittenCode);
});
}
/**
* Используйте этот метод для предсказания значений стека, если загружаются поля
*/
@Override
public Object getFieldValueOrNull(BasicValue v, String owner, String name, String desc) {
return null;
}
/**
* Используйте этот метод для предсказания значений стека, если вызываются методы на известных объектах
*/
@Override
public Object getMethodReturnOrNull(BasicValue v, String owner, String name, String desc, List<? extends ConstantValue> values) {
if (name.equals("toCharArray") && owner.equals("java/lang/String")) {
if (!values.get(0).isKnown()) {
// цель вызова неизвестна, мы не можем вычислить возвращаемое значение
return null;
}
return ((String) values.get(0).getValue()).toCharArray();
}
return null;
}
}
Не забудьте добавить ваше исполнение в дерево в классе me.nov.threadtear.execution.ExecutionLink!
Есть несколько приёмов, которые помогут успешно идентифицировать и деобфусцировать JAR-файлы. Перед запуском исполнений декомпилируйте код, чтобы выяснить, что нужно использовать. Для этого можно использовать встроенный декомпилятор.
Лучший порядок для деобфускации: «generic executions» > «access deobfuscation» > «string deobfuscation» > «cleaning executions».
Обфускаторы проявляют определённые шаблоны, которые можно использовать для их идентификации. Самый простой способ идентифицировать обфускатор — просмотреть файл META-INF/MANIFEST.MF.
Возможно, там будет атрибут Obfuscated-By: XXX или Protected-By: XXX.
Чрезвычайно обфусцированный код (особенно поток выполнения), часто заметен по методу дешифровки строк в статическом инициализаторе, содержащем switch,
или по методам дешифровки строк с очень длинным блоком switch (около 250 вариантов).
ZKM — один из лучших (и старейших) обфускаторов для Java, и очень дорогой. Сайт у обфускатора такой же древний, как он сам.

Если ваш JAR-файл содержит особые классы с огромными алгоритмами дешифровки, используемыми для обфускации строк и доступа, это, вероятно, Stringer.
Защита неплохая, и Stringer — один из самых дорогих обфускаторов. В отличие от обычных обфускаторов, он не включает обфускацию имён.
Скорее используется как «второй слой». Вероятно, 90% людей, использующих этот обфускатор, пользуются кряком, так как он стоит дороже автомобиля.
Если ваш файл обфусцирован несколькими обфускаторами, и среди них есть Stringer, начинайте деобфускацию со Stringer, так как обфускация Stringer не может быть перезаписана.
(Из-за пользовательской подписи JAR и использования имён методов при дешифровке строк)

Имена классов вроде IiIlIlIiIl или aUx, cOn, PrX указывают на обфускацию Allatori.
Allatori очень распространён среди обфусцированных JAR-файлов, потому что предлагает бесплатную демо-версию, доступную в несколько кликов. Обфускация не слишком сложна для обратной разработки.

Paramorphism — как младший брат Stringer, выглядит похоже, но не так хорош. У него также есть интересные функции, направленные на сбой инструментов обратной разработки, которые легко удаляются.
Стойкость обфускации сравнима с Allatori.

Для других обфускаторов можно попробовать generic executions или открыть issue, и я посмотрю, что можно сделать.
Перед выбором исполнения обратите внимание на всплывающие подсказки при наведении. Они содержат краткое описание того, что делает исполнение, а также теги, которые помогут понять, как изменится поведение вашего JAR-файла.
Threadtear лицензирован под GNU General Public License 3.0
Этот инструмент потребовал огромной работы. Если я сэкономил ваше время, и вы хотите угостить меня кофе, вы можете сделать это здесь: Donate with Bitcoin
Используйте Threadtear только в законных целях. Threadtear не предназначен для взлома, а скорее для анализа вредоносного ПО.
Пожалуйста, откройте issue или напишите мне на почту, если какой-либо трансформер не работает должным образом, и приложите лог.
Обратите внимание, что выходные файлы, скорее всего, не будут запускаться. Если вы всё же хотите попробовать их запустить, используйте аргумент JVM -noverify!
Этот инструмент предназначен для использования с Java 8, но, вероятно, будет работать и на более новых версиях.