obfuskator/info/evo-improvements.md
loki5512344 139aea9a8f
frames,jar,passes: verifier-driven hardening from real jars
Proven by paranoid runs over lorep (17 classes, fully green),
LoVisual (4081 classes) and oc2r (1278 classes) with per-class
java -Xverify:all, each fix first reproduced as a red test:

- merge: Top absorbs instead of Top-wins (javac declares Top for
  slots unwritten on some inflow; precise types reject those edges).
  Regression: loop_head_keeps_top_for_loop_local.
- emit: collapse [Long|Double, Top] pairs on operand stacks
  (javac writes one entry; the pair is rejected). Regression:
  wide_stack_join_emits_single_entries.
- xfer ArrayLoad: push the wide pair, fixing lcmp underflow.
  Regression: array_load_wide_pushes_pair.
- xfer <init>: convert only the receiver's own allocation site
  instead of every Uninitialized (nested construction clobbered
  the outer object). Regression: nested_construction_keeps_outer.
- xfer ANewArray: array operands gain one dimension ([B] -> [[B)
  instead of an illegal L-wrap ([L[B;). Covered by the same test.
- jar load: duplicate class names (multi-release module-info
  twins) keep the first occurrence instead of crashing save.
  Test: duplicate_class_names_keep_first.
- strings/constants: skip methods past MAX_METHOD_INSNS (8000)
  so giants cannot overflow the 64KB writer ceiling. Tests:
  oversized_methods_are_skipped in both passes.
- merge errors now name both types being merged.

Also: em-dash removed repo-wide (724 spots, rule added to
todo.md), allow-lints only in tests (scratch example deleted),
code comments migrated to English in every file this change
touches.
2026-10-09 13:47:27 +02:00

185 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Как сделать лучше: план улучшений для нашего обфускатора
Привязка: референс - 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` статической
константы, значение поля, инициализируемого в `<clinit>`, размер массива;
- для `int`/`long`/`float`/`double` - отдельные схемы (bit-cast, разложение по
мантиссе), потому что `double`-константы особенно заметны.
- opaque-предикаты делать **настоящими**: всегда истинные, но зависящие от состояния,
которое статически не выводится (напр. инвариант «массив отсортирован по заполнению»,
проверка `System.nanoTime()`-мьютекса, сравнение двух полей, всегда равных после
`<clinit>`, но не доказуемых в 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 - только для действительно ценных кусков, остальное не оправдывает цену.