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.
76 lines
5.6 KiB
Markdown
76 lines
5.6 KiB
Markdown
# Где защита EvoVis провалилась - чего нам делать нельзя
|
||
|
||
Разобрано полностью за один проход, без единого рантайм-хака. Каждый пункт -
|
||
что именно дало ключ атаки.
|
||
|
||
## 1. Ключ шифрования строк лежит рядом со шифротекстом
|
||
|
||
`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` -
|
||
открыто. Семантика класса читается по одному `javap -p`. Класс с тремя понятными
|
||
методами опознаётся за секунду, даже если имя `wb`.
|
||
|
||
## 3. Метаданные и отладочная информация не тронуты
|
||
|
||
- `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
|
||
автоматов. Декомпилятор восстанавливает CFG, потому что:
|
||
- `switch` по **константным** меткам (метки не закодированы);
|
||
- все переходы - прямые присваивания в одном basic block;
|
||
- нет вычислимых на входе предикатов, нет переходов через таблицы/массивы;
|
||
- нет обфускации самих тел блоков (тела - обычный код).
|
||
|
||
## 5. Opaque-предикаты сводятся к `A+B`
|
||
|
||
`(A^B)+((A&B)<<1)` и `(A|B)+(A&B)` - это школьные тождества. Распознаются
|
||
регуляркой, сворачиваются в один проход, заодно выдают «бесплатный» фолдинг
|
||
остальных констант. Ни одного настоящего opaque-предиката (зависящего от состояния
|
||
памяти/окружения и всегда истинного) в коде нет.
|
||
|
||
## 6. Декой-классы детектируются reachability
|
||
|
||
42 файла, ни одного входящего ссылока, `dangling=0`. Мгновенно вычисляются BFS'ом
|
||
от entrypoint'ов (их, кстати, 4 и они раскрыты в `fabric.mod.json`). Балласт не
|
||
смешан с графом и не создаёт ложных рёбер.
|
||
|
||
## 7. Каждая защита живёт отдельно и называется своим именем
|
||
|
||
`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`, `SealedBoot`,
|
||
`NativeGuard` - названия прямо говорят атакующему, куда смотреть. Плюс open-source
|
||
идентификаторы: `aln` упомянут в рефлексии строкой, `awc` - по имени вызова.
|
||
|
||
## 8. Вторичные компоненты вообще без защиты
|
||
|
||
- `evo-agent.jar` (расшифровщик payload) - открытые строки, `SourceFile`, без обфускации.
|
||
- Лаунчер .NET - статическая XOR-таблица `b ^ i ^ 0xAA`, имена сервисов не тронуты.
|
||
- Защита ключа в нативе сильная, но **код мода** защищён слабее, чем ключ.
|
||
|
||
## 9. Шифр без аутентификации и без per-string salt
|
||
|
||
LCG+XOR: нет MAC, нет случайного сида на строку, нет индивидуального алгоритма.
|
||
Частичное повреждение шифротекста не обнаруживается, анализ по частотам работает.
|
||
|
||
## Опровержение (чтобы не повторять чужие ошибки в выводах)
|
||
|
||
- «Сдвиги в байткоде могут быть отрицательными (`>> -8` == `>> 24`)» - по всем
|
||
1277 классам **0** таких случаев (32 559 сдвигов, константные - всегда `1`).
|
||
- `awc.g(...)` (session-gate) в этом билде **не вызывается ни разу**.
|