# Как сделать лучше: план улучшений для нашего обфускатора Привязка: референс - EvoVis (`evo-obfuscation-full.md`), провалы - `evo-weaknesses.md`. Ниже - что делаем иначе, в порядке отдачи/цены. Для каждой техники - уровень угрозы, которую она закрывает (скрипт-деобфускатор / статический анализ / ручной реверс). --- ## A. Базовый уровень (делаем обязательно, дёшево) ### A1. Обфускация всех символов, не только типов - Имена классов, **полей, методов, локальных переменных, параметров**, конструкторов, record-компонентов, enum-констант, label'ов, атрибутов `SourceFile`. - Индивидуальный алфавит на каждый символ (см. B1), запрет ключевых слов и одинаковых имён в одном scope. - Никаких «readable» ярлыков: у EvoVis `@rn(name=…)` и `description` отдали каталог модулей целиком. Если метаданные нужны рантайму - шифруем их (C2). ### A2. Вычистить отладочную информацию - `-g` → `LineNumberTable` / `LocalVariableTable` / `SourceFile` / `InnerClasses` / `EnclosingMethod` / `MethodParameters` / `Signature` - по policy: либо полностью выкинуть, либо заменить на шум (см. B4). - Ресурсы тоже часть кода: mixin-JSON, `fabric.mod.json`, access widener, service loaders, SPI-файлы - шифруем/шифруем имена внутри, раскрываем в рантайме. ### A3. Никаких сигнатурных имён у защитного кода `KeyAssembler`, `RuntimeGuard`, `NativeGuard` → рандомные имена, а сам защитный код **смешиваем с обычным** (секции, которые выглядят как мусорные утилиты). Рефлексия по именам в открытом виде (`Class.forName("aln")`) - запрещена: имена отдаются только из зашифрованной строки (C2), лучше - через indirection-таблицу. ### A4. Обфусцировать весь артефакт, а не только ядро Правило: **нет незащищённого компонента**. У EvoVis лаунчер и java-agent были открыты целиком. Пайплайн один на: основной код, агент/плагин, лаунчер/хостер, нативная часть, ресурсы. --- ## B. Строки и константы ### B1. Пер-строчный ключ + нелинейный шифр Вместо общего LCG: - seed = `random_u64` на строку (храним неявно: derive из позиции/ид + master-seed); - алгоритм выбирается из набора (3–5 схем: RC4-like, chacha-с fragments, XTEA-блочный, plain XOR с keystream от BLAKE3, base85+flate перед шифром) - **перемешивание схем между строками**, чтобы один скрипт не покрыл всё; - AAD/MAC: `HMAC(fragment, plaintext_hash)` - повреждение ловится, подмена - тоже; - keystream не повторяется: nonce включает индекс строки. - **против обучения по частотам**: словарь/частотный анализ не работает, если схема и nonce разные. ### B2. Ключ строки выводится в рантайме, а не лежит рядом Никаких `d(cipher, literal_key)`: - ключ = `KDF(master_key, class_id, method_id, string_index)` либо приходит из защищённого контекста (см. D3); - вычисление - через серию операций, **растворённых в теле вызывающего метода** (не выделенная функция `decrypt(s)` - иначе её найдут и развернут); - мастер-секрет не в константах: собирается из нескольких мест + env/хост-фактора. ### B3. Константы - не тождества, а вычисления Отказ от `(A^B)+((A&B)<<1)` (опознаётся regex'ом). Вместо этого: - константа собирается из **нескольких узлов**, разнесённых по разным basic block'ам и разным методам (возврат частей, сложение на месте использования); - включаем в цепочку **зависимые от окружения** величины, которые статически вычислимы не полностью: `hashCode()` строки-литерала, `System.identityHashCode` статической константы, значение поля, инициализируемого в ``, размер массива; - для `int`/`long`/`float`/`double` - отдельные схемы (bit-cast, разложение по мантиссе), потому что `double`-константы особенно заметны. - opaque-предикаты делать **настоящими**: всегда истинные, но зависящие от состояния, которое статически не выводится (напр. инвариант «массив отсортирован по заполнению», проверка `System.nanoTime()`-мьютекса, сравнение двух полей, всегда равных после ``, но не доказуемых в CFG). ### B4. Ложная отладочная информация Не только удалить `LineNumberTable`/`LocalVariableTable`, но и **подложить дезориентирующие**: ложные имена локальных переменных, строки из другого проекта, `SourceFile` с вводящим именем. Инструменты (jadx/CFR) и люди верят отладочке сильнее, чем байткоду. --- ## C. Контрольный поток ### C1. Несколько схем флаттенинга вместо одной Минимум набор (применять **случайно и по-разному в разных методах**): 1. **Switch по вычислимому индексу**: `table[ (x*31 ^ y) & mask ]` → адрес возврата; переходы через индексы, а не метки. 2. **Распределённое состояние**: как у EvoVis (`i + i2`), но одно слагаемое - из поля объекта/статики, второе - из параметра, обновление - через вызов метода; **метки case не константные**, а пересчитанные (opaque). 3. **Бинарное дерево/бабочка**: вместо линейного списка состояний - рандомизированный граф переходов, эквивалентный исходному CFG (проверяется верификатором). 4. **Вызов через стек/ретурн-адрес**: эмуляция `goto` через `athrow`-как-прыжок (exception-as-goto), `finally`-цепочки, или virtual-dispatch по фейковому интерфейсу. 5. **Прячем тела блоков**: каждый блок зашифрован и раскрывается по очереди (см. D2), чтобы деобфускатор не мог читать тело, не эмулируя VM. ### C2. Обфускация CFG должна сохранять семантику → нужен верификатор Условие приёмки: **анализ эквивалентности до/после** (структурный: разбор в SSA, сравнение支配-графов; либо выполнение тестов на эталонных программах). Плюс автоматические метрики (см. F). ### C3. Реальный объём ≠ раздувание У EvoVis выигрыш - только в размере декомпилята, но и цена (читаемость для нас же). Оптимальнее: комбинировать обфускацию с **компиляцией в другой целевой код** (см. E), где раздувание не надо - там и так нечем читать. --- ## D. Уровень «нужно эмулировать, а не читать» ### D1. Маленький VM / interpreter для критичных кусков Свой bytecode (10–20 опкодов: арифметика, ветвление, вызов, доступ к полю), инструкции в зашифрованном массиве, диспетчер - обычный код с обфусцированным CFG. Экспортируем в VM только чувствительное: лицензирование, ключи, критическую логику. Это единственный уровень, который реально останавливает ручной реверс. ### D2. Раскрываемое по шагам тело (split-function / function-splitting) Метод делится на фрагменты (по basic block'ам), каждый зашифрован отдельно, порядок выполнения задаётся таблицей. Деобфускатор обязан строить CFG, прежде чем увидит хоть строку. ### D3. Анти-анализ в рантайме (мягкий) - self-check целостности: `SHA-256` своих классов/секций → при расхождении **меняем поведение**, а не падаем (у EvoVis падение выдавало мусорный ключ - правильно: не сообщать атакующему, что он пойман); - детект отладчика/трейсера/инструментов эмуляции - реакция: ложные данные, замедление, отключение функций; - **не детектить и не ломать легитимные сценарии** (обновления, IDE, символы) - ложные срабатывания дороже, чем польза защиты. ### D4. Честный anti-tamper, а не «зашейрь ключ» У EvoVis сильная идея: ключ = изолированная доля в подменённом JVM + HMAC от hwid + порча при детекте. Минус - вся схема работает только на своём окружении и падает на другой ОС/сборке. Для нас: секрет должен переживать **наши** легитимные платформы, а ломаться только при модификации кода/подмене целостности. --- ## E. Если цель - не JVM, а свой натив/Rust-бинарник Референс JVM-слабости здесь не работают автоматически, но появляются свои: - **строки**: все plaintext-строки в `.rodata` - шифровать, раскрывать лениво; не держать AES-ключ рядом с шифротекстом (та же ошибка, что у `awc.d`); - **имена**: `stripped` бинарник + рандомизация символов; в Rust - макрос, который переименовывает `pub`-символы и убирает `#[doc]`/Debug-имена; - **panic/Debug-вывод**: `{:?}` отдаёт имена типов и полей - для чувствительных типов свой `Debug`, либо не выводить; - **CFG**: обфускация на уровне MIR/LLVM-IR (passes): bogus control flow, flattening на `switch` по вычисленному индексу, вставка недостижимых блоков, размытие границ функций (inlining + outlined); - **константы**: как B3, плюс вынос в вычисляемые таблицы; - **анти-анализ**: `ptrace`-selfcheck, проверка `/proc/self/maps` на свои секции, timing-checks; реакция - молчаливая порча данных; - **обфускация метаданных Rust**: `TypeId`/`Any`, `std::any::type_name`, логи `tracing` - выключать в release. --- ## F. Метрики качества (обязательно, иначе «обфускация есть» ничего не значит) Автотесты на каждую технику, гоняются в CI: 1. **Утечки имён**: `strings`/анализ констант - не должно остаться ни одного осмысленного идентификатора из списка-эталона (ошибка = фейл). 2. **Читаемость декомпилята**: прогнать JADX/CFR на собранный артефакт → метрики: доля распознанных структур (try/catch, switch, циклы), доля классов с восстановленными именами, число строк «смыслового» кода. 3. **Автоматический деобфускатор-противник**: минимальный скрипт-анализатор (тот же `expr`-свёртка, regex на паттерны) - если он снимает >X% строк/констант, техника провалена. Держим этот «красный список паттернов» в репозитории. 4. **Семантика**: тесты/эталонные программы до и после - 100% совпадение. 5. **Размер и скорость**: бюджет (≤ N% размера, ≤ M% времени исполнения), отдельно - бюджет на рантайм-раскрытие строк (кэш, чтобы не платить на каждый вызов). 6. **Стабильность между сборками**: имена/схемы меняются от билда к билду (у EvoVis шифр один на все версии - окно для обучения). ## Приоритет 1. A1+A2+A4 (символы, отладка, всё артефакты) - дёшево, biggest leak у EvoVis. 2. B1+B2 (пер-строчные ключи, вывод ключа) - снимает главный провал `awc.d`. 3. C1 несколько схем флаттенинга + C2 верификатор. 4. F метрики - чтобы не спорить на глаз. 5. C3/E (другой целевой код) - как долгосрочный путь. 6. D1 VM - только для действительно ценных кусков, остальное не оправдывает цену.