obfuskator/info/evo-weaknesses.md
loki5512344 139aea9a8f
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.
2026-10-09 13:47:27 +02:00

76 lines
5.6 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 провалилась - чего нам делать нельзя
Разобрано полностью за один проход, без единого рантайм-хака. Каждый пункт -
что именно дало ключ атаки.
## 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) в этом билде **не вызывается ни разу**.