diff --git a/docs/learnings/lessons.md b/docs/learnings/lessons.md deleted file mode 100644 index fea68bb..0000000 --- a/docs/learnings/lessons.md +++ /dev/null @@ -1,146 +0,0 @@ -# Уроки: что защита EvoVis сделала плохо - -Источник: `info/evo-weaknesses.md` - полный разбор за один проход, без -рантайм-хаков. Здесь - что из этого следует для **нашей** архитектуры. - -Правило, которое стоит держать головой: **защита ломается не в рантайме, -а на этапе, когда атакующий смотрит на статику.** Всё, что видно в `javap -p` -или в содержимом jar, - это утечка. - -## Сводная таблица: провал → наше решение - -| № | провал EvoVis | наше решение | пасс | статус | -|---|---|---|---|---| -| 1 | ключ лежит рядом с шифротекстом | вывод мастер-секрета в декодере, ключ из `master‖salt` | Strings | **готово** | -| 2 | переименованы типы, но не члены | rename полей/методов + семейства virtual | Rename | **готово** | -| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | **готово** (`SourceFile`; номера строк - план) | -| 4 | шаблонный flattening | 4 схемы CFG + мешанина внутри метода | Flow | **готово** (4 схемы, приёмка `java`) | -| 5 | opaque-предикаты = `A+B` | ротация тождеств + предикаты от состояния | Constants | **готово** (int/long) | -| 6 | junk-классы вне графа reachability | `weave_reachable` - обязательная вплетённость | Noise | **готово** (без аннотаций) | -| 7 | имена говорят «я защита» | все артефакты получают данные псевдонимы | Rename | **готово** | -| 8 | вторичные компоненты без защиты | весь jar, а не только основной класс | Resources | **готово** | -| 9 | шифр без MAC и без per-string salt | salt на строку, уникальный nonce, MAC | Strings | **готово** | - -## 1. Ключ рядом с шифротекстом - -`awc.d("", )` - ключ был **константным выражением -в вызове**. Свёртка выражения - ~200 строк любого парсера. Итог: 8993 вызова -расшифрованы детерминированно, **0 обращений к JVM**. - -Хуже: один шифр на весь продукт (LCG с фиксированными `a/c`) - зная одну -пару «шифротекст → текст», остальные раскрываются по обучению. - -**Что делаем иначе.** - -- Ключ строки = `F(master, salt)`, `master` **выводится** в рантайме и не - является константой в `LDC`. -- `salt` лежит рядом с шифротекстом, но сам по себе бесполезен без `master`. -- **Ротация схем** (`xor-splitmix` / `xtea-ctr`): одна расшифровка не даёт - ключа ко всему jar. Плюс per-string `nonce`, чтобы пары «текст→шифр» - не повторялись и частотный анализ не работал. -- MAC: подмена байта не «тихо» портит текст. - -Остаточный риск честно признан: целевой анализ декодера раскрывает текст. -Задача - поднять цену, а не сделать невозможным. - -## 2. Члены не переименованы - -`onDraw`, `getProjectiles`, `isAuthorized()` - открытые имена. Класс `wb` -опознаётся за секунду по трём понятным методам. - -**Что делаем иначе:** переименование полей и методов входит в базовый -уровень, а не в «продвинутый». Уже готово, включая: - -- **семейства virtual** - переименование на всю иерархию, чтобы не - разъединить переопределения и не сломать `invokevirtual`; -- **keep-список** - private/static/final можно сразу, `rename_public_members` - по умолчанию выключен для библиотек, чтобы не сломать API; -- **автодетект рефлексии по имени** - `Class.forName`, `getMethod`, - `getField` с литералами попадают в keep автоматически. - -## 3. Отладочная информация - -`SourceFile` оригинальные у 160 классов, `LineNumberTable` - 10 218, -`LocalVariableTable` - имена локальных переменных, `@rn(name, description)` - -каталог всех модулей с описаниями. - -**Что делаем иначе:** пасс `DebugInfo` - strip `SourceFile`, -`LineNumberTable`, `LocalVariableTable`, `Signature`, `InnerClasses` + -**ложная** отладочная информация (чужие имена и номера строк, чтобы -деобфускатор выдавал правдоподобный, но чужой код). - -Отсюда обязательное требование: **маппинги в отчёте** - иначе мы сами не -читаем свои stack trace'и. - -## 4. Шаблонный flattening - -Один regex нашёл 3530 автоматов: `while(true) { switch(i + i2) }`, метки - -константы, переходы - присваивания в одном basic block, тела блоков не -тронуты. - -**Что делаем иначе:** не одна схема, а **четыре** (`state-spread`, -`computed-switch`, `butterfly`, `exception-goto`), с ротацией между методами -и с перемешиванием внутри метода. Плюс `butterfly` и `exception-goto` -ломают «все переходы - присваивания». - -Это единственная техника с **высоким риском**: любая ошибка в CFG = -`VerifyError`. Поэтому блокером Flow объявлен не «напишем потом», а -`StackMapTable` - см. [`../stack.md`](../stack.md) §3. Без верификатора -Flow не включается. - -## 5. Opaque-предикаты - -`(A^B)+((A&B)<<1)` - школьное тождество, узнаётся регуляркой и даёт -бесплатный фолдинг остальных констант. Настоящих предикатов (зависящих -от состояния и всегда истинных) нет. - -**Что делаем иначе:** ротация семейств тождеств (арифметика, побитово, -сдвиги, произведение соседних чисел, инвариант по модулю) + предикаты, -завязанные на состояние, а не на пару констант. - -## 6. Reachability убивает junk - -42 файла, ни одного входящего ссылока, `dangling=0` → BFS от 4 -entrypoint'ов, которые открыто лежат в `fabric.mod.json`. - -**Что делаем иначе:** `weave_reachable` - junk попадает в граф -достижимости, создаёт ложные рёбра и участвует в вызовах. Это свойство -пасса, а не настройка: без вплетённости junk не включается вовсе. - -## 7. Самоописание защиты - -`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`, -`SealedBoot`, `NativeGuard` - названия указывают атакующему, куда смотреть. - -**Что делаем иначе:** псевдонимы генерируются одним `NameGenerator` для -всех сущностей - классы, пакеты, поля, методы, junk и даже имена атрибутов -и схем выглядят как обычные данные. Отдельного «защитного» стиля не -существует, поэтому отличить его от остального кода не по чему. - -## 8. Вторичные компоненты - -`evo-agent.jar` вообще без обфускации, .NET-лаунчер - статическая XOR-таблица. - -**Что делаем иначе:** обфускация применяется ко **всему архиву**, включая -ресурсы (`fabric.mod.json`, mixins, refmap, access widener) - см. пасс -`Resources`. Отдельного «ядро защищено, остальное нет» не бывает. - -## 9. Шифр без аутентификации - -Нет MAC, нет per-string сида → повреждение не ловится, частотный анализ -работает. - -**Уже сделано:** `salt` на строку, уникальный `nonce`, FNV-1a MAC в -`noxis-runtime::scheme`. `cipher:tag` лежат рядом - порча обнаруживается. - -## Что подтвердилось и не нужно повторять в выводах - -Разбор дал и **опровержения** - важно не строить защиту на мифах: - -- «сдвиги в байткоде бывают отрицательными» - по 1277 классам **0** случаев - (32 559 сдвигов, константные всегда `1`); -- `awc.g(...)` (session-gate) в билде **не вызывался ни разу**. - -Вывод: проверять гипотезу на статистике **до** того, как вкладываться -в технику против неё. Отсюда требование к `noxis-verify` - не «красиво -обфусцировано», а измеримые метрики ([`priorities.md`](priorities.md)). diff --git a/docs/learnings/priorities.md b/docs/learnings/priorities.md deleted file mode 100644 index b188c97..0000000 --- a/docs/learnings/priorities.md +++ /dev/null @@ -1,120 +0,0 @@ -# Что улучшим: стратегия и приоритеты - -Материал собран из `info/evo-improvements.md` (план после разбора EvoVis) и -`info/evo-weaknesses.md` (провалы). Здесь - что мы берём, в каком порядке -и **что осознанно не делаем**. - -## Стратегия в одном абзаце - -Максимум защиты при минимуме риска сломать рантайм. Дешёвые техники, -дающие 80% результата, включаются по умолчанию; дорогие и опасные - -включаются **только** после того, как для них есть верификатор. Каждая -техника имеет измеримый критерий «сработала/не сработала», иначе это -не защита, а мнение. - -## P0 - базовый уровень (дёшево, снимает главные утечки) - -| пункт | что это | пасс | статус | -|---|---|---|---| -| A1 | переименование **всех** символов: классы, поля, методы, локальные переменные, параметры, record-компоненты, enum-константы, `SourceFile` | Rename | ✅ готово | -| A2 | вычистить `LineNumberTable`/`LocalVariableTable`/`SourceFile`/`InnerClasses`/`Signature`/`MethodParameters` | DebugInfo | ✅ готово | -| A3 | никаких сигнатурных имён у защитного кода, защитный код смешан с обычным | Rename + Noise | ✅ готово | -| A4 | обфусцировать **весь** архив, включая ресурсы, mixin-JSON, access widener, service loader'ы | Resources | ✅ готово | -| - | маппинг в отчёт (иначе не читаем свои же stack trace) | Rename | ✅ готово | - -Это самый дешёвый блок и одновременно **самый большой утечка** в разобранном -образце: оригинальные `SourceFile` у 160 классов, имена полей и методов, -каталог модулей в атрибутах. - -## P1 - строки и константы - -| пункт | что это | пасс | статус | -|---|---|---|---| -| B1 | ключ на строку + ротация 3–5 схем + MAC + уникальный nonce | Strings | ✅ готово | -| B2 | **вывод** ключа в рантайме, а не константа в вызове | Strings + генератор декодера | ✅ готово | -| B3 | константы - не тождества, а вычисления, разнесённые по блокам | Constants | ✅ готово (int/long) | -| B4 | **ложная** отладочная информация: чужие имена переменных и строк | DebugInfo | ✅ `SourceFile` | - -B2 - главное отличие от разобранного образца: у него ключ был константным -выражением в вызове и расшифровывался статически, без JVM. Мастер-секрет -теперь собирается в рантайме из двух частей (`k1 ^ k2`) декодером класса - -см. [`../stack.md`](../stack.md) §4. B4 пока покрывает только `SourceFile`; -ложные номера строк (`fake_line_numbers`) - см. [`../../todo.md`](../../todo.md) §3.6. - -## P2 - контрольный поток - -| пункт | что это | статус | -|---|---|---| -| C1 | **несколько схем** флаттенинга вместо одной, с мешаниной внутри метода | план | -| C2 | верификатор обязателен: ошибка CFG = `VerifyError` | ✅ готово (кадры + `java -Xverify:all`) | -| C3 | реальный объём ≠ раздувание: бюджет на размер и время | ✅ размер; время - план | - -Приоритет здесь строго подчинён: **C2 раньше C1**. Кадры `StackMapTable` -и приёмка `java -Xverify:all` уже готовы (см. [`../stack.md`](../stack.md) -§3) - единственное, чего не хватает, это сами схемы флаттенинга (C1). - -## P3 - измеримость (метрики, без которых ничего не доказано) - -Обязательный блок, иначе фраза «обфускация есть» ничего не означает. -Крейт `noxis-verify` реализован - вот что из списка уже цифра, а что ещё мнение: - -1. **Утечки имён.** ✅ `collect_names`/`find_leaks` - сверка идентификаторов - с учётом keep-списка. -2. **Читаемость декомпилята.** ⬜ Прогон JADX/CFR → доля распознанных - структур - не начато. -3. **Противник.** ✅ `scan_secrets` + `RED_PATTERNS` держим в репозитории; - ⬜ полноценный скрипт-деобфускатор (свёртка констант) как тест - план. -4. **Семантика.** ⬜ Эталонные программы до/после - не начато - (сейчас семантику подтверждает только `java -Xverify:all`). -5. **Бюджеты.** ✅ размер (`inflation_ratio`/`check_inflation`); - ⬜ время исполнения - не начато. -6. **Стабильность между сборками.** ✅ `mappings_changed` - маппинги - обязаны отличаться между прогонами с разным seed. - -Итог: `noxis-verify` закрывает красные паттерны, утечки имён, -`java -Xverify:all`, бюджет размера и стабильность маппингов. Остаются -декомпилят-метрика, бюджет времени и эталонная семантика - три пункта, -где «нам кажется защищённым» всё ещё мнение, а не цифра. - -## P4 - долгий горизонт (пока не делаем) - -| пункт | что это | почему не сразу | -|---|---|---| -| D1 | маленький VM/interpreter для ценных кусков | цена окупается только на ключевом коде | -| D2 | split-function / раскрываемое по шагам тело | режет обычный профилировщик, сложная отладка | -| D3 | мягкий анти-анализ в рантайме | требует своего нативного компонента | -| D4 | честный anti-tamper, а не «зашейрь ключ» | целая инженерная задача, не пасс | -| E | собственный нативный/Rust-цель вместо JVM | другой продукт, не наша цель | - -D1 - явно ограничен: включать только для действительно ценных функций, -остальное оправдания не имеет. E - отдельный продукт; если он когда-нибудь -понадобится, наш IR как раз делает его возможным (те же пассы, другой -writer). - -## Что мы добавили сверх исходного плана - -1. **Структурный гейт при компиляции.** Лимиты «250 строк / 4 файла» - гоняются в `build.rs` - см. [`../README.md`](../README.md). Без этого - план P0–P3 превращается в один гигантский файл через месяц. -2. **Собственный IR с метками.** Исходный план предполагал работу поверх - готового парсера. `Insn::Mark(Label)` убирает пересчёт смещений из - всех пассов - это меняет цену каждой техники, меняющей CFG. -3. **Детерминированный PRNG с доменным разделением.** Без этого - пункт «стабильность между сборками» невозможно проверить тестом. -4. **Keep как отдельный крейт (`noxis-analysis`), а не флаг CLI.** - Рефлексия, JNI, сериализация и entrypoint'ы - это анализ иерархии, - а не настройка, которую надо помнить руками. - -## Порядок выполнения - -Смотрите [`../../todo.md`](../../todo.md). P0–P3 и P3-измеримость уже -готовы; единственный крупный пункт, который остался, - схемы `Flow` (C1), -потому что верификатор (C2) для них уже есть: - -```text -✅ P0 DebugInfo + Resources → главные утечки сняты -✅ P1 Strings (B2) + Constants → ключ выводится, схемы ротируются -✅ P1 backend (ristretto) + frames -✅ P2 Flow (C1) - готов (09.10.2026): 4 схемы + ротация + мешанина, приёмка настоящим `java` -✅ P3 метрики, красные паттерны, CLI -``` diff --git a/docs/learnings/reference.md b/docs/learnings/reference.md deleted file mode 100644 index eac6d74..0000000 --- a/docs/learnings/reference.md +++ /dev/null @@ -1,150 +0,0 @@ -# Референс: Fabric Mod Obfuscator (Java + ASM) - -Разобранный образец лежит в `/tmp/opencode/obf_extract/OBF` (JAR из `OBF.rar`). -Нужен не как «правильный ответ», а как **карта решений, которые уже были -приняты и проверены на живых кейсах**. - -## 1. Что это - -| | | -|---|---| -| язык | Java 21, Maven, сборка `mvnw package` | -| движок | **ASM** (читает `.class`, строит маппинги, переписывает байткод) | -| объём | 18 классов в `org.example`, 3224 строки | -| цель | Fabric/Minecraft-моды | -| интерфейс | CLI + Swing GUI (drag-and-drop) | - -## 2. Карта модулей → наш план - -| модуль | LOC | что делает | у нас | -|---|---:|---|---| -| `JarRemapper` | 409 | обход архива, remap всех ссылок | `noxis-jar` + `rename::rewrite` | -| `InheritanceAnalyzer` | 373 | иерархия, безопасность переименования | `noxis-analysis::hierarchy` ✅ | -| `ObfuscatorGui` | 276 | GUI | не нужно (CLI) | -| `MappingGenerator` | 262 | маппинг → файл | `noxis-core::mapping` ✅ | -| `JunkClassGenerator` | 251 | junk-классы | пасс `noise` | -| `StringDecoderGenerator` | 205 | генерит класс-декодер | `noxis-runtime` + P2 генератор | -| `ObfuscatorRunner` | 202 | конвейер, оркестрация | `noxis-passes::standard_pipeline` ✅ | -| `NumberObfuscator` | 185 | константы / opaque | пасс `constants` | -| `ControlFlowObfuscator` | 183 | flattening | пасс `flow` ✅ (4 схемы, свои кадры) | -| `FabricKeepRuleScanner` | 147 | keep из конфига Fabric | `noxis-analysis::keep` ✅ | -| `Main` | 134 | CLI-точки | `noxis` (clap) | -| `StringEncryptor` | 132 | AES-128 строк + chunk'и | `noxis-runtime::scheme` ✅ | -| `NameGenerator` | 131 | имена | `noxis-passes::namegen` ✅ | -| `JunkAnnotationInjector` | 79 | junk-аннотации | пасс `noise` | -| `PackageGenerator` | 78 | пакеты | `namegen::generate_packages` ✅ | -| `AccessWidener` | 73 | патч access widener | `noxis-jar` + пасс `resources` | -| `DebugInfoStripper` | 69 | strip отладки | пасс `debug` | -| `StringFilter` | 35 | что считать строкой | функция внутри `strings` | - -**Вывод:** структура совпадает с нашей почти один в один - это подтверждает -раскладку крейтов. Ключевое отличие: у них всё живёт в одном jar'е и -одном проекте; у нас конвейер отделён от I/O и от CLI. - -## 3. Что берём - -### 3.1 Порядок пассов - -У них: remap → strings → numbers → flow → junk → debug. Тот же инвариант, -что и у нас: **переименование раньше всех техник, порождающих новые ссылки**, -а `debug` - последним. - -### 3.2 Наследование как условие безопасности - -`InheritanceAnalyzer` - самый большой после remap'ера модуль. Совпадает с -нашим выводом: переименование virtual-методов **без** иерархии ломает -`invokevirtual`. У нас это `hierarchy` + семейства в `rename`. - -### 3.3 Keep-сканер - -`FabricKeepRuleScanner` читает правило из конфигурации Fabric и не даёт -сломать то, что JVM находит по имени. Мы делаем то же самое + расширяем -рефлексией по строкам. - -### 3.4 Подпись срезается - -`*.SF/*.RSA/*.DSA/*.EC` удаляются при перепаковке - после записи подпись -всё равно невалидна, а её наличие заставляет JVM падать. Это входит в -`noxis-jar`. - -### 3.5 Маппинг в файл + ложные ограничения - -`--mappings` пишет соответствие «новое → старое» - без него автор сам не -читает stack trace. Плюс честные off-ключи: `--no-strings`, `--no-flow`, -`--no-junk`, `--keep-lines`, `--keep-source`, `--keep-vars`, `--no-widen`. - -Мы берём то же: **каждый пасс выключаем по отдельности**, иначе диагностика -проблемы «что сломало» превращается в угадывание. - -### 3.6 Seed - -`--seed` - детерминированный прогон. У нас это базовое свойство -`noxis-core`, без отдельного флага. - -## 4. Что не берём - -### 4.1 Java + ASM - -Главное отличие стека. Пара Java+ASM даёт родную JVM, готовые visitor'ы и -`CheckClassAdapter` - но и: - -- нужно раскатывать JDK для сборки и запуска тестов; -- наш проект - Rust (`deny(unsafe_code)`, свои лимиты, свои гейты); -- отсутствует возможность отдавать один бинарь. - -Цена, которую мы платим: `StackMapTable` придётся считать самим (через -inference-верификатор - [`../stack.md`](../stack.md) §3). Это осознанно. - -### 4.2 Swing GUI - -Лишняя точка входа: GUI дублирует CLI и удваивает площадь тестирования. -У нас - только clap. - -### 4.3 `StringDecoderGenerator` как «генератор текста» - -Они генерируют Java-исходник декодера и компилируют. Для нас это означало бы -зависимость от `javac` на этапе обфускации. Наш путь - генерировать -**байткод напрямую** через наш IR (`noxis-runtime` держит схемы, генератор -собирает `ClassFile`). - -### 4.4 AES-128 + Base64 как единственная схема - -Одна схема на весь архив = тот же провал, что у EvoVis §1/§9. У нас -ротация схем, per-string salt, nonce и MAC. - -### 4.5 `--flatten-flow` только «для простых методов» - -Ограничение из их README: агрессивный switch-gate включается только на -простых методах. Это признаёт, что **верификация у них эмпирическая**. -Мы вместо этого делаем верификатор обязательным условием включения `flow`. - -## 5. Что проверяем на их smoke-тестах - -В `target/` референса лежат готовые проверочные кейсы - их стоит -переиспользовать как наш regression-набор: - -| кейс | что ломает | -|---|---| -| `smoke-safe` | базовый round-trip, `Sample`/`MyMixin` | -| `smoke-inheritance` | `Base`/`Child`/`InheritedChild`/`ExternalChild` - семейства virtual и мосты | -| `smoke-flow` | `LoadAll` - загрузка всех классов, ловит `VerifyError` | -| `smoke-fields` | переименование полей и статических полей | -| `smoke-aes` | шифрование строк, `RunStrings` сверяет расшифровку | -| `Probe.java` | точечная проверка одной гипотезы | - -Это ровно тот класс тестов, который нужен `noxis-verify`: **загрузить -и выполнить**, а не «выглядит обфусцированным». - -## 6. Итог - -| берём | не берём | -|---|---| -| порядок пассов | стек Java + ASM | -| иерархия как условие rename | Swing GUI | -| keep-сканер | генератор Java-исходника | -| срез подписей, патч ресурсов | единственная схема шифра | -| маппинг в отчёт, честные off-ключи | эмпирическая верификация flow | -| их smoke-кейсы как regression | - | - -Полный перечень провалов защиты, которую они **не** закрывают, - в -[`lessons.md`](lessons.md).