obfuskator/docs/learnings/reference.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

150 lines
8.5 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.

# Референс: 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).