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.
This commit is contained in:
Loki 2026-10-09 13:47:27 +02:00
parent 2172206e83
commit 139aea9a8f
Signed by: boba
GPG key ID: 253067914055423B
96 changed files with 1053 additions and 831 deletions

View file

@ -10,7 +10,7 @@
## 1. Elastic Security Labs: агент с IDA Pro против Tigress (апрель 2026)
[«The Cost of Understanding»](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation) —
[«The Cost of Understanding»](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation) -
Claude Opus 4.6 с доступом к IDA Pro (через MCP) реверсил crackme,
обфусцированный [Tigress](https://tigress.wtf/), затем команда сама собрала
("vibecode"-ила) три защиты, нацеленные именно на модель. Это ближе всего
@ -26,32 +26,32 @@ Claude Opus 4.6 с доступом к IDA Pro (через MCP) реверсил
indirect/regs и switch/stack).
- Opus решил ~40% из 20 валидных задач. Успех стоил в среднем **$2.39**,
провал — **$4.83**. Ни одна задача уровня "многослойные комбинации" не
провал - **$4.83**. Ни одна задача уровня "многослойные комбинации" не
была решена.
- **`JIT` (XTEA + рантайм-расшифровка) оказался самой сложной одиночной
трансформацией** — модель понимала структуру движка, но не смогла
трансформацией** - модель понимала структуру движка, но не смогла
восстановить ключевые константы. Это ровно наш подход в `Strings`
(ключ выводится в рантайме, не лежит в вызове), только применённый ко
**всему коду метода**, а не к строкам.
- На самых жёстких комбинациях цена и время выросли в ~4.5× и ~4.3×, но
задачи, которые Opus уже умел решать, он решил всё равно. **Flatten + MBA
било по модели сильнее, чем Virtualize + MBA** — авторы объясняют это тем,
било по модели сильнее, чем Virtualize + MBA** - авторы объясняют это тем,
что флаттенинг сильнее раздувает размер функции.
### Три кастомные защиты, нацеленные на агента
| защита | идея | результат |
|---|---|---|
| Matryoshka Wall | crackme внутри 100 000 вложенных слоёв ChaCha20 (+ подделка под Salsa20) | v1 — агент сдался не расшифровав ($1.50); v2 (каждый ключ зависит от предыдущего слоя) — исчерпал бюджет $10 за 56 минут, 0 успехов |
| Double Fond | патч open-source крипто-библиотеки: скрытый загрузчик через изменённую lookup-таблицу, шифрованные указатели, RWX-сегмент | v7 — 1/5 у "осведомлённого" агента (12 мин, $5.20), 0/5 у "слепого" |
| Dispatch Maze | шифр раздроблен на 20 фрагментов по 3000 функций, большинство — ловушки | v1 решена за 12 минут ($2.56); v2 (разнообразные шаблоны кода + ложные переходы) — провал агента через 46 минут и $8.83 |
| Matryoshka Wall | crackme внутри 100 000 вложенных слоёв ChaCha20 (+ подделка под Salsa20) | v1 - агент сдался не расшифровав ($1.50); v2 (каждый ключ зависит от предыдущего слоя) - исчерпал бюджет $10 за 56 минут, 0 успехов |
| Double Fond | патч open-source крипто-библиотеки: скрытый загрузчик через изменённую lookup-таблицу, шифрованные указатели, RWX-сегмент | v7 - 1/5 у "осведомлённого" агента (12 мин, $5.20), 0/5 у "слепого" |
| Dispatch Maze | шифр раздроблен на 20 фрагментов по 3000 функций, большинство - ловушки | v1 решена за 12 минут ($2.56); v2 (разнообразные шаблоны кода + ложные переходы) - провал агента через 46 минут и $8.83 |
### Что из этого реально работает, по механизму
1. **Контекстное окно.** Длинные тупиковые пути (decoy-функции, ложные
переходы) забивают контекст и портят рассуждение модели.
2. **Бюджет шагов/денег.** Каждый впустую потраченный цикл стоит агенту
реальных токенов; если он упирается в лимит и сдаётся — это победа
реальных токенов; если он упирается в лимит и сдаётся - это победа
защиты, даже если код теоретически вскрываем.
3. **Shortcut bias.** Вводящая в заблуждение структура (ловушки в
Dispatch Maze) заставляет модель остановиться раньше времени.
@ -59,14 +59,14 @@ indirect/regs и switch/stack).
**Ключевое экономическое наблюдение авторов:** нативное исполнение занимает
~55 мс, а статический провал агента стоит **$1.50–$9.67**. Это и есть
реальный рычаг — не "нечитаемость навсегда", а разрыв между ценой атаки и
реальный рычаг - не "нечитаемость навсегда", а разрыв между ценой атаки и
ценой сборки для нас. Авторы прямо предупреждают: преимущество схлопнется,
когда у агентов появятся инструменты динамического исполнения (сейчас
тестировался только статический анализ).
## 2. Acoda: атака на safety-alignment и токенизацию (ICSE 2026)
[Acoda](https://arxiv.org/pdf/2606.11755) — другая постановка задачи: не
[Acoda](https://arxiv.org/pdf/2606.11755) - другая постановка задачи: не
агент с дизассемблером, а LLM, которой **в чат вставили исходный код** и
попросили его проанализировать/деобфусцировать. Генетический алгоритм
подбирает комбинацию из 8 трансформаций, до 70% ASR (attack success rate)
@ -81,7 +81,7 @@ indirect/regs и switch/stack).
| до анализа | Vulnerability Code Injection | фрагмент с SQL-инъекцией/`pickle.loads`/command injection в заведомо мёртвой ветке (`while (сложное_но_ложное_условие)`) |
| во время анализа | Misleading Summarization | модель сама просится описать код, затем это описание (намеренно неверное) вклеивается рядом с кодом |
| во время анализа | String Obfuscation | `base64` + словарь, расшифровка даёт слова типа "virus"/"weapon" |
| во время анализа | Try-Except Wrapping | настоящая логика лежит в `except`-ветке, в `try` — правдоподобный, но бесполезный код |
| во время анализа | Try-Except Wrapping | настоящая логика лежит в `except`-ветке, в `try` - правдоподобный, но бесполезный код |
| после анализа | EOS Token Insertion | в текст вставлены литералы токенов-разделителей (`<|EOT|>`), из-за чего генерация обрывается раньше |
### Честная оценка: что из этого применимо к нам
@ -93,24 +93,24 @@ indirect/regs и switch/stack).
- `DebugInfo` уже вычищает всё, что могло бы стать комментарием или
осмысленным описанием (`LineNumberTable`, `Signature`, исходные имена);
в скомпилированном `.class` комментариев не бывает в принципе.
- Цель Acoda — заставить модель **отказаться анализировать свой же код**,
- Цель Acoda - заставить модель **отказаться анализировать свой же код**,
вставленный в чат пользователем. У нас код анализирует **чужой** агент
чужого jar — нам не нужно, чтобы он отказался, если он не наш союзник;
и вызывать намеренно подозрительные паттерны в легитимном продукте —
чужого jar - нам не нужно, чтобы он отказался, если он не наш союзник;
и вызывать намеренно подозрительные паттерны в легитимном продукте -
прямой риск получить ложное срабатывание антивируса/EDR на свой же jar.
Это противоречит `README.md` → «Ответственное использование».
- **EOS Token Insertion** не переносится: это свойство текстового
стриминга токенов модели, а не байткода.
Единственное, что частично переносится: **String Obfuscation** и
**Variable Renaming** — ровно то, что `Strings` и `Rename` уже делают,
просто дешевле (у Acoda — обратимый `base64`, у нас — ротация шифров с
**Variable Renaming** - ровно то, что `Strings` и `Rename` уже делают,
просто дешевле (у Acoda - обратимый `base64`, у нас - ротация шифров с
выводом ключа в рантайме, см. [`../stack.md`](../stack.md) §4-5). Здесь мы
уже на шаг впереди статьи, а не позади.
**Vulnerability Code Injection** как идея (мёртвый код, похожий на уязвимый
паттерн) пересекается с нашим `Noise`, но внедрять **реально похожие на
эксплойт сигнатуры** в выдаваемый клиентам jar — плохая идея для легального
эксплойт сигнатуры** в выдаваемый клиентам jar - плохая идея для легального
продукта защиты: это именно то, от чего предостерегает
[`info/evo-weaknesses.md`](../../info/evo-weaknesses.md) с другой стороны
(не подставлять защищённый продукт под чужие red-листы).
@ -118,24 +118,24 @@ indirect/regs и switch/stack).
## 3. Что добавить в noxis практически
Экономический рычаг из Elastic (цена агента > цены нашей сборки) ложится на
архитектуру без переделок, конкретные пункты — в [`../../todo.md`](../../todo.md)
архитектуру без переделок, конкретные пункты - в [`../../todo.md`](../../todo.md)
§7:
1. **Вложенные слои шифрования строк** (аналог Matryoshka Wall) — расширить
1. **Вложенные слои шифрования строк** (аналог Matryoshka Wall) - расширить
`StringsConfig` полем "глубина", decoder разворачивает N слоёв вместо
одного. Цена агента растёт с глубиной почти линейно, цена сборки — нет.
2. **Больше decoy-точек ветвления ключа** (аналог Dispatch Maze) — наш
одного. Цена агента растёт с глубиной почти линейно, цена сборки - нет.
2. **Больше decoy-точек ветвления ключа** (аналог Dispatch Maze) - наш
декодер уже собирает мастер-секрет из `k1^k2` (`stack.md` §4); можно
поднять число частей и раскидать их сборку по случайным junk-методам
из `Noise`, а не держать в одном месте.
3. **MBA вместо простых тождеств в `Constants`** — то же решение, что ловит
3. **MBA вместо простых тождеств в `Constants`** - то же решение, что ловит
и символьный исполнитель, и SMT-решатель, и, по данным Elastic,
"Flatten + MBA" бьёт сильнее, чем просто Flatten.
4. **Не делать:** имитацию вредоносных паттернов, prompt injection в
строках, EOS-токены. Либо не переносится на байткод, либо создаёт риск
ложного срабатывания защитных систем на легитимном продукте.
Критерий "сработало/не сработало" для всего этого — как и для остальных
Критерий "сработало/не сработало" для всего этого - как и для остальных
техник: прогон конкретного агента на конкретной сборке, а не вера в
методичку. Эффект устаревает с каждой новой версией модели.
@ -151,17 +151,17 @@ indirect/regs и switch/stack).
- **Для нас это ломает собственное допущение** из
[`../stack.md`](../stack.md) §3: `noxis-verify` верифицирует метод через
`java -Xverify:all`, потому что это *обычный* байткод с валидным
`StackMapTable`. Виртуализированный метод — уже не обычный байткод,
`StackMapTable`. Виртуализированный метод - уже не обычный байткод,
верификатор проверит только обвязку интерпретатора, не тело. Big-ticket
item, не для ближайшего спринта.
- **MBA** укладывается без архитектурных изменений — см. выше, §3.
- **MBA** укладывается без архитектурных изменений - см. выше, §3.
## Источники
- [Elastic Security Labs — The Cost of Understanding: LLM-Driven Reverse Engineering vs Iterative LLM Obfuscation](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation)
- [Elastic Security Labs - The Cost of Understanding: LLM-Driven Reverse Engineering vs Iterative LLM Obfuscation](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation)
- [Acoda: Adversarial Code Obfuscation for Defending against LLM-based Analysis (arXiv 2606.11755, ICSE-Companion '26)](https://arxiv.org/pdf/2606.11755)
- [A Systematic Study on Static Control Flow Obfuscation Techniques in Java (arXiv 1809.11037)](https://arxiv.org/pdf/1809.11037)
- [Promon Shield: Virtual Machine obfuscation SDK protection](https://promon.io/security-news/virtual-machine-obfuscation-sdk-protection)
- [CoTDeceptor: Adversarial Code Obfuscation Against CoT-Enhanced LLM Code Agents (arXiv 2512.21250)](https://arxiv.org/pdf/2512.21250)
- [Deconstructing Obfuscation: a four-dimensional framework for evaluating LLM deobfuscation (arXiv 2505.19887)](https://arxiv.org/pdf/2505.19887)
- [Tigress — диверсифицирующий обфускатор для C, использованный в бенчмарке Elastic](https://tigress.wtf/)
- [Tigress - диверсифицирующий обфускатор для C, использованный в бенчмарке Elastic](https://tigress.wtf/)

View file

@ -1,11 +1,11 @@
# Уроки: что защита EvoVis сделала плохо
Источник: `info/evo-weaknesses.md` — полный разбор за один проход, без
рантайм-хаков. Здесь — что из этого следует для **нашей** архитектуры.
Источник: `info/evo-weaknesses.md` - полный разбор за один проход, без
рантайм-хаков. Здесь - что из этого следует для **нашей** архитектуры.
Правило, которое стоит держать головой: **защита ломается не в рантайме,
а на этапе, когда атакующий смотрит на статику.** Всё, что видно в `javap -p`
или в содержимом jar, — это утечка.
или в содержимом jar, - это утечка.
## Сводная таблица: провал → наше решение
@ -13,21 +13,21 @@
|---|---|---|---|---|
| 1 | ключ лежит рядом с шифротекстом | вывод мастер-секрета в декодере, ключ из `master‖salt` | Strings | **готово** |
| 2 | переименованы типы, но не члены | rename полей/методов + семейства virtual | Rename | **готово** |
| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | **готово** (`SourceFile`; номера строк — план) |
| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | **готово** (`SourceFile`; номера строк - план) |
| 4 | шаблонный flattening | 4 схемы CFG + мешанина внутри метода | Flow | **готово** (4 схемы, приёмка `java`) |
| 5 | opaque-предикаты = `A+B` | ротация тождеств + предикаты от состояния | Constants | **готово** (int/long) |
| 6 | junk-классы вне графа reachability | `weave_reachable` — обязательная вплетённость | Noise | **готово** (без аннотаций) |
| 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 вызова
`awc.d("<cipher>", <opaque-выражение>)` - ключ был **константным выражением
в вызове**. Свёртка выражения - ~200 строк любого парсера. Итог: 8993 вызова
расшифрованы детерминированно, **0 обращений к JVM**.
Хуже: один шифр на весь продукт (LCG с фиксированными `a/c`) — зная одну
Хуже: один шифр на весь продукт (LCG с фиксированными `a/c`) - зная одну
пару «шифротекст → текст», остальные раскрываются по обучению.
**Что делаем иначе.**
@ -41,56 +41,56 @@
- MAC: подмена байта не «тихо» портит текст.
Остаточный риск честно признан: целевой анализ декодера раскрывает текст.
Задача — поднять цену, а не сделать невозможным.
Задача - поднять цену, а не сделать невозможным.
## 2. Члены не переименованы
`onDraw`, `getProjectiles`, `isAuthorized()` — открытые имена. Класс `wb`
`onDraw`, `getProjectiles`, `isAuthorized()` - открытые имена. Класс `wb`
опознаётся за секунду по трём понятным методам.
**Что делаем иначе:** переименование полей и методов входит в базовый
уровень, а не в «продвинутый». Уже готово, включая:
- **семейства virtual** — переименование на всю иерархию, чтобы не
- **семейства virtual** - переименование на всю иерархию, чтобы не
разъединить переопределения и не сломать `invokevirtual`;
- **keep-список** — private/static/final можно сразу, `rename_public_members`
- **keep-список** - private/static/final можно сразу, `rename_public_members`
по умолчанию выключен для библиотек, чтобы не сломать API;
- **автодетект рефлексии по имени** — `Class.forName`, `getMethod`,
- **автодетект рефлексии по имени** - `Class.forName`, `getMethod`,
`getField` с литералами попадают в keep автоматически.
## 3. Отладочная информация
`SourceFile` оригинальные у 160 классов, `LineNumberTable` — 10 218,
`LocalVariableTable` — имена локальных переменных, `@rn(name, description)` —
`SourceFile` оригинальные у 160 классов, `LineNumberTable` - 10 218,
`LocalVariableTable` - имена локальных переменных, `@rn(name, description)` -
каталог всех модулей с описаниями.
**Что делаем иначе:** пасс `DebugInfo` — strip `SourceFile`,
**Что делаем иначе:** пасс `DebugInfo` - strip `SourceFile`,
`LineNumberTable`, `LocalVariableTable`, `Signature`, `InnerClasses` +
**ложная** отладочная информация (чужие имена и номера строк, чтобы
деобфускатор выдавал правдоподобный, но чужой код).
Отсюда обязательное требование: **маппинги в отчёте** — иначе мы сами не
Отсюда обязательное требование: **маппинги в отчёте** - иначе мы сами не
читаем свои stack trace'и.
## 4. Шаблонный flattening
Один regex нашёл 3530 автоматов: `while(true) { switch(i + i2) }`, метки —
константы, переходы — присваивания в одном basic block, тела блоков не
Один 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. Без верификатора
`StackMapTable` - см. [`../stack.md`](../stack.md) §3. Без верификатора
Flow не включается.
## 5. Opaque-предикаты
`(A^B)+((A&B)<<1)` — школьное тождество, узнаётся регуляркой и даёт
`(A^B)+((A&B)<<1)` - школьное тождество, узнаётся регуляркой и даёт
бесплатный фолдинг остальных констант. Настоящих предикатов (зависящих
от состояния и всегда истинных) нет.
@ -103,26 +103,26 @@ Flow не включается.
42 файла, ни одного входящего ссылока, `dangling=0` → BFS от 4
entrypoint'ов, которые открыто лежат в `fabric.mod.json`.
**Что делаем иначе:** `weave_reachable` — junk попадает в граф
**Что делаем иначе:** `weave_reachable` - junk попадает в граф
достижимости, создаёт ложные рёбра и участвует в вызовах. Это свойство
пасса, а не настройка: без вплетённости junk не включается вовсе.
## 7. Самоописание защиты
`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`,
`SealedBoot`, `NativeGuard` — названия указывают атакующему, куда смотреть.
`SealedBoot`, `NativeGuard` - названия указывают атакующему, куда смотреть.
**Что делаем иначе:** псевдонимы генерируются одним `NameGenerator` для
всех сущностей — классы, пакеты, поля, методы, junk и даже имена атрибутов
всех сущностей - классы, пакеты, поля, методы, junk и даже имена атрибутов
и схем выглядят как обычные данные. Отдельного «защитного» стиля не
существует, поэтому отличить его от остального кода не по чему.
## 8. Вторичные компоненты
`evo-agent.jar` вообще без обфускации, .NET-лаунчер — статическая XOR-таблица.
`evo-agent.jar` вообще без обфускации, .NET-лаунчер - статическая XOR-таблица.
**Что делаем иначе:** обфускация применяется ко **всему архиву**, включая
ресурсы (`fabric.mod.json`, mixins, refmap, access widener) — см. пасс
ресурсы (`fabric.mod.json`, mixins, refmap, access widener) - см. пасс
`Resources`. Отдельного «ядро защищено, остальное нет» не бывает.
## 9. Шифр без аутентификации
@ -131,16 +131,16 @@ entrypoint'ов, которые открыто лежат в `fabric.mod.json`.
работает.
**Уже сделано:** `salt` на строку, уникальный `nonce`, FNV-1a MAC в
`noxis-runtime::scheme`. `cipher:tag` лежат рядом — порча обнаруживается.
`noxis-runtime::scheme`. `cipher:tag` лежат рядом - порча обнаруживается.
## Что подтвердилось и не нужно повторять в выводах
Разбор дал и **опровержения** — важно не строить защиту на мифах:
Разбор дал и **опровержения** - важно не строить защиту на мифах:
- «сдвиги в байткоде бывают отрицательными» — по 1277 классам **0** случаев
- «сдвиги в байткоде бывают отрицательными» - по 1277 классам **0** случаев
(32 559 сдвигов, константные всегда `1`);
- `awc.g(...)` (session-gate) в билде **не вызывался ни разу**.
Вывод: проверять гипотезу на статистике **до** того, как вкладываться
в технику против неё. Отсюда требование к `noxis-verify` — не «красиво
в технику против неё. Отсюда требование к `noxis-verify` - не «красиво
обфусцировано», а измеримые метрики ([`priorities.md`](priorities.md)).

View file

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

View file

@ -37,7 +37,7 @@
| `DebugInfoStripper` | 69 | strip отладки | пасс `debug` |
| `StringFilter` | 35 | что считать строкой | функция внутри `strings` |
**Вывод:** структура совпадает с нашей почти один в один — это подтверждает
**Вывод:** структура совпадает с нашей почти один в один - это подтверждает
раскладку крейтов. Ключевое отличие: у них всё живёт в одном jar'е и
одном проекте; у нас конвейер отделён от I/O и от CLI.
@ -47,11 +47,11 @@
У них: remap → strings → numbers → flow → junk → debug. Тот же инвариант,
что и у нас: **переименование раньше всех техник, порождающих новые ссылки**,
а `debug` — последним.
а `debug` - последним.
### 3.2 Наследование как условие безопасности
`InheritanceAnalyzer` — самый большой после remap'ера модуль. Совпадает с
`InheritanceAnalyzer` - самый большой после remap'ера модуль. Совпадает с
нашим выводом: переименование virtual-методов **без** иерархии ломает
`invokevirtual`. У нас это `hierarchy` + семейства в `rename`.
@ -63,13 +63,13 @@
### 3.4 Подпись срезается
`*.SF/*.RSA/*.DSA/*.EC` удаляются при перепаковке — после записи подпись
`*.SF/*.RSA/*.DSA/*.EC` удаляются при перепаковке - после записи подпись
всё равно невалидна, а её наличие заставляет JVM падать. Это входит в
`noxis-jar`.
### 3.5 Маппинг в файл + ложные ограничения
`--mappings` пишет соответствие «новое → старое» — без него автор сам не
`--mappings` пишет соответствие «новое → старое» - без него автор сам не
читает stack trace. Плюс честные off-ключи: `--no-strings`, `--no-flow`,
`--no-junk`, `--keep-lines`, `--keep-source`, `--keep-vars`, `--no-widen`.
@ -78,7 +78,7 @@
### 3.6 Seed
`--seed` — детерминированный прогон. У нас это базовое свойство
`--seed` - детерминированный прогон. У нас это базовое свойство
`noxis-core`, без отдельного флага.
## 4. Что не берём
@ -86,24 +86,24 @@
### 4.1 Java + ASM
Главное отличие стека. Пара Java+ASM даёт родную JVM, готовые visitor'ы и
`CheckClassAdapter` — но и:
`CheckClassAdapter` - но и:
- нужно раскатывать JDK для сборки и запуска тестов;
- наш проект — Rust (`deny(unsafe_code)`, свои лимиты, свои гейты);
- наш проект - Rust (`deny(unsafe_code)`, свои лимиты, свои гейты);
- отсутствует возможность отдавать один бинарь.
Цена, которую мы платим: `StackMapTable` придётся считать самим (через
inference-верификатор — [`../stack.md`](../stack.md) §3). Это осознанно.
inference-верификатор - [`../stack.md`](../stack.md) §3). Это осознанно.
### 4.2 Swing GUI
Лишняя точка входа: GUI дублирует CLI и удваивает площадь тестирования.
У нас — только clap.
У нас - только clap.
### 4.3 `StringDecoderGenerator` как «генератор текста»
Они генерируют Java-исходник декодера и компилируют. Для нас это означало бы
зависимость от `javac` на этапе обфускации. Наш путь — генерировать
зависимость от `javac` на этапе обфускации. Наш путь - генерировать
**байткод напрямую** через наш IR (`noxis-runtime` держит схемы, генератор
собирает `ClassFile`).
@ -120,14 +120,14 @@ inference-верификатор — [`../stack.md`](../stack.md) §3). Это
## 5. Что проверяем на их smoke-тестах
В `target/` референса лежат готовые проверочные кейсы — их стоит
В `target/` референса лежат готовые проверочные кейсы - их стоит
переиспользовать как наш regression-набор:
| кейс | что ломает |
|---|---|
| `smoke-safe` | базовый round-trip, `Sample`/`MyMixin` |
| `smoke-inheritance` | `Base`/`Child`/`InheritedChild`/`ExternalChild` — семейства virtual и мосты |
| `smoke-flow` | `LoadAll` — загрузка всех классов, ловит `VerifyError` |
| `smoke-inheritance` | `Base`/`Child`/`InheritedChild`/`ExternalChild` - семейства virtual и мосты |
| `smoke-flow` | `LoadAll` - загрузка всех классов, ловит `VerifyError` |
| `smoke-fields` | переименование полей и статических полей |
| `smoke-aes` | шифрование строк, `RunStrings` сверяет расшифровку |
| `Probe.java` | точечная проверка одной гипотезы |
@ -144,7 +144,7 @@ inference-верификатор — [`../stack.md`](../stack.md) §3). Это
| keep-сканер | генератор Java-исходника |
| срез подписей, патч ресурсов | единственная схема шифра |
| маппинг в отчёт, честные off-ключи | эмпирическая верификация flow |
| их smoke-кейсы как regression | — |
| их smoke-кейсы как regression | - |
Полный перечень провалов защиты, которую они **не** закрывают, — в
Полный перечень провалов защиты, которую они **не** закрывают, - в
[`lessons.md`](lessons.md).