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.
This commit is contained in:
Loki 2026-10-09 13:47:27 +02:00
parent 2172206e83
commit 139aea9a8f
Signed by: boba
GPG key ID: 253067914055423B
96 changed files with 1053 additions and 831 deletions

View file

@ -1,4 +1,4 @@
# info/ — материал по обфускации EvoVis (для нашего обфускатора)
# info/ - материал по обфускации EvoVis (для нашего обфускатора)
Источник: `LoVisual/ref/evo/` (реверс EvoVis / EvolutionDLC, Minecraft-клиент).
Сюда перенесено только то, что касается защиты/обфускации кода.
@ -7,7 +7,7 @@
|---|---|
| `evo-obfuscation.md` | базовое описание трёх уровней обфускации (перенесено as-is из `ref/evo/obfuscation.md`) |
| `evo-obfuscation-full.md` | **сводный разбор**: все уровни + .NET-лаунчер + натив/антиотладка, с кодом и проверенными цифрами |
| `evo-weaknesses.md` | где защита провалилась при разборе — чего нам делать нельзя (+ опровержения) |
| `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-шейры, антиотладка, порча ключа |

View file

@ -1,7 +1,7 @@
# Как сделать лучше: план улучшений для нашего обфускатора
Привязка: референс — EvoVis (`evo-obfuscation-full.md`), провалы — `evo-weaknesses.md`.
Ниже — что делаем иначе, в порядке отдачи/цены. Для каждой техники — уровень угрозы,
Привязка: референс - EvoVis (`evo-obfuscation-full.md`), провалы - `evo-weaknesses.md`.
Ниже - что делаем иначе, в порядке отдачи/цены. Для каждой техники - уровень угрозы,
которую она закрывает (скрипт-деобфускатор / статический анализ / ручной реверс).
---
@ -14,20 +14,20 @@
- Индивидуальный алфавит на каждый символ (см. B1), запрет ключевых слов и
одинаковых имён в одном scope.
- Никаких «readable» ярлыков: у EvoVis `@rn(name=…)` и `description` отдали каталог
модулей целиком. Если метаданные нужны рантайму — шифруем их (C2).
модулей целиком. Если метаданные нужны рантайму - шифруем их (C2).
### A2. Вычистить отладочную информацию
- `-g` → `LineNumberTable` / `LocalVariableTable` / `SourceFile` / `InnerClasses`
/ `EnclosingMethod` / `MethodParameters` / `Signature` — по policy: либо полностью
/ `EnclosingMethod` / `MethodParameters` / `Signature` - по policy: либо полностью
выкинуть, либо заменить на шум (см. B4).
- Ресурсы тоже часть кода: mixin-JSON, `fabric.mod.json`, access widener, service
loaders, SPI-файлы — шифруем/шифруем имена внутри, раскрываем в рантайме.
loaders, SPI-файлы - шифруем/шифруем имена внутри, раскрываем в рантайме.
### A3. Никаких сигнатурных имён у защитного кода
`KeyAssembler`, `RuntimeGuard`, `NativeGuard` → рандомные имена, а сам защитный код
**смешиваем с обычным** (секции, которые выглядят как мусорные утилиты).
Рефлексия по именам в открытом виде (`Class.forName("aln")`) — запрещена:
имена отдаются только из зашифрованной строки (C2), лучше — через indirection-таблицу.
Рефлексия по именам в открытом виде (`Class.forName("aln")`) - запрещена:
имена отдаются только из зашифрованной строки (C2), лучше - через indirection-таблицу.
### A4. Обфусцировать весь артефакт, а не только ядро
Правило: **нет незащищённого компонента**. У EvoVis лаунчер и java-agent были
@ -42,9 +42,9 @@
Вместо общего LCG:
- seed = `random_u64` на строку (храним неявно: derive из позиции/ид + master-seed);
- алгоритм выбирается из набора (3–5 схем: RC4-like, chacha-с fragments, XTEA-блочный,
plain XOR с keystream от BLAKE3, base85+flate перед шифром) — **перемешивание схем
plain XOR с keystream от BLAKE3, base85+flate перед шифром) - **перемешивание схем
между строками**, чтобы один скрипт не покрыл всё;
- AAD/MAC: `HMAC(fragment, plaintext_hash)` — повреждение ловится, подмена — тоже;
- AAD/MAC: `HMAC(fragment, plaintext_hash)` - повреждение ловится, подмена - тоже;
- keystream не повторяется: nonce включает индекс строки.
- **против обучения по частотам**: словарь/частотный анализ не работает, если схема
и nonce разные.
@ -53,18 +53,18 @@
Никаких `d(cipher, literal_key)`:
- ключ = `KDF(master_key, class_id, method_id, string_index)` либо приходит из
защищённого контекста (см. D3);
- вычисление — через серию операций, **растворённых в теле вызывающего метода**
(не выделенная функция `decrypt(s)` — иначе её найдут и развернут);
- вычисление - через серию операций, **растворённых в теле вызывающего метода**
(не выделенная функция `decrypt(s)` - иначе её найдут и развернут);
- мастер-секрет не в константах: собирается из нескольких мест + env/хост-фактора.
### B3. Константы — не тождества, а вычисления
### B3. Константы - не тождества, а вычисления
Отказ от `(A^B)+((A&B)<<1)` (опознаётся regex'ом). Вместо этого:
- константа собирается из **нескольких узлов**, разнесённых по разным basic block'ам
и разным методам (возврат частей, сложение на месте использования);
- включаем в цепочку **зависимые от окружения** величины, которые статически вычислимы
не полностью: `hashCode()` строки-литерала, `System.identityHashCode` статической
константы, значение поля, инициализируемого в `<clinit>`, размер массива;
- для `int`/`long`/`float`/`double` — отдельные схемы (bit-cast, разложение по
- для `int`/`long`/`float`/`double` - отдельные схемы (bit-cast, разложение по
мантиссе), потому что `double`-константы особенно заметны.
- opaque-предикаты делать **настоящими**: всегда истинные, но зависящие от состояния,
которое статически не выводится (напр. инвариант «массив отсортирован по заполнению»,
@ -85,10 +85,10 @@
Минимум набор (применять **случайно и по-разному в разных методах**):
1. **Switch по вычислимому индексу**: `table[ (x*31 ^ y) & mask ]` → адрес возврата;
переходы через индексы, а не метки.
2. **Распределённое состояние**: как у EvoVis (`i + i2`), но одно слагаемое —
из поля объекта/статики, второе — из параметра, обновление — через вызов метода;
2. **Распределённое состояние**: как у EvoVis (`i + i2`), но одно слагаемое -
из поля объекта/статики, второе - из параметра, обновление - через вызов метода;
**метки case не константные**, а пересчитанные (opaque).
3. **Бинарное дерево/бабочка**: вместо линейного списка состояний — рандомизированный
3. **Бинарное дерево/бабочка**: вместо линейного списка состояний - рандомизированный
граф переходов, эквивалентный исходному CFG (проверяется верификатором).
4. **Вызов через стек/ретурн-адрес**: эмуляция `goto` через `athrow`-как-прыжок
(exception-as-goto), `finally`-цепочки, или virtual-dispatch по фейковому интерфейсу.
@ -101,9 +101,9 @@
Плюс автоматические метрики (см. F).
### C3. Реальный объём ≠ раздувание
У EvoVis выигрыш — только в размере декомпилята, но и цена (читаемость для нас же).
У EvoVis выигрыш - только в размере декомпилята, но и цена (читаемость для нас же).
Оптимальнее: комбинировать обфускацию с **компиляцией в другой целевой код**
(см. E), где раздувание не надо — там и так нечем читать.
(см. E), где раздувание не надо - там и так нечем читать.
---
@ -111,7 +111,7 @@
### D1. Маленький VM / interpreter для критичных кусков
Свой bytecode (10–20 опкодов: арифметика, ветвление, вызов, доступ к полю),
инструкции в зашифрованном массиве, диспетчер — обычный код с обфусцированным CFG.
инструкции в зашифрованном массиве, диспетчер - обычный код с обфусцированным CFG.
Экспортируем в VM только чувствительное: лицензирование, ключи, критическую логику.
Это единственный уровень, который реально останавливает ручной реверс.
@ -122,38 +122,38 @@
### D3. Анти-анализ в рантайме (мягкий)
- self-check целостности: `SHA-256` своих классов/секций → при расхождении
**меняем поведение**, а не падаем (у EvoVis падение выдавало мусорный ключ —
**меняем поведение**, а не падаем (у EvoVis падение выдавало мусорный ключ -
правильно: не сообщать атакующему, что он пойман);
- детект отладчика/трейсера/инструментов эмуляции — реакция: ложные данные,
- детект отладчика/трейсера/инструментов эмуляции - реакция: ложные данные,
замедление, отключение функций;
- **не детектить и не ломать легитимные сценарии** (обновления, IDE, символы) —
- **не детектить и не ломать легитимные сценарии** (обновления, IDE, символы) -
ложные срабатывания дороже, чем польза защиты.
### D4. Честный anti-tamper, а не «зашейрь ключ»
У EvoVis сильная идея: ключ = изолированная доля в подменённом JVM + HMAC от hwid +
порча при детекте. Минус — вся схема работает только на своём окружении и падает
порча при детекте. Минус - вся схема работает только на своём окружении и падает
на другой ОС/сборке. Для нас: секрет должен переживать **наши** легитимные платформы,
а ломаться только при модификации кода/подмене целостности.
---
## E. Если цель — не JVM, а свой натив/Rust-бинарник
## E. Если цель - не JVM, а свой натив/Rust-бинарник
Референс JVM-слабости здесь не работают автоматически, но появляются свои:
- **строки**: все plaintext-строки в `.rodata` — шифровать, раскрывать лениво;
- **строки**: все plaintext-строки в `.rodata` - шифровать, раскрывать лениво;
не держать AES-ключ рядом с шифротекстом (та же ошибка, что у `awc.d`);
- **имена**: `stripped` бинарник + рандомизация символов; в Rust — макрос, который
- **имена**: `stripped` бинарник + рандомизация символов; в Rust - макрос, который
переименовывает `pub`-символы и убирает `#[doc]`/Debug-имена;
- **panic/Debug-вывод**: `{:?}` отдаёт имена типов и полей — для чувствительных типов
- **panic/Debug-вывод**: `{:?}` отдаёт имена типов и полей - для чувствительных типов
свой `Debug`, либо не выводить;
- **CFG**: обфускация на уровне MIR/LLVM-IR (passes): bogus control flow, flattening
на `switch` по вычисленному индексу, вставка недостижимых блоков, размытие
границ функций (inlining + outlined);
- **константы**: как B3, плюс вынос в вычисляемые таблицы;
- **анти-анализ**: `ptrace`-selfcheck, проверка `/proc/self/maps` на свои секции,
timing-checks; реакция — молчаливая порча данных;
timing-checks; реакция - молчаливая порча данных;
- **обфускация метаданных Rust**: `TypeId`/`Any`, `std::any::type_name`, логи
`tracing` — выключать в release.
`tracing` - выключать в release.
---
@ -161,25 +161,25 @@
Автотесты на каждую технику, гоняются в CI:
1. **Утечки имён**: `strings`/анализ констант — не должно остаться ни одного
1. **Утечки имён**: `strings`/анализ констант - не должно остаться ни одного
осмысленного идентификатора из списка-эталона (ошибка = фейл).
2. **Читаемость декомпилята**: прогнать JADX/CFR на собранный артефакт →
метрики: доля распознанных структур (try/catch, switch, циклы), доля классов
с восстановленными именами, число строк «смыслового» кода.
3. **Автоматический деобфускатор-противник**: минимальный скрипт-анализатор
(тот же `expr`-свёртка, regex на паттерны) — если он снимает >X% строк/констант,
(тот же `expr`-свёртка, regex на паттерны) - если он снимает >X% строк/констант,
техника провалена. Держим этот «красный список паттернов» в репозитории.
4. **Семантика**: тесты/эталонные программы до и после — 100% совпадение.
4. **Семантика**: тесты/эталонные программы до и после - 100% совпадение.
5. **Размер и скорость**: бюджет (≤ N% размера, ≤ M% времени исполнения),
отдельно — бюджет на рантайм-раскрытие строк (кэш, чтобы не платить на каждый вызов).
отдельно - бюджет на рантайм-раскрытие строк (кэш, чтобы не платить на каждый вызов).
6. **Стабильность между сборками**: имена/схемы меняются от билда к билду
(у EvoVis шифр один на все версии — окно для обучения).
(у EvoVis шифр один на все версии - окно для обучения).
## Приоритет
1. A1+A2+A4 (символы, отладка, всё артефакты) — дёшево, biggest leak у EvoVis.
2. B1+B2 (пер-строчные ключи, вывод ключа) — снимает главный провал `awc.d`.
1. A1+A2+A4 (символы, отладка, всё артефакты) - дёшево, biggest leak у EvoVis.
2. B1+B2 (пер-строчные ключи, вывод ключа) - снимает главный провал `awc.d`.
3. C1 несколько схем флаттенинга + C2 верификатор.
4. F метрики — чтобы не спорить на глаз.
5. C3/E (другой целевой код) — как долгосрочный путь.
6. D1 VM — только для действительно ценных кусков, остальное не оправдывает цену.
4. F метрики - чтобы не спорить на глаз.
5. C3/E (другой целевой код) - как долгосрочный путь.
6. D1 VM - только для действительно ценных кусков, остальное не оправдывает цену.

View file

@ -25,18 +25,18 @@ cd ref/evo/reverse
|---|---|---|---|
| `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** + новый экспорт |
| `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/evoshare/sources/java/lang/EvoShare.java` (JADX), вызывающий код -
`mod/agent-src/sources/evo/agent/key/NativeJvmShare.java`.
---
## 1. evoguard.dll — JNI-«нативный шейр»
## 1. evoguard.dll - JNI-«нативный шейр»
Экспорт ровно один (плюс mingw-рантайм `_CRT_INIT`, `DllMainCRTStartup`, `atexit`,
`sha256_block/update/final`, `env_flags_debug`, `hmac_sha256.constprop.0`) — **`JNI_OnLoad` нет**,
`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]`
@ -67,7 +67,7 @@ return res;
d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2
```
Итог: `NativeGuard.share(hwid) = HMAC-SHA256(key_eg, hwid_utf8)`, при детекте отладки —
Итог: `NativeGuard.share(hwid) = HMAC-SHA256(key_eg, hwid_utf8)`, при детекте отладки -
`XOR 0xA5` (не ошибка, а **молчаливо неверный шейр**).
### Платформенный паритет (`evoguard`)
@ -83,9 +83,9 @@ d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2
ровно `d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2`; следующие 32
байта = тот же ключ с XOR `0x5c`. SHA-IV рядом (`0x180003090` = `67e6096a…`).
Во всех сборках есть строки `IsDebuggerPresent`, `jdwp`, `agentlib`, `JAVA_TOOL_OPTIONS`,
`_JAVA_OPTIONS` — логика антиотладки одинаковая. **Linux-сборки натива нет вообще**.
`_JAVA_OPTIONS` - логика антиотладки одинаковая. **Linux-сборки натива нет вообще**.
Потребитель: `alm.java` (KeyAssembler) — шейр идёт в 4-of-4 сборку Шамира:
Потребитель: `alm.java` (KeyAssembler) - шейр идёт в 4-of-4 сборку Шамира:
```java
byte[] nativeShare = NativeGuard.share(hwid);
@ -97,7 +97,7 @@ ama.combine(new int[]{share.x(), share.xIntegrity(), share.xNative(), share.xHwi
---
## 2. java.dll — `java.lang.EvoShare.computeRaw` (патченный JDK)
## 2. java.dll - `java.lang.EvoShare.computeRaw` (патченный JDK)
Экспорт: `Java_java_lang_EvoShare_computeRaw` (@ `0x180001ca4`, 286 байт) +
импорт `JVM_EvoForeignAgentPresent` **из `jvm.dll`** (см. §3).
@ -121,9 +121,9 @@ longlong Java_java_lang_EvoShare_computeRaw(JNIEnv* env, jclass cls, jbyteArray
`FUN_180001e4c(input, len, out32)`:
1. `FUN_180001f1c(&DAT_180010e90, 0x20, input, len, out32)` — **стандартный HMAC-SHA256**
1. `FUN_180001f1c(&DAT_180010e90, 0x20, input, len, out32)` - **стандартный HMAC-SHA256**
(внутри явные SHA-IV `0x6a09e667…`, копирование ключа в ipad/opad);
2. затем антиотладка, при срабатывании — `out32 ^= 0xA5…`:
2. затем антиотладка, при срабатывании - `out32 ^= 0xA5…`:
- `IsDebuggerPresent()`;
- `CheckRemoteDebuggerPresent()`;
- загружены модули `jdwp.dll`, `dt_socket.dll`, `dt_shmem.dll` (транспорты JDWP);
@ -135,16 +135,16 @@ longlong Java_java_lang_EvoShare_computeRaw(JNIEnv* env, jclass cls, jbyteArray
70ea2f4857869bdc8606a29a6c8cdcedbe92c18bef1aad106024db104a8f25d1
```
Ключи `evoguard.dll` и `java.dll` **разные** — это два независимых шейра.
Ключи `evoguard.dll` и `java.dll` **разные** - это два независимых шейра.
Константы-маски: `0xA5` — антиотладка (натив), `0x3C` — «чужой агент» из HotSpot,
`0x5A` — Java-уровень (см. §4).
Константы-маски: `0xA5` - антиотладка (натив), `0x3C` - «чужой агент» из HotSpot,
`0x5A` - Java-уровень (см. §4).
---
## 3. jvm.dll — экспорт `JVM_EvoForeignAgentPresent`
## 3. jvm.dll - экспорт `JVM_EvoForeignAgentPresent`
`mod/evojdk_x/bin/server/jvm.dll` (15,4 МБ) — тот же HotSpot, что и в `java.dll`, но с
`mod/evojdk_x/bin/server/jvm.dll` (15,4 МБ) - тот же HotSpot, что и в `java.dll`, но с
добавленным экспортом (в таблице экспортов: `[4281] +base[4282] 10b9 JVM_EvoForeignAgentPresent`).
`java.dll` импортирует его и вызывает в `computeRaw`.
@ -171,7 +171,7 @@ undefined1 JVM_EvoForeignAgentPresent(void) {
### Откуда берётся список и что такое `+0x53`
- `DAT_180e2c160` — **глава глобального связного списка JVMTI-агентов** (stock-механизм HotSpot,
- `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` байт, инициализирует
@ -196,8 +196,8 @@ JVMTI-списке есть **хотя бы один не-instrument агент*
Вызов из `EvoShare.computeRaw` → `XOR 0x3C` на HMAC-шейре (см. §2, §5).
Также в `jvm_focus.c` — две функции, содержащие доступы к полю `0x53`
(`FUN_180268640`, `FUN_1804a7920`), — это чтения в фильтрах классов/загрузчика, к
Также в `jvm_focus.c` - две функции, содержащие доступы к полю `0x53`
(`FUN_180268640`, `FUN_1804a7920`), - это чтения в фильтрах классов/загрузчика, к
логике агентов отношения не имеют. Прямой записи `mov byte [reg+0x53]` в программе нет:
поле инициализируется в конструкторе широкой записью через `strcmp`-результат.
@ -210,12 +210,12 @@ 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 — третий уровень порчи
out[i] = (byte) (out[i] ^ 90); // 0x5A - третий уровень порчи
return out;
}
```
`agentMismatch()` — сканирует `VM.getRuntimeArguments()`:
`agentMismatch()` - сканирует `VM.getRuntimeArguments()`:
- аргумент `-agentpath:` или `-agentlib:` (любой нативный агент, в т.ч. `-agentlib:jdwp`) → `true`;
- `-javaagent:<jar>[=opts]`: берёт путь jar, читает файл, считает SHA-256 и сравнивает с
@ -230,11 +230,11 @@ AGENT_CORRUPT_MASK = 90 // 0x5A
EXPECTED_AGENT_SHA256 = 374ace507bd0cc2d0ce6dbbe07e50ddb3173ff819e92470271e2bf0fccff2038
```
Проверено: `sha256sum mod/evo-agent.jar` = `374ace50…` — **пиннинг на свой агент
Проверено: `sha256sum mod/evo-agent.jar` = `374ace50…` - **пиннинг на свой агент
`evo-agent.jar`**. То есть JVM должен быть запущен либо вообще без `-javaagent`,
либо именно с их агентом; чужой агент тихо ломает шейр.
Потребитель — `evo.agent.key.NativeJvmShare` (в `evo-agent.jar`):
Потребитель - `evo.agent.key.NativeJvmShare` (в `evo-agent.jar`):
```java
Method compute = Class.forName("java.lang.EvoShare").getMethod("compute", byte[].class);
@ -242,7 +242,7 @@ byte[] share = (byte[]) compute.invoke(null, hwid.getBytes(UTF_8));
// ClassNotFoundException → KeyProviderException("not running on the EvoVis custom JVM")
```
То есть **payload-ключ требует их кастомного JDK** — на стоковом `java` шейр получить нельзя.
То есть **payload-ключ требует их кастомного JDK** - на стоковом `java` шейр получить нельзя.
---
@ -255,19 +255,19 @@ byte[] share = (byte[]) compute.invoke(null, hwid.getBytes(UTF_8));
| натив JDK | `EvoShare.computeRaw` | `JVM_EvoForeignAgentPresent() == 1`: есть `-agentlib:`/`-agentpath:` агент либо ≥2 `-javaagent:` | `XOR 0x3C` |
| Java JDK | `EvoShare.compute` | `-agentpath:`/`-agentlib:` или `-javaagent:` с чужим sha256 | `XOR 0x5A` |
Во всех случаях результат остаётся валидным `byte[32]` — ошибка не сигнализируется,
Во всех случаях результат остаётся валидным `byte[32]` - ошибка не сигнализируется,
ключ просто собирается неверно (в `alm.java` дальше идёт `combine` Шамира и `alz.wipe`).
Две разные схемы HMAC (разные ключи) дают два разных шейра:
- `NativeGuard.share(hwid)` — в сборку ключа мода (`alm.java`, 4-of-4 Шамир);
- `EvoShare.compute(hwid)` — в цепочку ключа payload'а агента (`NativeJvmShare`, `EvoAgent`).
- `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),
`mod/jar/assets/evolution/native/macos/evovis-audiotap` - **Mach-O universal (x86_64 + arm64),
174 848 байт, исполняемый файл** (`_main` + `__mh_execute_header`, не dylib).
Факты по `strings` / `r2`:
@ -275,17 +275,17 @@ byte[] share = (byte[]) compute.invoke(null, hwid.getBytes(UTF_8));
- написан на **Swift** (секции `__swift5_typeref/capture/entry/reflstr/fieldmd/types`),
класс `main.AudioTap` (`_TtC4main8AudioTap`);
- зависимости: **ScreenCaptureKit**, CoreMedia, AVFoundation, CoreAudio (swiftCoreAudio),
Foundation, IOKit — то есть захват системного звука/видеопотока через ScreenCaptureKit;
Foundation, IOKit - то есть захват системного звука/видеопотока через ScreenCaptureKit;
- делегат `stream(_:didOutputSampleBuffer:of:)` (`SCStream` + `CMSampleBuffer`) и
`stream(_:didStopWithError:)`; поля `output: NSFileHandle`, `mono: [Float]`;
- логи/метки `evovis.audiotap.audio`, `evovis.audiotap.screen`;
- **сетевых строк нет** (ни `http`, ни сокетов, ни доменов) — данные пишутся в
- **сетевых строк нет** (ни `http`, ни сокетов, ни доменов) - данные пишутся в
`NSFileHandle` (pipe/stdout), то есть процесс-потребитель мод запускает локально.
Вывод: это локальный helper для тапа аудио (и экрана) на macOS, результат уходит пайпом
в родительский процесс; в аудио/сеть наружу ничего не отдаётся.
Декомпиляция Ghidra для него не запускалась (предмет — Swift + macOS-фреймворки, на
Декомпиляция Ghidra для него не запускалась (предмет - Swift + macOS-фреймворки, на
уровне строк/символов картина уже ясна); запускается по необходимости:
```bash

View file

@ -22,25 +22,25 @@
- **jadx переименовал их класс `if` → `Cif`** (keyword): класс `Cif.class` в джаре не существует,
поэтому весь мёртвый код с ссылками на `Cif` вырезан, а не пересобран.
- `?? r1;` (недообфусцированный фрагмент) и `throw th;` вне catch — в вырезанных методах.
- Обфускация `setDaemon((A^B)+((A&B)<<1))` — это int-эквивалент `true` (A+B=1), javac не берёт.
- `?? 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` — открываются все их шлюзы
Профиль - локальный dummy.
- `isAuthorized()` → `return true` - открываются все их шлюзы
(`Panel`, `Camera2`, `First`, `Friends`, `Crosshair2`, `KeyAssembler`, `SessionHeartbeat`).
- `user()` → lazy-dummy: если профиль не задан, создаёт тот же dummy.
**`token == null`** — принципиально: `KeyAssembler.token()` и `SessionHeartbeat.token()`
**`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`, их команда отчёта).
- `submit(String)` → no-op (единственный внешний вызов - `dn`/`Message2`, их команда отчёта).
- Удалены `feedback`, `buildBody`, `run` (достигались только из `submit`).
### `alr` = `SessionHeartbeat` (сессия/отключение модулей)
@ -50,12 +50,12 @@
## Почему остальное работает без их сервера
- **Обычные строки мода** — `awc.d(String,int)`: статический LCG+XOR, ключа не требует.
- **`SecureStrings`** (5 API-блобов: probe/friends/messages/cosmetic_sync/config_share) —
- **Обычные строки мода** - `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` — живут только после сборки
- `KeyAssembler`/`SealedBoot`/`RuntimeGuard`/JNI `NativeGuard` - живут только после сборки
ключа, то есть недостижимы при `token == null`.
## Оставлено (осознанно)
@ -82,7 +82,7 @@ OK: auth=local, report=no-op, heartbeat=off
## Ограничение
В игре не прогоняли: здесь Linux, а их `evojdk` только win-x64. Запускать патченный джар —
на Windows (их лаунчер или свой fabric-лодер); при проблемах — смотреть лог на загрузке
В игре не прогоняли: здесь Linux, а их `evojdk` только win-x64. Запускать патченный джар -
на Windows (их лаунчер или свой fabric-лодер); при проблемах - смотреть лог на загрузке
модулей. Ожидаемо «пустым» (серверное) останется: список друзей, сообщения, косметика синк,
аватарки, тикеты.

View file

@ -1,8 +1,8 @@
# Обфускация EvoVis / EvolutionDLC — полный сводный разбор
# Обфускация EvoVis / EvolutionDLC - полный сводный разбор
Всё проверено по артефактам в `LoVisual/ref/evo/` (jadx-декомпиляция `evovis-dec.jar`,
`javap -v` по 1277 классам корня, Rust-скрипты `scripts/`). Цифры, которые пересчитаны
вручную, помечены ✓; расхождения с исходным README — в конце.
вручную, помечены ✓; расхождения с исходным README - в конце.
Пакеты: 1405 `.class` всего = **1277 в default package** (корень jar) + **16 в `a/`**
+ **111 в `b/`** + 128 вложенных (`evo/**`, зависимости). jadx даёт 6991 `.java`
@ -10,32 +10,32 @@
---
## Уровень 1 — имена
## Уровень 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`…): декомпилятор вынужден их
`"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:` — по ним восстанавливаются оригинальные имена.
`"AttestClient.java"`, `"PayloadCrypto.java"`, … у миксинов - `"GameRendererMixin.java"`.
160 файлов содержат `compiled from:` - по ним восстанавливаются оригинальные имена.
- **Отладочная информация осталась**: 10 218 `LineNumberTable` и 151
`LocalVariableTable` ✓ (локальные имены переменных живут у security-классов:
`awc` — 5, `aln` — 11).
`awc` - 5, `aln` - 11).
- Аннотации модулей не тронуты: `@rn(name = "HitFix", category = rl.COMBAT,
description = "Makes attacks land on the hitbox you actually see…")` — 57 классов ✓,
description = "Makes attacks land on the hitbox you actually see…")` - 57 классов ✓,
то есть каталог модулей читается без всякой деобфускации.
- **Enum-константы не переименованы**: `SCALE`, `OPACITY`, `SPAWN`, `KEYFRAME`.
При этом 102 enum jadx не восстановил (`Failed to restore enum class`) — обфускатор
При этом 102 enum jadx не восстановил (`Failed to restore enum class`) - обфускатор
сломал декомпиляторский паттерн enum, но имена констант в байткоде остались.
## Уровень 2 — строки
## Уровень 2 - строки
Класс `awc` = `evovis.security.StringRuntime` (раскрыт по `SourceFile`):
@ -48,16 +48,16 @@ for (i = 0; i < cipher.length(); i++) {
return new String(bytes, UTF_8).intern(); // + ConcurrentHashMap CACHE
```
- `awc.d(cipher, key)` — обычная строка. **Ключ передаётся как opaque-выражение прямо
- `awc.d(cipher, key)` - обычная строка. **Ключ передаётся как opaque-выражение прямо
в вызове**, ни одного «настоящего» ключа в рантайме нет.
- `awc.g(cipher, key)` — то же, но session-gate: сначала
- `awc.g(cipher, key)` - то же, но session-gate: сначала
`Class.forName("aln").getMethod("key").invoke(null)` (рефлексией, имя класса открытым
текстом), если ключа сессии нет — возвращает `null`.
- `SecureStrings` (5 API-блобов) — гейт по `KeyAssembler.key()`, при отсутствии ключа
текстом), если ключа сессии нет - возвращает `null`.
- `SecureStrings` (5 API-блобов) - гейт по `KeyAssembler.key()`, при отсутствии ключа
`get()` возвращает `null` и серверные фичи просто выключаются.
- Шифр одинаков для всех строк, кэш по шифротексту, `.intern()`.
## Уровень 3 — контрольный поток
## Уровень 3 - контрольный поток
Тело метода разложено в конечный автомат **с раздвоенным состоянием**:
@ -75,11 +75,11 @@ while (true) {
}
```
- Переход — присваивание **двум** переменным (`i`, `i2`) и обратный `goto` в начало
- Переход - присваивание **двум** переменным (`i`, `i2`) и обратный `goto` в начало
цикла. Состояние хранится не одной константой, а суммой двух opaque-полей:
константная пропаганда обязана проследить пару, а не одно присваивание.
- Формы встреч ✓: `switch (i + i2)` — 1826, `switch (i2 + i3)` — 1067,
`switch (i3 + i4)` — 283, `switch (i4 + i5)` — 161, дальше хвост до `i15 + i16`.
- Формы встреч ✓: `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 файла (реальный объём
@ -87,7 +87,7 @@ while (true) {
## Кодирование констант (opaque predicates)
Все целочисленные литералы — тождества по модулю 2³² (и 2⁶⁴):
Все целочисленные литералы - тождества по модулю 2³² (и 2⁶⁴):
| паттерн | равен |
|---|---|
@ -106,7 +106,7 @@ strArr[((-702410340) | 702410340) + ((-702410340) & 702410340)] = ... // ин
// long-вариант (Duration.ofSeconds(10))
(703617214384230201L ^ (-703617214384230191L)) + ((703617214384230201L & (-703617214384230191L)) << 1)
// сдвиговые константы тоже закодированы — см. «опровержение» ниже
// сдвиговые константы тоже закодированы - см. «опровержение» ниже
```
## Мёртвый код / декой-классы
@ -114,33 +114,33 @@ strArr[((-702410340) | 702410340) + ((-702410340) & 702410340)] = ... // ин
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`) — то есть это балласт, выключенный из графа,
ссылается на отброшенный (`dangling=0`) - то есть это балласт, выключенный из графа,
а не случайные остатки.
---
## Лаунчер (.NET) — отдельная, очень слабая схема
## Лаунчер (.NET) - отдельная, очень слабая схема
- Один байтовый блоб `byte[15927] _4` в `PrivateImplementationDetails`,
в статическом конструкторе `_4[i] = (byte)(_4[i] ^ i ^ 0xAA)`;
чтение через аксессоры `return _5[N] ?? _6(off, len)` → `Encoding.UTF8`.
- **911 аксессоров** ✓ (все в одном файле), вызовы по всему коду — итог 1226 замен
- **911 аксессоров** ✓ (все в одном файле), вызовы по всему коду - итог 1226 замен
(по `scripts/deobfuscate_launcher`).
- Имена сервисов не тронуты: `ModProvisioner`, `PayloadKeyClient`, `EvoVisAuth`,
`HardwareId`, `DeliveryMirror`. Никакой обфускации имён, никакой обфускации потока —
`HardwareId`, `DeliveryMirror`. Никакой обфускации имён, никакой обфускации потока -
одна статическая XOR-таблица, восстанавливается одним проходом.
- `strings.txt` — 929 расшифрованных записей.
- `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` —
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-фрагменты
3. **HWID** = `sha256("evovis|" + MachineGuid)` - привязка к машине, Shamir-фрагменты
ключа, аттестация (`evovis-protect.vercel.app`).
4. **`evo-agent.jar` вообще не обфусцирован** ✓ — читаемые строки, `SourceFile:
4. **`evo-agent.jar` вообще не обфусцирован** ✓ - читаемые строки, `SourceFile:
"EvoAgent.java"`, т. е. расшифровщик payload открыт.
5. Скрипт-логирование с детектом `$ExecutionContext.SessionState.LanguageMode`
(защита от поломки PowerShell-обфускации у себя).
@ -153,10 +153,10 @@ Reachability-BFS от bootstrap/`@rn`/`a`+`b`/security: **1370 классов
|---|---|
| 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, видимо, от усечённого прогона |
| «все строковые литералы зашифрованы `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. «Пустышки» — интерпретация |
| «сдвиговые константы в байткоде отрицательные (`>> -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 аксессоров подтверждено |

View file

@ -2,20 +2,20 @@
## Три уровня
1. **Имена** — 1277 классов вынесены в default package с именами вида `a…aa…`;
1. **Имена** - 1277 классов вынесены в default package с именами вида `a…aa…`;
миксины разбросаны по пакетам `a/` и `b/`. Смысловых имён классов в коде нет.
2. **Строки** — все строковые литералы зашифрованы вызовом `awc.d(cipher, key)`:
2. **Строки** - все строковые литералы зашифрованы вызовом `awc.d(cipher, key)`:
- LCG: `state = state * 1103515245 + 12345` (32-бит переполнение);
- байт: `cipherChar ^ (state >>> 24)`;
- результат — UTF-8 строка.
- результат - UTF-8 строка.
Ключи передаются «непрозрачными» целыми (см. ниже). Второй вариант `awc.g(...)`
— то же самое плюс session-gate: строка расшифровывается только после
- то же самое плюс session-gate: строка расшифровывается только после
проверки сессии через рефлексию `aln.key()`.
3. **Контрольный поток** — тела методов разложены в машину состояний:
3. **Контрольный поток** - тела методов разложены в машину состояний:
`while (true) { switch (состояние) { case <обфуск-константа>: ...; состояние = <следующая>; } }`.
Переходы между блоками — через присваивание целому и обратный `goto`.
Переходы между блоками - через присваивание целому и обратный `goto`.
Из-за этого объём декомпилированного кода раздут примерно в 25 раз
относительно реального.
@ -29,14 +29,14 @@
| `(A \| B) + (A & B)` | `A + B` |
Так прячутся даже `0`, `1`, `16`, `255`. Например, `true`/`false` приходят как
выражение, равное `1`/`0` (javac такого не генерирует — это работа обфускатора).
выражение, равное `1`/`0` (javac такого не генерирует - это работа обфускатора).
Сдвиги и маски тоже закодированы: сдвиговые константы в байткоде могут быть
отрицательными — JVM маскирует количество бит по младшим 5 битам
отрицательными - JVM маскирует количество бит по младшим 5 битам
(`ishl/ishr` со сдвигом `-8` работает как сдвиг на `24`), поэтому `>> -8`
в байткоде эквивалентен `>> 24` в исходнике.
## Прочее
- Декой-классы обфускатора — пустышки с бессмысленными членами, в логике не участвуют.
- Декой-классы обфускатора - пустышки с бессмысленными членами, в логике не участвуют.
- Из-за имён-ключевых слов (`if`, `do`, …) декомпилятор переименовывает такие
классы (`Cif`, `Cdo`) — в самом jar классов с такими именами нет.
классы (`Cif`, `Cdo`) - в самом jar классов с такими именами нет.

View file

@ -71,19 +71,19 @@ Fabric загружает мод → мод сам распаковывает ev
Ключевые свойства:
1. **Мод не лежит на диске в открытом виде** — расшифровка в памяти агента,
`candidate` — временный mount-файл.
1. **Мод не лежит на диске в открытом виде** - расшифровка в памяти агента,
`candidate` - временный mount-файл.
2. **Ключ собирается из двух мест**: hex из env (лаунчер) + `java.lang.EvoShare.compute(hwid)`
в **подменённом JVM** — поэтому мод гоняется только на их evojdk.
3. **Инъекция в Fabric** — не мод в `mods/`, а байткод-трансформация внутреннего
в **подменённом JVM** - поэтому мод гоняется только на их evojdk.
3. **Инъекция в Fabric** - не мод в `mods/`, а байткод-трансформация внутреннего
класса Fabric (`ArgumentModCandidateFinder`) через `java.lang.classfile` API.
Классы: `EvoAgent`, `EvoAgentHook`, `FabricDiscoveryInjector`, `PreparedMod`,
`NestedLibraries`.
4. Далее внутри мода — свой слой: `KeyAssembler` (Shamir: сервер + integrity + native + hwid),
4. Далее внутри мода - свой слой: `KeyAssembler` (Shamir: сервер + integrity + native + hwid),
`SealedBoot` (sealed.bin в этом билде отсутствует → fail-open), `SessionHeartbeat`.
## Что здесь не проверено
- Тело `java.dll` / `EvoShare` — натив не реверсился (план в `todo.md`).
- Тело `java.dll` / `EvoShare` - натив не реверсился (план в `todo.md`).
- Реальное поведение при запуске (нужен MC 26.2 + Fabric 0.19.5 + их evojdk + дисплей).
- `evoguard.dll` — не реверсился.
- `evoguard.dll` - не реверсился.

View file

@ -1,47 +1,47 @@
# Где защита EvoVis провалилась — чего нам делать нельзя
# Где защита EvoVis провалилась - чего нам делать нельзя
Разобрано полностью за один проход, без единого рантайм-хака. Каждый пункт —
Разобрано полностью за один проход, без единого рантайм-хака. Каждый пункт -
что именно дало ключ атаки.
## 1. Ключ шифрования строк лежит рядом со шифротекстом
`awc.d("<cipher>", <opaque-выражение>)` — ключ это **константное выражение в вызове**.
`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` —
`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 модулей с описаниями.
- `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
Один шаблон на весь продукт: `while(true) { switch (i + i2) }`, номер состояния -
сумма двух opaque-полей, переходы - присваивание пары. Один regex находит 3530
автоматов. Декомпилятор восстанавливает CFG, потому что:
- `switch` по **константным** меткам (метки не закодированы);
- все переходы — прямые присваивания в одном basic block;
- все переходы - прямые присваивания в одном basic block;
- нет вычислимых на входе предикатов, нет переходов через таблицы/массивы;
- нет обфускации самих тел блоков (тела — обычный код).
- нет обфускации самих тел блоков (тела - обычный код).
## 5. Opaque-предикаты сводятся к `A+B`
`(A^B)+((A&B)<<1)` и `(A|B)+(A&B)` — это школьные тождества. Распознаются
`(A^B)+((A&B)<<1)` и `(A|B)+(A&B)` - это школьные тождества. Распознаются
регуляркой, сворачиваются в один проход, заодно выдают «бесплатный» фолдинг
остальных констант. Ни одного настоящего opaque-предиката (зависящего от состояния
памяти/окружения и всегда истинного) в коде нет.
@ -55,13 +55,13 @@
## 7. Каждая защита живёт отдельно и называется своим именем
`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`, `SealedBoot`,
`NativeGuard` — названия прямо говорят атакующему, куда смотреть. Плюс open-source
идентификаторы: `aln` упомянут в рефлексии строкой, `awc` — по имени вызова.
`NativeGuard` - названия прямо говорят атакующему, куда смотреть. Плюс open-source
идентификаторы: `aln` упомянут в рефлексии строкой, `awc` - по имени вызова.
## 8. Вторичные компоненты вообще без защиты
- `evo-agent.jar` (расшифровщик payload) — открытые строки, `SourceFile`, без обфускации.
- Лаунчер .NET — статическая XOR-таблица `b ^ i ^ 0xAA`, имена сервисов не тронуты.
- `evo-agent.jar` (расшифровщик payload) - открытые строки, `SourceFile`, без обфускации.
- Лаунчер .NET - статическая XOR-таблица `b ^ i ^ 0xAA`, имена сервисов не тронуты.
- Защита ключа в нативе сильная, но **код мода** защищён слабее, чем ключ.
## 9. Шифр без аутентификации и без per-string salt
@ -71,6 +71,6 @@ LCG+XOR: нет MAC, нет случайного сида на строку, н
## Опровержение (чтобы не повторять чужие ошибки в выводах)
- «Сдвиги в байткоде могут быть отрицательными (`>> -8` == `>> 24`)» — по всем
1277 классам **0** таких случаев (32 559 сдвигов, константные — всегда `1`).
- «Сдвиги в байткоде могут быть отрицательными (`>> -8` == `>> 24`)» - по всем
1277 классам **0** таких случаев (32 559 сдвигов, константные - всегда `1`).
- `awc.g(...)` (session-gate) в этом билде **не вызывается ни разу**.