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

Новичок
Статус
Оффлайн
Регистрация
8 Июл 2024
Сообщения
1
Реакции[?]
0
Поинты[?]
0
Очень плотно сжатый материал, суть интересная. Нужно изучить asm библиотеку. Если отправляешь сорцы, буду рад.

спасибо за инфу, мне нравится подход proguard хоть это и не особо обфускатор, больше стриппер
 
Последнее редактирование:
Начинающий
Статус
Оффлайн
Регистрация
10 Июн 2022
Сообщения
51
Реакции[?]
1
Поинты[?]
1K
Всем ку

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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