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.
8.5 KiB
Референс: 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 §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.