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.
12 KiB
Обфускация 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и 151LocalVariableTable✓ (локальные имены переменных живут у 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 расшифрованных записей.
Смежные слои защиты (не «обфускация кода», но часть контура)
- Payload:
12B IV || AES-256-GCM(ct||tag), ключ из сервера, раскрывается цепочкой HMAC/HKDF отhwid+token+ доли изjava.dll— см.evo-payload-pipeline.md. - Антиотладка и порча ключа — 3 независимых уровня XOR-масок (
0xA5,0x3C,0x5A),IsDebuggerPresent,JAVA_TOOL_OPTIONS,JVM_EvoForeignAgentPresent— см.evo-native-reverse.md. - HWID =
sha256("evovis|" + MachineGuid)— привязка к машине, Shamir-фрагменты ключа, аттестация (evovis-protect.vercel.app). evo-agent.jarвообще не обфусцирован ✓ — читаемые строки,SourceFile: "EvoAgent.java", т. е. расшифровщик payload открыт.- Скрипт-логирование с детектом
$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 аксессоров подтверждено |