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.
This commit is contained in:
commit
a641e69455
140 changed files with 17197 additions and 0 deletions
18
info/README.md
Normal file
18
info/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# info/ — материал по обфускации EvoVis (для нашего обфускатора)
|
||||
|
||||
Источник: `LoVisual/ref/evo/` (реверс EvoVis / EvolutionDLC, Minecraft-клиент).
|
||||
Сюда перенесено только то, что касается защиты/обфускации кода.
|
||||
|
||||
| файл | что это |
|
||||
|---|---|
|
||||
| `evo-obfuscation.md` | базовое описание трёх уровней обфускации (перенесено as-is из `ref/evo/obfuscation.md`) |
|
||||
| `evo-obfuscation-full.md` | **сводный разбор**: все уровни + .NET-лаунчер + натив/антиотладка, с кодом и проверенными цифрами |
|
||||
| `evo-weaknesses.md` | где защита провалилась при разборе — чего нам делать нельзя (+ опровержения) |
|
||||
| `evo-improvements.md` | что сделать в своём обфускаторе, чтобы этих дыр не было |
|
||||
| `evo-payload-pipeline.md` | доставка: AES-256-GCM payload + java-agent + подменённый JVM |
|
||||
| `evo-native-reverse.md` | реверс `evoguard.dll` / `java.dll`: HMAC-шейры, антиотладка, порча ключа |
|
||||
| `evo-nobackend.md` | локальный патч их клиента (как вырезали бэкенд/телеметрию) |
|
||||
|
||||
Инструменты деобфускации (Rust, zero-dep) лежат в `LoVisual/ref/evo/scripts/`:
|
||||
`awc.rs` (LCG+XOR шифр строк), `expr.rs` (оценка opaque-констант),
|
||||
`deobfuscate_mod.rs`, `deobfuscate_launcher.rs`, `make_plain.rs`, `restructure.rs`.
|
||||
185
info/evo-improvements.md
Normal file
185
info/evo-improvements.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# Как сделать лучше: план улучшений для нашего обфускатора
|
||||
|
||||
Привязка: референс — EvoVis (`evo-obfuscation-full.md`), провалы — `evo-weaknesses.md`.
|
||||
Ниже — что делаем иначе, в порядке отдачи/цены. Для каждой техники — уровень угрозы,
|
||||
которую она закрывает (скрипт-деобфускатор / статический анализ / ручной реверс).
|
||||
|
||||
---
|
||||
|
||||
## A. Базовый уровень (делаем обязательно, дёшево)
|
||||
|
||||
### A1. Обфускация всех символов, не только типов
|
||||
- Имена классов, **полей, методов, локальных переменных, параметров**, конструкторов,
|
||||
record-компонентов, enum-констант, label'ов, атрибутов `SourceFile`.
|
||||
- Индивидуальный алфавит на каждый символ (см. B1), запрет ключевых слов и
|
||||
одинаковых имён в одном scope.
|
||||
- Никаких «readable» ярлыков: у EvoVis `@rn(name=…)` и `description` отдали каталог
|
||||
модулей целиком. Если метаданные нужны рантайму — шифруем их (C2).
|
||||
|
||||
### A2. Вычистить отладочную информацию
|
||||
- `-g` → `LineNumberTable` / `LocalVariableTable` / `SourceFile` / `InnerClasses`
|
||||
/ `EnclosingMethod` / `MethodParameters` / `Signature` — по policy: либо полностью
|
||||
выкинуть, либо заменить на шум (см. B4).
|
||||
- Ресурсы тоже часть кода: mixin-JSON, `fabric.mod.json`, access widener, service
|
||||
loaders, SPI-файлы — шифруем/шифруем имена внутри, раскрываем в рантайме.
|
||||
|
||||
### A3. Никаких сигнатурных имён у защитного кода
|
||||
`KeyAssembler`, `RuntimeGuard`, `NativeGuard` → рандомные имена, а сам защитный код
|
||||
**смешиваем с обычным** (секции, которые выглядят как мусорные утилиты).
|
||||
Рефлексия по именам в открытом виде (`Class.forName("aln")`) — запрещена:
|
||||
имена отдаются только из зашифрованной строки (C2), лучше — через indirection-таблицу.
|
||||
|
||||
### A4. Обфусцировать весь артефакт, а не только ядро
|
||||
Правило: **нет незащищённого компонента**. У EvoVis лаунчер и java-agent были
|
||||
открыты целиком. Пайплайн один на: основной код, агент/плагин, лаунчер/хостер,
|
||||
нативная часть, ресурсы.
|
||||
|
||||
---
|
||||
|
||||
## B. Строки и константы
|
||||
|
||||
### B1. Пер-строчный ключ + нелинейный шифр
|
||||
Вместо общего LCG:
|
||||
- seed = `random_u64` на строку (храним неявно: derive из позиции/ид + master-seed);
|
||||
- алгоритм выбирается из набора (3–5 схем: RC4-like, chacha-с fragments, XTEA-блочный,
|
||||
plain XOR с keystream от BLAKE3, base85+flate перед шифром) — **перемешивание схем
|
||||
между строками**, чтобы один скрипт не покрыл всё;
|
||||
- AAD/MAC: `HMAC(fragment, plaintext_hash)` — повреждение ловится, подмена — тоже;
|
||||
- keystream не повторяется: nonce включает индекс строки.
|
||||
- **против обучения по частотам**: словарь/частотный анализ не работает, если схема
|
||||
и nonce разные.
|
||||
|
||||
### B2. Ключ строки выводится в рантайме, а не лежит рядом
|
||||
Никаких `d(cipher, literal_key)`:
|
||||
- ключ = `KDF(master_key, class_id, method_id, string_index)` либо приходит из
|
||||
защищённого контекста (см. D3);
|
||||
- вычисление — через серию операций, **растворённых в теле вызывающего метода**
|
||||
(не выделенная функция `decrypt(s)` — иначе её найдут и развернут);
|
||||
- мастер-секрет не в константах: собирается из нескольких мест + env/хост-фактора.
|
||||
|
||||
### B3. Константы — не тождества, а вычисления
|
||||
Отказ от `(A^B)+((A&B)<<1)` (опознаётся regex'ом). Вместо этого:
|
||||
- константа собирается из **нескольких узлов**, разнесённых по разным basic block'ам
|
||||
и разным методам (возврат частей, сложение на месте использования);
|
||||
- включаем в цепочку **зависимые от окружения** величины, которые статически вычислимы
|
||||
не полностью: `hashCode()` строки-литерала, `System.identityHashCode` статической
|
||||
константы, значение поля, инициализируемого в `<clinit>`, размер массива;
|
||||
- для `int`/`long`/`float`/`double` — отдельные схемы (bit-cast, разложение по
|
||||
мантиссе), потому что `double`-константы особенно заметны.
|
||||
- opaque-предикаты делать **настоящими**: всегда истинные, но зависящие от состояния,
|
||||
которое статически не выводится (напр. инвариант «массив отсортирован по заполнению»,
|
||||
проверка `System.nanoTime()`-мьютекса, сравнение двух полей, всегда равных после
|
||||
`<clinit>`, но не доказуемых в CFG).
|
||||
|
||||
### B4. Ложная отладочная информация
|
||||
Не только удалить `LineNumberTable`/`LocalVariableTable`, но и **подложить
|
||||
дезориентирующие**: ложные имена локальных переменных, строки из другого проекта,
|
||||
`SourceFile` с вводящим именем. Инструменты (jadx/CFR) и люди верят отладочке сильнее,
|
||||
чем байткоду.
|
||||
|
||||
---
|
||||
|
||||
## C. Контрольный поток
|
||||
|
||||
### C1. Несколько схем флаттенинга вместо одной
|
||||
Минимум набор (применять **случайно и по-разному в разных методах**):
|
||||
1. **Switch по вычислимому индексу**: `table[ (x*31 ^ y) & mask ]` → адрес возврата;
|
||||
переходы через индексы, а не метки.
|
||||
2. **Распределённое состояние**: как у EvoVis (`i + i2`), но одно слагаемое —
|
||||
из поля объекта/статики, второе — из параметра, обновление — через вызов метода;
|
||||
**метки case не константные**, а пересчитанные (opaque).
|
||||
3. **Бинарное дерево/бабочка**: вместо линейного списка состояний — рандомизированный
|
||||
граф переходов, эквивалентный исходному CFG (проверяется верификатором).
|
||||
4. **Вызов через стек/ретурн-адрес**: эмуляция `goto` через `athrow`-как-прыжок
|
||||
(exception-as-goto), `finally`-цепочки, или virtual-dispatch по фейковому интерфейсу.
|
||||
5. **Прячем тела блоков**: каждый блок зашифрован и раскрывается по очереди
|
||||
(см. D2), чтобы деобфускатор не мог читать тело, не эмулируя VM.
|
||||
|
||||
### C2. Обфускация CFG должна сохранять семантику → нужен верификатор
|
||||
Условие приёмки: **анализ эквивалентности до/после** (структурный: разбор в SSA,
|
||||
сравнение支配-графов; либо выполнение тестов на эталонных программах).
|
||||
Плюс автоматические метрики (см. F).
|
||||
|
||||
### C3. Реальный объём ≠ раздувание
|
||||
У EvoVis выигрыш — только в размере декомпилята, но и цена (читаемость для нас же).
|
||||
Оптимальнее: комбинировать обфускацию с **компиляцией в другой целевой код**
|
||||
(см. E), где раздувание не надо — там и так нечем читать.
|
||||
|
||||
---
|
||||
|
||||
## D. Уровень «нужно эмулировать, а не читать»
|
||||
|
||||
### D1. Маленький VM / interpreter для критичных кусков
|
||||
Свой bytecode (10–20 опкодов: арифметика, ветвление, вызов, доступ к полю),
|
||||
инструкции в зашифрованном массиве, диспетчер — обычный код с обфусцированным CFG.
|
||||
Экспортируем в VM только чувствительное: лицензирование, ключи, критическую логику.
|
||||
Это единственный уровень, который реально останавливает ручной реверс.
|
||||
|
||||
### D2. Раскрываемое по шагам тело (split-function / function-splitting)
|
||||
Метод делится на фрагменты (по basic block'ам), каждый зашифрован отдельно,
|
||||
порядок выполнения задаётся таблицей. Деобфускатор обязан строить CFG, прежде чем
|
||||
увидит хоть строку.
|
||||
|
||||
### D3. Анти-анализ в рантайме (мягкий)
|
||||
- self-check целостности: `SHA-256` своих классов/секций → при расхождении
|
||||
**меняем поведение**, а не падаем (у EvoVis падение выдавало мусорный ключ —
|
||||
правильно: не сообщать атакующему, что он пойман);
|
||||
- детект отладчика/трейсера/инструментов эмуляции — реакция: ложные данные,
|
||||
замедление, отключение функций;
|
||||
- **не детектить и не ломать легитимные сценарии** (обновления, IDE, символы) —
|
||||
ложные срабатывания дороже, чем польза защиты.
|
||||
|
||||
### D4. Честный anti-tamper, а не «зашейрь ключ»
|
||||
У EvoVis сильная идея: ключ = изолированная доля в подменённом JVM + HMAC от hwid +
|
||||
порча при детекте. Минус — вся схема работает только на своём окружении и падает
|
||||
на другой ОС/сборке. Для нас: секрет должен переживать **наши** легитимные платформы,
|
||||
а ломаться только при модификации кода/подмене целостности.
|
||||
|
||||
---
|
||||
|
||||
## E. Если цель — не JVM, а свой натив/Rust-бинарник
|
||||
|
||||
Референс JVM-слабости здесь не работают автоматически, но появляются свои:
|
||||
- **строки**: все plaintext-строки в `.rodata` — шифровать, раскрывать лениво;
|
||||
не держать AES-ключ рядом с шифротекстом (та же ошибка, что у `awc.d`);
|
||||
- **имена**: `stripped` бинарник + рандомизация символов; в Rust — макрос, который
|
||||
переименовывает `pub`-символы и убирает `#[doc]`/Debug-имена;
|
||||
- **panic/Debug-вывод**: `{:?}` отдаёт имена типов и полей — для чувствительных типов
|
||||
свой `Debug`, либо не выводить;
|
||||
- **CFG**: обфускация на уровне MIR/LLVM-IR (passes): bogus control flow, flattening
|
||||
на `switch` по вычисленному индексу, вставка недостижимых блоков, размытие
|
||||
границ функций (inlining + outlined);
|
||||
- **константы**: как B3, плюс вынос в вычисляемые таблицы;
|
||||
- **анти-анализ**: `ptrace`-selfcheck, проверка `/proc/self/maps` на свои секции,
|
||||
timing-checks; реакция — молчаливая порча данных;
|
||||
- **обфускация метаданных Rust**: `TypeId`/`Any`, `std::any::type_name`, логи
|
||||
`tracing` — выключать в release.
|
||||
|
||||
---
|
||||
|
||||
## F. Метрики качества (обязательно, иначе «обфускация есть» ничего не значит)
|
||||
|
||||
Автотесты на каждую технику, гоняются в CI:
|
||||
|
||||
1. **Утечки имён**: `strings`/анализ констант — не должно остаться ни одного
|
||||
осмысленного идентификатора из списка-эталона (ошибка = фейл).
|
||||
2. **Читаемость декомпилята**: прогнать JADX/CFR на собранный артефакт →
|
||||
метрики: доля распознанных структур (try/catch, switch, циклы), доля классов
|
||||
с восстановленными именами, число строк «смыслового» кода.
|
||||
3. **Автоматический деобфускатор-противник**: минимальный скрипт-анализатор
|
||||
(тот же `expr`-свёртка, regex на паттерны) — если он снимает >X% строк/констант,
|
||||
техника провалена. Держим этот «красный список паттернов» в репозитории.
|
||||
4. **Семантика**: тесты/эталонные программы до и после — 100% совпадение.
|
||||
5. **Размер и скорость**: бюджет (≤ N% размера, ≤ M% времени исполнения),
|
||||
отдельно — бюджет на рантайм-раскрытие строк (кэш, чтобы не платить на каждый вызов).
|
||||
6. **Стабильность между сборками**: имена/схемы меняются от билда к билду
|
||||
(у EvoVis шифр один на все версии — окно для обучения).
|
||||
|
||||
## Приоритет
|
||||
|
||||
1. A1+A2+A4 (символы, отладка, всё артефакты) — дёшево, biggest leak у EvoVis.
|
||||
2. B1+B2 (пер-строчные ключи, вывод ключа) — снимает главный провал `awc.d`.
|
||||
3. C1 несколько схем флаттенинга + C2 верификатор.
|
||||
4. F метрики — чтобы не спорить на глаз.
|
||||
5. C3/E (другой целевой код) — как долгосрочный путь.
|
||||
6. D1 VM — только для действительно ценных кусков, остальное не оправдывает цену.
|
||||
295
info/evo-native-reverse.md
Normal file
295
info/evo-native-reverse.md
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
# Реверс нативных библиотек EvoVis (Ghidra headless)
|
||||
|
||||
Инструменты: Ghidra 12.1.2 (`/opt/ghidra/support/analyzeHeadless`), radare2, objdump, javap.
|
||||
Скрипты: `reverse/DumpDecomp.java` (дамп всех функций), `reverse/FocusDecomp.java` (точечная
|
||||
декомпиляция: точка входа + её вызываемые + все функции, пишущие в поле со смещением `0x53`).
|
||||
|
||||
Воспроизведение:
|
||||
|
||||
```bash
|
||||
cd ref/evo/reverse
|
||||
/opt/ghidra/support/analyzeHeadless "$PWD/proj" EvoRev \
|
||||
-import "$PWD/../mod/jar/META-INF/evo/native/win-x64/evoguard.dll" \
|
||||
-postScript DumpDecomp.java "$PWD/out/evoguard.c" -scriptPath "$PWD"
|
||||
/opt/ghidra/support/analyzeHeadless "$PWD/proj" EvoRev \
|
||||
-import "$PWD/../mod/evojdk_x/bin/java.dll" \
|
||||
-postScript DumpDecomp.java "$PWD/out/java_dll.c" -scriptPath "$PWD"
|
||||
/opt/ghidra/support/analyzeHeadless "$PWD/proj" EvoJvm \
|
||||
-import "$PWD/../mod/evojdk_x/bin/server/jvm.dll" \
|
||||
-postScript FocusDecomp.java "$PWD/out/jvm_focus.c" JVM_EvoForeignAgentPresent -scriptPath "$PWD"
|
||||
```
|
||||
|
||||
## Что реверсилось
|
||||
|
||||
| файл | размер | секций | содержимое |
|
||||
|---|---|---|---|
|
||||
| `mod/jar/META-INF/evo/native/win-x64/evoguard.dll` | 51 321 | 18 | своя маленькая JNI-библиотека (MinGW, PE32+) |
|
||||
| `mod/evojdk_x/bin/java.dll` | 111 104 | 6 | стандартный `java.dll` из JDK **+ новая функция** |
|
||||
| `mod/evojdk_x/bin/server/jvm.dll` | 15 463 936 | — | **патченный HotSpot** + новый экспорт |
|
||||
|
||||
Отдельно (Java, не натив): `mod/evojdk_x/classes/java/lang/EvoShare.class`,
|
||||
исходник — `mod/evoshare/sources/java/lang/EvoShare.java` (JADX), вызывающий код —
|
||||
`mod/agent-src/sources/evo/agent/key/NativeJvmShare.java`.
|
||||
|
||||
---
|
||||
|
||||
## 1. evoguard.dll — JNI-«нативный шейр»
|
||||
|
||||
Экспорт ровно один (плюс mingw-рантайм `_CRT_INIT`, `DllMainCRTStartup`, `atexit`,
|
||||
`sha256_block/update/final`, `env_flags_debug`, `hmac_sha256.constprop.0`) — **`JNI_OnLoad` нет**,
|
||||
`nm`-имена не вычищены (COFF-символы остались).
|
||||
|
||||
### `Java_evo_systems_security_natives_NativeGuard_nativeShare(JNIEnv*, jclass, jstring hwid)` → `byte[32]`
|
||||
|
||||
```c
|
||||
if (hwid == NULL) return NULL;
|
||||
str = (*env)->GetStringUTFChars(env, hwid, NULL);
|
||||
hmac_sha256(str, strlen(str), out32); // HMAC-SHA256 с фиксированным ключом, 32 байта
|
||||
(*env)->ReleaseStringUTFChars(env, hwid, str);
|
||||
|
||||
debugged = IsDebuggerPresent()
|
||||
|| CheckRemoteDebuggerPresent(GetCurrentProcess(), &flag) && flag
|
||||
|| env_flags_debug("JAVA_TOOL_OPTIONS")
|
||||
|| env_flags_debug("_JAVA_OPTIONS");
|
||||
if (debugged) out32 ^= 0xA5A5A5A5_A5A5A5A5 ...; // отравление результата
|
||||
|
||||
byte[] res = (*env)->NewByteArray(env, 32);
|
||||
(*env)->SetByteArrayRegion(env, res, 0, 32, out32);
|
||||
return res;
|
||||
```
|
||||
|
||||
`env_flags_debug(name)`: читает переменную окружения; если она содержит `jdwp` или `agentlib`
|
||||
→ считается отладкой. Плюс чек `IsDebuggerPresent` / `CheckRemoteDebuggerPresent`.
|
||||
|
||||
**Ключ HMAC (32 байта, `.rdata` @ `0x3b00251c0`):**
|
||||
|
||||
```
|
||||
d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2
|
||||
```
|
||||
|
||||
Итог: `NativeGuard.share(hwid) = HMAC-SHA256(key_eg, hwid_utf8)`, при детекте отладки —
|
||||
`XOR 0xA5` (не ошибка, а **молчаливо неверный шейр**).
|
||||
|
||||
### Платформенный паритет (`evoguard`)
|
||||
|
||||
| файл | размер | ключ | вывод |
|
||||
|---|---|---|---|
|
||||
| `win-x64/evoguard.dll` | 51 321 | `d42394b1…533bd2` в `.rdata` | базовый (реверс выше) |
|
||||
| `win-arm64/evoguard.dll` | 66 560 | **тот же**, но лежит как предвычисленный `key^0x36` / `key^0x5c` (ipad/opad) | код идентичен по смыслу |
|
||||
| `macos-x64/libevoguard.dylib` | 8 936 | `d42394b1…533bd2` (offset 2096) | тот же |
|
||||
| `macos-arm64/libevoguard.dylib` | 33 936 | `d42394b1…533bd2` (offset 2080) | тот же |
|
||||
|
||||
Проверка ARM64: байты по адресу `0x180003050` = `e215a287…d9650de4`, XOR `0x36` →
|
||||
ровно `d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2`; следующие 32
|
||||
байта = тот же ключ с XOR `0x5c`. SHA-IV рядом (`0x180003090` = `67e6096a…`).
|
||||
Во всех сборках есть строки `IsDebuggerPresent`, `jdwp`, `agentlib`, `JAVA_TOOL_OPTIONS`,
|
||||
`_JAVA_OPTIONS` — логика антиотладки одинаковая. **Linux-сборки натива нет вообще**.
|
||||
|
||||
Потребитель: `alm.java` (KeyAssembler) — шейр идёт в 4-of-4 сборку Шамира:
|
||||
|
||||
```java
|
||||
byte[] nativeShare = NativeGuard.share(hwid);
|
||||
ama.combine(new int[]{share.x(), share.xIntegrity(), share.xNative(), share.xHwid()},
|
||||
new byte[][]{share.y(), integrityShare, nativeShare, hwidShare});
|
||||
```
|
||||
|
||||
То есть «отравленный» нативный шейр ломает сборку ключа без единого сообщения об ошибке.
|
||||
|
||||
---
|
||||
|
||||
## 2. java.dll — `java.lang.EvoShare.computeRaw` (патченный JDK)
|
||||
|
||||
Экспорт: `Java_java_lang_EvoShare_computeRaw` (@ `0x180001ca4`, 286 байт) +
|
||||
импорт `JVM_EvoForeignAgentPresent` **из `jvm.dll`** (см. §3).
|
||||
|
||||
```c
|
||||
longlong Java_java_lang_EvoShare_computeRaw(JNIEnv* env, jclass cls, jbyteArray input) {
|
||||
if (input == NULL) return NULL;
|
||||
len = (*env)->GetArrayLength(env, input);
|
||||
bytes = (*env)->GetByteArrayElements(env, input, NULL);
|
||||
FUN_180001e4c(bytes, len, out32); // HMAC-SHA256 + антиотладка (см. ниже)
|
||||
(*env)->ReleaseByteArrayElements(env, input, bytes, 2/*JNI_ABORT*/);
|
||||
|
||||
if (JVM_EvoForeignAgentPresent() == 1) // из патченного HotSpot
|
||||
out32 ^= 0x3C3C3C3C_3C3C3C3C ...; // второй уровень порчи
|
||||
|
||||
jbyteArray res = (*env)->NewByteArray(env, 32);
|
||||
(*env)->SetByteArrayRegion(env, res, 0, 32, out32);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
`FUN_180001e4c(input, len, out32)`:
|
||||
|
||||
1. `FUN_180001f1c(&DAT_180010e90, 0x20, input, len, out32)` — **стандартный HMAC-SHA256**
|
||||
(внутри явные SHA-IV `0x6a09e667…`, копирование ключа в ipad/opad);
|
||||
2. затем антиотладка, при срабатывании — `out32 ^= 0xA5…`:
|
||||
- `IsDebuggerPresent()`;
|
||||
- `CheckRemoteDebuggerPresent()`;
|
||||
- загружены модули `jdwp.dll`, `dt_socket.dll`, `dt_shmem.dll` (транспорты JDWP);
|
||||
- `JAVA_TOOL_OPTIONS` / `_JAVA_OPTIONS` содержат `jdwp`/`agentlib`.
|
||||
|
||||
**Ключ HMAC (32 байта, `.rdata` @ `0x180010e90`):**
|
||||
|
||||
```
|
||||
70ea2f4857869bdc8606a29a6c8cdcedbe92c18bef1aad106024db104a8f25d1
|
||||
```
|
||||
|
||||
Ключи `evoguard.dll` и `java.dll` **разные** — это два независимых шейра.
|
||||
|
||||
Константы-маски: `0xA5` — антиотладка (натив), `0x3C` — «чужой агент» из HotSpot,
|
||||
`0x5A` — Java-уровень (см. §4).
|
||||
|
||||
---
|
||||
|
||||
## 3. jvm.dll — экспорт `JVM_EvoForeignAgentPresent`
|
||||
|
||||
`mod/evojdk_x/bin/server/jvm.dll` (15,4 МБ) — тот же HotSpot, что и в `java.dll`, но с
|
||||
добавленным экспортом (в таблице экспортов: `[4281] +base[4282] 10b9 JVM_EvoForeignAgentPresent`).
|
||||
`java.dll` импортирует его и вызывает в `computeRaw`.
|
||||
|
||||
Адрес: `0x1803a2340`. Декомпиляция: `reverse/out/jvm_focus.c` (функция + доступы к полю `0x53`),
|
||||
`reverse/out/jvm_more.c` (итератор), `reverse/out/jvm_list.c` (источник списка),
|
||||
`reverse/out/jvm_agent.c` (конструктор элемента).
|
||||
|
||||
### Что делает
|
||||
|
||||
```c
|
||||
undefined1 JVM_EvoForeignAgentPresent(void) {
|
||||
int instrument_cnt = 0; // iVar2
|
||||
int native_cnt = 0; // iVar5
|
||||
build_iterator(&it, &DAT_180e2c160, mode=3); // копия глобального списка с фильтром
|
||||
while (hasNext(&it)) {
|
||||
void* e = pop(&it);
|
||||
if (byte_at(e, 0x53)) instrument_cnt++; else native_cnt++;
|
||||
}
|
||||
destroy_iterator(&it);
|
||||
if (instrument_cnt < 2 && native_cnt == 0) return 0;
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
### Откуда берётся список и что такое `+0x53`
|
||||
|
||||
- `DAT_180e2c160` — **глава глобального связного списка JVMTI-агентов** (stock-механизм HotSpot,
|
||||
`+0x20` = `next`, `+0x58` размер элемента). Доказательство: рядом лежит функция с литералом
|
||||
`"Disabled all JVMTI agents during -XX:AOTMode=create"`, которая делает `DAT_180e2c160 = 0`;
|
||||
push-функция `FUN_1805f40e0(name, path, is_absolute)` аллоцирует `0x58` байт, инициализирует
|
||||
и lock-free пушит в голову; `FUN_1805f44b0` выдаёт
|
||||
`"Dynamic agent loading is only permitted in the live phase"`.
|
||||
- Конструктор элемента `FUN_1805f30d0` (см. `jvm_agent.c`):
|
||||
|
||||
```c
|
||||
*(bool *)(obj + 0x53) = (strcmp(name, "instrument") == 0); // «это javaagent/instrument»
|
||||
*(obj + 0x54) = param_5; // is_dynamic
|
||||
*(obj + 0x55) = 0; // фильтр итератора (mode 3 берёт только == 0)
|
||||
```
|
||||
|
||||
- Фильтр итератора (`FUN_1805f3880`) = `byte[e + 0x55] == 0`, `next` = `*(e + 0x20)`.
|
||||
|
||||
### Итоговая семантика
|
||||
|
||||
`JVM_EvoForeignAgentPresent()` возвращает `1` («чужой агент присутствует»), если в
|
||||
JVMTI-списке есть **хотя бы один не-instrument агент** (`-agentlib:`/`-agentpath:`, в т.ч.
|
||||
`-agentlib:jdwp`), **либо два и более instrument-агента** (второй `-javaagent:`).
|
||||
Единственный свой `-javaagent:evo-agent.jar` считается нормой.
|
||||
|
||||
Вызов из `EvoShare.computeRaw` → `XOR 0x3C` на HMAC-шейре (см. §2, §5).
|
||||
|
||||
Также в `jvm_focus.c` — две функции, содержащие доступы к полю `0x53`
|
||||
(`FUN_180268640`, `FUN_1804a7920`), — это чтения в фильтрах классов/загрузчика, к
|
||||
логике агентов отношения не имеют. Прямой записи `mov byte [reg+0x53]` в программе нет:
|
||||
поле инициализируется в конструкторе широкой записью через `strcmp`-результат.
|
||||
|
||||
---
|
||||
|
||||
## 4. `java.lang.EvoShare` (Java-сторона, живёт в патченном JDK)
|
||||
|
||||
```java
|
||||
public static byte[] compute(byte[] input) {
|
||||
byte[] out = computeRaw(input); // натив, §2
|
||||
if (out != null && out.length == 32 && agentMismatch())
|
||||
for (int i = 0; i < out.length; i++)
|
||||
out[i] = (byte) (out[i] ^ 90); // 0x5A — третий уровень порчи
|
||||
return out;
|
||||
}
|
||||
```
|
||||
|
||||
`agentMismatch()` — сканирует `VM.getRuntimeArguments()`:
|
||||
|
||||
- аргумент `-agentpath:` или `-agentlib:` (любой нативный агент, в т.ч. `-agentlib:jdwp`) → `true`;
|
||||
- `-javaagent:<jar>[=opts]`: берёт путь jar, читает файл, считает SHA-256 и сравнивает с
|
||||
`EXPECTED_AGENT_SHA256`; если не совпало → `true`;
|
||||
- `Throwable` → `false` (не портит).
|
||||
|
||||
Константы:
|
||||
|
||||
```
|
||||
SHARE_LENGTH = 32
|
||||
AGENT_CORRUPT_MASK = 90 // 0x5A
|
||||
EXPECTED_AGENT_SHA256 = 374ace507bd0cc2d0ce6dbbe07e50ddb3173ff819e92470271e2bf0fccff2038
|
||||
```
|
||||
|
||||
Проверено: `sha256sum mod/evo-agent.jar` = `374ace50…` — **пиннинг на свой агент
|
||||
`evo-agent.jar`**. То есть JVM должен быть запущен либо вообще без `-javaagent`,
|
||||
либо именно с их агентом; чужой агент тихо ломает шейр.
|
||||
|
||||
Потребитель — `evo.agent.key.NativeJvmShare` (в `evo-agent.jar`):
|
||||
|
||||
```java
|
||||
Method compute = Class.forName("java.lang.EvoShare").getMethod("compute", byte[].class);
|
||||
byte[] share = (byte[]) compute.invoke(null, hwid.getBytes(UTF_8));
|
||||
// ClassNotFoundException → KeyProviderException("not running on the EvoVis custom JVM")
|
||||
```
|
||||
|
||||
То есть **payload-ключ требует их кастомного JDK** — на стоковом `java` шейр получить нельзя.
|
||||
|
||||
---
|
||||
|
||||
## 5. Сводка: три независимых уровня порчи ключа
|
||||
|
||||
| уровень | где | условие | порча |
|
||||
|---|---|---|---|
|
||||
| натив evoguard | `NativeGuard.nativeShare` | debugger / `JAVA_TOOL_OPTIONS`/`_JAVA_OPTIONS` (`jdwp`,`agentlib`) | `XOR 0xA5` |
|
||||
| натив JDK | `EvoShare.computeRaw` → `FUN_180001e4c` | debugger / модули `jdwp.dll`,`dt_socket.dll`,`dt_shmem.dll` / те же env | `XOR 0xA5` |
|
||||
| натив JDK | `EvoShare.computeRaw` | `JVM_EvoForeignAgentPresent() == 1`: есть `-agentlib:`/`-agentpath:` агент либо ≥2 `-javaagent:` | `XOR 0x3C` |
|
||||
| Java JDK | `EvoShare.compute` | `-agentpath:`/`-agentlib:` или `-javaagent:` с чужим sha256 | `XOR 0x5A` |
|
||||
|
||||
Во всех случаях результат остаётся валидным `byte[32]` — ошибка не сигнализируется,
|
||||
ключ просто собирается неверно (в `alm.java` дальше идёт `combine` Шамира и `alz.wipe`).
|
||||
|
||||
Две разные схемы HMAC (разные ключи) дают два разных шейра:
|
||||
|
||||
- `NativeGuard.share(hwid)` — в сборку ключа мода (`alm.java`, 4-of-4 Шамир);
|
||||
- `EvoShare.compute(hwid)` — в цепочку ключа payload'а агента (`NativeJvmShare`, `EvoAgent`).
|
||||
|
||||
---
|
||||
|
||||
## 6. `evovis-audiotap` (macOS, Swift)
|
||||
|
||||
`mod/jar/assets/evolution/native/macos/evovis-audiotap` — **Mach-O universal (x86_64 + arm64),
|
||||
174 848 байт, исполняемый файл** (`_main` + `__mh_execute_header`, не dylib).
|
||||
|
||||
Факты по `strings` / `r2`:
|
||||
|
||||
- написан на **Swift** (секции `__swift5_typeref/capture/entry/reflstr/fieldmd/types`),
|
||||
класс `main.AudioTap` (`_TtC4main8AudioTap`);
|
||||
- зависимости: **ScreenCaptureKit**, CoreMedia, AVFoundation, CoreAudio (swiftCoreAudio),
|
||||
Foundation, IOKit — то есть захват системного звука/видеопотока через ScreenCaptureKit;
|
||||
- делегат `stream(_:didOutputSampleBuffer:of:)` (`SCStream` + `CMSampleBuffer`) и
|
||||
`stream(_:didStopWithError:)`; поля `output: NSFileHandle`, `mono: [Float]`;
|
||||
- логи/метки `evovis.audiotap.audio`, `evovis.audiotap.screen`;
|
||||
- **сетевых строк нет** (ни `http`, ни сокетов, ни доменов) — данные пишутся в
|
||||
`NSFileHandle` (pipe/stdout), то есть процесс-потребитель мод запускает локально.
|
||||
|
||||
Вывод: это локальный helper для тапа аудио (и экрана) на macOS, результат уходит пайпом
|
||||
в родительский процесс; в аудио/сеть наружу ничего не отдаётся.
|
||||
|
||||
Декомпиляция Ghidra для него не запускалась (предмет — Swift + macOS-фреймворки, на
|
||||
уровне строк/символов картина уже ясна); запускается по необходимости:
|
||||
|
||||
```bash
|
||||
/opt/ghidra/support/analyzeHeadless "$PWD/proj" EvoTap \
|
||||
-import "$PWD/../mod/jar/assets/evolution/native/macos/evovis-audiotap" \
|
||||
-postScript DumpDecomp.java "$PWD/out/audiotap.c" -scriptPath "$PWD"
|
||||
```
|
||||
88
info/evo-nobackend.md
Normal file
88
info/evo-nobackend.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Вырезка бэкенда, авторизации и телеметрии (2026-10-02)
|
||||
|
||||
Локальный патч **их** клиента (`mod/evovis-dec.jar`) по решению владельца: без их сервера,
|
||||
без их аккаунта, без отправки данных. Ничего из этого не трогает LoVisual.
|
||||
|
||||
## Артефакт
|
||||
|
||||
| Файл | Размер | Что это |
|
||||
|---|---|---|
|
||||
| `mod/evovis-dec-nobackend.jar` | 47 481 810 B, sha256 `8c1f262687c056b2…` | `evovis-dec.jar` +3 перепакованных класса |
|
||||
|
||||
Исходники патча: `patch/src/` (копии из `mod/jar-src/sources/defpackage/`), смоук: `patch/smoke/`.
|
||||
|
||||
## Метод
|
||||
|
||||
Их классы лежат **в корне джара** (default package; jadx называет его `defpackage`).
|
||||
Классы v69 = Java 25 → компилировали `/usr/lib/jvm/java-25-openjdk/bin/javac` против
|
||||
`evovis-dec.jar` + `~/.gradle/caches/fabric-loom/26.2/minecraft-merged.jar` (mojmap) + gson,
|
||||
затем `jar uf` в корень копии джара.
|
||||
|
||||
Важные артефакты jadx, которые пришлось учесть:
|
||||
|
||||
- **jadx переименовал их класс `if` → `Cif`** (keyword): класс `Cif.class` в джаре не существует,
|
||||
поэтому весь мёртвый код с ссылками на `Cif` вырезан, а не пересобран.
|
||||
- `?? r1;` (недообфусцированный фрагмент) и `throw th;` вне catch — в вырезанных методах.
|
||||
- Обфускация `setDaemon((A^B)+((A&B)<<1))` — это int-эквивалент `true` (A+B=1), javac не берёт.
|
||||
|
||||
## Что заменено
|
||||
|
||||
### `cp` = `Avatar2` (аккаунт)
|
||||
- `initialize()` → `setUser(new cr("Player", null, "user", 0, null))`:
|
||||
**нет** `POST /api/auth/me`, нет чтения `EVO_TOKEN`, нет лаунчер-сессии, нет `refreshMe` (HTTP).
|
||||
Профиль — локальный dummy.
|
||||
- `isAuthorized()` → `return true` — открываются все их шлюзы
|
||||
(`Panel`, `Camera2`, `First`, `Friends`, `Crosshair2`, `KeyAssembler`, `SessionHeartbeat`).
|
||||
- `user()` → lazy-dummy: если профиль не задан, создаёт тот же dummy.
|
||||
**`token == null`** — принципиально: `KeyAssembler.token()` и `SessionHeartbeat.token()`
|
||||
возвращают null → `AttestClient4` (`evovis-protect.vercel.app`: challenge/attest/heartbeat)
|
||||
**недостижим**, поток сборки Shamir-ключа крутится вхолостую (sleep 500 мс).
|
||||
- Удалён приватный `refreshMe()` (единственный источник HTTP на auth/me).
|
||||
|
||||
### `akj` = `Report` (телеметрия)
|
||||
- `submit(String)` → no-op (единственный внешний вызов — `dn`/`Message2`, их команда отчёта).
|
||||
- Удалены `feedback`, `buildBody`, `run` (достигались только из `submit`).
|
||||
|
||||
### `alr` = `SessionHeartbeat` (сессия/отключение модулей)
|
||||
- `initialize()` → no-op: поток `evo-session-heartbeat` не стартует → ни `ACTIVE`/`REVOKED`
|
||||
каждые 60 с, ни `REVOKED`×2 → «выключить все модули».
|
||||
- Удалены приватные `run/tick/suspend/restore/reassert/showNotification/manager/token/sleepQuietly`.
|
||||
|
||||
## Почему остальное работает без их сервера
|
||||
|
||||
- **Обычные строки мода** — `awc.d(String,int)`: статический LCG+XOR, ключа не требует.
|
||||
- **`SecureStrings`** (5 API-блобов: probe/friends/messages/cosmetic_sync/config_share) —
|
||||
`KeyAssembler.key()` всегда null → `get()` возвращает null → их серверные фичи
|
||||
(синк друзей, сообщения, косметика, конфиги) **выключаются сами**, без крашей на старте
|
||||
(null проверяется в самом `SecureStrings.get`).
|
||||
- `KeyAssembler`/`SealedBoot`/`RuntimeGuard`/JNI `NativeGuard` — живут только после сборки
|
||||
ключа, то есть недостижимы при `token == null`.
|
||||
|
||||
## Оставлено (осознанно)
|
||||
|
||||
| Что | Почему |
|
||||
|---|---|
|
||||
| `Ticket` (тикеты поддержки) | только по явному клику пользователя, с `token==null` отправлять нечего |
|
||||
| `Evovisuals2` (аватарки `head/*.png`, mc-heads) | косметика, грейсфейл, неактивно без друзей/аккаунта |
|
||||
| `Route` (зеркала `delivery*.evoproject.xyz`) | бесполезен: запросов к их хостам больше нет |
|
||||
| Сторонние API (lrclib, mc-heads, yt-dlp, whisper) | их бэкенд, не трогаем |
|
||||
|
||||
## Проверки
|
||||
|
||||
- `javap -c`: `cp.isAuthorized` → `iconst_1/ireturn`; `cp.initialize` → `new cr(…Player…)`;
|
||||
`akj.submit` → `return`; `alr.initialize` → `return`.
|
||||
- Смоук (`patch/smoke/Smoke.java`, JVM 25, classpath = патченный джар + MC + gson):
|
||||
|
||||
```
|
||||
isAuthorized = true
|
||||
user = Player token=null
|
||||
isSuspended = false
|
||||
OK: auth=local, report=no-op, heartbeat=off
|
||||
```
|
||||
|
||||
## Ограничение
|
||||
|
||||
В игре не прогоняли: здесь Linux, а их `evojdk` только win-x64. Запускать патченный джар —
|
||||
на Windows (их лаунчер или свой fabric-лодер); при проблемах — смотреть лог на загрузке
|
||||
модулей. Ожидаемо «пустым» (серверное) останется: список друзей, сообщения, косметика синк,
|
||||
аватарки, тикеты.
|
||||
162
info/evo-obfuscation-full.md
Normal file
162
info/evo-obfuscation-full.md
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# Обфускация 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 аксессоров подтверждено |
|
||||
42
info/evo-obfuscation.md
Normal file
42
info/evo-obfuscation.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Обфускация EvoVis / EvolutionDLC
|
||||
|
||||
## Три уровня
|
||||
|
||||
1. **Имена** — 1277 классов вынесены в default package с именами вида `a…aa…`;
|
||||
миксины разбросаны по пакетам `a/` и `b/`. Смысловых имён классов в коде нет.
|
||||
|
||||
2. **Строки** — все строковые литералы зашифрованы вызовом `awc.d(cipher, key)`:
|
||||
- LCG: `state = state * 1103515245 + 12345` (32-бит переполнение);
|
||||
- байт: `cipherChar ^ (state >>> 24)`;
|
||||
- результат — UTF-8 строка.
|
||||
Ключи передаются «непрозрачными» целыми (см. ниже). Второй вариант `awc.g(...)`
|
||||
— то же самое плюс session-gate: строка расшифровывается только после
|
||||
проверки сессии через рефлексию `aln.key()`.
|
||||
|
||||
3. **Контрольный поток** — тела методов разложены в машину состояний:
|
||||
`while (true) { switch (состояние) { case <обфуск-константа>: ...; состояние = <следующая>; } }`.
|
||||
Переходы между блоками — через присваивание целому и обратный `goto`.
|
||||
Из-за этого объём декомпилированного кода раздут примерно в 25 раз
|
||||
относительно реального.
|
||||
|
||||
## Кодировка целочисленных констант
|
||||
|
||||
Все литералы записаны через тождества (по модулю 2³²):
|
||||
|
||||
| Паттерн | Равен |
|
||||
|---|---|
|
||||
| `(A ^ B) + ((A & B) << 1)` | `A + B` |
|
||||
| `(A \| B) + (A & B)` | `A + B` |
|
||||
|
||||
Так прячутся даже `0`, `1`, `16`, `255`. Например, `true`/`false` приходят как
|
||||
выражение, равное `1`/`0` (javac такого не генерирует — это работа обфускатора).
|
||||
Сдвиги и маски тоже закодированы: сдвиговые константы в байткоде могут быть
|
||||
отрицательными — JVM маскирует количество бит по младшим 5 битам
|
||||
(`ishl/ishr` со сдвигом `-8` работает как сдвиг на `24`), поэтому `>> -8`
|
||||
в байткоде эквивалентен `>> 24` в исходнике.
|
||||
|
||||
## Прочее
|
||||
|
||||
- Декой-классы обфускатора — пустышки с бессмысленными членами, в логике не участвуют.
|
||||
- Из-за имён-ключевых слов (`if`, `do`, …) декомпилятор переименовывает такие
|
||||
классы (`Cif`, `Cdo`) — в самом jar классов с такими именами нет.
|
||||
89
info/evo-payload-pipeline.md
Normal file
89
info/evo-payload-pipeline.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Payload-раннер EvoVis: как мод попадает в игру
|
||||
|
||||
Разбор по коду (байт-код `evo-agent.jar`, `payload.json`, `runtime.json`, структура
|
||||
`evojdk_x/`). Источник: `javap -p -constants -cp mod/evo-agent.jar …`.
|
||||
|
||||
## Участники
|
||||
|
||||
| артефакт | роль |
|
||||
|---|---|
|
||||
| `mod/payload.json` | манифест: `version`, `file`, `sha256`, `size` |
|
||||
| `mod/evovis.payload` (47.5 MB) | зашифрованный jar мода (AES-GCM) |
|
||||
| `mod/evo-agent.jar` (60 KB) | Java-agent: расшифровывает payload и подсовывает его Fabric |
|
||||
| `mod/evojdk-win-x64.zip` → `bin/java.dll`, `classes/…/java/lang/EvoShare.class` | **подменённый JVM**: класс `java.lang.EvoShare.compute(...)` отдаёт вторую долю ключа |
|
||||
| `mod/jar/META-INF/jars/*.jar` (7 шт.) | вложенные зависимости, распаковываются агентом |
|
||||
| `mod/jar/META-INF/evo/native/{win-x64,win-arm64,macos-x64,macos-arm64}/evoguard.dll\|dylib` | нативная защита мода (внутри уже загруженного мода) |
|
||||
|
||||
## Манифест (`payload.json`)
|
||||
|
||||
```json
|
||||
{ "version": "2026.10.1.994", "file": "evovis.payload",
|
||||
"sha256": "e118274a92181e2a8ef04d2f6c45c4f6d4a10b87119d9d41c6f8e39b1bd5f485",
|
||||
"size": 47491101 }
|
||||
```
|
||||
|
||||
Лаунчер качает `payload.json` → сверяет sha256 → качает `evovis.payload`,
|
||||
`evo-agent.jar`, `evojdk-*.zip` → поднимает JVM с агентом.
|
||||
|
||||
## Конфигурация агента (из констант `EvoAgent`)
|
||||
|
||||
| константа | значение |
|
||||
|---|---|
|
||||
| `MOUNT_NAME` | `"evovis"` |
|
||||
| payload | свойство `evo.agent.payload` / env `EVO_AGENT_PAYLOAD` |
|
||||
| ключ | свойство `evo.agent.key` / env `EVO_AGENT_KEY` (hex, 32 байта) |
|
||||
| токен / hwid | env `EVO_TOKEN`, `EVO_HWID` |
|
||||
| вложенные библиотеки | `evo.agent.libraries` / `EVO_AGENT_LIBRARIES`, каталог `libraries` |
|
||||
|
||||
Манифест агента: `Premain-Class: evo.agent.EvoAgent`,
|
||||
`Launcher-Agent-Class: evo.agent.EvoAgent`,
|
||||
`Can-Redefine-Classes: true`, `Can-Retransform-Classes: true`.
|
||||
|
||||
## Цепочка запуска
|
||||
|
||||
```
|
||||
laунчер
|
||||
├─ sha256(payload.json) → качает evovis.payload / evo-agent.jar / evojdk
|
||||
├─ env: EVO_AGENT_PAYLOAD, EVO_AGENT_KEY, EVO_TOKEN, EVO_HWID, EVO_AGENT_LIBRARIES
|
||||
└─ java -javaagent:evo-agent.jar … (свой evojdk, в нём java.lang.EvoShare)
|
||||
│
|
||||
▼
|
||||
EvoAgent.premain/agentmain(args, inst)
|
||||
├─ keyProvider(): LocalKeyProvider(hex-ключ из env) + JvmShare.share(hwid)
|
||||
│ └─ NativeJvmShare → reflect вызов java.lang.EvoShare.compute(...) → 32 байта
|
||||
├─ PayloadCipher.decrypt(bytes(payload), key) // AES/GCM/NoPadding, IV 12B, tag 128b
|
||||
├─ NestedLibraries.extract(payloadBytes, dir) // META-INF/jars/*.jar → <dir>/libraries
|
||||
├─ PreparedMod(mount="evovis", candidate=Path, classpathUrl, libraries)
|
||||
└─ bootstrap(inst)
|
||||
├─ appendToSystemClassLoaderSearch(classpathUrl) → классы мода видны JVM
|
||||
└─ inst.addTransformer(FabricDiscoveryInjector)
|
||||
│
|
||||
▼
|
||||
FabricDiscoveryInjector.transform(...) // java.lang.classfile API (байткод-в-байткоде)
|
||||
├─ цель: net/fabricmc/loader/impl/discovery/ArgumentModCandidateFinder.findCandidates
|
||||
├─ вставляет вызов EvoAgentHook.contribute(Consumer) (слот CONSUMER_SLOT = 1)
|
||||
└─ consume.accept(PreparedMod) → Fabric видит мод-кандидат, которого нет на диске
|
||||
│
|
||||
▼
|
||||
Fabric загружает мод → мод сам распаковывает evoguard.dll из META-INF/evo/native/
|
||||
→ SealedBoot / KeyAssembler / SessionHeartbeat / NativeGuard
|
||||
```
|
||||
|
||||
Ключевые свойства:
|
||||
|
||||
1. **Мод не лежит на диске в открытом виде** — расшифровка в памяти агента,
|
||||
`candidate` — временный mount-файл.
|
||||
2. **Ключ собирается из двух мест**: hex из env (лаунчер) + `java.lang.EvoShare.compute(hwid)`
|
||||
в **подменённом JVM** — поэтому мод гоняется только на их evojdk.
|
||||
3. **Инъекция в Fabric** — не мод в `mods/`, а байткод-трансформация внутреннего
|
||||
класса Fabric (`ArgumentModCandidateFinder`) через `java.lang.classfile` API.
|
||||
Классы: `EvoAgent`, `EvoAgentHook`, `FabricDiscoveryInjector`, `PreparedMod`,
|
||||
`NestedLibraries`.
|
||||
4. Далее внутри мода — свой слой: `KeyAssembler` (Shamir: сервер + integrity + native + hwid),
|
||||
`SealedBoot` (sealed.bin в этом билде отсутствует → fail-open), `SessionHeartbeat`.
|
||||
|
||||
## Что здесь не проверено
|
||||
|
||||
- Тело `java.dll` / `EvoShare` — натив не реверсился (план в `todo.md`).
|
||||
- Реальное поведение при запуске (нужен MC 26.2 + Fabric 0.19.5 + их evojdk + дисплей).
|
||||
- `evoguard.dll` — не реверсился.
|
||||
76
info/evo-weaknesses.md
Normal file
76
info/evo-weaknesses.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Где защита 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) в этом билде **не вызывается ни разу**.
|
||||
Loading…
Add table
Add a link
Reference in a new issue