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

194 lines
13 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.

# Как работает: техники, плюсы, минусы
Каждая техника описана одинаково: **как работает → что даёт → что стоит /
где ломается**. Цена указана честно: обфускация - это всегда обмен
читаемости и времени против аналитика.
Итоговая таблица - в конце.
## 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`](../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 - единственная техника,
которая может сломать рантайм, и она же единственная, чья отдача
не покрывает риск без верификатора.