obfuskator/docs/techniques.md
loki5512344 f08bf60635 classfile: intern constant-pool entries on write (fix LVTT/LVT ClassFormatError)
ristretto's ConstantPool never deduplicates: every add_* pushes a new entry.
HotSpot matches each LocalVariableTypeTable entry to a LocalVariableTable
entry by raw name_index, so when a name (e.g. a local's) was interned twice
the two tables got different indices and the class failed to load:
'ClassFormatError: LVTT entry ... does not match any LVT entry'.

The writer now memoizes every pool builder by its semantic key and rebuilds
composite constants (Class/NameAndType/Fieldref/Methodref/MethodHandle/
Dynamic) from our own utf8/class primitives so ristretto never mints a
shadow Utf8. This also shrinks the pool (no more ldc -> ldc_w widening that
changed code length on a pure round-trip).

Split pool.rs into a pool/ module (refs in pool/refs.rs) to respect the
file-size gate. Regression test: tests/pool asserts the written pool has no
duplicate CONSTANT_Utf8. Verified: pure parse->write of the neoforge jar
(92 classes) now loads under java -Xverify:all with 0 errors.
2026-10-08 22:55:49 +02:00

183 lines
12 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` в рантайме.
- Шаблонная схема (все методы перестроены одинаково) детектируется
гистограммой длины блоков.
**Статус: не реализован; блокер — `StackMapTable` (см. [`stack.md`](stack.md)).**
## 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 — единственная техника,
которая может сломать рантайм, и она же единственная, чья отдача
не покрывает риск без верификатора.