obfuskator/info/evo-obfuscation-full.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

12 KiB
Raw Blame History

Обфускация EvoVis / EvolutionDLC - полный сводный разбор

Всё проверено по артефактам в LoVisual/ref/evo/ (jadx-декомпиляция evovis-dec.jar, javap -v по 1277 классам корня, Rust-скрипты scripts/). Цифры, которые пересчитаны вручную, помечены ✓; расхождения с исходным README - в конце.

Пакеты: 1405 .class всего = 1277 в default package (корень jar) + 16 в a/

  • 111 в b/ + 128 вложенных (evo/**, зависимости). jadx даёт 6991 .java (включая бандл-зависимости), из них defpackage/ ≈ 318 931 строк кода.

Уровень 1 - имена

  • Все классы мода вынесены в default package (defpackage/ в jadx) с именами a…zz (1277 шт.); миксины разложены по a/ (16) и b/ (111).
  • Миксины в evolutiondlc.mixins.json / holdmyitems.mixins.json тоже закодированы: "package": "b", список "y", "z", "al", … - смысловых имён в манифесте нет.
  • Часть имён - ключевые слова Java (if, do…): декомпилятор вынужден их переименовывать (Cif, Cdo), в самом jar классов с такими именами нет.
  • Члены (поля/методы) НЕ переименовываются ✓: wb.onDraw, getProjectiles, BreakingBad, aln.key(), setDaemon, REFRESH_THREAD и т. д. читаются как есть. Обфусцируются только типы и пакеты.
  • SourceFile частично сохранён ✓ (27 классов в корне + все 127 миксинов): javap даёт Compiled from "StringRuntime.java", "KeyAssembler.java", "AttestClient.java", "PayloadCrypto.java", … у миксинов - "GameRendererMixin.java". 160 файлов содержат compiled from: - по ним восстанавливаются оригинальные имена.
  • Отладочная информация осталась: 10 218 LineNumberTable и 151 LocalVariableTable ✓ (локальные имены переменных живут у security-классов: awc - 5, aln - 11).
  • Аннотации модулей не тронуты: @rn(name = "HitFix", category = rl.COMBAT, description = "Makes attacks land on the hitbox you actually see…") - 57 классов ✓, то есть каталог модулей читается без всякой деобфускации.
  • Enum-константы не переименованы: SCALE, OPACITY, SPAWN, KEYFRAME. При этом 102 enum jadx не восстановил (Failed to restore enum class) - обфускатор сломал декомпиляторский паттерн enum, но имена констант в байткоде остались.

Уровень 2 - строки

Класс awc = evovis.security.StringRuntime (раскрыт по SourceFile):

state = key;                                   // int, 32-бит wrap
for (i = 0; i < cipher.length(); i++) {
    state = state * 1103515245 + 12345;        // LCG
    bytes[i] = (byte)(cipher.charAt(i) ^ (state >>> 24));
}
return new String(bytes, UTF_8).intern();      // + ConcurrentHashMap CACHE
  • awc.d(cipher, key) - обычная строка. Ключ передаётся как opaque-выражение прямо в вызове, ни одного «настоящего» ключа в рантайме нет.
  • awc.g(cipher, key) - то же, но session-gate: сначала Class.forName("aln").getMethod("key").invoke(null) (рефлексией, имя класса открытым текстом), если ключа сессии нет - возвращает null.
  • SecureStrings (5 API-блобов) - гейт по KeyAssembler.key(), при отсутствии ключа get() возвращает null и серверные фичи просто выключаются.
  • Шифр одинаков для всех строк, кэш по шифротексту, .intern().

Уровень 3 - контрольный поток

Тело метода разложено в конечный автомат с раздвоенным состоянием:

int i  = (-513638579) ^ 765313817;                 // = A + B (полумладший)
int i2 = ((-513638579) & 765313817) << 1;           // = (A & B) << 1
while (true) {
    switch (i + i2) {                               // сумма = номер состояния
        case -1866771718: setUser(...); return;
        case -804841211:  crVar = cq.load();
                          i  = 1484979540 | 544286359;
                          i2 = 1484979540 & 544286359; break;
        case 251675238:  default: ...
    }
}
  • Переход - присваивание двум переменным (i, i2) и обратный goto в начало цикла. Состояние хранится не одной константой, а суммой двух opaque-полей: константная пропаганда обязана проследить пару, а не одно присваивание.
  • Формы встреч ✓: switch (i + i2) - 1826, switch (i2 + i3) - 1067, switch (i3 + i4) - 283, switch (i4 + i5) - 161, дальше хвост до i15 + i16. В сумме ≈ 3530 автоматов в 505 файлах.
  • while (true) + switch вместе встречаются в 726 файлах из 6991 ✓.
  • Из-за этого defpackage/ раздувается до ~319k строк на ~1242 файла (реальный объём кода в разы меньше; для сравнения у LoVisual 77 368 строк на 1928 файлов).

Кодирование констант (opaque predicates)

Все целочисленные литералы - тождества по модулю 2³² (и 2⁶⁴):

паттерн равен
(A ^ B) + ((A & B) << 1) A + B
(A | B) + (A & B) A + B
(A ^ -A) + ((A & -A) << 1) 0
(A | -A) + (A & -A) 0

Применяется везде, не только в ключах строк:

// true / 0 / 1
setDaemon((A ^ B) + ((A & B) << 1));
strArr[((-702410340) | 702410340) + ((-702410340) & 702410340)] = ...   // индекс 0

// long-вариант (Duration.ofSeconds(10))
(703617214384230201L ^ (-703617214384230191L)) + ((703617214384230201L & (-703617214384230191L)) << 1)

// сдвиговые константы тоже закодированы - см. «опровержение» ниже

Мёртвый код / декой-классы

Reachability-BFS от bootstrap/@rn/a+b/security: 1370 классов → 1316 живых, 42 unreachable ✓ (analysis/structure.md). Среди них apc (299 LOC, @Deprecated, implements aqg), интерфейсы (aa), enum'ы (bc, bd, be). Ни один живой файл не ссылается на отброшенный (dangling=0) - то есть это балласт, выключенный из графа, а не случайные остатки.


Лаунчер (.NET) - отдельная, очень слабая схема

  • Один байтовый блоб byte[15927] _4 в PrivateImplementationDetails, в статическом конструкторе _4[i] = (byte)(_4[i] ^ i ^ 0xAA); чтение через аксессоры return _5[N] ?? _6(off, len) → Encoding.UTF8.
  • 911 аксессоров ✓ (все в одном файле), вызовы по всему коду - итог 1226 замен (по scripts/deobfuscate_launcher).
  • Имена сервисов не тронуты: ModProvisioner, PayloadKeyClient, EvoVisAuth, HardwareId, DeliveryMirror. Никакой обфускации имён, никакой обфускации потока - одна статическая XOR-таблица, восстанавливается одним проходом.
  • strings.txt - 929 расшифрованных записей.

Смежные слои защиты (не «обфускация кода», но часть контура)

  1. Payload: 12B IV || AES-256-GCM(ct||tag), ключ из сервера, раскрывается цепочкой HMAC/HKDF от hwid + token + доли из java.dll - см. evo-payload-pipeline.md.
  2. Антиотладка и порча ключа - 3 независимых уровня XOR-масок (0xA5, 0x3C, 0x5A), IsDebuggerPresent, JAVA_TOOL_OPTIONS, JVM_EvoForeignAgentPresent - см. evo-native-reverse.md.
  3. HWID = sha256("evovis|" + MachineGuid) - привязка к машине, Shamir-фрагменты ключа, аттестация (evovis-protect.vercel.app).
  4. evo-agent.jar вообще не обфусцирован ✓ - читаемые строки, SourceFile: "EvoAgent.java", т. е. расшифровщик payload открыт.
  5. Скрипт-логирование с детектом $ExecutionContext.SessionState.LanguageMode (защита от поломки PowerShell-обфускации у себя).

Проверка исходных утверждений

утверждение (obfuscation.md / README) статус
1277 классов в default package, 16 в a/, 111 в b/ ✓ подтверждено
LCG 1103515245 / 12345, XOR со старшим байтом ✓ подтверждено (awc.java)
«все строковые литералы зашифрованы awc.d» частично: 8993 вызова awc.d( в jar-src ✓, но awc.g( - 0 вызовов в этом билде (описанная ветка не используется)
8924 вызова / 5367 уникальных строк расхождение: в файлах 8993 вызовов, mod_strings.txt - 5665 строк, tsv - 9248 пар. README, видимо, от усечённого прогона
opaque-тождества (A^B)+((A&B)<<1) и (A|B)+(A&B) ✓ подтверждено, есть и long-вариант
«сдвиговые константы в байткоде отрицательные (>> -8 == >> 24)» ✗ не подтверждено: по javap всех 1277 классов - 32 559 сдвигов, ни одного отрицательного непосредственного операнда (все константные сдвиги - это iconst_1 из opaque-паттерна << 1). В исходниках тоже 0 вхождений >> -. Скорее всего, наблюдение из другого билда/декомпилятора
«декой-классы - пустышки с бессмысленными членами» ⚠ reachability (42 файла) подтверждается, но часть из них - осмысленный код (apc, 299 LOC, @Deprecated), часть - пустые интерфейсы/enum. «Пустышки» - интерпретация
«объём декомпилированного кода раздут ~25 раз» ⚠ порядок величины верен (319k строк против, возможно, ~15k реальных), точный коэффициент не измерен
«911 accessor'ов, 1226 вызовов» в лаунчере ✓ 911 аксессоров подтверждено