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.
This commit is contained in:
parent
2172206e83
commit
139aea9a8f
96 changed files with 1053 additions and 831 deletions
|
|
@ -1,43 +1,43 @@
|
|||
# Как работает: техники, плюсы, минусы
|
||||
|
||||
Каждая техника описана одинаково: **как работает → что даёт → что стоит /
|
||||
где ломается**. Цена указана честно: обфускация — это всегда обмен
|
||||
где ломается**. Цена указана честно: обфускация - это всегда обмен
|
||||
читаемости и времени против аналитика.
|
||||
|
||||
Итоговая таблица — в конце.
|
||||
Итоговая таблица - в конце.
|
||||
|
||||
## 1. Переименование (`Rename`)
|
||||
|
||||
**Как работает.** Классы получают случайные имена в случайных пакетах
|
||||
(или в исходном пакете, если `spread_packages = false`). Поля и методы
|
||||
переименовываются, если `method_rename_safety` считает их безопасными
|
||||
(private/static/final — сразу; virtual — только «семейством» на всю
|
||||
(private/static/final - сразу; virtual - только «семейством» на всю
|
||||
иерархию, чтобы не разъединить переопределения и мостовые методы).
|
||||
Затем переписываются дескрипторы, `LDC`-классы, вызовы, поля,
|
||||
исключительная таблица, `Signature`/`Exceptions`/`InnerClasses`.
|
||||
|
||||
**Плюсы.**
|
||||
- Самая дешёвая и самая отдающая техника: читает `javap -p` любой анализатор.
|
||||
- Не меняет семантику — риск сломать код минимален.
|
||||
- Не меняет семантику - риск сломать код минимален.
|
||||
- Маппиинг пишется в отчёт → stack trace'и переводятся обратно.
|
||||
|
||||
**Минусы / цена.**
|
||||
- Keep-список обязателен: рефлексия по имени (`Class.forName("pkg.C")`),
|
||||
JNI, `serialVersionUID`, сериализация, entrypoint'ы. Пропустил — получил
|
||||
JNI, `serialVersionUID`, сериализация, entrypoint'ы. Пропустил - получил
|
||||
`ClassNotFoundException` в рантайме.
|
||||
- Рефлексия через строки мы ловим автоматически (анализ `Class.forName`,
|
||||
`getMethod`/`getField` и т.п.), но динамическую конкатенацию — нет.
|
||||
`getMethod`/`getField` и т.п.), но динамическую конкатенацию - нет.
|
||||
- JSON-(де)сериализация по именам полей (Gson/Jackson/`Codec`) не ловится:
|
||||
биндинг идёт через `X.class`-литерал, не строку — см. `todo.md` §2.
|
||||
биндинг идёт через `X.class`-литерал, не строку - см. `todo.md` §2.
|
||||
- Для библиотек `rename_public_members = false` по умолчанию: менять
|
||||
публичный API библиотеки — сломать всех потребителей.
|
||||
публичный API библиотеки - сломать всех потребителей.
|
||||
|
||||
Шесть стилей имён (`ShortLatin`, `IlO0`, `Unicode`, `MixedLatin`,
|
||||
`DecoyWords`, `Plausible`) — компромисс по-разному: `IlO0`/`Unicode` явно
|
||||
`DecoyWords`, `Plausible`) - компромисс по-разному: `IlO0`/`Unicode` явно
|
||||
сигнализируют «это обфускация», `Plausible` маскируется под обычный
|
||||
прикладной код (`TextureCache`, `resolveToken`), ценой того, что при
|
||||
ручном чтении он менее заметно отличается от настоящей логики, чем
|
||||
намеренный мусор — скрыть обфускацию и скрыть её цену ценой чуть большей
|
||||
намеренный мусор - скрыть обфускацию и скрыть её цену ценой чуть большей
|
||||
путаницы для собственной команды при отладке маппингов.
|
||||
|
||||
**Статус: готово.**
|
||||
|
|
@ -51,13 +51,13 @@
|
|||
уникален на строку, MAC ловит порчу.
|
||||
|
||||
**Плюсы.**
|
||||
- Строки — главный источник логики (`"admin"`, URL, SQL, regex).
|
||||
- Строки - главный источник логики (`"admin"`, URL, SQL, regex).
|
||||
- Ротация схем: один скрипт деобфускации не покрывает весь jar.
|
||||
- MAC: подмена шифротекста не «молча» портит текст, а падает.
|
||||
|
||||
**Минусы / цена.**
|
||||
- Декодер добавляет вызов на каждый доступ к строке → оверхед в рантайме.
|
||||
- Целевой анализ декодера всё равно раскроет текст; задача — поднять цену,
|
||||
- Целевой анализ декодера всё равно раскроет текст; задача - поднять цену,
|
||||
а не сделать невозможным.
|
||||
- Мастер-секрет собирается в декодере из двух частей (`k1 ^ k2`).
|
||||
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
**Как работает.** Числовая константа не хранится как есть, а собирается из
|
||||
разных выражений: `x ^ k`, `a + b`, `(a << 1) + c`. Плюс настоящие
|
||||
opaque-предикаты — условия, истинность которых зависит от инварианта
|
||||
opaque-предикаты - условия, истинность которых зависит от инварианта
|
||||
(например, `i*(i+1) % 2 == 0`), а не от константы.
|
||||
|
||||
**Плюсы.**
|
||||
|
|
@ -80,7 +80,7 @@ opaque-предикаты — условия, истинность которы
|
|||
(`executor` для `LCONST/ICONST`-цепочек).
|
||||
- Развдувание кода растёт по квадрату от числа вставок.
|
||||
- Шаблонный opaque-предикат (всегда один и тот же `A+B`) узнаётся
|
||||
паттерном — отсюда требование нескольких схем.
|
||||
паттерном - отсюда требование нескольких схем.
|
||||
|
||||
**Статус: готово** для `int`/`long` (`xor`/`add`/`shift` + opaque-предикаты);
|
||||
`float`/`double` не трогаются.
|
||||
|
|
@ -98,22 +98,22 @@ opaque-предикаты — условия, истинность которы
|
|||
|
||||
**Минусы / цена.**
|
||||
- **Самая дорогая и самая опасная техника.** Требует пересчёта
|
||||
`StackMapTable` для классов версии ≥ 50 — иначе JVM не грузит класс.
|
||||
`StackMapTable` для классов версии ≥ 50 - иначе JVM не грузит класс.
|
||||
- Ломает профилировщики, coverage-инструменты и отладку.
|
||||
- Нужен верификатор: любая ошибка в CFG — это `VerifyError` в рантайме.
|
||||
- Нужен верификатор: любая ошибка в CFG - это `VerifyError` в рантайме.
|
||||
- Шаблонная схема (все методы перестроены одинаково) детектируется
|
||||
гистограммой длины блоков.
|
||||
|
||||
**Статус: готов** (`noxis-passes/src/flow/`). Одна схема на метод (ротация
|
||||
через PRNG) плюс нарезка тела `goto`-сплайсами во всех схемах; кадр
|
||||
`StackMapTable` пересчитывается после каждой перестройки. Приёмка —
|
||||
`StackMapTable` пересчитывается после каждой перестройки. Приёмка -
|
||||
настоящий `java -Xverify:all` и исполнение под JVM (`calc` печатает 45
|
||||
после всех схем). Ограничения: `<init>` пропускается (форма
|
||||
super-call-first); методы с `jsr`/`ret`, короче 3 инструкций и длиннее
|
||||
`max_insns` пропускаются; `encrypt_block_bodies` добавляет opaque-guard
|
||||
на входе. Два правила, выученных на крови (см. `StackMapTable`):
|
||||
вставленная метка не должна делить байт-смещение с чужой (иначе кадры
|
||||
мерджатся из несовместимых состояний — после каждой такой метки `nop`),
|
||||
мерджатся из несовместимых состояний - после каждой такой метки `nop`),
|
||||
а инструкция после терминальной обязана иметь свой кадр (поэтому dead-блоки
|
||||
входят через opaque-ветвь и кончаются `athrow`, а не висят недостижимыми).
|
||||
|
||||
|
|
@ -129,32 +129,32 @@ super-call-first); методы с `jsr`/`ret`, короче 3 инструкц
|
|||
|
||||
**Минусы / цена.**
|
||||
- Reachability-анализ отсеивает junk мгновенно: класс, на который никто
|
||||
не ссылается, — мёртвый груз. Отсюда обязательное `weave_reachable`.
|
||||
- Пустые методы/тела — признак, который ищут первым.
|
||||
не ссылается, - мёртвый груз. Отсюда обязательное `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.
|
||||
подмены) - см. [`todo.md`](../todo.md) §3.6.
|
||||
|
||||
## 7. Ресурсы (`Resources`)
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ super-call-first); методы с `jsr`/`ret`, короче 3 инструкц
|
|||
|
||||
**Плюсы.**
|
||||
- Без этого обфускация Fabric-мода просто не стартует.
|
||||
- Убранные подписи — иначе jar считается подделанным.
|
||||
- Убранные подписи - иначе jar считается подделанным.
|
||||
|
||||
**Минусы / цена.**
|
||||
- Патч только текстовых ресурсов: бинарные (картинки, классы в `versions/`)
|
||||
|
|
@ -189,6 +189,6 @@ super-call-first); методы с `jsr`/`ret`, короче 3 инструкц
|
|||
| Flow | ★★★★ | **высокий** | высокая | план, кадры готовы |
|
||||
|
||||
Вывод, который стоит держать в голове: **Rename + Resources + DebugInfo**
|
||||
дают большую часть защиты за небольшую цену. Flow — единственная техника,
|
||||
дают большую часть защиты за небольшую цену. Flow - единственная техника,
|
||||
которая может сломать рантайм, и она же единственная, чья отдача
|
||||
не покрывает риск без верификатора.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue