# Обфускация 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`): ```java 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 - контрольный поток Тело метода разложено в конечный автомат **с раздвоенным состоянием**: ```java 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` | Применяется **везде**, не только в ключах строк: ```java // 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 аксессоров подтверждено |