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

8.5 KiB
Raw Blame History

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