(◣_◢)
куда тебе сурс пастар ты ничего не поймеш што тамМожно сурс?
куда тебе сурс пастар ты ничего не поймеш што тамМожно сурс?
дай фуллВсем ку
Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе
Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamicКод:iconst_0 <- положить 0 на вершину стека ldc 100000 <- положить 100000 на вершину стека iadd <- сложить два числа которые лежат на стеке на стеке окажется одно число - 100000
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стекеКод:iconst_0 iconst_1 invokestatic Class.method (II)V -> Class.method(0, 1);
также используются инструкцииКод: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 это
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)Java:p ^ 0xffffffff // aka push p push 0xffffffff xor
И сделаем xorJava:// Может быть можно было обойтись без такой ебли со стеком 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)); }
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.Java:public static void xor(List<AbstractInsnNode> instructions) { xnor(instructions); not(instructions); }
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
А IBitwiseStubGenerator, IMathStubGenerator - интерфейсы которые обещают реализациюJava:public abstract class StubGeneratorFactory { public abstract IBitwiseStubGenerator createBitwiseStubGenerator(); public abstract IMathStubGenerator createMathStubGenerator(); }
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); } }
Тут нужно пояснить за ASMUtils.getIntegerOrNull.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); })); } }
В 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 блок.Код:Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде. Надо лишь воссоздать стек и локали того места куда ты собрался прыгать. Никакой декомпилятор такой прыжок не разберет. Но для такого финта нужно сделать калькулятор стека и локалей. Он вроде даже где то есть на гите. Но в данной статье такой прыжок разобран не будет.
И добавить try-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; }
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 в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Проект предоставляет различный материал, относящийся к сфере киберспорта, программирования, ПО для игр, а также позволяет его участникам общаться на многие другие темы. Почта для жалоб: admin@yougame.biz