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

5.6 KiB
Raw Blame History

Где защита 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) в этом билде не вызывается ни разу.