А знаешь как? Я всего-лишь публикую (создаю темы), а админ мне платит. Трачу деньги на мороженое, робуксы и сервера в Minecraft. А ещё на паль из Китая.
Вы используете устаревший браузер. Этот и другие сайты могут отображаться в нём некорректно. Вам необходимо обновить браузер или попробовать использовать другой.
ты считаешь в жизни не нужно делиться своим опытом с другими? считаешь этот форум или раздел мусоров? найди по лучше и выложи туда
продолжаем про тему делиться опытом с другими. один математик нашёл формулу, другой допилил, физик через неё вывел ещё формулу, и так далее мы пришли к тому, что сейчас общаемся в интернете, а в итоге что? вы выбираете сраться на форуме с пастерами?)
даже @metafaze сюда что-то, да выкладывает
ну в этом я согласен, но когда я начинал такой разжёванной информации почти не было.
ты считаешь в жизни не нужно делиться своим опытом с другими? считаешь этот форум или раздел мусоров? найди по лучше и выложи туда
продолжаем про тему делиться опытом с другими. один математик нашёл формулу, другой допилил, физик через неё вывел ещё формулу, и так далее мы пришли к тому, что сейчас общаемся в интернете, а в итоге что? вы выбираете сраться на форуме с пастерами?)
даже @metafaze сюда что-то, да выкладывает
Да чувак, у тебя даже под 17 джаву по коду пиздец-первое. Ну давай прикинем сколько лет назад ты там начинал ну допустим 3. 3 года назад на ютубе миллион гайдов по ASM(библиотеке к слову 8 год идëт, т так,что не врубаюсь ваще ты слепой газонюх чтоле спецификацию к такой старой либе не найти) было и спецификацию никто не отменял.По итогу статья которую ты высрал- наговнокоженная миллион раз до этого разжëванная инфа, где куча неточностей, обсëров и откровенной хуйни.
Посмотреть вложение 285548
почему бы и нет? Теория компиляции
Java - компилируемый язык, но что же это значит? Сначала узнаем про интерпретируемые языки. Например: когда вы в пишите в консоли python main.py питон читает ваш файл по строчкам и выполняет. Компилируемые языки же прежде чем выполнять код преобразуют (компилируют) исходный файл в более низкоуровневое представление (байткод). После компиляции для каждого файла создаётся структура класса в которую и помещается этот код. И далее эти классы уже выполняет непосредственно джава машина (jvm). То есть по сути джава машина и есть интерпретатор. Байткод
Прежде чем писать обфускатор нужно научиться понимать байткод. Состоит он из
идущих по порядку. Посмотреть его можно через множество утилит (ByteEdit, Recaf, JByteMod, ThreadTear и другие) к которым мы вернёмся чуть позже, но пока, что нам хватит обычного javap -c Class, вывод для обычного System.out.println(1) будет таким:
Код:
Compiled from "Main.java"
public class Main {
public Main();
Code: // Bytecode
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code: // Bytecode
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1
4: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
7: return
}
Переменная определяется внутри метода, а Поле - внутри класса. ЭТО ВАЖНО
Так как !!поле!! System.out является статическим, чтобы его получить компилятор использует getstatic инструкцию. Далее это поле помещается на стек, за ним помещается iconst_1 - что означает константу int со значением 1. А после происходит выполнение функции println через invokevirtual с аргументами из стека и затем стек очищается. Understand?
Это конечно всё интересно, скажите вы, но, что за java/io/PrintStream.println:(I)V и java/lang/System.out:Ljava/io/PrintStream;? Помимо названия класса и member'а (out, println) указывается ещё дополнительная информация: для поля out мы говорим что её тип - Ljava/io/PrintStream;, а для метода println указываем дескриптор. В скобочках пишутся типы аргументов метода, а далее возвращаемый тип (всё в
aload, astore, areturn - Используются только для объектов. Для примитивов (long, int, ...) такие же инструкции - но с префиксом типа (то есть для int iload, для long lload, для double dload).
Для того, чтобы поместить int есть такие условия:
iconst_0, iconst_1, iconst_2, iconst_4, iconst_5 - От 0 до 5
bipush <значение> - Если значение не больше минимального значения байта (Byte.MIN_VALUE -2⁷) и не меньше максимального значения байта (Byte.MAX_VALUE 2⁷-1)
sipush <значение> - Если значение не больше минимального значения шорта (Short.MIN_VALUE) и не меньше максимального значения шорта (Short.MAX_VALUE)
Для long по другому:
- lconst_0, lconst_1 - От 0 до 1
В любых остальных случаях, а также для строк используется ldc.
Ах да, забыл рассказать про метки. Метка - это просто грубо говоря "секция", которая группирует один список инструкций от других. Зачем это нужно? Покажу на примере:
Java:
if (somebool)
System.out.println(1);
else
System.out.println(2);
В байткоде это выглядит примерно так:
Код:
1: // Метка 1
aload somebool // Помещаем somebool на стек
ifeq 2 // Если somebool == false, то перемещаемся на метку 2
// Иначе остаёмся на этой метке и продолжаем код если somebool оказался true
// Байткод для System.out.println(1);
2:
// Байткод для System.out.println(2);
Практика
Подготавливаем проект
Для модификации байткода и парсинга класс файла мы возьмём библиотеку ow2.asm (также есть bytebuddy). ow2.asm:tree содержит основные классы для удобной работы с классами и остальными компонентами (ClassNode, MethodNode, ...)
Dont stop
Продолжаем. Читаем класс:
Java:
ClassReader classReader = new ClassReader(Files.readAllBytes(new File("...").toPath()));
ClassNode classFile = new ClassNode();
classReader.accept(classFile, 0);
System.out.printf("%s extends %s\n", classFile.name, classFile.superName);
Если сделали всё правильно должно выдать: Main extends java/lang/Object
Выведем названия всех методов:
Java:
for (MethodNode method : classFile.methods)
System.out.println(method.name);
Вывод:
<init>
main
Небольшое отступление, существуют "специальные методы": <init> - Конструктор класса, <clinit> - Конструкция static {} выполняемая после загрузки класса.
Далее переберём все инструкции:
Java:
for (AbstractInsnNode instruction : method.instructions)
System.out.printf("\t%s - %d\n", instruction.getClass().getSimpleName(), instruction.getOpcode());
Теперь чтобы их фильтровать можно использовать либо опкод, либо класс инструкции. Как мы помним большинство инструкций предназначены, например, для действий с полями. В ASM их тоже отделяют:
Для всех опкодов есть свой класс Opcodes.
Давайте напишем самую тупую обфускацию - будем просто после каждого вызова (MethodInsnNode класс для инструкций invoke) помещать в переменную число 0:
Java:
if (instruction instanceof MethodInsnNode) { // Если инструкция invoke
LabelNode startNode = new LabelNode(); // Метка начала
LabelNode labelNode = new LabelNode(); // Создаём метку, она будет указана до какого момента локальная переменная будет "жить"
int index = method.localVariables.size() + 1; // Высчитываем новый индекс для переменной
method.localVariables.add(new LocalVariableNode("abobus"+method.localVariables.size(), "I", null, (LabelNode) method.instructions.getFirst(), labelNode, index)); // Добавляем переменную
// Добавляем инструкции
InsnList instructions = new InsnList();
instructions.add(startNode);
instructions.add(new InsnNode(Opcodes.ICONST_0)); // Помещаем 0 на стек
instructions.add(new VarInsnNode(Opcodes.ISTORE, index)); // Сохраняем значение 0 в переменную
instructions.add(endNode);
method.instructions.insert(instruction, instructions); // Вставляем наши новые инструкции после текущей (insertBefore чтобы вставить до)
}
Сохраняем класс файл:
Java:
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classFile.accept(classWriter);
Files.write(new File("...").toPath(), classWriter.toByteArray());
Флаг COMPUTE_MAXS автоматически пересчитывает максимальный размер для стека и переменных. Это необходимо из-за того, что мы добавили новую переменную. Можно делать это и вручную - через method.maxLocals++
Запускаем:
Код:
java -cp . Main
1
Работает, теперь посмотрим, что нам покажет декомпилятор: Посмотреть вложение 285551 Двигаемся дальше
Первая обфускация у нас уже есть, осталось сделать что-нибудь по сложнее. Чистим код
Почистим немного код. Я создам интерфейс трансформера:
Метод через AES PKCS
Base64 это всё равно легко, а вот AES шифрование уже лучше. Джава из коробки поддерживает шифрование по ключу:
Java:
SecretKeySpec spec = new SecretKeySpec("aaaaaaaaaaaaaaaa".getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
String encrypted = Base64.getEncoder().encodeToString(cipher.doFinal("text22".getBytes()));
System.out.println(encrypted);
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
String decrypted = new String(cipher.doFinal(Base64.getDecoder().decode(encrypted)));
System.out.println(decrypted);
aaaaaaaaaaaaaaaa - ключ. Он обязательно должен быть 16, 24 или 32 байта в зависимости от кодировки. Чтобы немножко поприкалываться можно использовать символы, которые клеятся друг на друга (
Теперь есть проблема - если часто использовать эту строку (например: в читах в каждом кадре дешифровать по сотни строк), то он не оптимизирован. Поэтому мы создадим HashMap куда поместим хэш (рандом число для каждой строки) и дешифрованную строку. Пример как это будет выглядеть:
Java:
public class Main {
private static final Map<Long, String> hashes;
public static void main(String[] args) {
System.out.println(hashes.get(1337));
}
static {
hashes = new HashMap<>();
// метод дешифровки
hashes.put(1337, decrypted);
}
}
Также этим способом избавимся от -noverify. Кстати о -noverify, jvm проверяет инструкции каждого метода и если творить прям лютую дичь то он может дать невалид и -noverify флаг скипает эту проверку.
Реализуем (много кода, выложил на pastebin) -
Для прикола над декомпиляторами можно указать пробел в виде названия переменной или поля.
Смотрим:
Без -noverify!
Recaf с CFR декомпилятором:
Java decompiler GUI (jd-gui):
Flow
Строки это конечно интересно, но сильно они нас конечно не запутывают. Задача flow - насрать в код или изменить его настолько, чтобы оригинальный код отыскать было не так легко. Например: перед вызовом метода можно добавить switch с кучей рандомных значений и кейсов, где лишь одно правильное. Такая реализация есть в Bozar obfuscator:
Посмотреть вложение 285548
почему бы и нет? Теория компиляции
Java - компилируемый язык, но что же это значит? Сначала узнаем про интерпретируемые языки. Например: когда вы в пишите в консоли python main.py питон читает ваш файл по строчкам и выполняет. Компилируемые языки же прежде чем выполнять код преобразуют (компилируют) исходный файл в более низкоуровневое представление (байткод). После компиляции для каждого файла создаётся структура класса в которую и помещается этот код. И далее эти классы уже выполняет непосредственно джава машина (jvm). То есть по сути джава машина и есть интерпретатор. Байткод
Прежде чем писать обфускатор нужно научиться понимать байткод. Состоит он из
идущих по порядку. Посмотреть его можно через множество утилит (ByteEdit, Recaf, JByteMod, ThreadTear и другие) к которым мы вернёмся чуть позже, но пока, что нам хватит обычного javap -c Class, вывод для обычного System.out.println(1) будет таким:
Код:
Compiled from "Main.java"
public class Main {
public Main();
Code: // Bytecode
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code: // Bytecode
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1
4: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
7: return
}
Переменная определяется внутри метода, а Поле - внутри класса. ЭТО ВАЖНО
Так как !!поле!! System.out является статическим, чтобы его получить компилятор использует getstatic инструкцию. Далее это поле помещается на стек, за ним помещается iconst_1 - что означает константу int со значением 1. А после происходит выполнение функции println через invokevirtual с аргументами из стека и затем стек очищается. Understand?
Это конечно всё интересно, скажите вы, но, что за java/io/PrintStream.println:(I)V и java/lang/System.out:Ljava/io/PrintStream;? Помимо названия класса и member'а (out, println) указывается ещё дополнительная информация: для поля out мы говорим что её тип - Ljava/io/PrintStream;, а для метода println указываем дескриптор. В скобочках пишутся типы аргументов метода, а далее возвращаемый тип (всё в
aload, astore, areturn - Используются только для объектов. Для примитивов (long, int, ...) такие же инструкции - но с префиксом типа (то есть для int iload, для long lload, для double dload).
Для того, чтобы поместить int есть такие условия:
iconst_0, iconst_1, iconst_2, iconst_4, iconst_5 - От 0 до 5
bipush <значение> - Если значение не больше минимального значения байта (Byte.MIN_VALUE -2⁷) и не меньше максимального значения байта (Byte.MAX_VALUE 2⁷-1)
sipush <значение> - Если значение не больше минимального значения шорта (Short.MIN_VALUE) и не меньше максимального значения шорта (Short.MAX_VALUE)
Для long по другому:
- lconst_0, lconst_1 - От 0 до 1
В любых остальных случаях, а также для строк используется ldc.
Ах да, забыл рассказать про метки. Метка - это просто грубо говоря "секция", которая группирует один список инструкций от других. Зачем это нужно? Покажу на примере:
Java:
if (somebool)
System.out.println(1);
else
System.out.println(2);
В байткоде это выглядит примерно так:
Код:
1: // Метка 1
aload somebool // Помещаем somebool на стек
ifeq 2 // Если somebool == false, то перемещаемся на метку 2
// Иначе остаёмся на этой метке и продолжаем код если somebool оказался true
// Байткод для System.out.println(1);
2:
// Байткод для System.out.println(2);
Практика
Подготавливаем проект
Для модификации байткода и парсинга класс файла мы возьмём библиотеку ow2.asm (также есть bytebuddy). ow2.asm:tree содержит основные классы для удобной работы с классами и остальными компонентами (ClassNode, MethodNode, ...)
Dont stop
Продолжаем. Читаем класс:
Java:
ClassReader classReader = new ClassReader(Files.readAllBytes(new File("...").toPath()));
ClassNode classFile = new ClassNode();
classReader.accept(classFile, 0);
System.out.printf("%s extends %s\n", classFile.name, classFile.superName);
Если сделали всё правильно должно выдать: Main extends java/lang/Object
Выведем названия всех методов:
Java:
for (MethodNode method : classFile.methods)
System.out.println(method.name);
Вывод:
<init>
main
Небольшое отступление, существуют "специальные методы": <init> - Конструктор класса, <clinit> - Конструкция static {} выполняемая после загрузки класса.
Далее переберём все инструкции:
Java:
for (AbstractInsnNode instruction : method.instructions)
System.out.printf("\t%s - %d\n", instruction.getClass().getSimpleName(), instruction.getOpcode());
Теперь чтобы их фильтровать можно использовать либо опкод, либо класс инструкции. Как мы помним большинство инструкций предназначены, например, для действий с полями. В ASM их тоже отделяют:
Для всех опкодов есть свой класс Opcodes.
Давайте напишем самую тупую обфускацию - будем просто после каждого вызова (MethodInsnNode класс для инструкций invoke) помещать в переменную число 0:
Java:
if (instruction instanceof MethodInsnNode) { // Если инструкция invoke
LabelNode startNode = new LabelNode(); // Метка начала
LabelNode labelNode = new LabelNode(); // Создаём метку, она будет указана до какого момента локальная переменная будет "жить"
int index = method.localVariables.size() + 1; // Высчитываем новый индекс для переменной
method.localVariables.add(new LocalVariableNode("abobus"+method.localVariables.size(), "I", null, (LabelNode) method.instructions.getFirst(), labelNode, index)); // Добавляем переменную
// Добавляем инструкции
InsnList instructions = new InsnList();
instructions.add(startNode);
instructions.add(new InsnNode(Opcodes.ICONST_0)); // Помещаем 0 на стек
instructions.add(new VarInsnNode(Opcodes.ISTORE, index)); // Сохраняем значение 0 в переменную
instructions.add(endNode);
method.instructions.insert(instruction, instructions); // Вставляем наши новые инструкции после текущей (insertBefore чтобы вставить до)
}
Сохраняем класс файл:
Java:
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classFile.accept(classWriter);
Files.write(new File("...").toPath(), classWriter.toByteArray());
Флаг COMPUTE_MAXS автоматически пересчитывает максимальный размер для стека и переменных. Это необходимо из-за того, что мы добавили новую переменную. Можно делать это и вручную - через method.maxLocals++
Запускаем:
Код:
java -cp . Main
1
Работает, теперь посмотрим, что нам покажет декомпилятор: Посмотреть вложение 285551 Двигаемся дальше
Первая обфускация у нас уже есть, осталось сделать что-нибудь по сложнее. Чистим код
Почистим немного код. Я создам интерфейс трансформера:
Метод через AES PKCS
Base64 это всё равно легко, а вот AES шифрование уже лучше. Джава из коробки поддерживает шифрование по ключу:
Java:
SecretKeySpec spec = new SecretKeySpec("aaaaaaaaaaaaaaaa".getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
String encrypted = Base64.getEncoder().encodeToString(cipher.doFinal("text22".getBytes()));
System.out.println(encrypted);
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
String decrypted = new String(cipher.doFinal(Base64.getDecoder().decode(encrypted)));
System.out.println(decrypted);
aaaaaaaaaaaaaaaa - ключ. Он обязательно должен быть 16, 24 или 32 байта в зависимости от кодировки. Чтобы немножко поприкалываться можно использовать символы, которые клеятся друг на друга (
Теперь есть проблема - если часто использовать эту строку (например: в читах в каждом кадре дешифровать по сотни строк), то он не оптимизирован. Поэтому мы создадим HashMap куда поместим хэш (рандом число для каждой строки) и дешифрованную строку. Пример как это будет выглядеть:
Java:
public class Main {
private static final Map<Long, String> hashes;
public static void main(String[] args) {
System.out.println(hashes.get(1337));
}
static {
hashes = new HashMap<>();
// метод дешифровки
hashes.put(1337, decrypted);
}
}
Также этим способом избавимся от -noverify. Кстати о -noverify, jvm проверяет инструкции каждого метода и если творить прям лютую дичь то он может дать невалид и -noverify флаг скипает эту проверку.
Реализуем (много кода, выложил на pastebin) -
Для прикола над декомпиляторами можно указать пробел в виде названия переменной или поля.
Смотрим:
Без -noverify!
Recaf с CFR декомпилятором:
Java decompiler GUI (jd-gui):
Flow
Строки это конечно интересно, но сильно они нас конечно не запутывают. Задача flow - насрать в код или изменить его настолько, чтобы оригинальный код отыскать было не так легко. Например: перед вызовом метода можно добавить switch с кучей рандомных значений и кейсов, где лишь одно правильное. Такая реализация есть в Bozar obfuscator:
Посмотреть вложение 285548
почему бы и нет? Теория компиляции
Java - компилируемый язык, но что же это значит? Сначала узнаем про интерпретируемые языки. Например: когда вы в пишите в консоли python main.py питон читает ваш файл по строчкам и выполняет. Компилируемые языки же прежде чем выполнять код преобразуют (компилируют) исходный файл в более низкоуровневое представление (байткод). После компиляции для каждого файла создаётся структура класса в которую и помещается этот код. И далее эти классы уже выполняет непосредственно джава машина (jvm). То есть по сути джава машина и есть интерпретатор. Байткод
Прежде чем писать обфускатор нужно научиться понимать байткод. Состоит он из
идущих по порядку. Посмотреть его можно через множество утилит (ByteEdit, Recaf, JByteMod, ThreadTear и другие) к которым мы вернёмся чуть позже, но пока, что нам хватит обычного javap -c Class, вывод для обычного System.out.println(1) будет таким:
Код:
Compiled from "Main.java"
public class Main {
public Main();
Code: // Bytecode
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code: // Bytecode
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1
4: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
7: return
}
Переменная определяется внутри метода, а Поле - внутри класса. ЭТО ВАЖНО
Так как !!поле!! System.out является статическим, чтобы его получить компилятор использует getstatic инструкцию. Далее это поле помещается на стек, за ним помещается iconst_1 - что означает константу int со значением 1. А после происходит выполнение функции println через invokevirtual с аргументами из стека и затем стек очищается. Understand?
Это конечно всё интересно, скажите вы, но, что за java/io/PrintStream.println:(I)V и java/lang/System.out:Ljava/io/PrintStream;? Помимо названия класса и member'а (out, println) указывается ещё дополнительная информация: для поля out мы говорим что её тип - Ljava/io/PrintStream;, а для метода println указываем дескриптор. В скобочках пишутся типы аргументов метода, а далее возвращаемый тип (всё в
aload, astore, areturn - Используются только для объектов. Для примитивов (long, int, ...) такие же инструкции - но с префиксом типа (то есть для int iload, для long lload, для double dload).
Для того, чтобы поместить int есть такие условия:
iconst_0, iconst_1, iconst_2, iconst_4, iconst_5 - От 0 до 5
bipush <значение> - Если значение не больше минимального значения байта (Byte.MIN_VALUE -2⁷) и не меньше максимального значения байта (Byte.MAX_VALUE 2⁷-1)
sipush <значение> - Если значение не больше минимального значения шорта (Short.MIN_VALUE) и не меньше максимального значения шорта (Short.MAX_VALUE)
Для long по другому:
- lconst_0, lconst_1 - От 0 до 1
В любых остальных случаях, а также для строк используется ldc.
Ах да, забыл рассказать про метки. Метка - это просто грубо говоря "секция", которая группирует один список инструкций от других. Зачем это нужно? Покажу на примере:
Java:
if (somebool)
System.out.println(1);
else
System.out.println(2);
В байткоде это выглядит примерно так:
Код:
1: // Метка 1
aload somebool // Помещаем somebool на стек
ifeq 2 // Если somebool == false, то перемещаемся на метку 2
// Иначе остаёмся на этой метке и продолжаем код если somebool оказался true
// Байткод для System.out.println(1);
2:
// Байткод для System.out.println(2);
Практика
Подготавливаем проект
Для модификации байткода и парсинга класс файла мы возьмём библиотеку ow2.asm (также есть bytebuddy). ow2.asm:tree содержит основные классы для удобной работы с классами и остальными компонентами (ClassNode, MethodNode, ...)
Dont stop
Продолжаем. Читаем класс:
Java:
ClassReader classReader = new ClassReader(Files.readAllBytes(new File("...").toPath()));
ClassNode classFile = new ClassNode();
classReader.accept(classFile, 0);
System.out.printf("%s extends %s\n", classFile.name, classFile.superName);
Если сделали всё правильно должно выдать: Main extends java/lang/Object
Выведем названия всех методов:
Java:
for (MethodNode method : classFile.methods)
System.out.println(method.name);
Вывод:
<init>
main
Небольшое отступление, существуют "специальные методы": <init> - Конструктор класса, <clinit> - Конструкция static {} выполняемая после загрузки класса.
Далее переберём все инструкции:
Java:
for (AbstractInsnNode instruction : method.instructions)
System.out.printf("\t%s - %d\n", instruction.getClass().getSimpleName(), instruction.getOpcode());
Теперь чтобы их фильтровать можно использовать либо опкод, либо класс инструкции. Как мы помним большинство инструкций предназначены, например, для действий с полями. В ASM их тоже отделяют:
Для всех опкодов есть свой класс Opcodes.
Давайте напишем самую тупую обфускацию - будем просто после каждого вызова (MethodInsnNode класс для инструкций invoke) помещать в переменную число 0:
Java:
if (instruction instanceof MethodInsnNode) { // Если инструкция invoke
LabelNode startNode = new LabelNode(); // Метка начала
LabelNode labelNode = new LabelNode(); // Создаём метку, она будет указана до какого момента локальная переменная будет "жить"
int index = method.localVariables.size() + 1; // Высчитываем новый индекс для переменной
method.localVariables.add(new LocalVariableNode("abobus"+method.localVariables.size(), "I", null, (LabelNode) method.instructions.getFirst(), labelNode, index)); // Добавляем переменную
// Добавляем инструкции
InsnList instructions = new InsnList();
instructions.add(startNode);
instructions.add(new InsnNode(Opcodes.ICONST_0)); // Помещаем 0 на стек
instructions.add(new VarInsnNode(Opcodes.ISTORE, index)); // Сохраняем значение 0 в переменную
instructions.add(endNode);
method.instructions.insert(instruction, instructions); // Вставляем наши новые инструкции после текущей (insertBefore чтобы вставить до)
}
Сохраняем класс файл:
Java:
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classFile.accept(classWriter);
Files.write(new File("...").toPath(), classWriter.toByteArray());
Флаг COMPUTE_MAXS автоматически пересчитывает максимальный размер для стека и переменных. Это необходимо из-за того, что мы добавили новую переменную. Можно делать это и вручную - через method.maxLocals++
Запускаем:
Код:
java -cp . Main
1
Работает, теперь посмотрим, что нам покажет декомпилятор: Посмотреть вложение 285551 Двигаемся дальше
Первая обфускация у нас уже есть, осталось сделать что-нибудь по сложнее. Чистим код
Почистим немного код. Я создам интерфейс трансформера:
Метод через AES PKCS
Base64 это всё равно легко, а вот AES шифрование уже лучше. Джава из коробки поддерживает шифрование по ключу:
Java:
SecretKeySpec spec = new SecretKeySpec("aaaaaaaaaaaaaaaa".getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
String encrypted = Base64.getEncoder().encodeToString(cipher.doFinal("text22".getBytes()));
System.out.println(encrypted);
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
String decrypted = new String(cipher.doFinal(Base64.getDecoder().decode(encrypted)));
System.out.println(decrypted);
aaaaaaaaaaaaaaaa - ключ. Он обязательно должен быть 16, 24 или 32 байта в зависимости от кодировки. Чтобы немножко поприкалываться можно использовать символы, которые клеятся друг на друга (
Теперь есть проблема - если часто использовать эту строку (например: в читах в каждом кадре дешифровать по сотни строк), то он не оптимизирован. Поэтому мы создадим HashMap куда поместим хэш (рандом число для каждой строки) и дешифрованную строку. Пример как это будет выглядеть:
Java:
public class Main {
private static final Map<Long, String> hashes;
public static void main(String[] args) {
System.out.println(hashes.get(1337));
}
static {
hashes = new HashMap<>();
// метод дешифровки
hashes.put(1337, decrypted);
}
}
Также этим способом избавимся от -noverify. Кстати о -noverify, jvm проверяет инструкции каждого метода и если творить прям лютую дичь то он может дать невалид и -noverify флаг скипает эту проверку.
Реализуем (много кода, выложил на pastebin) -
Для прикола над декомпиляторами можно указать пробел в виде названия переменной или поля.
Смотрим:
Без -noverify!
Recaf с CFR декомпилятором:
Java decompiler GUI (jd-gui):
Flow
Строки это конечно интересно, но сильно они нас конечно не запутывают. Задача flow - насрать в код или изменить его настолько, чтобы оригинальный код отыскать было не так легко. Например: перед вызовом метода можно добавить switch с кучей рандомных значений и кейсов, где лишь одно правильное. Такая реализация есть в Bozar obfuscator: