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