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

Пользователь
Статус
Оффлайн
Регистрация
22 Авг 2022
Сообщения
298
Реакции[?]
44
Поинты[?]
34K
Pasted image 20240912220842.png
почему бы и нет?
Теория компиляции
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
Работает, теперь посмотрим, что нам покажет декомпилятор:
Pasted image 20240911221939.png
Двигаемся дальше
Первая обфускация у нас уже есть, осталось сделать что-нибудь по сложнее.
Чистим код
Почистим немного код. Я создам интерфейс трансформера:
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; 
    } 
}
Смотрим:
Без имени-1.png

Метод через 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); 
            } 
        } 
    } 
}
 
Начинающий
Статус
Оффлайн
Регистрация
20 Июн 2024
Сообщения
107
Реакции[?]
1
Поинты[?]
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);
            }
        }
    }
}
Скинь уже забилженную защиту попробую наложить
 
Начинающий
Статус
Оффлайн
Регистрация
31 Авг 2023
Сообщения
505
Реакции[?]
6
Поинты[?]
5K
Начинающий
Статус
Оффлайн
Регистрация
20 Апр 2021
Сообщения
795
Реакции[?]
12
Поинты[?]
17K
Когда за 3 года не добился роли пользователь и теперь всем кидаешь свои жалкие попкорны сообщением :joycat: 🤙 🤙
я сижу тут максимум от начала лета, а ты как самый главный пастер, который пытается поднять самооценку, которая где то на дне Марианской впадины, благодаря тому что у меня акк зареган 3 года назад? ок чувак, завидуй мне что я старше тебя, завидуй мне что ты сидишь тут год без прерывно обновляя страницу, в надежде на реакцию " 👍 "
 
Начинающий
Статус
Оффлайн
Регистрация
31 Авг 2023
Сообщения
505
Реакции[?]
6
Поинты[?]
5K
я сижу тут максимум от начала лета, а ты как самый главный пастер, который пытается поднять самооценку, которая где то на дне Марианской впадины, благодаря тому что у меня акк зареган 3 года назад? ок чувак, завидуй мне что я старше тебя, завидуй мне что ты сидишь тут год без прерывно обновляя страницу, в надежде на реакцию " 👍 "
Я главный пастер ? Когда ты отказывался со мной идти в дискорд отвечать на вопросы по jav$ :joycat: :joycat: :joycat: :joycat:
 
Начинающий
Статус
Оффлайн
Регистрация
12 Авг 2024
Сообщения
6
Реакции[?]
0
Поинты[?]
0
Пиздец как актуально использовать asm после выхода 22 джавы конечно. Да и вобщем статья мусор.
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
12 Авг 2024
Сообщения
6
Реакции[?]
0
Поинты[?]
0
Бл, я это повнимательней почитал такой атаз. Плюсы у нас теперь в байт код компилятся. И в статье еще дохуя таких ошибок. Кароче ещё один высер.
 
Последнее редактирование:
Пользователь
Статус
Оффлайн
Регистрация
22 Авг 2022
Сообщения
298
Реакции[?]
44
Поинты[?]
34K
Пиздец как актуально использовать asm после выхода 22 джавы конечно. Да и вобщем статья мусор.
ну напишм свою статю, эта для тех кто вообще ничего не знает
 
Начинающий
Статус
Оффлайн
Регистрация
12 Авг 2024
Сообщения
6
Реакции[?]
0
Поинты[?]
0
ну напишм свою статю, эта для тех кто вообще ничего не знает
Чувак, а нахера ты "тех кто ничего не знает" пытаешься в байт код вкатить??? Да и чувак зачем мне какие статейки по байт коду писать для кубоголовых форумчан? Ну и почему ты ответил на комментарий где я тебе указываю на неактуальность ASM в сравнения с Class-File Api? И почему ты не ответил ничего на то где тебе лицом тыкнули в твой обсёр(мой комментарий ниже относительно первого).
ну напишм свою статю, эта для тех кто вообще ничего не знает
Ну и опять же в ответ на то что это совсем для "ничего не знающих", в этом разделе форума только такие и есть, например тут два пастера уже успели посраться на тему кто же из них самый гейский гей. Ты что-ли совсем не уважаешь себя и те свои 5 минут которые ты затратил на высер этой статейки?
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
8 Апр 2023
Сообщения
186
Реакции[?]
13
Поинты[?]
17K
Чувак, а нахера ты "тех кто ничего не знает" пытаешься в байт код вкатить??? Да и чувак зачем мне какие статейки по байт коду писать для кубоголовых форумчан? Ну и почему ты ответил на комментарий где я тебе указываю на неактуальность ASM в сравнения с Class-File Api? И почему ты не ответил ничего на то где тебе лицом тыкнули в твой обсёр(мой комментарий ниже относительно первого).

Ну и опять же в ответ на то чт бого это совсем для "ничего не знающих", в этом разделе форума только такие и есть, например тут два пастера уже успели посраться на тему кто же из них самый гейский гей. Ты что-ли совсем не уважаешь себя и те свои 5 минут которые ты затратил на высер этой статейки?
Вкатить в байт код :anguished:
 
Сверху Снизу