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

146 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Уроки: что защита 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`](../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)).