PoC Life
-
Автор темы
- #1
Перед прочтением основного контента ниже, пожалуйста, обратите внимание на обновление внутри секции Майна на нашем форуме. У нас появились:
- бесплатные читы для Майнкрафт — любое использование на свой страх и риск;
- маркетплейс Майнкрафт — абсолютно любая коммерция, связанная с игрой, за исключением продажи читов (аккаунты, предоставления услуг, поиск кодеров читов и так далее);
- приватные читы для Minecraft — в этом разделе только платные хаки для игры, покупайте группу "Продавец" и выставляйте на продажу свой софт;
- обсуждения и гайды — всё тот же раздел с вопросами, но теперь модернизированный: поиск нужных хаков, пати с игроками-читерами и другая полезная информация.
Спасибо!
почему бы и нет?
Теория компиляции
Java - компилируемый язык, но что же это значит? Сначала узнаем про интерпретируемые языки. Например: когда вы в пишите в консоли
python main.py
питон читает ваш файл по строчкам и выполняет. Компилируемые языки же прежде чем выполнять код преобразуют (компилируют) исходный файл в более низкоуровневое представление (байткод). После компиляции для каждого файла создаётся структура класса в которую и помещается этот код. И далее эти классы уже выполняет непосредственно джава машина (jvm). То есть по сути джава машина и есть интерпретатор.Байткод
Прежде чем писать обфускатор нужно научиться понимать байткод. Состоит он из
Пожалуйста, авторизуйтесь для просмотра ссылки.
идущих по порядку. Посмотреть его можно через множество утилит (ByteEdit, Recaf, JByteMod, ThreadTear и другие) к которым мы вернёмся чуть позже, но пока, что нам хватит обычного javap -c Class
, вывод для обычного System.out.println(1) будет таким:
Код:
Compiled from "Main.java"
public class Main {
public Main();
Code: // Bytecode
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code: // Bytecode
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1
4: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
7: return
}
Так как !!поле!! System.out является статическим, чтобы его получить компилятор использует getstatic инструкцию. Далее это поле помещается на стек, за ним помещается iconst_1 - что означает константу int со значением 1. А после происходит выполнение функции println через invokevirtual с аргументами из стека и затем стек очищается. Understand?
Это конечно всё интересно, скажите вы, но, что за
java/io/PrintStream.println:(I)V
и java/lang/System.out:Ljava/io/PrintStream;
? Помимо названия класса и member'а (out, println) указывается ещё дополнительная информация: для поля out мы говорим что её тип - Ljava/io/PrintStream;
, а для метода println указываем дескриптор. В скобочках пишутся типы аргументов метода, а далее возвращаемый тип (всё в
Пожалуйста, авторизуйтесь для просмотра ссылки.
).Основные инструкции
Каждую инструкцию байткода знать не обязательно, обойдёмся и основными.
- getfield <класс поля> <название> <дескриптор> - Получает поле
- getstatic <класс поля> <название> <дескриптор> - Получает статическое поле
- putfield <класс поля> <название> <дескриптор> - Помещает значение из стека в поле
- putstatic <класс поля> <название> <дескриптор> - Помещает значение из стека в статическое поле
- goto <метка> - Перейти на метку
- ifeq, ifne, iflt, ifge, ifgt, ifle <метка> - Перейти на метку Пожалуйста, авторизуйтесь для просмотра ссылки.
- aload <индекс> - Поместить локальную переменную на стек
- astore <индекс> - Поместить значение из стека в локальную переменную
- areturn - Вернуть значение из стека как результат
- aconst_null - null
- invokevirtual <класс метода> <название> <дескриптор> - Вызов метода из класса
- invokestatic <класс метода> <название> <дескриптор> - Вызов статического метода из класса
- invokespecial <класс метода> <название> <дескриптор> - Вызов метода из класса для вызовов superclass (наследованный класс)и для приватных методов
- invokeinterface <класс метода> <название> <дескриптор> - Вызов метода из интерфейса
- invokedynamic - Вызов метода из CallSite (это пока не затрагиваем, Пожалуйста, авторизуйтесь для просмотра ссылки.)
aload, astore, areturn - Используются только для объектов. Для примитивов (long, int, ...) такие же инструкции - но с префиксом типа (то есть для int iload, для long lload, для double dload).
Для того, чтобы поместить int есть такие условия:
- iconst_0, iconst_1, iconst_2, iconst_4, iconst_5 - От 0 до 5
- bipush <значение> - Если значение не больше минимального значения байта (Byte.MIN_VALUE -2⁷) и не меньше максимального значения байта (Byte.MAX_VALUE 2⁷-1)
- sipush <значение> - Если значение не больше минимального значения шорта (Short.MIN_VALUE) и не меньше максимального значения шорта (Short.MAX_VALUE)
- lconst_0, lconst_1 - От 0 до 1
В любых остальных случаях, а также для строк используется ldc.
Ах да, забыл рассказать про метки. Метка - это просто грубо говоря "секция", которая группирует один список инструкций от других. Зачем это нужно? Покажу на примере:
Java:
if (somebool)
System.out.println(1);
else
System.out.println(2);
Код:
1: // Метка 1
aload somebool // Помещаем somebool на стек
ifeq 2 // Если somebool == false, то перемещаемся на метку 2
// Иначе остаёмся на этой метке и продолжаем код если somebool оказался true
// Байткод для System.out.println(1);
2:
// Байткод для System.out.println(2);
Подготавливаем проект
Для модификации байткода и парсинга класс файла мы возьмём библиотеку ow2.asm (также есть bytebuddy). ow2.asm:tree содержит основные классы для удобной работы с классами и остальными компонентами (ClassNode, MethodNode, ...)
Dont stop
Продолжаем. Читаем класс:
Java:
ClassReader classReader = new ClassReader(Files.readAllBytes(new File("...").toPath()));
ClassNode classFile = new ClassNode();
classReader.accept(classFile, 0);
System.out.printf("%s extends %s\n", classFile.name, classFile.superName);
Main extends java/lang/Object
Выведем названия всех методов:
Java:
for (MethodNode method : classFile.methods)
System.out.println(method.name);
<init>
main
Небольшое отступление, существуют "специальные методы":
<init>
- Конструктор класса, <clinit>
- Конструкция static {}
выполняемая после загрузки класса.Далее переберём все инструкции:
Java:
for (AbstractInsnNode instruction : method.instructions)
System.out.printf("\t%s - %d\n", instruction.getClass().getSimpleName(), instruction.getOpcode());
Код:
// instruction class - opcode
LabelNode - -1
LineNumberNode - -1
InsnNode - 4
VarInsnNode - 54
LabelNode - -1
LineNumberNode - -1
VarInsnNode - 21
JumpInsnNode - 153
LabelNode - -1
Давайте напишем самую тупую обфускацию - будем просто после каждого вызова (MethodInsnNode класс для инструкций invoke) помещать в переменную число 0:
Java:
if (instruction instanceof MethodInsnNode) { // Если инструкция invoke
LabelNode startNode = new LabelNode(); // Метка начала
LabelNode labelNode = new LabelNode(); // Создаём метку, она будет указана до какого момента локальная переменная будет "жить"
int index = method.localVariables.size() + 1; // Высчитываем новый индекс для переменной
method.localVariables.add(new LocalVariableNode("abobus"+method.localVariables.size(), "I", null, (LabelNode) method.instructions.getFirst(), labelNode, index)); // Добавляем переменную
// Добавляем инструкции
InsnList instructions = new InsnList();
instructions.add(startNode);
instructions.add(new InsnNode(Opcodes.ICONST_0)); // Помещаем 0 на стек
instructions.add(new VarInsnNode(Opcodes.ISTORE, index)); // Сохраняем значение 0 в переменную
instructions.add(endNode);
method.instructions.insert(instruction, instructions); // Вставляем наши новые инструкции после текущей (insertBefore чтобы вставить до)
}
Java:
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classFile.accept(classWriter);
Files.write(new File("...").toPath(), classWriter.toByteArray());
method.maxLocals++
Запускаем:
Код:
java -cp . Main
1
Двигаемся дальше
Первая обфускация у нас уже есть, осталось сделать что-нибудь по сложнее.
Чистим код
Почистим немного код. Я создам интерфейс трансформера:
Java:
public interface Transformer {
void transformClass(ClassNode classNode);
void transformMethod(MethodNode methodNode);
}
Java:
public class TransformerService {
private final List<Transformer> transformers;
public TransformerService() {
this.transformers = new LinkedList<>();
}
public void add(Transformer transformer) {
this.transformers.add(transformer);
}
public void obfuscate(ClassNode classNode) {
this.transformers.forEach(transformer -> transformer.transformClass(classNode));
classNode.methods.forEach(methodNode -> {
this.transformers.forEach(transformer -> transformer.transformMethod(methodNode));
});
}
}
Java:
TransformerService service = new TransformerService();
service.add(...);
service.obfuscate(classFile);
Обфускация строк
Создадим трансформер под строки:
Java:
public abstract class StringTransformer implements Transformer {
abstract public InsnList generateEncrypt(AbstractInsnNode instruction, String string);
@Override
public void transformMethod(MethodNode methodNode) {
for (AbstractInsnNode instruction : methodNode.instructions) {
if (instruction instanceof LdcInsnNode ldcInsnNode && ldcInsnNode.cst instanceof String) {
methodNode.instructions.insert(instruction, generateEncrypt(instruction, (String) ldcInsnNode.cst));
methodNode.instructions.remove(instruction);
}
}
}
}
Кстати пока писал вспомнил, что у ASM есть удобное получение дескрипторов через Type#getDescriptor
Java:
public class Base64StringTransformer extends StringTransformer {
@Override
public InsnList generateEncrypt(AbstractInsnNode instruction, String string) {
InsnList instructions = new InsnList();
// new String(Base64.getDecoder().decode("encoded string"))
instructions.add(new InsnNode(Opcodes.DUP));
instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/Base64", "getDecoder", "()Ljava/util/Base64$Decoder;"));
instructions.add(new LdcInsnNode(Base64.getEncoder().encodeToString(string.getBytes())));
instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B"));
instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/String", "<init>", "([B)V"));
return instructions;
}
}
Метод через AES PKCS
Base64 это всё равно легко, а вот AES шифрование уже лучше. Джава из коробки поддерживает шифрование по ключу:
Java:
SecretKeySpec spec = new SecretKeySpec("aaaaaaaaaaaaaaaa".getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
String encrypted = Base64.getEncoder().encodeToString(cipher.doFinal("text22".getBytes()));
System.out.println(encrypted);
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
String decrypted = new String(cipher.doFinal(Base64.getDecoder().decode(encrypted)));
System.out.println(decrypted);
Пожалуйста, авторизуйтесь для просмотра ссылки.
)Реализуем:
Java:
public class AESStringTransformer extends StringTransformer {
@Override
public InsnList generateEncrypt(MethodNode methodNode, AbstractInsnNode instruction, String string) {
InsnList instructions = new InsnList();
try {
SecretKeySpec spec = new SecretKeySpec("x̊a".getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(new byte[cipher.getBlockSize()]));
String encrypted = Base64.getEncoder().encodeToString(cipher.doFinal(string.getBytes()));
LabelNode startNode = new LabelNode();
LabelNode endNode = new LabelNode();
int index = methodNode.localVariables.size() + 1;
methodNode.localVariables.add(new LocalVariableNode("cipher"+index, Type.getDescriptor(Cipher.class), null, startNode, endNode, index));
instructions.add(startNode);
instructions.add(new LdcInsnNode("AES/CBC/PKCS5Padding"));
instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(Cipher.class), "getInstance", "(Ljava/lang/String;)Ljavax/crypto/Cipher;"));
instructions.add(new VarInsnNode(Opcodes.ASTORE, index));
instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
instructions.add(new InsnNode(Opcodes.ICONST_2)); // Cipher.DECRYPT_MODE
instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(SecretKeySpec.class)));
instructions.add(new InsnNode(Opcodes.DUP));
instructions.add(new LdcInsnNode("x̊a"));
instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "getBytes", "()[B"));
instructions.add(new LdcInsnNode("AES"));
instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(SecretKeySpec.class), "<init>", "([BLjava/lang/String;)V"));
instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(IvParameterSpec.class)));
instructions.add(new InsnNode(Opcodes.DUP));
instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Cipher.class), "getBlockSize", "()I"));
instructions.add(new IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_BYTE));
instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(IvParameterSpec.class), "<init>", "([B)V"));
instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Cipher.class), "init", "(ILjava/security/Key;Ljava/security/spec/AlgorithmParameterSpec;)V"));
instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(String.class)));
instructions.add(new InsnNode(Opcodes.DUP));
instructions.add(new VarInsnNode(Opcodes.ALOAD, index));
instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(Base64.class), "getDecoder", "()Ljava/util/Base64$Decoder;"));
instructions.add(new LdcInsnNode(encrypted));
instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B"));
instructions.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Cipher.class), "doFinal", "([B)[B"));
instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(String.class), "<init>", "([B)V"));
instructions.add(endNode);
} catch (Exception exception) {
exception.printStackTrace();
}
return instructions;
}
}
Теперь есть проблема - если часто использовать эту строку (например: в читах в каждом кадре дешифровать по сотни строк), то он не оптимизирован. Поэтому мы создадим HashMap куда поместим хэш (рандом число для каждой строки) и дешифрованную строку. Пример как это будет выглядеть:
Java:
public class Main {
private static final Map<Long, String> hashes;
public static void main(String[] args) {
System.out.println(hashes.get(1337));
}
static {
hashes = new HashMap<>();
// метод дешифровки
hashes.put(1337, decrypted);
}
}
-noverify
, jvm проверяет инструкции каждого метода и если творить прям лютую дичь то он может дать невалид и -noverify флаг скипает эту проверку.Реализуем (много кода, выложил на pastebin) -
Пожалуйста, авторизуйтесь для просмотра ссылки.
Для прикола над декомпиляторами можно указать пробел в виде названия переменной или поля.
Смотрим:
Без -noverify!
Recaf с CFR декомпилятором:
Java decompiler GUI (jd-gui):
Flow
Строки это конечно интересно, но сильно они нас конечно не запутывают. Задача flow - насрать в код или изменить его настолько, чтобы оригинальный код отыскать было не так легко. Например: перед вызовом метода можно добавить switch с кучей рандомных значений и кейсов, где лишь одно правильное. Такая реализация есть в Bozar obfuscator:
Пожалуйста, авторизуйтесь для просмотра ссылки.
Что-то серьёзное я делать не буду, просто после каждого вызова метода буду пихать
if (что-нибудь, что не выполнится) throw ...;
Java:
public class FlowTransformer implements Transformer {
@Override
public void transformClass(ClassNode classNode) {
}
@Override
public void transformMethod(MethodNode methodNode) {
for (AbstractInsnNode instruction : methodNode.instructions) {
if (instruction instanceof MethodInsnNode) {
InsnList instructions = new InsnList();
LabelNode startLabelNode = new LabelNode();
LabelNode labelNode = new LabelNode();
// 0*0 + 5 == 0
instructions.add(new InsnNode(Opcodes.ICONST_0));
instructions.add(new InsnNode(Opcodes.DUP));
instructions.add(new InsnNode(Opcodes.IMUL));
instructions.add(new InsnNode(Opcodes.ICONST_5));
instructions.add(new InsnNode(Opcodes.IADD));
instructions.add(new JumpInsnNode(Opcodes.IFNE, labelNode));
instructions.add(startLabelNode);
instructions.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(RuntimeException.class)));
instructions.add(new InsnNode(Opcodes.DUP));
instructions.add(new LdcInsnNode("-"));
instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(RuntimeException.class), "<init>", "(Ljava/lang/String;)V"));
instructions.add(new InsnNode(Opcodes.ATHROW));
instructions.add(new JumpInsnNode(Opcodes.GOTO, startLabelNode));
instructions.add(labelNode);
methodNode.instructions.insert(instruction, instructions);
}
}
}
}