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:
parent
2172206e83
commit
139aea9a8f
96 changed files with 1053 additions and 831 deletions
|
|
@ -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-шейры, антиотладка, порча ключа |
|
||||
|
|
|
|||
|
|
@ -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 - только для действительно ценных кусков, остальное не оправдывает цену.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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-лодер); при проблемах - смотреть лог на загрузке
|
||||
модулей. Ожидаемо «пустым» (серверное) останется: список друзей, сообщения, косметика синк,
|
||||
аватарки, тикеты.
|
||||
|
|
|
|||
|
|
@ -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 аксессоров подтверждено |
|
||||
|
|
|
|||
|
|
@ -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 классов с такими именами нет.
|
||||
|
|
|
|||
|
|
@ -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` - не реверсился.
|
||||
|
|
|
|||
|
|
@ -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) в этом билде **не вызывается ни разу**.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue