obfuskator/info/evo-obfuscation-full.md
loki5512344 a641e69455 Initial commit: noxis JVM bytecode obfuscator
Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis,
noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check).
AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
2026-10-08 22:01:03 +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 аксессоров подтверждено