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

15 KiB
Raw Permalink Blame History

Как сделать лучше: план улучшений для нашего обфускатора

Привязка: референс - 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 - только для действительно ценных кусков, остальное не оправдывает цену.