эксперт в майнкрафт апи
-
Автор темы
- #1
Всем ку
Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе
Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Вызовы метода происходят через инструкции invokestatic, invokeinterface, invokevirtual, invokespecial, invokedynamic
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Если требуется receiver для конкретизации виртуального метода, то он должен лежать за аргументами метода на стеке
также используются инструкции
invokespecial - вызов конкретного метода от объекта(используется при вызове super метода/вызова конструктора класса)
invokeinterface - вызов метода интерфейса
Отдельно нужно выделить invokedynamic
invokedynamic - динамичный вызов метода.Достигается с помощью вызова bootstrap метода который определяет вызываемый метод вследствии.
<- используется после компиляции лямбда выражений, конкатенации строк в джаве >=9 версии.
Теперь нужно спарсить все классы с jar файла для модификации инструкций.Для парса можно использовать org.ow2.asm ->
Написание парсера очень скучное и в целом бесполезное для освещения занятие.Поэтому пропустим этот момент и перейдем сразу же к модификации байткода.
Мы будем итерироваться по всем спаршенным ClassNode, обходить каждый MethodNode внутри и менять его инструкции на нужные нам.
Но что менять?
В данной статье будет рассмотрена модификация операций связанных с числом.
Для этого мы будем использовать булевы операции a.k.a XOR.Но использовать просто XOR не очень интересное занятие, поэтому немного зайдем в дискретную математику.
Через отрицание(NOT), конъюнкцию(AND), дизъюнкцию(ADD) можно выразить импликацию, эквиваленцию, исключающее или(XOR), штрихи шиффера и стрелку пирса.
Почему бы этим и не заняться?
Из учебника следует, что исключающее или выражается через отрицание эквивалентности(XOR - это != для двух битов), т.е в тасклист нужно добавить NOT, эквиваленцию.
Эквиваленция выражается через
А отрицание эквиваленции будет выглядеть как
Протестируем
И увидим
А это значит, что все работает.
Но не все так просто.
jvm не имеет на борту сета инструкций NOT.
Интересно.Ведь синтаксисом она поддерживается, а значит является синтаксическим сахаром.
Реализация not это
Сделаем методы которые будет нам крафтить сет инструкций для not, а также для xnor(эквивалентности)
И сделаем xor
Теперь осталось все это прикрепить к парсеру.Сразу же сделаем более гибкую систему.Сейчас опишу ее.
У нас будет абстракция - StubGenerator который будет генерировать паттерны какой либо задачности(побитовые, математические).
StubGenerator`ы будут определятся абстрактной фабрикой, т.е
А IBitwiseStubGenerator, IMathStubGenerator - интерфейсы которые обещают реализацию
Реализуем дефолтную фабрику
А потом и сами стаб генераторы
Окей.Заменим все инструкции которые ложат числа на стек нашим пушем.
Тут нужно пояснить за 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 для его замены.
Код в декомпиляторе после такой модификации уже начинает выглядеть довольно жутко.
В отличии от ассемблера, где на прыжок влияет RFLAGS, тут на все влияет значение в ToS.
Значение int`а в ToS будет означать выполнится прыжок или же нет.
Есть унарные прыжки и бинарные.
Унарный сравнивает значение ToS с нулем.(IFNE, IFEQ и тд)(if not equals zero, if equals zero)
Бинарные сравнивают два значения со стека.(IF_ICMPNE, IF_ICMPEQ)
Добавим ToS ^ 0 нашим ксором перед унарным джампом.
Сегодня мы
Сделаем "декомпозицию" булевых операций что бы превратить код в фарш
Изуродуем методы
Сделаем так что бы классы вообще не открывались в декомпиляторе
Что следует знать про jvm интерпретатор
jvm интепретатор хранит информацию в двух местах - локали, стек.
Локали выступают в виде локальных переменных, стек используется для работы над какими либо данными.
Элемент локали/стека занимает 4 байта.Wide типы(Double/Long) записываются через 2 элемента стека/локалей.
Чаще всего операции происходят со стеком, что позволяет сжать кол-во нужных инструкций до ~200 и записывать их в один байт.
Код:
iconst_0 <- положить 0 на вершину стека
ldc 100000 <- положить 100000 на вершину стека
iadd <- сложить два числа которые лежат на стеке
на стеке окажется одно число - 100000
ToS - вершина стека/Top of Stack
Аргументы передаются в обратном направлении с ToS.
Код:
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 это
Java:
p ^ 0xffffffff
// aka
push p
push 0xffffffff
xor
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));
}
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();
}
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);
}));
}
}
В 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.
Код:
Теоретически, в джаве возможно сделать прыжок хоть на какой оффсет в байткоде.
Надо лишь воссоздать стек и локали того места куда ты собрался прыгать.
Никакой декомпилятор такой прыжок не разберет.
Но для такого финта нужно сделать калькулятор стека и локалей.
Он вроде даже где то есть на гите.
Но в данной статье такой прыжок разобран не будет.
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 в джава мире.
Если кому то нужен сурскод, то я могу загрузить его в данную тему.
Последнее редактирование: