Гайд Пишем свой первый обфускатор

PoC Life
Пользователь
Статус
Оффлайн
Регистрация
22 Авг 2022
Сообщения
331
Реакции[?]
47
Поинты[?]
37K
где я тебе указываю на неактуальность ASM в сравнения с Class-File Api?
статья под джаву 17, не указал.

в этом разделе форума только такие и есть
ну в этом я согласен, но когда я начинал такой разжёванной информации почти не было.

Да и чувак зачем мне какие статейки по байт коду писать для кубоголовых форумчан?
ты считаешь в жизни не нужно делиться своим опытом с другими? считаешь этот форум или раздел мусоров? найди по лучше и выложи туда

продолжаем про тему делиться опытом с другими. один математик нашёл формулу, другой допилил, физик через неё вывел ещё формулу, и так далее мы пришли к тому, что сейчас общаемся в интернете, а в итоге что? вы выбираете сраться на форуме с пастерами?)
даже metafaze сюда что-то, да выкладывает
 
Забаненный
Статус
Оффлайн
Регистрация
12 Авг 2024
Сообщения
9
Реакции[?]
0
Поинты[?]
0
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
статья под джаву 17, не указал.


ну в этом я согласен, но когда я начинал такой разжёванной информации почти не было.


ты считаешь в жизни не нужно делиться своим опытом с другими? считаешь этот форум или раздел мусоров? найди по лучше и выложи туда

продолжаем про тему делиться опытом с другими. один математик нашёл формулу, другой допилил, физик через неё вывел ещё формулу, и так далее мы пришли к тому, что сейчас общаемся в интернете, а в итоге что? вы выбираете сраться на форуме с пастерами?)
даже metafaze сюда что-то, да выкладывает
Да чувак, у тебя даже под 17 джаву по коду пиздец-первое. Ну давай прикинем сколько лет назад ты там начинал ну допустим 3. 3 года назад на ютубе миллион гайдов по ASM(библиотеке к слову 8 год идëт, т так,что не врубаюсь ваще ты слепой газонюх чтоле спецификацию к такой старой либе не найти) было и спецификацию никто не отменял.По итогу статья которую ты высрал- наговнокоженная миллион раз до этого разжëванная инфа, где куча неточностей, обсëров и откровенной хуйни.
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
23 Апр 2024
Сообщения
383
Реакции[?]
0
Поинты[?]
1K
Посмотреть вложение 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 указываем дескриптор. В скобочках пишутся типы аргументов метода, а далее возвращаемый тип (всё в
Пожалуйста, авторизуйтесь для просмотра ссылки.
).
Основные инструкции
Каждую инструкцию байткода знать не обязательно, обойдёмся и основными.
  • getfield <класс поля> <название> <дескриптор> - Получает поле
  • getstatic <класс поля> <название> <дескриптор> - Получает статическое поле
  • putfield <класс поля> <название> <дескриптор> - Помещает значение из стека в поле
  • putstatic <класс поля> <название> <дескриптор> - Помещает значение из стека в статическое поле
  • goto <метка> - Перейти на метку
  • ifeq, ifne, iflt, ifge, ifgt, ifle <метка> - Перейти на метку
    Пожалуйста, авторизуйтесь для просмотра ссылки.
  • aload <индекс> - Поместить локальную переменную на стек
  • astore <индекс> - Поместить значение из стека в локальную переменную
  • areturn - Вернуть значение из стека как результат
  • aconst_null - null
  • invokevirtual <класс метода> <название> <дескриптор> - Вызов метода из класса
  • invokestatic <класс метода> <название> <дескриптор> - Вызов статического метода из класса
  • invokespecial <класс метода> <название> <дескриптор> - Вызов метода из класса для вызовов superclass (наследованный класс)и для приватных методов
  • invokeinterface <класс метода> <название> <дескриптор> - Вызов метода из интерфейса
  • invokedynamic - Вызов метода из CallSite (это пока не затрагиваем,
    Пожалуйста, авторизуйтесь для просмотра ссылки.
    )

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 их тоже отделяют:
Код:
// instruction class - opcode
LabelNode            -  -1
LineNumberNode       -  -1
InsnNode             -  4
VarInsnNode          -  54
LabelNode            -  -1
LineNumberNode       -  -1
VarInsnNode          -  21
JumpInsnNode         -  153
LabelNode            -  -1
Для всех опкодов есть свой класс 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
Двигаемся дальше
Первая обфускация у нас уже есть, осталось сделать что-нибудь по сложнее.
Чистим код
Почистим немного код. Я создам интерфейс трансформера:
Java:
public interface Transformer {
    void transformClass(ClassNode classNode);
   
    void transformMethod(MethodNode methodNode);
}
Сервис:
Java:
public class TransformerService {

    private final List<Transformer> transformers;

    public TransformerService() {
        this.transformers = new LinkedList<>();
    }

    public void add(Transformer transformer) {
        this.transformers.add(transformer);
    }

    public void obfuscate(ClassNode classNode) {
        this.transformers.forEach(transformer -> transformer.transformClass(classNode));
        classNode.methods.forEach(methodNode -> {
            this.transformers.forEach(transformer -> transformer.transformMethod(methodNode));
        });
    }
}
Используем:
Java:
TransformerService service = new TransformerService();
service.add(...);
service.obfuscate(classFile);
Вот теперь писать чуть легче.
Обфускация строк
Создадим трансформер под строки:
Java:
public abstract class StringTransformer implements Transformer {

    abstract public InsnList generateEncrypt(AbstractInsnNode instruction, String string);

    @Override
    public void transformMethod(MethodNode methodNode) {
        for (AbstractInsnNode instruction : methodNode.instructions) {
            if (instruction instanceof LdcInsnNode ldcInsnNode && ldcInsnNode.cst instanceof String) {
                methodNode.instructions.insert(instruction, generateEncrypt(instruction, (String) ldcInsnNode.cst));
                methodNode.instructions.remove(instruction);
            }
        }
    }
}
Метод через Base64
Кстати пока писал вспомнил, что у ASM есть удобное получение дескрипторов через Type#getDescriptor
Java:
public class Base64StringTransformer extends StringTransformer {

    @Override
    public InsnList generateEncrypt(AbstractInsnNode instruction, String string) {
        InsnList instructions = new InsnList();
       
        // new String(Base64.getDecoder().decode("encoded string"))
        instructions.add(new InsnNode(Opcodes.DUP));
        instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/Base64", "getDecoder", "()Ljava/util/Base64$Decoder;"));
        instructions.add(new LdcInsnNode(Base64.getEncoder().encodeToString(string.getBytes())));
        instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B"));
        instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/String", "<init>", "([B)V"));

        return instructions;
    }
}
Смотрим:
Посмотреть вложение 285552

Метод через 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 байта в зависимости от кодировки. Чтобы немножко поприкалываться можно использовать символы, которые клеятся друг на друга (
Пожалуйста, авторизуйтесь для просмотра ссылки.
)
Реализуем:
Java:
public class AESStringTransformer extends StringTransformer {

    @Override
    public InsnList generateEncrypt(MethodNode methodNode, AbstractInsnNode instruction, String string) {
        InsnList instructions = new InsnList();

        try {
            SecretKeySpec spec = new SecretKeySpec("x̊a".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(string.getBytes()));

            LabelNode startNode = new LabelNode();
            LabelNode endNode = new LabelNode();
            int index = methodNode.localVariables.size() + 1;
            methodNode.localVariables.add(new LocalVariableNode("cipher"+index, Type.getDescriptor(Cipher.class), null, startNode, endNode, index));

            instructions.add(startNode);

            instructions.add(new LdcInsnNode("AES/CBC/PKCS5Padding"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(Cipher.class), "getInstance", "(Ljava/lang/String;)Ljavax/crypto/Cipher;"));
            instructions.add(new VarInsnNode(Opcodes.ASTORE, index));

            instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
            instructions.add(new InsnNode(Opcodes.ICONST_2)); // Cipher.DECRYPT_MODE
            instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(SecretKeySpec.class)));
            instructions.add(new InsnNode(Opcodes.DUP));
            instructions.add(new LdcInsnNode("x̊a"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "getBytes", "()[B"));
            instructions.add(new LdcInsnNode("AES"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(SecretKeySpec.class), "<init>", "([BLjava/lang/String;)V"));
            instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(IvParameterSpec.class)));
            instructions.add(new InsnNode(Opcodes.DUP));
            instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Cipher.class), "getBlockSize", "()I"));
            instructions.add(new IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_BYTE));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(IvParameterSpec.class), "<init>", "([B)V"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Cipher.class), "init", "(ILjava/security/Key;Ljava/security/spec/AlgorithmParameterSpec;)V"));

            instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(String.class)));
            instructions.add(new InsnNode(Opcodes.DUP));
            instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(Base64.class), "getDecoder", "()Ljava/util/Base64$Decoder;"));
            instructions.add(new LdcInsnNode(encrypted));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Cipher.class), "doFinal", "([B)[B"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(String.class), "<init>", "([B)V"));

            instructions.add(endNode);
        } catch (Exception exception) {
            exception.printStackTrace();
        }

        return instructions;
    }
}

Теперь есть проблема - если часто использовать эту строку (например: в читах в каждом кадре дешифровать по сотни строк), то он не оптимизирован. Поэтому мы создадим 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:
Пожалуйста, авторизуйтесь для просмотра ссылки.

Что-то серьёзное я делать не буду, просто после каждого вызова метода буду пихать if (что-нибудь, что не выполнится) throw ...;
Java:
public class FlowTransformer implements Transformer {

    @Override
    public void transformClass(ClassNode classNode) {

    }

    @Override
    public void transformMethod(MethodNode methodNode) {
        for (AbstractInsnNode instruction : methodNode.instructions) {
            if (instruction instanceof MethodInsnNode) {
                InsnList instructions = new InsnList();

                LabelNode startLabelNode = new LabelNode();
                LabelNode labelNode = new LabelNode();

                // 0*0 + 5 == 0
                instructions.add(new InsnNode(Opcodes.ICONST_0));
                instructions.add(new InsnNode(Opcodes.DUP));
                instructions.add(new InsnNode(Opcodes.IMUL));
                instructions.add(new InsnNode(Opcodes.ICONST_5));
                instructions.add(new InsnNode(Opcodes.IADD));
                instructions.add(new JumpInsnNode(Opcodes.IFNE, labelNode));
                instructions.add(startLabelNode);
                instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(RuntimeException.class)));
                instructions.add(new InsnNode(Opcodes.DUP));
                instructions.add(new LdcInsnNode("-"));
                instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(RuntimeException.class), "<init>", "(Ljava/lang/String;)V"));
                instructions.add(new InsnNode(Opcodes.ATHROW));
                instructions.add(new JumpInsnNode(Opcodes.GOTO, startLabelNode));
                instructions.add(labelNode);

                methodNode.instructions.insert(instruction, instructions);
            }
        }
    }
}
so good! ☺
 
Начинающий
Статус
Оффлайн
Регистрация
4 Июл 2021
Сообщения
189
Реакции[?]
1
Поинты[?]
2K
Пользователь
Статус
Оффлайн
Регистрация
3 Май 2021
Сообщения
260
Реакции[?]
48
Поинты[?]
40K
Посмотреть вложение 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 указываем дескриптор. В скобочках пишутся типы аргументов метода, а далее возвращаемый тип (всё в
Пожалуйста, авторизуйтесь для просмотра ссылки.
).
Основные инструкции
Каждую инструкцию байткода знать не обязательно, обойдёмся и основными.
  • getfield <класс поля> <название> <дескриптор> - Получает поле
  • getstatic <класс поля> <название> <дескриптор> - Получает статическое поле
  • putfield <класс поля> <название> <дескриптор> - Помещает значение из стека в поле
  • putstatic <класс поля> <название> <дескриптор> - Помещает значение из стека в статическое поле
  • goto <метка> - Перейти на метку
  • ifeq, ifne, iflt, ifge, ifgt, ifle <метка> - Перейти на метку
    Пожалуйста, авторизуйтесь для просмотра ссылки.
  • aload <индекс> - Поместить локальную переменную на стек
  • astore <индекс> - Поместить значение из стека в локальную переменную
  • areturn - Вернуть значение из стека как результат
  • aconst_null - null
  • invokevirtual <класс метода> <название> <дескриптор> - Вызов метода из класса
  • invokestatic <класс метода> <название> <дескриптор> - Вызов статического метода из класса
  • invokespecial <класс метода> <название> <дескриптор> - Вызов метода из класса для вызовов superclass (наследованный класс)и для приватных методов
  • invokeinterface <класс метода> <название> <дескриптор> - Вызов метода из интерфейса
  • invokedynamic - Вызов метода из CallSite (это пока не затрагиваем,
    Пожалуйста, авторизуйтесь для просмотра ссылки.
    )

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 их тоже отделяют:
Код:
// instruction class - opcode
LabelNode            -  -1
LineNumberNode       -  -1
InsnNode             -  4
VarInsnNode          -  54
LabelNode            -  -1
LineNumberNode       -  -1
VarInsnNode          -  21
JumpInsnNode         -  153
LabelNode            -  -1
Для всех опкодов есть свой класс 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
Двигаемся дальше
Первая обфускация у нас уже есть, осталось сделать что-нибудь по сложнее.
Чистим код
Почистим немного код. Я создам интерфейс трансформера:
Java:
public interface Transformer {
    void transformClass(ClassNode classNode);
   
    void transformMethod(MethodNode methodNode);
}
Сервис:
Java:
public class TransformerService {

    private final List<Transformer> transformers;

    public TransformerService() {
        this.transformers = new LinkedList<>();
    }

    public void add(Transformer transformer) {
        this.transformers.add(transformer);
    }

    public void obfuscate(ClassNode classNode) {
        this.transformers.forEach(transformer -> transformer.transformClass(classNode));
        classNode.methods.forEach(methodNode -> {
            this.transformers.forEach(transformer -> transformer.transformMethod(methodNode));
        });
    }
}
Используем:
Java:
TransformerService service = new TransformerService();
service.add(...);
service.obfuscate(classFile);
Вот теперь писать чуть легче.
Обфускация строк
Создадим трансформер под строки:
Java:
public abstract class StringTransformer implements Transformer {

    abstract public InsnList generateEncrypt(AbstractInsnNode instruction, String string);

    @Override
    public void transformMethod(MethodNode methodNode) {
        for (AbstractInsnNode instruction : methodNode.instructions) {
            if (instruction instanceof LdcInsnNode ldcInsnNode && ldcInsnNode.cst instanceof String) {
                methodNode.instructions.insert(instruction, generateEncrypt(instruction, (String) ldcInsnNode.cst));
                methodNode.instructions.remove(instruction);
            }
        }
    }
}
Метод через Base64
Кстати пока писал вспомнил, что у ASM есть удобное получение дескрипторов через Type#getDescriptor
Java:
public class Base64StringTransformer extends StringTransformer {

    @Override
    public InsnList generateEncrypt(AbstractInsnNode instruction, String string) {
        InsnList instructions = new InsnList();
       
        // new String(Base64.getDecoder().decode("encoded string"))
        instructions.add(new InsnNode(Opcodes.DUP));
        instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/Base64", "getDecoder", "()Ljava/util/Base64$Decoder;"));
        instructions.add(new LdcInsnNode(Base64.getEncoder().encodeToString(string.getBytes())));
        instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B"));
        instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/String", "<init>", "([B)V"));

        return instructions;
    }
}
Смотрим:
Посмотреть вложение 285552

Метод через 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 байта в зависимости от кодировки. Чтобы немножко поприкалываться можно использовать символы, которые клеятся друг на друга (
Пожалуйста, авторизуйтесь для просмотра ссылки.
)
Реализуем:
Java:
public class AESStringTransformer extends StringTransformer {

    @Override
    public InsnList generateEncrypt(MethodNode methodNode, AbstractInsnNode instruction, String string) {
        InsnList instructions = new InsnList();

        try {
            SecretKeySpec spec = new SecretKeySpec("x̊a".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(string.getBytes()));

            LabelNode startNode = new LabelNode();
            LabelNode endNode = new LabelNode();
            int index = methodNode.localVariables.size() + 1;
            methodNode.localVariables.add(new LocalVariableNode("cipher"+index, Type.getDescriptor(Cipher.class), null, startNode, endNode, index));

            instructions.add(startNode);

            instructions.add(new LdcInsnNode("AES/CBC/PKCS5Padding"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(Cipher.class), "getInstance", "(Ljava/lang/String;)Ljavax/crypto/Cipher;"));
            instructions.add(new VarInsnNode(Opcodes.ASTORE, index));

            instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
            instructions.add(new InsnNode(Opcodes.ICONST_2)); // Cipher.DECRYPT_MODE
            instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(SecretKeySpec.class)));
            instructions.add(new InsnNode(Opcodes.DUP));
            instructions.add(new LdcInsnNode("x̊a"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "getBytes", "()[B"));
            instructions.add(new LdcInsnNode("AES"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(SecretKeySpec.class), "<init>", "([BLjava/lang/String;)V"));
            instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(IvParameterSpec.class)));
            instructions.add(new InsnNode(Opcodes.DUP));
            instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Cipher.class), "getBlockSize", "()I"));
            instructions.add(new IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_BYTE));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(IvParameterSpec.class), "<init>", "([B)V"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Cipher.class), "init", "(ILjava/security/Key;Ljava/security/spec/AlgorithmParameterSpec;)V"));

            instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(String.class)));
            instructions.add(new InsnNode(Opcodes.DUP));
            instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(Base64.class), "getDecoder", "()Ljava/util/Base64$Decoder;"));
            instructions.add(new LdcInsnNode(encrypted));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Cipher.class), "doFinal", "([B)[B"));
            instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(String.class), "<init>", "([B)V"));

            instructions.add(endNode);
        } catch (Exception exception) {
            exception.printStackTrace();
        }

        return instructions;
    }
}

Теперь есть проблема - если часто использовать эту строку (например: в читах в каждом кадре дешифровать по сотни строк), то он не оптимизирован. Поэтому мы создадим 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:
Пожалуйста, авторизуйтесь для просмотра ссылки.

Что-то серьёзное я делать не буду, просто после каждого вызова метода буду пихать if (что-нибудь, что не выполнится) throw ...;
Java:
public class FlowTransformer implements Transformer {

    @Override
    public void transformClass(ClassNode classNode) {

    }

    @Override
    public void transformMethod(MethodNode methodNode) {
        for (AbstractInsnNode instruction : methodNode.instructions) {
            if (instruction instanceof MethodInsnNode) {
                InsnList instructions = new InsnList();

                LabelNode startLabelNode = new LabelNode();
                LabelNode labelNode = new LabelNode();

                // 0*0 + 5 == 0
                instructions.add(new InsnNode(Opcodes.ICONST_0));
                instructions.add(new InsnNode(Opcodes.DUP));
                instructions.add(new InsnNode(Opcodes.IMUL));
                instructions.add(new InsnNode(Opcodes.ICONST_5));
                instructions.add(new InsnNode(Opcodes.IADD));
                instructions.add(new JumpInsnNode(Opcodes.IFNE, labelNode));
                instructions.add(startLabelNode);
                instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(RuntimeException.class)));
                instructions.add(new InsnNode(Opcodes.DUP));
                instructions.add(new LdcInsnNode("-"));
                instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(RuntimeException.class), "<init>", "(Ljava/lang/String;)V"));
                instructions.add(new InsnNode(Opcodes.ATHROW));
                instructions.add(new JumpInsnNode(Opcodes.GOTO, startLabelNode));
                instructions.add(labelNode);

                methodNode.instructions.insert(instruction, instructions);
            }
        }
    }
}
а куда это вставлять в main cpp?
 
Сверху Снизу