# Уроки: что защита 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("", )` — ключ был **константным выражением в вызове**. Свёртка выражения — ~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`](../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`](priorities.md)).