Initial commit: noxis JVM bytecode obfuscator

Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis,
noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check).
AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
This commit is contained in:
Loki 2026-10-08 22:01:03 +02:00
commit a641e69455
140 changed files with 17197 additions and 0 deletions

150
docs/learnings/reference.md Normal file
View file

@ -0,0 +1,150 @@
# Референс: 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` (ждёт `StackMapTable`) |
| `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).