Гайд Пишем обфускатор на Java

эксперт в майнкрафт апи
Read Only
Статус
Оффлайн
Регистрация
25 Янв 2023
Сообщения
676
Реакции[?]
284
Поинты[?]
22K
Всем ку

Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе


Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Код:
iconst_0 <- положить 0 на вершину стека
ldc 100000 <- положить 100000 на вершину стека

iadd <- сложить два числа которые лежат на стеке

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
ldc "string" <- положить строку на стек
ldc 30 <- положить 30 на стек
invokevirtual java/lang/String.substring(I)V
->
"string".substring(30)
также используются инструкции
invokespecial - вызов конкретного метода от объекта(используется при вызове super метода/вызова конструктора класса)
invokeinterface - вызов метода интерфейса
Отдельно нужно выделить invokedynamic
invokedynamic - динамичный вызов метода.Достигается с помощью вызова bootstrap метода который определяет вызываемый метод вследствии.
<- используется после компиляции лямбда выражений, конкатенации строк в джаве >=9 версии.

Теперь нужно спарсить все классы с jar файла для модификации инструкций.Для парса можно использовать org.ow2.asm ->
Пожалуйста, авторизуйтесь для просмотра ссылки.


Написание парсера очень скучное и в целом бесполезное для освещения занятие.Поэтому пропустим этот момент и перейдем сразу же к модификации байткода.
Мы будем итерироваться по всем спаршенным ClassNode, обходить каждый MethodNode внутри и менять его инструкции на нужные нам.
Но что менять?
В данной статье будет рассмотрена модификация операций связанных с числом.
Для этого мы будем использовать булевы операции a.k.a XOR.Но использовать просто XOR не очень интересное занятие, поэтому немного зайдем в дискретную математику.
Через отрицание(NOT), конъюнкцию(AND), дизъюнкцию(ADD) можно выразить импликацию, эквиваленцию, исключающее или(XOR), штрихи шиффера и стрелку пирса.
Почему бы этим и не заняться?
Из учебника следует, что исключающее или выражается через отрицание эквивалентности(XOR - это != для двух битов), т.е в тасклист нужно добавить NOT, эквиваленцию.
Эквиваленция выражается через
Код:
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
~(~p & ~q | p & q)
Протестируем
Java:
    public static void main(String[] args) {
        int firstOperand = ThreadLocalRandom.current().nextInt();
        int secondOperand = ThreadLocalRandom.current().nextInt();
        System.out.format("%d %d\n", firstOperand, secondOperand);
        System.out.format("%d %d\n", xor(firstOperand, secondOperand), firstOperand ^ secondOperand);
    }

    public static int xor(int p, int q) {
        return ~(~p & ~q | p & q);
    }
И увидим
Код:
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
    // Может быть можно было обойтись без такой ебли со стеком
    public static void xnor(List<AbstractInsnNode> instructions) {
        instructions.add(new InsnNode(DUP2));
        not(instructions);
        instructions.add(new InsnNode(SWAP));
        not(instructions);
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(DUP_X2));
        instructions.add(new InsnNode(POP));
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(IOR));
    }

    public static void not(List<AbstractInsnNode> instructions) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        // Сломаем паттерн для декомпиляторов что б код выглядел достаточно жутко
        // и не превращал not в амперсанд
        instructions.add(new LdcInsnNode(randomValue));
        instructions.add(new LdcInsnNode(~randomValue));
        instructions.add(new InsnNode(IXOR));
        instructions.add(new InsnNode(IXOR));
    }
И сделаем xor
Java:
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

    public abstract IMathStubGenerator createMathStubGenerator();
}
А IBitwiseStubGenerator, IMathStubGenerator - интерфейсы которые обещают реализацию
Java:
public interface IBitwiseStubGenerator extends IStubGenerator {
    void xor(List<AbstractInsnNode> instructions);

    void xnor(List<AbstractInsnNode> instructions);
}
Java:
public interface IMathStubGenerator extends IStubGenerator {
    void push(List<AbstractInsnNode> instructions, int value);
}
Реализуем дефолтную фабрику
Java:
public class DefaultStubGeneratorFactory extends StubGeneratorFactory {
    @Override
    public IBitwiseStubGenerator createBitwiseStubGenerator() {
        return new BitwiseStubGenerator();
    }

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
public class MathStubGenerator implements IMathStubGenerator {
    @Override
    public void push(List<AbstractInsnNode> instructions, int value) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        int outValue = value ^ randomValue;
        instructions.add(new LdcInsnNode(outValue));
        instructions.add(new LdcInsnNode(randomValue));
        ASMUtils.xor(instructions);
    }
}
Java:
public class BitwiseStubGenerator implements IBitwiseStubGenerator {
    @Override
    public void xor(List<AbstractInsnNode> instructions) {
        ASMUtils.xor(instructions);
    }

    @Override
    public void xnor(List<AbstractInsnNode> instructions) {
        ASMUtils.xnor(instructions);
    }
}
Окей.Заменим все инструкции которые ложат числа на стек нашим пушем.
Java:
public class NumberTransformer implements ITransformer {
    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                Integer operand = ASMUtils.getIntegerOrNull(instruction);
                if (operand != null) {
                    mathStubGenerator.push(instructions, operand);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }
}
Тут нужно пояснить за ASMUtils.getIntegerOrNull.
В java байткоде операнды операций хранятся в константпуле т.е имеют ссылку
<instruction> <constant_pool_index_<index>>
где
constant_pool_index_<index> может ссылаться, например на какой либо метод или число.
Индекс константпула читается в два байта при парсинге, т.е и в байткоде он должен занимать два байта.
Но ведь нет смысла записывать например 0 или 1 в 4 байта и ещё байт на инструкцию.Поэтому в сете инструкций есть ICONST_0, ICONST_1 и тд - инструкции без операндов.
При этом также есть BIPUSH (byte integer push) которая занимает 2 байта(1 на байткод индекс и 1 на сам byte integer)
И SIPUSH(short integer push) которыя занимает 3 байта(1 байт на индекс и 2 на сам short integer)
Поэтому нам нужно правильно спарсить integer для его замены.
Java:
    public static Integer getIntegerOrNull(AbstractInsnNode node) {
        if (node.getOpcode() >= ICONST_M1 && node.getOpcode() <= ICONST_5) {
            return node.getOpcode() - ICONST_0; // Получение значения по индексу.Они упорядочены.
        } else if (node.getOpcode() == SIPUSH || node.getOpcode() == BIPUSH) {
            return ((IntInsnNode) node).operand;
        } else if (node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof Integer) {
            return (int) ((LdcInsnNode) node).cst;
        }
        return null;
    }
Код в декомпиляторе после такой модификации уже начинает выглядеть довольно жутко.
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Условные джампы.
В отличии от ассемблера, где на прыжок влияет RFLAGS, тут на все влияет значение в ToS.
Значение int`а в ToS будет означать выполнится прыжок или же нет.
Есть унарные прыжки и бинарные.
Унарный сравнивает значение ToS с нулем.(IFNE, IFEQ и тд)(if not equals zero, if equals zero)
Бинарные сравнивают два значения со стека.(IF_ICMPNE, IF_ICMPEQ)
Добавим ToS ^ 0 нашим ксором перед унарным джампом.​
Java:
public class JumpTransformer implements ITransformer {
    /*
    IFEQ = 153;
    IFNE = 154;
    IFLT = 155;
    IFGE = 156;
    IFGT = 157;
    IFLE = 158;
     */
    private static final List<Integer> INTEGER_COMPARE_JUMPS_LIST = IntStream.range(IFEQ, IFLE + 1).boxed().collect(Collectors.toList());

    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();

        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isIntegerCompareJump(instruction)) {
                    mathStubGenerator.push(instructions, 0);
                    bitwiseStubGenerator.xor(instructions);
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }

    private static boolean isIntegerCompareJump(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof JumpInsnNode && INTEGER_COMPARE_JUMPS_LIST.contains(abstractInsnNode.getOpcode());
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Также добавим try-catch блоки.
Тут тоже надо внести небольшое пояснение.
После парсинга класса он проходит этап верификации байткода.Он рассчитывает актуальный стек и локали для каждой инструкции.Когда в коде попадается какой либо условный прыжок то верификатор запускает ещё одну "нить" верификации на место прыжка.Если произойдет "коллизия" двух "нитей", то их стек и локали должны совпадать.
Когда мы попадаем в catch блок из try блока, наш стек очищается и в нем оказывается Throwable класс.Поэтому нам желательно выйти с метода в catch блоке.Сделать это можно с помощью return, но придется подбирать return подходящий для сигнатуры метода.Поэтому я использую athrow.
Код:
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
    public static ClassNode createExceptionClass(String className) {
        ClassNode classNode = new ClassNode();
        classNode.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Exception", null);
        MethodNode methodNode = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        methodNode.instructions.add(new VarInsnNode(ALOAD, 0));
        methodNode.instructions.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Exception", "<init>", "()V"));
        methodNode.instructions.add(new InsnNode(RETURN));
        classNode.methods.add(methodNode);
        return classNode;
    }
И добавить try-catch блоки в код.
Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
ну и раз уж на то пошло и у нас есть блок кода который не выполняется грех не вставить туда крашер что бы никакой декомпилятор не смог декомпилировать метод.
В джава 7 завезли инструкцию invokedynamic.Я уже рассказывал о ней выше, но повторю чуть чуть поподробнее.
Она вызывает bootstrap метод и получает оттуда Callable - оболочку над MethodHandle который в свою очередь используется для вызова метода.
invokedynamic несет в себе статичные аргументы - аргументы bootstrap метода который получит управление при первом обращении к invokedynamic.
По дефолту bootstrap метод имеет 3 аргумента(MethodHandles.Lookup, String - метод нейм, MethodType - сигнатура метода)
Но можно добавлять свои.
Я нашел забавную штучку, связанную с тем, что если декомпилятор не получит статичных аргументов в invokedynamic то он очень сильно расстроится и перестанет показывать декомпилированный код.Совсем.
Скопируем invokedynamic вызов создания трамплина в лямбда метод и удалим там статичные аргументы.(В таком случае вообще все декомпиляторы которые я знаю отваливаются)

Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InvokeDynamicInsnNode("lol", "()V", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false)));
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Ну собстна и все как бы
Пожалуйста, авторизуйтесь для просмотра ссылки.

Спасибо что прочли статью.Если есть какие то корректировки, поправьте пжлста.
Данный обфускатор не претендует на место хорошего java обфускатора и теоретически снимается аналогами VTIL/etc в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
19 Авг 2022
Сообщения
4
Реакции[?]
0
Поинты[?]
0
Всем ку

Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе


Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Код:
iconst_0 <- положить 0 на вершину стека
ldc 100000 <- положить 100000 на вершину стека

iadd <- сложить два числа которые лежат на стеке

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
ldc "string" <- положить строку на стек
ldc 30 <- положить 30 на стек
invokevirtual java/lang/String.substring(I)V
->
"string".substring(30)
также используются инструкции
invokespecial - вызов конкретного метода от объекта(используется при вызове super метода/вызова конструктора класса)
invokeinterface - вызов метода интерфейса
Отдельно нужно выделить invokedynamic
invokedynamic - динамичный вызов метода.Достигается с помощью вызова bootstrap метода который определяет вызываемый метод вследствии.
<- используется после компиляции лямбда выражений, конкатенации строк в джаве >=9 версии.

Теперь нужно спарсить все классы с jar файла для модификации инструкций.Для парса можно использовать org.ow2.asm ->
Пожалуйста, авторизуйтесь для просмотра ссылки.


Написание парсера очень скучное и в целом бесполезное для освещения занятие.Поэтому пропустим этот момент и перейдем сразу же к модификации байткода.
Мы будем итерироваться по всем спаршенным ClassNode, обходить каждый MethodNode внутри и менять его инструкции на нужные нам.
Но что менять?
В данной статье будет рассмотрена модификация операций связанных с числом.
Для этого мы будем использовать булевы операции a.k.a XOR.Но использовать просто XOR не очень интересное занятие, поэтому немного зайдем в дискретную математику.
Через отрицание(NOT), конъюнкцию(AND), дизъюнкцию(ADD) можно выразить импликацию, эквиваленцию, исключающее или(XOR), штрихи шиффера и стрелку пирса.
Почему бы этим и не заняться?
Из учебника следует, что исключающее или выражается через отрицание эквивалентности(XOR - это != для двух битов), т.е в тасклист нужно добавить NOT, эквиваленцию.
Эквиваленция выражается через
Код:
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
~(~p & ~q | p & q)
Протестируем
Java:
    public static void main(String[] args) {
        int firstOperand = ThreadLocalRandom.current().nextInt();
        int secondOperand = ThreadLocalRandom.current().nextInt();
        System.out.format("%d %d\n", firstOperand, secondOperand);
        System.out.format("%d %d\n", xor(firstOperand, secondOperand), firstOperand ^ secondOperand);
    }

    public static int xor(int p, int q) {
        return ~(~p & ~q | p & q);
    }
И увидим
Код:
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
    // Может быть можно было обойтись без такой ебли со стеком
    public static void xnor(List<AbstractInsnNode> instructions) {
        instructions.add(new InsnNode(DUP2));
        not(instructions);
        instructions.add(new InsnNode(SWAP));
        not(instructions);
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(DUP_X2));
        instructions.add(new InsnNode(POP));
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(IOR));
    }

    public static void not(List<AbstractInsnNode> instructions) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        // Сломаем паттерн для декомпиляторов что б код выглядел достаточно жутко
        // и не превращал not в амперсанд
        instructions.add(new LdcInsnNode(randomValue));
        instructions.add(new LdcInsnNode(~randomValue));
        instructions.add(new InsnNode(IXOR));
        instructions.add(new InsnNode(IXOR));
    }
И сделаем xor
Java:
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

    public abstract IMathStubGenerator createMathStubGenerator();
}
А IBitwiseStubGenerator, IMathStubGenerator - интерфейсы которые обещают реализацию
Java:
public interface IBitwiseStubGenerator extends IStubGenerator {
    void xor(List<AbstractInsnNode> instructions);

    void xnor(List<AbstractInsnNode> instructions);
}
Java:
public interface IMathStubGenerator extends IStubGenerator {
    void push(List<AbstractInsnNode> instructions, int value);
}
Реализуем дефолтную фабрику
Java:
public class DefaultStubGeneratorFactory extends StubGeneratorFactory {
    @Override
    public IBitwiseStubGenerator createBitwiseStubGenerator() {
        return new BitwiseStubGenerator();
    }

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
public class MathStubGenerator implements IMathStubGenerator {
    @Override
    public void push(List<AbstractInsnNode> instructions, int value) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        int outValue = (value ^ randomValue);
        instructions.add(new LdcInsnNode(outValue));
        instructions.add(new LdcInsnNode(randomValue));
        ASMUtils.xor(instructions);
    }
}
Java:
public class BitwiseStubGenerator implements IBitwiseStubGenerator {
    @Override
    public void xor(List<AbstractInsnNode> instructions) {
        ASMUtils.xor(instructions);
    }

    @Override
    public void xnor(List<AbstractInsnNode> instructions) {
        ASMUtils.xnor(instructions);
    }
}
Окей.Заменим все инструкции которые ложат числа на стек нашим пушем.
Java:
public class NumberTransformer implements ITransformer {
    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                Integer operand = ASMUtils.getIntegerOrNull(instruction);
                if (operand != null) {
                    mathStubGenerator.push(instructions, operand);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }
}
Тут нужно пояснить за ASMUtils.getIntegerOrNull.
В java байткоде операнды операций хранятся в константпуле т.е имеют ссылку
<instruction> <constant_pool_index_<index>>
где
constant_pool_index_<index> может ссылаться, например на какой либо метод или число.
Индекс константпула читается в два байта при парсинге, т.е и в байткоде он должен занимать два байта.
Но ведь нет смысла записывать например 0 или 1 в 4 байта и ещё байт на инструкцию.Поэтому в сете инструкций есть ICONST_0, ICONST_1 и тд - инструкции без операндов.
При этом также есть BIPUSH (byte integer push) которая занимает 2 байта(1 на байткод индекс и 1 на сам byte integer)
И SIPUSH(short integer push) которыя занимает 3 байта(1 байт на индекс и 2 на сам short integer)
Поэтому нам нужно правильно спарсить integer для его замены.
Java:
    public static Integer getIntegerOrNull(AbstractInsnNode node) {
        if (node.getOpcode() >= ICONST_M1 && node.getOpcode() <= ICONST_5) {
            return node.getOpcode() - ICONST_0; // Получение значения по индексу.Они упорядочены.
        } else if (node.getOpcode() == SIPUSH || node.getOpcode() == BIPUSH) {
            return ((IntInsnNode) node).operand;
        } else if (node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof Integer) {
            return (int) ((LdcInsnNode) node).cst;
        }
        return null;
    }
Код в декомпиляторе после такой модификации уже начинает выглядеть довольно жутко.
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Условные джампы.
В отличии от ассемблера, где на прыжок влияет RFLAGS, тут на все влияет значение в ToS.
Значение int`а в ToS будет означать выполнится прыжок или же нет.
Есть унарные прыжки и бинарные.
Унарный сравнивает значение ToS с нулем.(IFNE, IFEQ и тд)(if not equals zero, if equals zero)
Бинарные сравнивают два значения со стека.(IF_ICMPNE, IF_ICMPEQ)
Добавим ToS ^ 0 нашим ксором перед унарным джампом.​
Java:
public class JumpTransformer implements ITransformer {
    /*
    IFEQ = 153;
    IFNE = 154;
    IFLT = 155;
    IFGE = 156;
    IFGT = 157;
    IFLE = 158;
     */
    private static final List<Integer> INTEGER_COMPARE_JUMPS_LIST = IntStream.range(IFEQ, IFLE + 1).boxed().collect(Collectors.toList());

    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();

        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isIntegerCompareJump(instruction)) {
                    mathStubGenerator.push(instructions, 0);
                    bitwiseStubGenerator.xor(instructions);
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }

    private static boolean isIntegerCompareJump(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof JumpInsnNode && INTEGER_COMPARE_JUMPS_LIST.contains(abstractInsnNode.getOpcode());
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Также добавим try-catch блоки.
Тут тоже надо внести небольшое пояснение.
После парсинга класса он проходит этап верификации байткода.Он рассчитывает актуальный стек и локали для каждой инструкции.Когда в коде попадается какой либо условный прыжок то верификатор запускает ещё одну "нить" верификации на место прыжка.Если произойдет "коллизия" двух "нитей", то их стек и локали должны совпадать.
Когда мы попадаем в catch блок из try блока, наш стек очищается и в нем оказывается Throwable класс.Поэтому нам желательно выйти с метода в catch блоке.Сделать это можно с помощью return, но придется подбирать return подходящий для сигнатуры метода.Поэтому я использую athrow.
Код:
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
    public static ClassNode createExceptionClass(String className) {
        ClassNode classNode = new ClassNode();
        classNode.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Exception", null);
        MethodNode methodNode = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        methodNode.instructions.add(new VarInsnNode(ALOAD, 0));
        methodNode.instructions.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Exception", "<init>", "()V"));
        methodNode.instructions.add(new InsnNode(RETURN));
        classNode.methods.add(methodNode);
        return classNode;
    }
И добавить try-catch блоки в код.
Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
ну и раз уж на то пошло и у нас есть блок кода который не выполняется грех не вставить туда крашер что бы никакой декомпилятор не смог декомпилировать метод.
В джава 7 завезли инструкцию invokedynamic.Я уже рассказывал о ней выше, но повторю чуть чуть поподробнее.
Она вызывает bootstrap метод и получает оттуда Callable - оболочку над MethodHandle который в свою очередь используется для вызова метода.
invokedynamic несет в себе статичные аргументы - аргументы bootstrap метода который получит управление при первом обращении к invokedynamic.
По дефолту bootstrap метод имеет 3 аргумента(MethodHandles.Lookup, String - метод нейм, MethodType - сигнатура метода)
Но можно добавлять свои.
Я нашел забавную штучку, связанную с тем, что если декомпилятор не получит статичных аргументов в invokedynamic то он очень сильно расстроится и перестанет показывать декомпилированный код.Совсем.
Скопируем invokedynamic вызов создания трамплина в лямбда метод и удалим там статичные аргументы.(В таком случае вообще все декомпиляторы которые я знаю отваливаются)

Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InvokeDynamicInsnNode("lol", "()V", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false)));
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Ну собстна и все как бы
Пожалуйста, авторизуйтесь для просмотра ссылки.

Спасибо что прочли статью.Если есть какие то корректировки, поправьте пжлста.
Данный обфускатор не претендует на место хорошего java обфускатора и теоретически снимается аналогами VTIL/etc в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Жосткий +реп
 
Начинающий
Статус
Оффлайн
Регистрация
19 Авг 2022
Сообщения
4
Реакции[?]
0
Поинты[?]
0
теперь твоя паста будет не отломана молись.... на нее.. проси что бы скинул обфу.
Что бля
теперь твоя паста будет не отломана молись.... на нее.. проси что бы скинул обфу.
Я просто написал харош челу за годные туторы
 
Начинающий
Статус
Оффлайн
Регистрация
16 Авг 2022
Сообщения
41
Реакции[?]
1
Поинты[?]
1K
Всем ку

Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе


Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Код:
iconst_0 <- положить 0 на вершину стека
ldc 100000 <- положить 100000 на вершину стека

iadd <- сложить два числа которые лежат на стеке

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
ldc "string" <- положить строку на стек
ldc 30 <- положить 30 на стек
invokevirtual java/lang/String.substring(I)V
->
"string".substring(30)
также используются инструкции
invokespecial - вызов конкретного метода от объекта(используется при вызове super метода/вызова конструктора класса)
invokeinterface - вызов метода интерфейса
Отдельно нужно выделить invokedynamic
invokedynamic - динамичный вызов метода.Достигается с помощью вызова bootstrap метода который определяет вызываемый метод вследствии.
<- используется после компиляции лямбда выражений, конкатенации строк в джаве >=9 версии.

Теперь нужно спарсить все классы с jar файла для модификации инструкций.Для парса можно использовать org.ow2.asm ->
Пожалуйста, авторизуйтесь для просмотра ссылки.


Написание парсера очень скучное и в целом бесполезное для освещения занятие.Поэтому пропустим этот момент и перейдем сразу же к модификации байткода.
Мы будем итерироваться по всем спаршенным ClassNode, обходить каждый MethodNode внутри и менять его инструкции на нужные нам.
Но что менять?
В данной статье будет рассмотрена модификация операций связанных с числом.
Для этого мы будем использовать булевы операции a.k.a XOR.Но использовать просто XOR не очень интересное занятие, поэтому немного зайдем в дискретную математику.
Через отрицание(NOT), конъюнкцию(AND), дизъюнкцию(ADD) можно выразить импликацию, эквиваленцию, исключающее или(XOR), штрихи шиффера и стрелку пирса.
Почему бы этим и не заняться?
Из учебника следует, что исключающее или выражается через отрицание эквивалентности(XOR - это != для двух битов), т.е в тасклист нужно добавить NOT, эквиваленцию.
Эквиваленция выражается через
Код:
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
~(~p & ~q | p & q)
Протестируем
Java:
    public static void main(String[] args) {
        int firstOperand = ThreadLocalRandom.current().nextInt();
        int secondOperand = ThreadLocalRandom.current().nextInt();
        System.out.format("%d %d\n", firstOperand, secondOperand);
        System.out.format("%d %d\n", xor(firstOperand, secondOperand), firstOperand ^ secondOperand);
    }

    public static int xor(int p, int q) {
        return ~(~p & ~q | p & q);
    }
И увидим
Код:
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
    // Может быть можно было обойтись без такой ебли со стеком
    public static void xnor(List<AbstractInsnNode> instructions) {
        instructions.add(new InsnNode(DUP2));
        not(instructions);
        instructions.add(new InsnNode(SWAP));
        not(instructions);
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(DUP_X2));
        instructions.add(new InsnNode(POP));
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(IOR));
    }

    public static void not(List<AbstractInsnNode> instructions) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        // Сломаем паттерн для декомпиляторов что б код выглядел достаточно жутко
        // и не превращал not в амперсанд
        instructions.add(new LdcInsnNode(randomValue));
        instructions.add(new LdcInsnNode(~randomValue));
        instructions.add(new InsnNode(IXOR));
        instructions.add(new InsnNode(IXOR));
    }
И сделаем xor
Java:
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

    public abstract IMathStubGenerator createMathStubGenerator();
}
А IBitwiseStubGenerator, IMathStubGenerator - интерфейсы которые обещают реализацию
Java:
public interface IBitwiseStubGenerator extends IStubGenerator {
    void xor(List<AbstractInsnNode> instructions);

    void xnor(List<AbstractInsnNode> instructions);
}
Java:
public interface IMathStubGenerator extends IStubGenerator {
    void push(List<AbstractInsnNode> instructions, int value);
}
Реализуем дефолтную фабрику
Java:
public class DefaultStubGeneratorFactory extends StubGeneratorFactory {
    @Override
    public IBitwiseStubGenerator createBitwiseStubGenerator() {
        return new BitwiseStubGenerator();
    }

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
public class MathStubGenerator implements IMathStubGenerator {
    @Override
    public void push(List<AbstractInsnNode> instructions, int value) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        int outValue = value ^ randomValue;
        instructions.add(new LdcInsnNode(outValue));
        instructions.add(new LdcInsnNode(randomValue));
        ASMUtils.xor(instructions);
    }
}
Java:
public class BitwiseStubGenerator implements IBitwiseStubGenerator {
    @Override
    public void xor(List<AbstractInsnNode> instructions) {
        ASMUtils.xor(instructions);
    }

    @Override
    public void xnor(List<AbstractInsnNode> instructions) {
        ASMUtils.xnor(instructions);
    }
}
Окей.Заменим все инструкции которые ложат числа на стек нашим пушем.
Java:
public class NumberTransformer implements ITransformer {
    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                Integer operand = ASMUtils.getIntegerOrNull(instruction);
                if (operand != null) {
                    mathStubGenerator.push(instructions, operand);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }
}
Тут нужно пояснить за ASMUtils.getIntegerOrNull.
В java байткоде операнды операций хранятся в константпуле т.е имеют ссылку
<instruction> <constant_pool_index_<index>>
где
constant_pool_index_<index> может ссылаться, например на какой либо метод или число.
Индекс константпула читается в два байта при парсинге, т.е и в байткоде он должен занимать два байта.
Но ведь нет смысла записывать например 0 или 1 в 4 байта и ещё байт на инструкцию.Поэтому в сете инструкций есть ICONST_0, ICONST_1 и тд - инструкции без операндов.
При этом также есть BIPUSH (byte integer push) которая занимает 2 байта(1 на байткод индекс и 1 на сам byte integer)
И SIPUSH(short integer push) которыя занимает 3 байта(1 байт на индекс и 2 на сам short integer)
Поэтому нам нужно правильно спарсить integer для его замены.
Java:
    public static Integer getIntegerOrNull(AbstractInsnNode node) {
        if (node.getOpcode() >= ICONST_M1 && node.getOpcode() <= ICONST_5) {
            return node.getOpcode() - ICONST_0; // Получение значения по индексу.Они упорядочены.
        } else if (node.getOpcode() == SIPUSH || node.getOpcode() == BIPUSH) {
            return ((IntInsnNode) node).operand;
        } else if (node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof Integer) {
            return (int) ((LdcInsnNode) node).cst;
        }
        return null;
    }
Код в декомпиляторе после такой модификации уже начинает выглядеть довольно жутко.
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Условные джампы.
В отличии от ассемблера, где на прыжок влияет RFLAGS, тут на все влияет значение в ToS.
Значение int`а в ToS будет означать выполнится прыжок или же нет.
Есть унарные прыжки и бинарные.
Унарный сравнивает значение ToS с нулем.(IFNE, IFEQ и тд)(if not equals zero, if equals zero)
Бинарные сравнивают два значения со стека.(IF_ICMPNE, IF_ICMPEQ)
Добавим ToS ^ 0 нашим ксором перед унарным джампом.​
Java:
public class JumpTransformer implements ITransformer {
    /*
    IFEQ = 153;
    IFNE = 154;
    IFLT = 155;
    IFGE = 156;
    IFGT = 157;
    IFLE = 158;
     */
    private static final List<Integer> INTEGER_COMPARE_JUMPS_LIST = IntStream.range(IFEQ, IFLE + 1).boxed().collect(Collectors.toList());

    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();

        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isIntegerCompareJump(instruction)) {
                    mathStubGenerator.push(instructions, 0);
                    bitwiseStubGenerator.xor(instructions);
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }

    private static boolean isIntegerCompareJump(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof JumpInsnNode && INTEGER_COMPARE_JUMPS_LIST.contains(abstractInsnNode.getOpcode());
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Также добавим try-catch блоки.
Тут тоже надо внести небольшое пояснение.
После парсинга класса он проходит этап верификации байткода.Он рассчитывает актуальный стек и локали для каждой инструкции.Когда в коде попадается какой либо условный прыжок то верификатор запускает ещё одну "нить" верификации на место прыжка.Если произойдет "коллизия" двух "нитей", то их стек и локали должны совпадать.
Когда мы попадаем в catch блок из try блока, наш стек очищается и в нем оказывается Throwable класс.Поэтому нам желательно выйти с метода в catch блоке.Сделать это можно с помощью return, но придется подбирать return подходящий для сигнатуры метода.Поэтому я использую athrow.
Код:
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
    public static ClassNode createExceptionClass(String className) {
        ClassNode classNode = new ClassNode();
        classNode.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Exception", null);
        MethodNode methodNode = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        methodNode.instructions.add(new VarInsnNode(ALOAD, 0));
        methodNode.instructions.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Exception", "<init>", "()V"));
        methodNode.instructions.add(new InsnNode(RETURN));
        classNode.methods.add(methodNode);
        return classNode;
    }
И добавить try-catch блоки в код.
Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
ну и раз уж на то пошло и у нас есть блок кода который не выполняется грех не вставить туда крашер что бы никакой декомпилятор не смог декомпилировать метод.
В джава 7 завезли инструкцию invokedynamic.Я уже рассказывал о ней выше, но повторю чуть чуть поподробнее.
Она вызывает bootstrap метод и получает оттуда Callable - оболочку над MethodHandle который в свою очередь используется для вызова метода.
invokedynamic несет в себе статичные аргументы - аргументы bootstrap метода который получит управление при первом обращении к invokedynamic.
По дефолту bootstrap метод имеет 3 аргумента(MethodHandles.Lookup, String - метод нейм, MethodType - сигнатура метода)
Но можно добавлять свои.
Я нашел забавную штучку, связанную с тем, что если декомпилятор не получит статичных аргументов в invokedynamic то он очень сильно расстроится и перестанет показывать декомпилированный код.Совсем.
Скопируем invokedynamic вызов создания трамплина в лямбда метод и удалим там статичные аргументы.(В таком случае вообще все декомпиляторы которые я знаю отваливаются)

Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InvokeDynamicInsnNode("lol", "()V", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false)));
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Ну собстна и все как бы
Пожалуйста, авторизуйтесь для просмотра ссылки.

Спасибо что прочли статью.Если есть какие то корректировки, поправьте пжлста.
Данный обфускатор не претендует на место хорошего java обфускатора и теоретически снимается аналогами VTIL/etc в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Для пастеров слишком сложно
 
Забаненный
Статус
Оффлайн
Регистрация
19 Май 2020
Сообщения
98
Реакции[?]
13
Поинты[?]
6K
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Забаненный
Статус
Оффлайн
Регистрация
19 Май 2020
Сообщения
98
Реакции[?]
13
Поинты[?]
6K
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Начинающий
Статус
Оффлайн
Регистрация
16 Авг 2022
Сообщения
41
Реакции[?]
1
Поинты[?]
1K
Забаненный
Статус
Оффлайн
Регистрация
19 Май 2020
Сообщения
98
Реакции[?]
13
Поинты[?]
6K
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Эксперт
Статус
Оффлайн
Регистрация
29 Мар 2021
Сообщения
1,605
Реакции[?]
607
Поинты[?]
48K
XOR - это != для двух битов
чего нахуй
Никакой декомпилятор такой прыжок не разберет.
Не поддерживается текущими, или это физически невозможно? Ведь если ты толкаешь инструкции на ребилд стека на сам стек то это и в декомпиле видно будет..
Приятное чтиво, не знал что в жвм нет not инсна. +rep
 
эксперт в майнкрафт апи
Read Only
Статус
Оффлайн
Регистрация
25 Янв 2023
Сообщения
676
Реакции[?]
284
Поинты[?]
22K
ну что то на уровне
Код:
bool firstBit = false;
bool secondBit = false;
bool xor = firstBit != secondBit;
Не поддерживается текущими, или это физически невозможно
не нашел декомпилятора который сейчас поддерживает адекватную декомпиляцию такого кода.большинство просто крашатся.физически возможно...
Ведь если ты толкаешь инструкции на ребилд стека на сам стек то это и в декомпиле видно будет..
это не меняет сути и нужно лишь для того что бы наебать java verifier.
 
Начинающий
Статус
Оффлайн
Регистрация
6 Апр 2023
Сообщения
57
Реакции[?]
1
Поинты[?]
1K
Всем ку

Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе


Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Код:
iconst_0 <- положить 0 на вершину стека
ldc 100000 <- положить 100000 на вершину стека

iadd <- сложить два числа которые лежат на стеке

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
ldc "string" <- положить строку на стек
ldc 30 <- положить 30 на стек
invokevirtual java/lang/String.substring(I)V
->
"string".substring(30)
также используются инструкции
invokespecial - вызов конкретного метода от объекта(используется при вызове super метода/вызова конструктора класса)
invokeinterface - вызов метода интерфейса
Отдельно нужно выделить invokedynamic
invokedynamic - динамичный вызов метода.Достигается с помощью вызова bootstrap метода который определяет вызываемый метод вследствии.
<- используется после компиляции лямбда выражений, конкатенации строк в джаве >=9 версии.

Теперь нужно спарсить все классы с jar файла для модификации инструкций.Для парса можно использовать org.ow2.asm ->
Пожалуйста, авторизуйтесь для просмотра ссылки.


Написание парсера очень скучное и в целом бесполезное для освещения занятие.Поэтому пропустим этот момент и перейдем сразу же к модификации байткода.
Мы будем итерироваться по всем спаршенным ClassNode, обходить каждый MethodNode внутри и менять его инструкции на нужные нам.
Но что менять?
В данной статье будет рассмотрена модификация операций связанных с числом.
Для этого мы будем использовать булевы операции a.k.a XOR.Но использовать просто XOR не очень интересное занятие, поэтому немного зайдем в дискретную математику.
Через отрицание(NOT), конъюнкцию(AND), дизъюнкцию(ADD) можно выразить импликацию, эквиваленцию, исключающее или(XOR), штрихи шиффера и стрелку пирса.
Почему бы этим и не заняться?
Из учебника следует, что исключающее или выражается через отрицание эквивалентности(XOR - это != для двух битов), т.е в тасклист нужно добавить NOT, эквиваленцию.
Эквиваленция выражается через
Код:
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
~(~p & ~q | p & q)
Протестируем
Java:
    public static void main(String[] args) {
        int firstOperand = ThreadLocalRandom.current().nextInt();
        int secondOperand = ThreadLocalRandom.current().nextInt();
        System.out.format("%d %d\n", firstOperand, secondOperand);
        System.out.format("%d %d\n", xor(firstOperand, secondOperand), firstOperand ^ secondOperand);
    }

    public static int xor(int p, int q) {
        return ~(~p & ~q | p & q);
    }
И увидим
Код:
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
    // Может быть можно было обойтись без такой ебли со стеком
    public static void xnor(List<AbstractInsnNode> instructions) {
        instructions.add(new InsnNode(DUP2));
        not(instructions);
        instructions.add(new InsnNode(SWAP));
        not(instructions);
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(DUP_X2));
        instructions.add(new InsnNode(POP));
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(IOR));
    }

    public static void not(List<AbstractInsnNode> instructions) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        // Сломаем паттерн для декомпиляторов что б код выглядел достаточно жутко
        // и не превращал not в амперсанд
        instructions.add(new LdcInsnNode(randomValue));
        instructions.add(new LdcInsnNode(~randomValue));
        instructions.add(new InsnNode(IXOR));
        instructions.add(new InsnNode(IXOR));
    }
И сделаем xor
Java:
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

    public abstract IMathStubGenerator createMathStubGenerator();
}
А IBitwiseStubGenerator, IMathStubGenerator - интерфейсы которые обещают реализацию
Java:
public interface IBitwiseStubGenerator extends IStubGenerator {
    void xor(List<AbstractInsnNode> instructions);

    void xnor(List<AbstractInsnNode> instructions);
}
Java:
public interface IMathStubGenerator extends IStubGenerator {
    void push(List<AbstractInsnNode> instructions, int value);
}
Реализуем дефолтную фабрику
Java:
public class DefaultStubGeneratorFactory extends StubGeneratorFactory {
    @Override
    public IBitwiseStubGenerator createBitwiseStubGenerator() {
        return new BitwiseStubGenerator();
    }

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
public class MathStubGenerator implements IMathStubGenerator {
    @Override
    public void push(List<AbstractInsnNode> instructions, int value) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        int outValue = value ^ randomValue;
        instructions.add(new LdcInsnNode(outValue));
        instructions.add(new LdcInsnNode(randomValue));
        ASMUtils.xor(instructions);
    }
}
Java:
public class BitwiseStubGenerator implements IBitwiseStubGenerator {
    @Override
    public void xor(List<AbstractInsnNode> instructions) {
        ASMUtils.xor(instructions);
    }

    @Override
    public void xnor(List<AbstractInsnNode> instructions) {
        ASMUtils.xnor(instructions);
    }
}
Окей.Заменим все инструкции которые ложат числа на стек нашим пушем.
Java:
public class NumberTransformer implements ITransformer {
    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                Integer operand = ASMUtils.getIntegerOrNull(instruction);
                if (operand != null) {
                    mathStubGenerator.push(instructions, operand);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }
}
Тут нужно пояснить за ASMUtils.getIntegerOrNull.
В java байткоде операнды операций хранятся в константпуле т.е имеют ссылку
<instruction> <constant_pool_index_<index>>
где
constant_pool_index_<index> может ссылаться, например на какой либо метод или число.
Индекс константпула читается в два байта при парсинге, т.е и в байткоде он должен занимать два байта.
Но ведь нет смысла записывать например 0 или 1 в 4 байта и ещё байт на инструкцию.Поэтому в сете инструкций есть ICONST_0, ICONST_1 и тд - инструкции без операндов.
При этом также есть BIPUSH (byte integer push) которая занимает 2 байта(1 на байткод индекс и 1 на сам byte integer)
И SIPUSH(short integer push) которыя занимает 3 байта(1 байт на индекс и 2 на сам short integer)
Поэтому нам нужно правильно спарсить integer для его замены.
Java:
    public static Integer getIntegerOrNull(AbstractInsnNode node) {
        if (node.getOpcode() >= ICONST_M1 && node.getOpcode() <= ICONST_5) {
            return node.getOpcode() - ICONST_0; // Получение значения по индексу.Они упорядочены.
        } else if (node.getOpcode() == SIPUSH || node.getOpcode() == BIPUSH) {
            return ((IntInsnNode) node).operand;
        } else if (node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof Integer) {
            return (int) ((LdcInsnNode) node).cst;
        }
        return null;
    }
Код в декомпиляторе после такой модификации уже начинает выглядеть довольно жутко.
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Условные джампы.
В отличии от ассемблера, где на прыжок влияет RFLAGS, тут на все влияет значение в ToS.
Значение int`а в ToS будет означать выполнится прыжок или же нет.
Есть унарные прыжки и бинарные.
Унарный сравнивает значение ToS с нулем.(IFNE, IFEQ и тд)(if not equals zero, if equals zero)
Бинарные сравнивают два значения со стека.(IF_ICMPNE, IF_ICMPEQ)
Добавим ToS ^ 0 нашим ксором перед унарным джампом.​
Java:
public class JumpTransformer implements ITransformer {
    /*
    IFEQ = 153;
    IFNE = 154;
    IFLT = 155;
    IFGE = 156;
    IFGT = 157;
    IFLE = 158;
     */
    private static final List<Integer> INTEGER_COMPARE_JUMPS_LIST = IntStream.range(IFEQ, IFLE + 1).boxed().collect(Collectors.toList());

    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();

        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isIntegerCompareJump(instruction)) {
                    mathStubGenerator.push(instructions, 0);
                    bitwiseStubGenerator.xor(instructions);
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }

    private static boolean isIntegerCompareJump(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof JumpInsnNode && INTEGER_COMPARE_JUMPS_LIST.contains(abstractInsnNode.getOpcode());
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Также добавим try-catch блоки.
Тут тоже надо внести небольшое пояснение.
После парсинга класса он проходит этап верификации байткода.Он рассчитывает актуальный стек и локали для каждой инструкции.Когда в коде попадается какой либо условный прыжок то верификатор запускает ещё одну "нить" верификации на место прыжка.Если произойдет "коллизия" двух "нитей", то их стек и локали должны совпадать.
Когда мы попадаем в catch блок из try блока, наш стек очищается и в нем оказывается Throwable класс.Поэтому нам желательно выйти с метода в catch блоке.Сделать это можно с помощью return, но придется подбирать return подходящий для сигнатуры метода.Поэтому я использую athrow.
Код:
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
    public static ClassNode createExceptionClass(String className) {
        ClassNode classNode = new ClassNode();
        classNode.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Exception", null);
        MethodNode methodNode = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        methodNode.instructions.add(new VarInsnNode(ALOAD, 0));
        methodNode.instructions.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Exception", "<init>", "()V"));
        methodNode.instructions.add(new InsnNode(RETURN));
        classNode.methods.add(methodNode);
        return classNode;
    }
И добавить try-catch блоки в код.
Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
ну и раз уж на то пошло и у нас есть блок кода который не выполняется грех не вставить туда крашер что бы никакой декомпилятор не смог декомпилировать метод.
В джава 7 завезли инструкцию invokedynamic.Я уже рассказывал о ней выше, но повторю чуть чуть поподробнее.
Она вызывает bootstrap метод и получает оттуда Callable - оболочку над MethodHandle который в свою очередь используется для вызова метода.
invokedynamic несет в себе статичные аргументы - аргументы bootstrap метода который получит управление при первом обращении к invokedynamic.
По дефолту bootstrap метод имеет 3 аргумента(MethodHandles.Lookup, String - метод нейм, MethodType - сигнатура метода)
Но можно добавлять свои.
Я нашел забавную штучку, связанную с тем, что если декомпилятор не получит статичных аргументов в invokedynamic то он очень сильно расстроится и перестанет показывать декомпилированный код.Совсем.
Скопируем invokedynamic вызов создания трамплина в лямбда метод и удалим там статичные аргументы.(В таком случае вообще все декомпиляторы которые я знаю отваливаются)

Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InvokeDynamicInsnNode("lol", "()V", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false)));
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Ну собстна и все как бы
Пожалуйста, авторизуйтесь для просмотра ссылки.

Спасибо что прочли статью.Если есть какие то корректировки, поправьте пжлста.
Данный обфускатор не претендует на место хорошего java обфускатора и теоретически снимается аналогами VTIL/etc в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
сурсы дай пж
 
Начинающий
Статус
Онлайн
Регистрация
3 Май 2023
Сообщения
536
Реакции[?]
3
Поинты[?]
2K
Всем ку

Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе


Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Код:
iconst_0 <- положить 0 на вершину стека
ldc 100000 <- положить 100000 на вершину стека

iadd <- сложить два числа которые лежат на стеке

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
ldc "string" <- положить строку на стек
ldc 30 <- положить 30 на стек
invokevirtual java/lang/String.substring(I)V
->
"string".substring(30)
также используются инструкции
invokespecial - вызов конкретного метода от объекта(используется при вызове super метода/вызова конструктора класса)
invokeinterface - вызов метода интерфейса
Отдельно нужно выделить invokedynamic
invokedynamic - динамичный вызов метода.Достигается с помощью вызова bootstrap метода который определяет вызываемый метод вследствии.
<- используется после компиляции лямбда выражений, конкатенации строк в джаве >=9 версии.

Теперь нужно спарсить все классы с jar файла для модификации инструкций.Для парса можно использовать org.ow2.asm ->
Пожалуйста, авторизуйтесь для просмотра ссылки.


Написание парсера очень скучное и в целом бесполезное для освещения занятие.Поэтому пропустим этот момент и перейдем сразу же к модификации байткода.
Мы будем итерироваться по всем спаршенным ClassNode, обходить каждый MethodNode внутри и менять его инструкции на нужные нам.
Но что менять?
В данной статье будет рассмотрена модификация операций связанных с числом.
Для этого мы будем использовать булевы операции a.k.a XOR.Но использовать просто XOR не очень интересное занятие, поэтому немного зайдем в дискретную математику.
Через отрицание(NOT), конъюнкцию(AND), дизъюнкцию(ADD) можно выразить импликацию, эквиваленцию, исключающее или(XOR), штрихи шиффера и стрелку пирса.
Почему бы этим и не заняться?
Из учебника следует, что исключающее или выражается через отрицание эквивалентности(XOR - это != для двух битов), т.е в тасклист нужно добавить NOT, эквиваленцию.
Эквиваленция выражается через
Код:
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
~(~p & ~q | p & q)
Протестируем
Java:
    public static void main(String[] args) {
        int firstOperand = ThreadLocalRandom.current().nextInt();
        int secondOperand = ThreadLocalRandom.current().nextInt();
        System.out.format("%d %d\n", firstOperand, secondOperand);
        System.out.format("%d %d\n", xor(firstOperand, secondOperand), firstOperand ^ secondOperand);
    }

    public static int xor(int p, int q) {
        return ~(~p & ~q | p & q);
    }
И увидим
Код:
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
    // Может быть можно было обойтись без такой ебли со стеком
    public static void xnor(List<AbstractInsnNode> instructions) {
        instructions.add(new InsnNode(DUP2));
        not(instructions);
        instructions.add(new InsnNode(SWAP));
        not(instructions);
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(DUP_X2));
        instructions.add(new InsnNode(POP));
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(IOR));
    }

    public static void not(List<AbstractInsnNode> instructions) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        // Сломаем паттерн для декомпиляторов что б код выглядел достаточно жутко
        // и не превращал not в амперсанд
        instructions.add(new LdcInsnNode(randomValue));
        instructions.add(new LdcInsnNode(~randomValue));
        instructions.add(new InsnNode(IXOR));
        instructions.add(new InsnNode(IXOR));
    }
И сделаем xor
Java:
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

    public abstract IMathStubGenerator createMathStubGenerator();
}
А IBitwiseStubGenerator, IMathStubGenerator - интерфейсы которые обещают реализацию
Java:
public interface IBitwiseStubGenerator extends IStubGenerator {
    void xor(List<AbstractInsnNode> instructions);

    void xnor(List<AbstractInsnNode> instructions);
}
Java:
public interface IMathStubGenerator extends IStubGenerator {
    void push(List<AbstractInsnNode> instructions, int value);
}
Реализуем дефолтную фабрику
Java:
public class DefaultStubGeneratorFactory extends StubGeneratorFactory {
    @Override
    public IBitwiseStubGenerator createBitwiseStubGenerator() {
        return new BitwiseStubGenerator();
    }

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
public class MathStubGenerator implements IMathStubGenerator {
    @Override
    public void push(List<AbstractInsnNode> instructions, int value) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        int outValue = value ^ randomValue;
        instructions.add(new LdcInsnNode(outValue));
        instructions.add(new LdcInsnNode(randomValue));
        ASMUtils.xor(instructions);
    }
}
Java:
public class BitwiseStubGenerator implements IBitwiseStubGenerator {
    @Override
    public void xor(List<AbstractInsnNode> instructions) {
        ASMUtils.xor(instructions);
    }

    @Override
    public void xnor(List<AbstractInsnNode> instructions) {
        ASMUtils.xnor(instructions);
    }
}
Окей.Заменим все инструкции которые ложат числа на стек нашим пушем.
Java:
public class NumberTransformer implements ITransformer {
    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                Integer operand = ASMUtils.getIntegerOrNull(instruction);
                if (operand != null) {
                    mathStubGenerator.push(instructions, operand);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }
}
Тут нужно пояснить за ASMUtils.getIntegerOrNull.
В java байткоде операнды операций хранятся в константпуле т.е имеют ссылку
<instruction> <constant_pool_index_<index>>
где
constant_pool_index_<index> может ссылаться, например на какой либо метод или число.
Индекс константпула читается в два байта при парсинге, т.е и в байткоде он должен занимать два байта.
Но ведь нет смысла записывать например 0 или 1 в 4 байта и ещё байт на инструкцию.Поэтому в сете инструкций есть ICONST_0, ICONST_1 и тд - инструкции без операндов.
При этом также есть BIPUSH (byte integer push) которая занимает 2 байта(1 на байткод индекс и 1 на сам byte integer)
И SIPUSH(short integer push) которыя занимает 3 байта(1 байт на индекс и 2 на сам short integer)
Поэтому нам нужно правильно спарсить integer для его замены.
Java:
    public static Integer getIntegerOrNull(AbstractInsnNode node) {
        if (node.getOpcode() >= ICONST_M1 && node.getOpcode() <= ICONST_5) {
            return node.getOpcode() - ICONST_0; // Получение значения по индексу.Они упорядочены.
        } else if (node.getOpcode() == SIPUSH || node.getOpcode() == BIPUSH) {
            return ((IntInsnNode) node).operand;
        } else if (node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof Integer) {
            return (int) ((LdcInsnNode) node).cst;
        }
        return null;
    }
Код в декомпиляторе после такой модификации уже начинает выглядеть довольно жутко.
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Условные джампы.
В отличии от ассемблера, где на прыжок влияет RFLAGS, тут на все влияет значение в ToS.
Значение int`а в ToS будет означать выполнится прыжок или же нет.
Есть унарные прыжки и бинарные.
Унарный сравнивает значение ToS с нулем.(IFNE, IFEQ и тд)(if not equals zero, if equals zero)
Бинарные сравнивают два значения со стека.(IF_ICMPNE, IF_ICMPEQ)
Добавим ToS ^ 0 нашим ксором перед унарным джампом.​
Java:
public class JumpTransformer implements ITransformer {
    /*
    IFEQ = 153;
    IFNE = 154;
    IFLT = 155;
    IFGE = 156;
    IFGT = 157;
    IFLE = 158;
     */
    private static final List<Integer> INTEGER_COMPARE_JUMPS_LIST = IntStream.range(IFEQ, IFLE + 1).boxed().collect(Collectors.toList());

    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();

        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isIntegerCompareJump(instruction)) {
                    mathStubGenerator.push(instructions, 0);
                    bitwiseStubGenerator.xor(instructions);
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }

    private static boolean isIntegerCompareJump(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof JumpInsnNode && INTEGER_COMPARE_JUMPS_LIST.contains(abstractInsnNode.getOpcode());
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Также добавим try-catch блоки.
Тут тоже надо внести небольшое пояснение.
После парсинга класса он проходит этап верификации байткода.Он рассчитывает актуальный стек и локали для каждой инструкции.Когда в коде попадается какой либо условный прыжок то верификатор запускает ещё одну "нить" верификации на место прыжка.Если произойдет "коллизия" двух "нитей", то их стек и локали должны совпадать.
Когда мы попадаем в catch блок из try блока, наш стек очищается и в нем оказывается Throwable класс.Поэтому нам желательно выйти с метода в catch блоке.Сделать это можно с помощью return, но придется подбирать return подходящий для сигнатуры метода.Поэтому я использую athrow.
Код:
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
    public static ClassNode createExceptionClass(String className) {
        ClassNode classNode = new ClassNode();
        classNode.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Exception", null);
        MethodNode methodNode = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        methodNode.instructions.add(new VarInsnNode(ALOAD, 0));
        methodNode.instructions.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Exception", "<init>", "()V"));
        methodNode.instructions.add(new InsnNode(RETURN));
        classNode.methods.add(methodNode);
        return classNode;
    }
И добавить try-catch блоки в код.
Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
ну и раз уж на то пошло и у нас есть блок кода который не выполняется грех не вставить туда крашер что бы никакой декомпилятор не смог декомпилировать метод.
В джава 7 завезли инструкцию invokedynamic.Я уже рассказывал о ней выше, но повторю чуть чуть поподробнее.
Она вызывает bootstrap метод и получает оттуда Callable - оболочку над MethodHandle который в свою очередь используется для вызова метода.
invokedynamic несет в себе статичные аргументы - аргументы bootstrap метода который получит управление при первом обращении к invokedynamic.
По дефолту bootstrap метод имеет 3 аргумента(MethodHandles.Lookup, String - метод нейм, MethodType - сигнатура метода)
Но можно добавлять свои.
Я нашел забавную штучку, связанную с тем, что если декомпилятор не получит статичных аргументов в invokedynamic то он очень сильно расстроится и перестанет показывать декомпилированный код.Совсем.
Скопируем invokedynamic вызов создания трамплина в лямбда метод и удалим там статичные аргументы.(В таком случае вообще все декомпиляторы которые я знаю отваливаются)

Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InvokeDynamicInsnNode("lol", "()V", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false)));
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Ну собстна и все как бы
Пожалуйста, авторизуйтесь для просмотра ссылки.

Спасибо что прочли статью.Если есть какие то корректировки, поправьте пжлста.
Данный обфускатор не претендует на место хорошего java обфускатора и теоретически снимается аналогами VTIL/etc в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Нифига себе
 
Начинающий
Статус
Оффлайн
Регистрация
16 Дек 2023
Сообщения
515
Реакции[?]
8
Поинты[?]
4K
Всем ку

Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе


Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Код:
iconst_0 <- положить 0 на вершину стека
ldc 100000 <- положить 100000 на вершину стека

iadd <- сложить два числа которые лежат на стеке

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
ldc "string" <- положить строку на стек
ldc 30 <- положить 30 на стек
invokevirtual java/lang/String.substring(I)V
->
"string".substring(30)
также используются инструкции
invokespecial - вызов конкретного метода от объекта(используется при вызове super метода/вызова конструктора класса)
invokeinterface - вызов метода интерфейса
Отдельно нужно выделить invokedynamic
invokedynamic - динамичный вызов метода.Достигается с помощью вызова bootstrap метода который определяет вызываемый метод вследствии.
<- используется после компиляции лямбда выражений, конкатенации строк в джаве >=9 версии.

Теперь нужно спарсить все классы с jar файла для модификации инструкций.Для парса можно использовать org.ow2.asm ->
Пожалуйста, авторизуйтесь для просмотра ссылки.


Написание парсера очень скучное и в целом бесполезное для освещения занятие.Поэтому пропустим этот момент и перейдем сразу же к модификации байткода.
Мы будем итерироваться по всем спаршенным ClassNode, обходить каждый MethodNode внутри и менять его инструкции на нужные нам.
Но что менять?
В данной статье будет рассмотрена модификация операций связанных с числом.
Для этого мы будем использовать булевы операции a.k.a XOR.Но использовать просто XOR не очень интересное занятие, поэтому немного зайдем в дискретную математику.
Через отрицание(NOT), конъюнкцию(AND), дизъюнкцию(ADD) можно выразить импликацию, эквиваленцию, исключающее или(XOR), штрихи шиффера и стрелку пирса.
Почему бы этим и не заняться?
Из учебника следует, что исключающее или выражается через отрицание эквивалентности(XOR - это != для двух битов), т.е в тасклист нужно добавить NOT, эквиваленцию.
Эквиваленция выражается через
Код:
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
~(~p & ~q | p & q)
Протестируем
Java:
    public static void main(String[] args) {
        int firstOperand = ThreadLocalRandom.current().nextInt();
        int secondOperand = ThreadLocalRandom.current().nextInt();
        System.out.format("%d %d\n", firstOperand, secondOperand);
        System.out.format("%d %d\n", xor(firstOperand, secondOperand), firstOperand ^ secondOperand);
    }

    public static int xor(int p, int q) {
        return ~(~p & ~q | p & q);
    }
И увидим
Код:
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
    // Может быть можно было обойтись без такой ебли со стеком
    public static void xnor(List<AbstractInsnNode> instructions) {
        instructions.add(new InsnNode(DUP2));
        not(instructions);
        instructions.add(new InsnNode(SWAP));
        not(instructions);
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(DUP_X2));
        instructions.add(new InsnNode(POP));
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(IOR));
    }

    public static void not(List<AbstractInsnNode> instructions) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        // Сломаем паттерн для декомпиляторов что б код выглядел достаточно жутко
        // и не превращал not в амперсанд
        instructions.add(new LdcInsnNode(randomValue));
        instructions.add(new LdcInsnNode(~randomValue));
        instructions.add(new InsnNode(IXOR));
        instructions.add(new InsnNode(IXOR));
    }
И сделаем xor
Java:
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

    public abstract IMathStubGenerator createMathStubGenerator();
}
А IBitwiseStubGenerator, IMathStubGenerator - интерфейсы которые обещают реализацию
Java:
public interface IBitwiseStubGenerator extends IStubGenerator {
    void xor(List<AbstractInsnNode> instructions);

    void xnor(List<AbstractInsnNode> instructions);
}
Java:
public interface IMathStubGenerator extends IStubGenerator {
    void push(List<AbstractInsnNode> instructions, int value);
}
Реализуем дефолтную фабрику
Java:
public class DefaultStubGeneratorFactory extends StubGeneratorFactory {
    @Override
    public IBitwiseStubGenerator createBitwiseStubGenerator() {
        return new BitwiseStubGenerator();
    }

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
public class MathStubGenerator implements IMathStubGenerator {
    @Override
    public void push(List<AbstractInsnNode> instructions, int value) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        int outValue = value ^ randomValue;
        instructions.add(new LdcInsnNode(outValue));
        instructions.add(new LdcInsnNode(randomValue));
        ASMUtils.xor(instructions);
    }
}
Java:
public class BitwiseStubGenerator implements IBitwiseStubGenerator {
    @Override
    public void xor(List<AbstractInsnNode> instructions) {
        ASMUtils.xor(instructions);
    }

    @Override
    public void xnor(List<AbstractInsnNode> instructions) {
        ASMUtils.xnor(instructions);
    }
}
Окей.Заменим все инструкции которые ложат числа на стек нашим пушем.
Java:
public class NumberTransformer implements ITransformer {
    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                Integer operand = ASMUtils.getIntegerOrNull(instruction);
                if (operand != null) {
                    mathStubGenerator.push(instructions, operand);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }
}
Тут нужно пояснить за ASMUtils.getIntegerOrNull.
В java байткоде операнды операций хранятся в константпуле т.е имеют ссылку
<instruction> <constant_pool_index_<index>>
где
constant_pool_index_<index> может ссылаться, например на какой либо метод или число.
Индекс константпула читается в два байта при парсинге, т.е и в байткоде он должен занимать два байта.
Но ведь нет смысла записывать например 0 или 1 в 4 байта и ещё байт на инструкцию.Поэтому в сете инструкций есть ICONST_0, ICONST_1 и тд - инструкции без операндов.
При этом также есть BIPUSH (byte integer push) которая занимает 2 байта(1 на байткод индекс и 1 на сам byte integer)
И SIPUSH(short integer push) которыя занимает 3 байта(1 байт на индекс и 2 на сам short integer)
Поэтому нам нужно правильно спарсить integer для его замены.
Java:
    public static Integer getIntegerOrNull(AbstractInsnNode node) {
        if (node.getOpcode() >= ICONST_M1 && node.getOpcode() <= ICONST_5) {
            return node.getOpcode() - ICONST_0; // Получение значения по индексу.Они упорядочены.
        } else if (node.getOpcode() == SIPUSH || node.getOpcode() == BIPUSH) {
            return ((IntInsnNode) node).operand;
        } else if (node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof Integer) {
            return (int) ((LdcInsnNode) node).cst;
        }
        return null;
    }
Код в декомпиляторе после такой модификации уже начинает выглядеть довольно жутко.
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Условные джампы.
В отличии от ассемблера, где на прыжок влияет RFLAGS, тут на все влияет значение в ToS.
Значение int`а в ToS будет означать выполнится прыжок или же нет.
Есть унарные прыжки и бинарные.
Унарный сравнивает значение ToS с нулем.(IFNE, IFEQ и тд)(if not equals zero, if equals zero)
Бинарные сравнивают два значения со стека.(IF_ICMPNE, IF_ICMPEQ)
Добавим ToS ^ 0 нашим ксором перед унарным джампом.​
Java:
public class JumpTransformer implements ITransformer {
    /*
    IFEQ = 153;
    IFNE = 154;
    IFLT = 155;
    IFGE = 156;
    IFGT = 157;
    IFLE = 158;
     */
    private static final List<Integer> INTEGER_COMPARE_JUMPS_LIST = IntStream.range(IFEQ, IFLE + 1).boxed().collect(Collectors.toList());

    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();

        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isIntegerCompareJump(instruction)) {
                    mathStubGenerator.push(instructions, 0);
                    bitwiseStubGenerator.xor(instructions);
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }

    private static boolean isIntegerCompareJump(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof JumpInsnNode && INTEGER_COMPARE_JUMPS_LIST.contains(abstractInsnNode.getOpcode());
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Также добавим try-catch блоки.
Тут тоже надо внести небольшое пояснение.
После парсинга класса он проходит этап верификации байткода.Он рассчитывает актуальный стек и локали для каждой инструкции.Когда в коде попадается какой либо условный прыжок то верификатор запускает ещё одну "нить" верификации на место прыжка.Если произойдет "коллизия" двух "нитей", то их стек и локали должны совпадать.
Когда мы попадаем в catch блок из try блока, наш стек очищается и в нем оказывается Throwable класс.Поэтому нам желательно выйти с метода в catch блоке.Сделать это можно с помощью return, но придется подбирать return подходящий для сигнатуры метода.Поэтому я использую athrow.
Код:
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
    public static ClassNode createExceptionClass(String className) {
        ClassNode classNode = new ClassNode();
        classNode.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Exception", null);
        MethodNode methodNode = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        methodNode.instructions.add(new VarInsnNode(ALOAD, 0));
        methodNode.instructions.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Exception", "<init>", "()V"));
        methodNode.instructions.add(new InsnNode(RETURN));
        classNode.methods.add(methodNode);
        return classNode;
    }
И добавить try-catch блоки в код.
Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
ну и раз уж на то пошло и у нас есть блок кода который не выполняется грех не вставить туда крашер что бы никакой декомпилятор не смог декомпилировать метод.
В джава 7 завезли инструкцию invokedynamic.Я уже рассказывал о ней выше, но повторю чуть чуть поподробнее.
Она вызывает bootstrap метод и получает оттуда Callable - оболочку над MethodHandle который в свою очередь используется для вызова метода.
invokedynamic несет в себе статичные аргументы - аргументы bootstrap метода который получит управление при первом обращении к invokedynamic.
По дефолту bootstrap метод имеет 3 аргумента(MethodHandles.Lookup, String - метод нейм, MethodType - сигнатура метода)
Но можно добавлять свои.
Я нашел забавную штучку, связанную с тем, что если декомпилятор не получит статичных аргументов в invokedynamic то он очень сильно расстроится и перестанет показывать декомпилированный код.Совсем.
Скопируем invokedynamic вызов создания трамплина в лямбда метод и удалим там статичные аргументы.(В таком случае вообще все декомпиляторы которые я знаю отваливаются)

Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InvokeDynamicInsnNode("lol", "()V", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false)));
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Ну собстна и все как бы
Пожалуйста, авторизуйтесь для просмотра ссылки.

Спасибо что прочли статью.Если есть какие то корректировки, поправьте пжлста.
Данный обфускатор не претендует на место хорошего java обфускатора и теоретически снимается аналогами VTIL/etc в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Залей пожалуйста данный обфускатор,хочу из своей джарки сделать фарш...
 
Начинающий
Статус
Оффлайн
Регистрация
4 Дек 2021
Сообщения
132
Реакции[?]
6
Поинты[?]
3K
Всем ку

Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе


Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Код:
iconst_0 <- положить 0 на вершину стека
ldc 100000 <- положить 100000 на вершину стека

iadd <- сложить два числа которые лежат на стеке

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
ldc "string" <- положить строку на стек
ldc 30 <- положить 30 на стек
invokevirtual java/lang/String.substring(I)V
->
"string".substring(30)
также используются инструкции
invokespecial - вызов конкретного метода от объекта(используется при вызове super метода/вызова конструктора класса)
invokeinterface - вызов метода интерфейса
Отдельно нужно выделить invokedynamic
invokedynamic - динамичный вызов метода.Достигается с помощью вызова bootstrap метода который определяет вызываемый метод вследствии.
<- используется после компиляции лямбда выражений, конкатенации строк в джаве >=9 версии.

Теперь нужно спарсить все классы с jar файла для модификации инструкций.Для парса можно использовать org.ow2.asm ->
Пожалуйста, авторизуйтесь для просмотра ссылки.


Написание парсера очень скучное и в целом бесполезное для освещения занятие.Поэтому пропустим этот момент и перейдем сразу же к модификации байткода.
Мы будем итерироваться по всем спаршенным ClassNode, обходить каждый MethodNode внутри и менять его инструкции на нужные нам.
Но что менять?
В данной статье будет рассмотрена модификация операций связанных с числом.
Для этого мы будем использовать булевы операции a.k.a XOR.Но использовать просто XOR не очень интересное занятие, поэтому немного зайдем в дискретную математику.
Через отрицание(NOT), конъюнкцию(AND), дизъюнкцию(ADD) можно выразить импликацию, эквиваленцию, исключающее или(XOR), штрихи шиффера и стрелку пирса.
Почему бы этим и не заняться?
Из учебника следует, что исключающее или выражается через отрицание эквивалентности(XOR - это != для двух битов), т.е в тасклист нужно добавить NOT, эквиваленцию.
Эквиваленция выражается через
Код:
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
~(~p & ~q | p & q)
Протестируем
Java:
    public static void main(String[] args) {
        int firstOperand = ThreadLocalRandom.current().nextInt();
        int secondOperand = ThreadLocalRandom.current().nextInt();
        System.out.format("%d %d\n", firstOperand, secondOperand);
        System.out.format("%d %d\n", xor(firstOperand, secondOperand), firstOperand ^ secondOperand);
    }

    public static int xor(int p, int q) {
        return ~(~p & ~q | p & q);
    }
И увидим
Код:
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
    // Может быть можно было обойтись без такой ебли со стеком
    public static void xnor(List<AbstractInsnNode> instructions) {
        instructions.add(new InsnNode(DUP2));
        not(instructions);
        instructions.add(new InsnNode(SWAP));
        not(instructions);
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(DUP_X2));
        instructions.add(new InsnNode(POP));
        instructions.add(new InsnNode(IAND));
        instructions.add(new InsnNode(IOR));
    }

    public static void not(List<AbstractInsnNode> instructions) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        // Сломаем паттерн для декомпиляторов что б код выглядел достаточно жутко
        // и не превращал not в амперсанд
        instructions.add(new LdcInsnNode(randomValue));
        instructions.add(new LdcInsnNode(~randomValue));
        instructions.add(new InsnNode(IXOR));
        instructions.add(new InsnNode(IXOR));
    }
И сделаем xor
Java:
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

    public abstract IMathStubGenerator createMathStubGenerator();
}
А IBitwiseStubGenerator, IMathStubGenerator - интерфейсы которые обещают реализацию
Java:
public interface IBitwiseStubGenerator extends IStubGenerator {
    void xor(List<AbstractInsnNode> instructions);

    void xnor(List<AbstractInsnNode> instructions);
}
Java:
public interface IMathStubGenerator extends IStubGenerator {
    void push(List<AbstractInsnNode> instructions, int value);
}
Реализуем дефолтную фабрику
Java:
public class DefaultStubGeneratorFactory extends StubGeneratorFactory {
    @Override
    public IBitwiseStubGenerator createBitwiseStubGenerator() {
        return new BitwiseStubGenerator();
    }

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
public class MathStubGenerator implements IMathStubGenerator {
    @Override
    public void push(List<AbstractInsnNode> instructions, int value) {
        int randomValue = ThreadLocalRandom.current().nextInt();
        int outValue = value ^ randomValue;
        instructions.add(new LdcInsnNode(outValue));
        instructions.add(new LdcInsnNode(randomValue));
        ASMUtils.xor(instructions);
    }
}
Java:
public class BitwiseStubGenerator implements IBitwiseStubGenerator {
    @Override
    public void xor(List<AbstractInsnNode> instructions) {
        ASMUtils.xor(instructions);
    }

    @Override
    public void xnor(List<AbstractInsnNode> instructions) {
        ASMUtils.xnor(instructions);
    }
}
Окей.Заменим все инструкции которые ложат числа на стек нашим пушем.
Java:
public class NumberTransformer implements ITransformer {
    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                Integer operand = ASMUtils.getIntegerOrNull(instruction);
                if (operand != null) {
                    mathStubGenerator.push(instructions, operand);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }
}
Тут нужно пояснить за ASMUtils.getIntegerOrNull.
В java байткоде операнды операций хранятся в константпуле т.е имеют ссылку
<instruction> <constant_pool_index_<index>>
где
constant_pool_index_<index> может ссылаться, например на какой либо метод или число.
Индекс константпула читается в два байта при парсинге, т.е и в байткоде он должен занимать два байта.
Но ведь нет смысла записывать например 0 или 1 в 4 байта и ещё байт на инструкцию.Поэтому в сете инструкций есть ICONST_0, ICONST_1 и тд - инструкции без операндов.
При этом также есть BIPUSH (byte integer push) которая занимает 2 байта(1 на байткод индекс и 1 на сам byte integer)
И SIPUSH(short integer push) которыя занимает 3 байта(1 байт на индекс и 2 на сам short integer)
Поэтому нам нужно правильно спарсить integer для его замены.
Java:
    public static Integer getIntegerOrNull(AbstractInsnNode node) {
        if (node.getOpcode() >= ICONST_M1 && node.getOpcode() <= ICONST_5) {
            return node.getOpcode() - ICONST_0; // Получение значения по индексу.Они упорядочены.
        } else if (node.getOpcode() == SIPUSH || node.getOpcode() == BIPUSH) {
            return ((IntInsnNode) node).operand;
        } else if (node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof Integer) {
            return (int) ((LdcInsnNode) node).cst;
        }
        return null;
    }
Код в декомпиляторе после такой модификации уже начинает выглядеть довольно жутко.
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Условные джампы.
В отличии от ассемблера, где на прыжок влияет RFLAGS, тут на все влияет значение в ToS.
Значение int`а в ToS будет означать выполнится прыжок или же нет.
Есть унарные прыжки и бинарные.
Унарный сравнивает значение ToS с нулем.(IFNE, IFEQ и тд)(if not equals zero, if equals zero)
Бинарные сравнивают два значения со стека.(IF_ICMPNE, IF_ICMPEQ)
Добавим ToS ^ 0 нашим ксором перед унарным джампом.​
Java:
public class JumpTransformer implements ITransformer {
    /*
    IFEQ = 153;
    IFNE = 154;
    IFLT = 155;
    IFGE = 156;
    IFGT = 157;
    IFLE = 158;
     */
    private static final List<Integer> INTEGER_COMPARE_JUMPS_LIST = IntStream.range(IFEQ, IFLE + 1).boxed().collect(Collectors.toList());

    @Override
    public void process(TransformerContext transformerContext) {
        StubGeneratorFactory stubGeneratorFactory = transformerContext.stubGeneratorFactory;
        IBitwiseStubGenerator bitwiseStubGenerator = stubGeneratorFactory.createBitwiseStubGenerator();
        IMathStubGenerator mathStubGenerator = stubGeneratorFactory.createMathStubGenerator();

        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.forEach(classNode -> classNode.methods.forEach(methodNode -> {
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isIntegerCompareJump(instruction)) {
                    mathStubGenerator.push(instructions, 0);
                    bitwiseStubGenerator.xor(instructions);
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
        }));
    }

    private static boolean isIntegerCompareJump(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof JumpInsnNode && INTEGER_COMPARE_JUMPS_LIST.contains(abstractInsnNode.getOpcode());
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
Также добавим try-catch блоки.
Тут тоже надо внести небольшое пояснение.
После парсинга класса он проходит этап верификации байткода.Он рассчитывает актуальный стек и локали для каждой инструкции.Когда в коде попадается какой либо условный прыжок то верификатор запускает ещё одну "нить" верификации на место прыжка.Если произойдет "коллизия" двух "нитей", то их стек и локали должны совпадать.
Когда мы попадаем в catch блок из try блока, наш стек очищается и в нем оказывается Throwable класс.Поэтому нам желательно выйти с метода в catch блоке.Сделать это можно с помощью return, но придется подбирать return подходящий для сигнатуры метода.Поэтому я использую athrow.
Код:
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
    public static ClassNode createExceptionClass(String className) {
        ClassNode classNode = new ClassNode();
        classNode.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Exception", null);
        MethodNode methodNode = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null);
        methodNode.instructions.add(new VarInsnNode(ALOAD, 0));
        methodNode.instructions.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Exception", "<init>", "()V"));
        methodNode.instructions.add(new InsnNode(RETURN));
        classNode.methods.add(methodNode);
        return classNode;
    }
И добавить try-catch блоки в код.
Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Пожалуйста, авторизуйтесь для просмотра ссылки.
->
Пожалуйста, авторизуйтесь для просмотра ссылки.
ну и раз уж на то пошло и у нас есть блок кода который не выполняется грех не вставить туда крашер что бы никакой декомпилятор не смог декомпилировать метод.
В джава 7 завезли инструкцию invokedynamic.Я уже рассказывал о ней выше, но повторю чуть чуть поподробнее.
Она вызывает bootstrap метод и получает оттуда Callable - оболочку над MethodHandle который в свою очередь используется для вызова метода.
invokedynamic несет в себе статичные аргументы - аргументы bootstrap метода который получит управление при первом обращении к invokedynamic.
По дефолту bootstrap метод имеет 3 аргумента(MethodHandles.Lookup, String - метод нейм, MethodType - сигнатура метода)
Но можно добавлять свои.
Я нашел забавную штучку, связанную с тем, что если декомпилятор не получит статичных аргументов в invokedynamic то он очень сильно расстроится и перестанет показывать декомпилированный код.Совсем.
Скопируем invokedynamic вызов создания трамплина в лямбда метод и удалим там статичные аргументы.(В таком случае вообще все декомпиляторы которые я знаю отваливаются)

Java:
public class TryCatchTransformer implements ITransformer {
    private static final String EXCEPTION_CLASS_NAME = "exception_class";
    private static final ClassNode EXCEPTION_CLASS = ASMUtils.createExceptionClass(EXCEPTION_CLASS_NAME);

    @Override
    public void process(TransformerContext transformerContext) {
        List<ClassNode> classNodes = transformerContext.classNodes;
        classNodes.add(EXCEPTION_CLASS);
        classNodes.forEach(classNode -> classNode.methods.stream().filter(new InitializationFilter()).forEach(methodNode -> {
            List<TryCatchBlockNode> tryCatches = new ArrayList<>();
            List<AbstractInsnNode> instructions = new ArrayList<>();
            for (AbstractInsnNode instruction : methodNode.instructions) {
                if (isValidInstructionForAddTryCatchBlock(instruction)) {
                    LabelNode start = new LabelNode();
                    LabelNode end = new LabelNode();
                    LabelNode handler = new LabelNode();
                    LabelNode exit = new LabelNode();
                    TryCatchBlockNode tryCatchBlockNode = new TryCatchBlockNode(start, end, handler, EXCEPTION_CLASS_NAME);
                    tryCatches.add(tryCatchBlockNode);
                    instructions.add(start);
                    instructions.add(instruction);
                    instructions.add(end);
                    instructions.add(new JumpInsnNode(GOTO, exit));
                    instructions.add(handler);
                    instructions.add(new InvokeDynamicInsnNode("lol", "()V", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", false)));
                    instructions.add(new InsnNode(ATHROW));
                    instructions.add(exit);
                    continue;
                }
                instructions.add(instruction);
            }
            methodNode.instructions.clear();
            instructions.forEach(methodNode.instructions::add);
            methodNode.tryCatchBlocks.addAll(tryCatches);
        }));
    }

    private static boolean isValidInstructionForAddTryCatchBlock(AbstractInsnNode abstractInsnNode) {
        return abstractInsnNode instanceof MethodInsnNode || abstractInsnNode instanceof FieldInsnNode;
    }
}
Ну собстна и все как бы
Пожалуйста, авторизуйтесь для просмотра ссылки.

Спасибо что прочли статью.Если есть какие то корректировки, поправьте пжлста.
Данный обфускатор не претендует на место хорошего java обфускатора и теоретически снимается аналогами VTIL/etc в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Можно сурс?
 
Сверху Снизу