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:
parent
2172206e83
commit
139aea9a8f
96 changed files with 1053 additions and 831 deletions
|
|
@ -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)).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue