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

@ -1,47 +1,47 @@
# Где защита EvoVis провалилась — чего нам делать нельзя
# Где защита EvoVis провалилась - чего нам делать нельзя
Разобрано полностью за один проход, без единого рантайм-хака. Каждый пункт —
Разобрано полностью за один проход, без единого рантайм-хака. Каждый пункт -
что именно дало ключ атаки.
## 1. Ключ шифрования строк лежит рядом со шифротекстом
`awc.d("<cipher>", <opaque-выражение>)` — ключ это **константное выражение в вызове**.
`awc.d("<cipher>", <opaque-выражение>)` - ключ это **константное выражение в вызове**.
Ни secrets, ни runtime-derivation: выражение просто сворачивается в `int` (любой
парсер вроде `scripts/expr.rs`, ~200 строк). Итог: **8993 вызова расшифрованы
детерминированно, 0 обращений к JVM**.
Хуже: шифр один на весь продукт (LCG с фиксированными `a/c`), т. е. зная одну
пару «шифротекст → текст», остальные не нужно даже сворачивать — можно учиться.
пару «шифротекст → текст», остальные не нужно даже сворачивать - можно учиться.
## 2. Обфусцированы только типы, но не члены
`onDraw`, `getProjectiles`, `BreakingBad`, `key()`, `isAuthorized()`, `setUser` —
`onDraw`, `getProjectiles`, `BreakingBad`, `key()`, `isAuthorized()`, `setUser` -
открыто. Семантика класса читается по одному `javap -p`. Класс с тремя понятными
методами опознаётся за секунду, даже если имя `wb`.
## 3. Метаданные и отладочная информация не тронуты
- `SourceFile: "StringRuntime.java"` / `"KeyAssembler.java"` / `"PayloadCrypto.java"` —
оригинальные имена 160 классов (все миксины — 100%).
- `LineNumberTable` (10 218) + `LocalVariableTable` (151) — имена локальных переменных.
- `@rn(name=..., description=...)` — полный каталог всех 57 модулей с описаниями.
- `SourceFile: "StringRuntime.java"` / `"KeyAssembler.java"` / `"PayloadCrypto.java"` -
оригинальные имена 160 классов (все миксины - 100%).
- `LineNumberTable` (10 218) + `LocalVariableTable` (151) - имена локальных переменных.
- `@rn(name=..., description=...)` - полный каталог всех 57 модулей с описаниями.
- `fabric.mod.json`, `*.mixins.json` (пути и состав), `accesswidener`, ресурсы.
Все эти вещи дешевле всего чинить и дороже всего отдавать.
## 4. Flattening сделан «по учебнику»
Один шаблон на весь продукт: `while(true) { switch (i + i2) }`, номер состояния —
сумма двух opaque-полей, переходы — присваивание пары. Один regex находит 3530
Один шаблон на весь продукт: `while(true) { switch (i + i2) }`, номер состояния -
сумма двух opaque-полей, переходы - присваивание пары. Один regex находит 3530
автоматов. Декомпилятор восстанавливает CFG, потому что:
- `switch` по **константным** меткам (метки не закодированы);
- все переходы — прямые присваивания в одном basic block;
- все переходы - прямые присваивания в одном basic block;
- нет вычислимых на входе предикатов, нет переходов через таблицы/массивы;
- нет обфускации самих тел блоков (тела — обычный код).
- нет обфускации самих тел блоков (тела - обычный код).
## 5. Opaque-предикаты сводятся к `A+B`
`(A^B)+((A&B)<<1)` и `(A|B)+(A&B)` — это школьные тождества. Распознаются
`(A^B)+((A&B)<<1)` и `(A|B)+(A&B)` - это школьные тождества. Распознаются
регуляркой, сворачиваются в один проход, заодно выдают «бесплатный» фолдинг
остальных констант. Ни одного настоящего opaque-предиката (зависящего от состояния
памяти/окружения и всегда истинного) в коде нет.
@ -55,13 +55,13 @@
## 7. Каждая защита живёт отдельно и называется своим именем
`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`, `SealedBoot`,
`NativeGuard` — названия прямо говорят атакующему, куда смотреть. Плюс open-source
идентификаторы: `aln` упомянут в рефлексии строкой, `awc` — по имени вызова.
`NativeGuard` - названия прямо говорят атакующему, куда смотреть. Плюс open-source
идентификаторы: `aln` упомянут в рефлексии строкой, `awc` - по имени вызова.
## 8. Вторичные компоненты вообще без защиты
- `evo-agent.jar` (расшифровщик payload) — открытые строки, `SourceFile`, без обфускации.
- Лаунчер .NET — статическая XOR-таблица `b ^ i ^ 0xAA`, имена сервисов не тронуты.
- `evo-agent.jar` (расшифровщик payload) - открытые строки, `SourceFile`, без обфускации.
- Лаунчер .NET - статическая XOR-таблица `b ^ i ^ 0xAA`, имена сервисов не тронуты.
- Защита ключа в нативе сильная, но **код мода** защищён слабее, чем ключ.
## 9. Шифр без аутентификации и без per-string salt
@ -71,6 +71,6 @@ LCG+XOR: нет MAC, нет случайного сида на строку, н
## Опровержение (чтобы не повторять чужие ошибки в выводах)
- «Сдвиги в байткоде могут быть отрицательными (`>> -8` == `>> 24`)» — по всем
1277 классам **0** таких случаев (32 559 сдвигов, константные — всегда `1`).
- «Сдвиги в байткоде могут быть отрицательными (`>> -8` == `>> 24`)» - по всем
1277 классам **0** таких случаев (32 559 сдвигов, константные - всегда `1`).
- `awc.g(...)` (session-gate) в этом билде **не вызывается ни разу**.