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