• Я зарабатываю 100 000 RUB / месяц на этом сайте!

    А знаешь как? Я всего-лишь публикую (создаю темы), а админ мне платит. Трачу деньги на мороженое, робуксы и сервера в Minecraft. А ещё на паль из Китая. 

    Хочешь так же? Пиши и узнавай условия: https://t.me/alex_redact
    Реклама: https://t.me/yougame_official

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

эксперт в майнкрафт апи
Участник
Участник
Статус
Оффлайн
Регистрация
25 Янв 2023
Сообщения
675
Реакции
285
Всем ку

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


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

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

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
Expand Collapse Copy
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
Expand Collapse Copy
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, эквиваленцию.
Эквиваленция выражается через
Код:
Expand Collapse Copy
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
Expand Collapse Copy
~(~p & ~q | p & q)
Протестируем
Java:
Expand Collapse Copy
    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);
    }
И увидим
Код:
Expand Collapse Copy
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
Expand Collapse Copy
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
Expand Collapse Copy
    // Может быть можно было обойтись без такой ебли со стеком
    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:
Expand Collapse Copy
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
Expand Collapse Copy
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

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

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

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
    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:
Expand Collapse Copy
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.
Код:
Expand Collapse Copy
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
Expand Collapse Copy
    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:
Expand Collapse Copy
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:
Expand Collapse Copy
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 в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
 
Последнее редактирование:
Всем ку

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


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

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

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
Expand Collapse Copy
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
Expand Collapse Copy
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, эквиваленцию.
Эквиваленция выражается через
Код:
Expand Collapse Copy
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
Expand Collapse Copy
~(~p & ~q | p & q)
Протестируем
Java:
Expand Collapse Copy
    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);
    }
И увидим
Код:
Expand Collapse Copy
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
Expand Collapse Copy
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
Expand Collapse Copy
    // Может быть можно было обойтись без такой ебли со стеком
    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:
Expand Collapse Copy
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
Expand Collapse Copy
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

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

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

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
    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:
Expand Collapse Copy
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.
Код:
Expand Collapse Copy
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
Expand Collapse Copy
    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:
Expand Collapse Copy
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:
Expand Collapse Copy
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 в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Жосткий +реп
 
мотематик вонючийй
 
теперь твоя паста будет не отломана молись.... на нее.. проси что бы скинул обфу.
Что бля
теперь твоя паста будет не отломана молись.... на нее.. проси что бы скинул обфу.
Я просто написал харош челу за годные туторы
 
Всем ку

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


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

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

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
Expand Collapse Copy
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
Expand Collapse Copy
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, эквиваленцию.
Эквиваленция выражается через
Код:
Expand Collapse Copy
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
Expand Collapse Copy
~(~p & ~q | p & q)
Протестируем
Java:
Expand Collapse Copy
    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);
    }
И увидим
Код:
Expand Collapse Copy
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
Expand Collapse Copy
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
Expand Collapse Copy
    // Может быть можно было обойтись без такой ебли со стеком
    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:
Expand Collapse Copy
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
Expand Collapse Copy
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

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

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

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
    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:
Expand Collapse Copy
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.
Код:
Expand Collapse Copy
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
Expand Collapse Copy
    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:
Expand Collapse Copy
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:
Expand Collapse Copy
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 в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Для пастеров слишком сложно
 
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Я не играю в Кубы хвх??????
 
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
XOR - это != для двух битов
чего нахуй
Никакой декомпилятор такой прыжок не разберет.
Не поддерживается текущими, или это физически невозможно? Ведь если ты толкаешь инструкции на ребилд стека на сам стек то это и в декомпиле видно будет..
Приятное чтиво, не знал что в жвм нет not инсна. +rep
 
ну что то на уровне
Код:
Expand Collapse Copy
bool firstBit = false;
bool secondBit = false;
bool xor = firstBit != secondBit;
Не поддерживается текущими, или это физически невозможно
не нашел декомпилятора который сейчас поддерживает адекватную декомпиляцию такого кода.большинство просто крашатся.физически возможно...
Ведь если ты толкаешь инструкции на ребилд стека на сам стек то это и в декомпиле видно будет..
это не меняет сути и нужно лишь для того что бы наебать java verifier.
 
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Всем ку

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


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

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

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
Expand Collapse Copy
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
Expand Collapse Copy
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, эквиваленцию.
Эквиваленция выражается через
Код:
Expand Collapse Copy
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
Expand Collapse Copy
~(~p & ~q | p & q)
Протестируем
Java:
Expand Collapse Copy
    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);
    }
И увидим
Код:
Expand Collapse Copy
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
Expand Collapse Copy
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
Expand Collapse Copy
    // Может быть можно было обойтись без такой ебли со стеком
    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:
Expand Collapse Copy
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
Expand Collapse Copy
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

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

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

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
    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:
Expand Collapse Copy
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.
Код:
Expand Collapse Copy
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
Expand Collapse Copy
    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:
Expand Collapse Copy
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:
Expand Collapse Copy
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 в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
сурсы дай пж
 
Всем ку

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


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

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

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
Expand Collapse Copy
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
Expand Collapse Copy
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, эквиваленцию.
Эквиваленция выражается через
Код:
Expand Collapse Copy
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
Expand Collapse Copy
~(~p & ~q | p & q)
Протестируем
Java:
Expand Collapse Copy
    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);
    }
И увидим
Код:
Expand Collapse Copy
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
Expand Collapse Copy
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
Expand Collapse Copy
    // Может быть можно было обойтись без такой ебли со стеком
    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:
Expand Collapse Copy
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
Expand Collapse Copy
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

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

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

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
    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:
Expand Collapse Copy
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.
Код:
Expand Collapse Copy
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
Expand Collapse Copy
    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:
Expand Collapse Copy
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:
Expand Collapse Copy
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 в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Нифига себе
 
Всем ку

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


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

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

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
Expand Collapse Copy
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
Expand Collapse Copy
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, эквиваленцию.
Эквиваленция выражается через
Код:
Expand Collapse Copy
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
Expand Collapse Copy
~(~p & ~q | p & q)
Протестируем
Java:
Expand Collapse Copy
    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);
    }
И увидим
Код:
Expand Collapse Copy
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
Expand Collapse Copy
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
Expand Collapse Copy
    // Может быть можно было обойтись без такой ебли со стеком
    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:
Expand Collapse Copy
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
Expand Collapse Copy
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

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

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

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
    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:
Expand Collapse Copy
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.
Код:
Expand Collapse Copy
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
Expand Collapse Copy
    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:
Expand Collapse Copy
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:
Expand Collapse Copy
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 в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Залей пожалуйста данный обфускатор,хочу из своей джарки сделать фарш...
 
Всем ку

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


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

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

на стеке окажется одно число - 100000
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
Expand Collapse Copy
iconst_0
iconst_1
invokestatic Class.method (II)V
->
Class.method(0, 1);
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
Код:
Expand Collapse Copy
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, эквиваленцию.
Эквиваленция выражается через
Код:
Expand Collapse Copy
~p & ~q | p & q
А отрицание эквиваленции будет выглядеть как
Код:
Expand Collapse Copy
~(~p & ~q | p & q)
Протестируем
Java:
Expand Collapse Copy
    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);
    }
И увидим
Код:
Expand Collapse Copy
-833861829 -731571016

438918019 438918019
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Java:
Expand Collapse Copy
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
Java:
Expand Collapse Copy
    // Может быть можно было обойтись без такой ебли со стеком
    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:
Expand Collapse Copy
    public static void xor(List<AbstractInsnNode> instructions) {
        xnor(instructions);
        not(instructions);
    }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
Java:
Expand Collapse Copy
public abstract class StubGeneratorFactory {
    public abstract IBitwiseStubGenerator createBitwiseStubGenerator();

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

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

    @Override
    public IMathStubGenerator createMathStubGenerator() {
        return new MathStubGenerator();
    }
}
А потом и сами стаб генераторы
Java:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
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:
Expand Collapse Copy
    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:
Expand Collapse Copy
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.
Код:
Expand Collapse Copy
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
Нам нужно добавить свой exception что бы случайно не попасть в catch блок.
Java:
Expand Collapse Copy
    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:
Expand Collapse Copy
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:
Expand Collapse Copy
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 в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Можно сурс?
 
Назад
Сверху Снизу