obfuskator/docs/techniques.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

13 KiB
Raw Blame History

Как работает: техники, плюсы, минусы

Каждая техника описана одинаково: как работает → что даёт → что стоит / где ломается. Цена указана честно: обфускация - это всегда обмен читаемости и времени против аналитика.

Итоговая таблица - в конце.

1. Переименование (Rename)

Как работает. Классы получают случайные имена в случайных пакетах (или в исходном пакете, если spread_packages = false). Поля и методы переименовываются, если method_rename_safety считает их безопасными (private/static/final - сразу; virtual - только «семейством» на всю иерархию, чтобы не разъединить переопределения и мостовые методы). Затем переписываются дескрипторы, LDC-классы, вызовы, поля, исключительная таблица, Signature/Exceptions/InnerClasses.

Плюсы.

  • Самая дешёвая и самая отдающая техника: читает javap -p любой анализатор.
  • Не меняет семантику - риск сломать код минимален.
  • Маппиинг пишется в отчёт → stack trace'и переводятся обратно.

Минусы / цена.

  • Keep-список обязателен: рефлексия по имени (Class.forName("pkg.C")), JNI, serialVersionUID, сериализация, entrypoint'ы. Пропустил - получил ClassNotFoundException в рантайме.
  • Рефлексия через строки мы ловим автоматически (анализ Class.forName, getMethod/getField и т.п.), но динамическую конкатенацию - нет.
  • JSON-(де)сериализация по именам полей (Gson/Jackson/Codec) не ловится: биндинг идёт через X.class-литерал, не строку - см. todo.md §2.
  • Для библиотек rename_public_members = false по умолчанию: менять публичный API библиотеки - сломать всех потребителей.

Шесть стилей имён (ShortLatin, IlO0, Unicode, MixedLatin, DecoyWords, Plausible) - компромисс по-разному: IlO0/Unicode явно сигнализируют «это обфускация», Plausible маскируется под обычный прикладной код (TextureCache, resolveToken), ценой того, что при ручном чтении он менее заметно отличается от настоящей логики, чем намеренный мусор - скрыть обфускацию и скрыть её цену ценой чуть большей путаницы для собственной команды при отладке маппингов.

Статус: готово.

2. Шифрование строк (Strings)

Как работает. Каждый LDC со строкой короче min_len заменяется на LDC cipher; LDC salt; INVOKESTATIC d(String,I)String. Ключ строки выводится из мастер-секрета + salt, не лежит рядом с шифротекстом. Между строками ротируются разные схемы (xor-splitmix, xtea-ctr), nonce уникален на строку, MAC ловит порчу.

Плюсы.

  • Строки - главный источник логики ("admin", URL, SQL, regex).
  • Ротация схем: один скрипт деобфускации не покрывает весь jar.
  • MAC: подмена шифротекста не «молча» портит текст, а падает.

Минусы / цена.

  • Декодер добавляет вызов на каждый доступ к строке → оверхед в рантайме.
  • Целевой анализ декодера всё равно раскроет текст; задача - поднять цену, а не сделать невозможным.
  • Мастер-секрет собирается в декодере из двух частей (k1 ^ k2).

Статус: готово (подстановка + генератор декодера v49 + сквозной тест исполнением под настоящим java).

3. Обфускация констант (Constants)

Как работает. Числовая константа не хранится как есть, а собирается из разных выражений: x ^ k, a + b, (a << 1) + c. Плюс настоящие opaque-предикаты - условия, истинность которых зависит от инварианта (например, i*(i+1) % 2 == 0), а не от константы.

Плюсы.

  • Дёшево, ломает поиск «магических чисел».
  • Оpaque-предикаты добавляют ветки, по которым деобфускатор должен пройти.

Минусы / цена.

  • Ребёнок числа раскрывается за минуты любым символном вычислителем (executor для LCONST/ICONST-цепочек).
  • Развдувание кода растёт по квадрату от числа вставок.
  • Шаблонный opaque-предикат (всегда один и тот же A+B) узнаётся паттерном - отсюда требование нескольких схем.

Статус: готово для int/long (xor/add/shift + opaque-предикаты); float/double не трогаются.

4. Обфускация контрольного потока (Flow)

Как работает. CFG перестраивается одной из схем: state-spread (таблица состояний + switch), computed-switch (счётчик-переменная вместо меток), butterfly (обратные переходы), exception-goto (управление через throw/catch).

Плюсы.

  • Линейный проход кода больше не читается как исходник.
  • В сочетании с opaque-предикатами резко поднимает стоимость трассировки.

Минусы / цена.

  • Самая дорогая и самая опасная техника. Требует пересчёта StackMapTable для классов версии ≥ 50 - иначе JVM не грузит класс.
  • Ломает профилировщики, coverage-инструменты и отладку.
  • Нужен верификатор: любая ошибка в CFG - это VerifyError в рантайме.
  • Шаблонная схема (все методы перестроены одинаково) детектируется гистограммой длины блоков.

Статус: готов (noxis-passes/src/flow/). Одна схема на метод (ротация через PRNG) плюс нарезка тела goto-сплайсами во всех схемах; кадр StackMapTable пересчитывается после каждой перестройки. Приёмка - настоящий java -Xverify:all и исполнение под JVM (calc печатает 45 после всех схем). Ограничения: <init> пропускается (форма super-call-first); методы с jsr/ret, короче 3 инструкций и длиннее max_insns пропускаются; encrypt_block_bodies добавляет opaque-guard на входе. Два правила, выученных на крови (см. StackMapTable): вставленная метка не должна делить байт-смещение с чужой (иначе кадры мерджатся из несовместимых состояний - после каждой такой метки nop), а инструкция после терминальной обязана иметь свой кадр (поэтому dead-блоки входят через opaque-ветвь и кончаются athrow, а не висят недостижимыми).

5. Шум (Noise)

Как работает. Junk-классы и junk-аннотации, вплетённые в граф достижимости (weave_reachable): класс считается достижимым, если на него есть ссылка из настоящего кода.

Плюсы.

  • Дешёвый объём: файлов много, логики ноль.
  • Мусор в дереве файлов ухудшает автоматический разбор.

Минусы / цена.

  • Reachability-анализ отсеивает junk мгновенно: класс, на который никто не ссылается, - мёртвый груз. Отсюда обязательное weave_reachable.
  • Пустые методы/тела - признак, который ищут первым.
  • Раздувание jar.

Статус: готово (junk-классы + weave_reachable; junk-аннотации пропускаются - сырые атрибуты не несут свежие индексы пула).

6. Отладочная информация (DebugInfo)

Как работает. Strip LineNumberTable, LocalVariableTable; подмена SourceFile на ложное имя; опционально strip Signature/InnerClasses. Плюс ложная отладочная информация - чужие имена переменных и номера строк.

Плюсы.

  • Практически бесплатно.
  • Восстанавливает исходные имена переменных и файлов - прямая утечка.

Минусы / цена.

  • Убивает нормальные stack trace'и (поэтому маппинги в отчёте обязательны).
  • Слишком «чистый» класс (LineNumberTable есть, а SourceFile = X.java, которого нет) - тоже признак.

Статус: готово (strip + spoof_source_file, детерминированная подмена от мастер-сида). Не реализовано: fake_line_numbers (ложные номера строк, а не только SourceFile) и strip_source_file (полное удаление вместо подмены) - см. todo.md §3.6.

7. Ресурсы (Resources)

Как работает. Патч текстовых ресурсов по маппингу: fabric.mod.json, *.mixins.json, refmap, access widener, META-INF/services/*. Вырезание старых подписей *.SF/*.RSA/*.DSA/*.EC. Ограничение дубликатов.

Плюсы.

  • Без этого обфускация Fabric-мода просто не стартует.
  • Убранные подписи - иначе jar считается подделанным.

Минусы / цена.

  • Патч только текстовых ресурсов: бинарные (картинки, классы в versions/) трогать нельзя.
  • Промах в патче → тихо неработающий refmap, ловится только запуском.

Статус: готово (патч текстовых ресурсов по маппингу, включая fabric.mod.json/*.mixins.json/META-INF/services/*; срез подписей в noxis-jar). Не реализовано: типизированный патч бинарных/специфичных форматов (refmap, access widener) сверх общего boundary-replace.

Сводная таблица

техника отдача риск цена статус
Rename ★★★★★ низкий низкая готово
Strings ★★★★ средний средняя готово
Resources ★★★★★ низкий низкая готово
DebugInfo ★★★ низкий низкая готово
Constants ★★ низкий низкая готово (int/long)
Noise ★ низкий раздувание готово (без аннотаций)
Flow ★★★★ высокий высокая план, кадры готовы

Вывод, который стоит держать в голове: Rename + Resources + DebugInfo дают большую часть защиты за небольшую цену. Flow - единственная техника, которая может сломать рантайм, и она же единственная, чья отдача не покрывает риск без верификатора.