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,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 аксессоров подтверждено |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue