obfuskator/docs/learnings/lessons.md
loki5512344 c3b8d4dd62 docs: sync status with code, add AI-resistance research, restyle README
Documentation had drifted from the actual implementation: the noxis
(CLI) and noxis-verify crates were fully built and tested but every
doc called them "not started". Verified against the code (build +
test run) and corrected status across README.md, todo.md,
architecture.md, techniques.md and learnings/{lessons,priorities}.md.

Also:
- restyled README.md (badges, no em dashes, usage examples, dropped
  the redundant "Принципы" section already covered elsewhere)
- added docs/learnings/ai-resistance.md: a sourced writeup of what
  actually works against LLM-driven reverse engineering (Elastic
  Security Labs' Opus 4.6 vs Tigress study, the Acoda paper) and what
  of it applies to noxis, plus concrete todo.md §7 items
- noxis-structure-check: the 250-line/4-file gate was scanning
  docs/**, which fought against writing documentation freely; it now
  only scans crates/*/{src,tests,benches} and only counts .rs files
2026-10-08 22:34:00 +02:00

10 KiB
Raw Blame History

Уроки: что защита EvoVis сделала плохо

Источник: info/evo-weaknesses.md — полный разбор за один проход, без рантайм-хаков. Здесь — что из этого следует для нашей архитектуры.

Правило, которое стоит держать головой: защита ломается не в рантайме, а на этапе, когда атакующий смотрит на статику. Всё, что видно в javap -p или в содержимом jar, — это утечка.

Сводная таблица: провал → наше решение

№ провал EvoVis наше решение пасс статус
1 ключ лежит рядом с шифротекстом вывод мастер-секрета в декодере, ключ из master‖salt Strings готово
2 переименованы типы, но не члены rename полей/методов + семейства virtual Rename готово
3 отладочная информация нетронута strip + ложный SourceFile/номера строк DebugInfo готово (SourceFile; номера строк — план)
4 шаблонный flattening 4 схемы CFG + мешанина внутри метода Flow план (кадры готовы)
5 opaque-предикаты = A+B ротация тождеств + предикаты от состояния Constants готово (int/long)
6 junk-классы вне графа reachability weave_reachable — обязательная вплетённость Noise готово (без аннотаций)
7 имена говорят «я защита» все артефакты получают данные псевдонимы Rename готово
8 вторичные компоненты без защиты весь jar, а не только основной класс Resources готово
9 шифр без MAC и без per-string salt salt на строку, уникальный nonce, MAC Strings готово

1. Ключ рядом с шифротекстом

awc.d("<cipher>", <opaque-выражение>) — ключ был константным выражением в вызове. Свёртка выражения — ~200 строк любого парсера. Итог: 8993 вызова расшифрованы детерминированно, 0 обращений к JVM.

Хуже: один шифр на весь продукт (LCG с фиксированными a/c) — зная одну пару «шифротекст → текст», остальные раскрываются по обучению.

Что делаем иначе.

  • Ключ строки = F(master, salt), master выводится в рантайме и не является константой в LDC.
  • salt лежит рядом с шифротекстом, но сам по себе бесполезен без master.
  • Ротация схем (xor-splitmix / xtea-ctr): одна расшифровка не даёт ключа ко всему jar. Плюс per-string nonce, чтобы пары «текст→шифр» не повторялись и частотный анализ не работал.
  • MAC: подмена байта не «тихо» портит текст.

Остаточный риск честно признан: целевой анализ декодера раскрывает текст. Задача — поднять цену, а не сделать невозможным.

2. Члены не переименованы

onDraw, getProjectiles, isAuthorized() — открытые имена. Класс wb опознаётся за секунду по трём понятным методам.

Что делаем иначе: переименование полей и методов входит в базовый уровень, а не в «продвинутый». Уже готово, включая:

  • семейства virtual — переименование на всю иерархию, чтобы не разъединить переопределения и не сломать invokevirtual;
  • keep-список — private/static/final можно сразу, rename_public_members по умолчанию выключен для библиотек, чтобы не сломать API;
  • автодетект рефлексии по имени — Class.forName, getMethod, getField с литералами попадают в keep автоматически.

3. Отладочная информация

SourceFile оригинальные у 160 классов, LineNumberTable — 10 218, LocalVariableTable — имена локальных переменных, @rn(name, description) — каталог всех модулей с описаниями.

Что делаем иначе: пасс DebugInfo — strip SourceFile, LineNumberTable, LocalVariableTable, Signature, InnerClasses + ложная отладочная информация (чужие имена и номера строк, чтобы деобфускатор выдавал правдоподобный, но чужой код).

Отсюда обязательное требование: маппинги в отчёте — иначе мы сами не читаем свои stack trace'и.

4. Шаблонный flattening

Один regex нашёл 3530 автоматов: while(true) { switch(i + i2) }, метки — константы, переходы — присваивания в одном basic block, тела блоков не тронуты.

Что делаем иначе: не одна схема, а четыре (state-spread, computed-switch, butterfly, exception-goto), с ротацией между методами и с перемешиванием внутри метода. Плюс butterfly и exception-goto ломают «все переходы — присваивания».

Это единственная техника с высоким риском: любая ошибка в CFG = VerifyError. Поэтому блокером Flow объявлен не «напишем потом», а StackMapTable — см. ../stack.md §3. Без верификатора Flow не включается.

5. Opaque-предикаты

(A^B)+((A&B)<<1) — школьное тождество, узнаётся регуляркой и даёт бесплатный фолдинг остальных констант. Настоящих предикатов (зависящих от состояния и всегда истинных) нет.

Что делаем иначе: ротация семейств тождеств (арифметика, побитово, сдвиги, произведение соседних чисел, инвариант по модулю) + предикаты, завязанные на состояние, а не на пару констант.

6. Reachability убивает junk

42 файла, ни одного входящего ссылока, dangling=0 → BFS от 4 entrypoint'ов, которые открыто лежат в fabric.mod.json.

Что делаем иначе: weave_reachable — junk попадает в граф достижимости, создаёт ложные рёбра и участвует в вызовах. Это свойство пасса, а не настройка: без вплетённости junk не включается вовсе.

7. Самоописание защиты

StringRuntime, KeyAssembler, AttestClient, RuntimeGuard, SealedBoot, NativeGuard — названия указывают атакующему, куда смотреть.

Что делаем иначе: псевдонимы генерируются одним NameGenerator для всех сущностей — классы, пакеты, поля, методы, junk и даже имена атрибутов и схем выглядят как обычные данные. Отдельного «защитного» стиля не существует, поэтому отличить его от остального кода не по чему.

8. Вторичные компоненты

evo-agent.jar вообще без обфускации, .NET-лаунчер — статическая XOR-таблица.

Что делаем иначе: обфускация применяется ко всему архиву, включая ресурсы (fabric.mod.json, mixins, refmap, access widener) — см. пасс Resources. Отдельного «ядро защищено, остальное нет» не бывает.

9. Шифр без аутентификации

Нет MAC, нет per-string сида → повреждение не ловится, частотный анализ работает.

Уже сделано: salt на строку, уникальный nonce, FNV-1a MAC в noxis-runtime::scheme. cipher:tag лежат рядом — порча обнаруживается.

Что подтвердилось и не нужно повторять в выводах

Разбор дал и опровержения — важно не строить защиту на мифах:

  • «сдвиги в байткоде бывают отрицательными» — по 1277 классам 0 случаев (32 559 сдвигов, константные всегда 1);
  • awc.g(...) (session-gate) в билде не вызывался ни разу.

Вывод: проверять гипотезу на статистике до того, как вкладываться в технику против неё. Отсюда требование к noxis-verify — не «красиво обфусцировано», а измеримые метрики (priorities.md).