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.
9.4 KiB
Что улучшим: стратегия и приоритеты
Материал собран из info/evo-improvements.md (план после разбора EvoVis) и
info/evo-weaknesses.md (провалы). Здесь - что мы берём, в каком порядке
и что осознанно не делаем.
Стратегия в одном абзаце
Максимум защиты при минимуме риска сломать рантайм. Дешёвые техники, дающие 80% результата, включаются по умолчанию; дорогие и опасные - включаются только после того, как для них есть верификатор. Каждая техника имеет измеримый критерий «сработала/не сработала», иначе это не защита, а мнение.
P0 - базовый уровень (дёшево, снимает главные утечки)
| пункт | что это | пасс | статус |
|---|---|---|---|
| A1 | переименование всех символов: классы, поля, методы, локальные переменные, параметры, record-компоненты, enum-константы, SourceFile |
Rename | ✅ готово |
| A2 | вычистить LineNumberTable/LocalVariableTable/SourceFile/InnerClasses/Signature/MethodParameters |
DebugInfo | ✅ готово |
| A3 | никаких сигнатурных имён у защитного кода, защитный код смешан с обычным | Rename + Noise | ✅ готово |
| A4 | обфусцировать весь архив, включая ресурсы, mixin-JSON, access widener, service loader'ы | Resources | ✅ готово |
| - | маппинг в отчёт (иначе не читаем свои же stack trace) | Rename | ✅ готово |
Это самый дешёвый блок и одновременно самый большой утечка в разобранном
образце: оригинальные SourceFile у 160 классов, имена полей и методов,
каталог модулей в атрибутах.
P1 - строки и константы
| пункт | что это | пасс | статус |
|---|---|---|---|
| B1 | ключ на строку + ротация 3–5 схем + MAC + уникальный nonce | Strings | ✅ готово |
| B2 | вывод ключа в рантайме, а не константа в вызове | Strings + генератор декодера | ✅ готово |
| B3 | константы - не тождества, а вычисления, разнесённые по блокам | Constants | ✅ готово (int/long) |
| B4 | ложная отладочная информация: чужие имена переменных и строк | DebugInfo | ✅ SourceFile |
B2 - главное отличие от разобранного образца: у него ключ был константным
выражением в вызове и расшифровывался статически, без JVM. Мастер-секрет
теперь собирается в рантайме из двух частей (k1 ^ k2) декодером класса -
см. ../stack.md §4. B4 пока покрывает только SourceFile;
ложные номера строк (fake_line_numbers) - см. ../../todo.md §3.6.
P2 - контрольный поток
| пункт | что это | статус |
|---|---|---|
| C1 | несколько схем флаттенинга вместо одной, с мешаниной внутри метода | план |
| C2 | верификатор обязателен: ошибка CFG = VerifyError |
✅ готово (кадры + java -Xverify:all) |
| C3 | реальный объём ≠ раздувание: бюджет на размер и время | ✅ размер; время - план |
Приоритет здесь строго подчинён: C2 раньше C1. Кадры StackMapTable
и приёмка java -Xverify:all уже готовы (см. ../stack.md
§3) - единственное, чего не хватает, это сами схемы флаттенинга (C1).
P3 - измеримость (метрики, без которых ничего не доказано)
Обязательный блок, иначе фраза «обфускация есть» ничего не означает.
Крейт noxis-verify реализован - вот что из списка уже цифра, а что ещё мнение:
- Утечки имён. ✅
collect_names/find_leaks- сверка идентификаторов с учётом keep-списка. - Читаемость декомпилята. ⬜ Прогон JADX/CFR → доля распознанных структур - не начато.
- Противник. ✅
scan_secrets+RED_PATTERNSдержим в репозитории; ⬜ полноценный скрипт-деобфускатор (свёртка констант) как тест - план. - Семантика. ⬜ Эталонные программы до/после - не начато
(сейчас семантику подтверждает только
java -Xverify:all). - Бюджеты. ✅ размер (
inflation_ratio/check_inflation); ⬜ время исполнения - не начато. - Стабильность между сборками. ✅
mappings_changed- маппинги обязаны отличаться между прогонами с разным seed.
Итог: noxis-verify закрывает красные паттерны, утечки имён,
java -Xverify:all, бюджет размера и стабильность маппингов. Остаются
декомпилят-метрика, бюджет времени и эталонная семантика - три пункта,
где «нам кажется защищённым» всё ещё мнение, а не цифра.
P4 - долгий горизонт (пока не делаем)
| пункт | что это | почему не сразу |
|---|---|---|
| D1 | маленький VM/interpreter для ценных кусков | цена окупается только на ключевом коде |
| D2 | split-function / раскрываемое по шагам тело | режет обычный профилировщик, сложная отладка |
| D3 | мягкий анти-анализ в рантайме | требует своего нативного компонента |
| D4 | честный anti-tamper, а не «зашейрь ключ» | целая инженерная задача, не пасс |
| E | собственный нативный/Rust-цель вместо JVM | другой продукт, не наша цель |
D1 - явно ограничен: включать только для действительно ценных функций, остальное оправдания не имеет. E - отдельный продукт; если он когда-нибудь понадобится, наш IR как раз делает его возможным (те же пассы, другой writer).
Что мы добавили сверх исходного плана
- Структурный гейт при компиляции. Лимиты «250 строк / 4 файла»
гоняются в
build.rs- см.../README.md. Без этого план P0–P3 превращается в один гигантский файл через месяц. - Собственный IR с метками. Исходный план предполагал работу поверх
готового парсера.
Insn::Mark(Label)убирает пересчёт смещений из всех пассов - это меняет цену каждой техники, меняющей CFG. - Детерминированный PRNG с доменным разделением. Без этого пункт «стабильность между сборками» невозможно проверить тестом.
- Keep как отдельный крейт (
noxis-analysis), а не флаг CLI. Рефлексия, JNI, сериализация и entrypoint'ы - это анализ иерархии, а не настройка, которую надо помнить руками.
Порядок выполнения
Смотрите ../../todo.md. P0–P3 и P3-измеримость уже
готовы; единственный крупный пункт, который остался, - схемы Flow (C1),
потому что верификатор (C2) для них уже есть:
✅ P0 DebugInfo + Resources → главные утечки сняты
✅ P1 Strings (B2) + Constants → ключ выводится, схемы ротируются
✅ P1 backend (ristretto) + frames
✅ P2 Flow (C1) - готов (09.10.2026): 4 схемы + ротация + мешанина, приёмка настоящим `java`
✅ P3 метрики, красные паттерны, CLI