FlowPass rebuilds one method per scheme picked from FlowConfig: state-spread (dense tableswitch dispatcher), computed-switch (sparse lookupswitch over a counter local), butterfly (branch inversion), exception-goto (dispatcher under try with a dead catch-all), plus opaque goto-splices inside every method and StackMapTable recomputation afterwards. Now in standard_pipeline (between constants and noise); --no-flow is an honest off-flag. Acceptance is real JVM, not shapes: per-scheme verify, rotation spread, calc-prints-45 execution, paranoid smoke on lorep-1.1.0 (131 methods rebuilt, no new failures over standard). Two verifier rules learned the hard way are now construction invariants: no inserted mark shares a bytecode offset with a foreign one (nop pads), and no frameless gap follows a terminal (dead blocks are opaque-entered and throw). Also fixes a latent frame-emitter bug the pass exposed: wide stack values were written as slot pairs ([Double, Top]) while javac writes one entry ([Double]); HotSpot rejects the pair. Stacks now collapse like locals (emit::stack_vars), with a regression test in classfile/tests/frames.rs.
150 lines
8.5 KiB
Markdown
150 lines
8.5 KiB
Markdown
# Референс: 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).
|