obfuskator/info/evo-native-reverse.md
loki5512344 139aea9a8f
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.
2026-10-09 13:47:27 +02:00

295 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Реверс нативных библиотек EvoVis (Ghidra headless)
Инструменты: Ghidra 12.1.2 (`/opt/ghidra/support/analyzeHeadless`), radare2, objdump, javap.
Скрипты: `reverse/DumpDecomp.java` (дамп всех функций), `reverse/FocusDecomp.java` (точечная
декомпиляция: точка входа + её вызываемые + все функции, пишущие в поле со смещением `0x53`).
Воспроизведение:
```bash
cd ref/evo/reverse
/opt/ghidra/support/analyzeHeadless "$PWD/proj" EvoRev \
-import "$PWD/../mod/jar/META-INF/evo/native/win-x64/evoguard.dll" \
-postScript DumpDecomp.java "$PWD/out/evoguard.c" -scriptPath "$PWD"
/opt/ghidra/support/analyzeHeadless "$PWD/proj" EvoRev \
-import "$PWD/../mod/evojdk_x/bin/java.dll" \
-postScript DumpDecomp.java "$PWD/out/java_dll.c" -scriptPath "$PWD"
/opt/ghidra/support/analyzeHeadless "$PWD/proj" EvoJvm \
-import "$PWD/../mod/evojdk_x/bin/server/jvm.dll" \
-postScript FocusDecomp.java "$PWD/out/jvm_focus.c" JVM_EvoForeignAgentPresent -scriptPath "$PWD"
```
## Что реверсилось
| файл | размер | секций | содержимое |
|---|---|---|---|
| `mod/jar/META-INF/evo/native/win-x64/evoguard.dll` | 51 321 | 18 | своя маленькая JNI-библиотека (MinGW, PE32+) |
| `mod/evojdk_x/bin/java.dll` | 111 104 | 6 | стандартный `java.dll` из JDK **+ новая функция** |
| `mod/evojdk_x/bin/server/jvm.dll` | 15 463 936 | - | **патченный HotSpot** + новый экспорт |
Отдельно (Java, не натив): `mod/evojdk_x/classes/java/lang/EvoShare.class`,
исходник - `mod/evoshare/sources/java/lang/EvoShare.java` (JADX), вызывающий код -
`mod/agent-src/sources/evo/agent/key/NativeJvmShare.java`.
---
## 1. evoguard.dll - JNI-«нативный шейр»
Экспорт ровно один (плюс mingw-рантайм `_CRT_INIT`, `DllMainCRTStartup`, `atexit`,
`sha256_block/update/final`, `env_flags_debug`, `hmac_sha256.constprop.0`) - **`JNI_OnLoad` нет**,
`nm`-имена не вычищены (COFF-символы остались).
### `Java_evo_systems_security_natives_NativeGuard_nativeShare(JNIEnv*, jclass, jstring hwid)` → `byte[32]`
```c
if (hwid == NULL) return NULL;
str = (*env)->GetStringUTFChars(env, hwid, NULL);
hmac_sha256(str, strlen(str), out32); // HMAC-SHA256 с фиксированным ключом, 32 байта
(*env)->ReleaseStringUTFChars(env, hwid, str);
debugged = IsDebuggerPresent()
|| CheckRemoteDebuggerPresent(GetCurrentProcess(), &flag) && flag
|| env_flags_debug("JAVA_TOOL_OPTIONS")
|| env_flags_debug("_JAVA_OPTIONS");
if (debugged) out32 ^= 0xA5A5A5A5_A5A5A5A5 ...; // отравление результата
byte[] res = (*env)->NewByteArray(env, 32);
(*env)->SetByteArrayRegion(env, res, 0, 32, out32);
return res;
```
`env_flags_debug(name)`: читает переменную окружения; если она содержит `jdwp` или `agentlib`
→ считается отладкой. Плюс чек `IsDebuggerPresent` / `CheckRemoteDebuggerPresent`.
**Ключ HMAC (32 байта, `.rdata` @ `0x3b00251c0`):**
```
d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2
```
Итог: `NativeGuard.share(hwid) = HMAC-SHA256(key_eg, hwid_utf8)`, при детекте отладки -
`XOR 0xA5` (не ошибка, а **молчаливо неверный шейр**).
### Платформенный паритет (`evoguard`)
| файл | размер | ключ | вывод |
|---|---|---|---|
| `win-x64/evoguard.dll` | 51 321 | `d42394b1…533bd2` в `.rdata` | базовый (реверс выше) |
| `win-arm64/evoguard.dll` | 66 560 | **тот же**, но лежит как предвычисленный `key^0x36` / `key^0x5c` (ipad/opad) | код идентичен по смыслу |
| `macos-x64/libevoguard.dylib` | 8 936 | `d42394b1…533bd2` (offset 2096) | тот же |
| `macos-arm64/libevoguard.dylib` | 33 936 | `d42394b1…533bd2` (offset 2080) | тот же |
Проверка ARM64: байты по адресу `0x180003050` = `e215a287…d9650de4`, XOR `0x36` →
ровно `d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2`; следующие 32
байта = тот же ключ с XOR `0x5c`. SHA-IV рядом (`0x180003090` = `67e6096a…`).
Во всех сборках есть строки `IsDebuggerPresent`, `jdwp`, `agentlib`, `JAVA_TOOL_OPTIONS`,
`_JAVA_OPTIONS` - логика антиотладки одинаковая. **Linux-сборки натива нет вообще**.
Потребитель: `alm.java` (KeyAssembler) - шейр идёт в 4-of-4 сборку Шамира:
```java
byte[] nativeShare = NativeGuard.share(hwid);
ama.combine(new int[]{share.x(), share.xIntegrity(), share.xNative(), share.xHwid()},
new byte[][]{share.y(), integrityShare, nativeShare, hwidShare});
```
То есть «отравленный» нативный шейр ломает сборку ключа без единого сообщения об ошибке.
---
## 2. java.dll - `java.lang.EvoShare.computeRaw` (патченный JDK)
Экспорт: `Java_java_lang_EvoShare_computeRaw` (@ `0x180001ca4`, 286 байт) +
импорт `JVM_EvoForeignAgentPresent` **из `jvm.dll`** (см. §3).
```c
longlong Java_java_lang_EvoShare_computeRaw(JNIEnv* env, jclass cls, jbyteArray input) {
if (input == NULL) return NULL;
len = (*env)->GetArrayLength(env, input);
bytes = (*env)->GetByteArrayElements(env, input, NULL);
FUN_180001e4c(bytes, len, out32); // HMAC-SHA256 + антиотладка (см. ниже)
(*env)->ReleaseByteArrayElements(env, input, bytes, 2/*JNI_ABORT*/);
if (JVM_EvoForeignAgentPresent() == 1) // из патченного HotSpot
out32 ^= 0x3C3C3C3C_3C3C3C3C ...; // второй уровень порчи
jbyteArray res = (*env)->NewByteArray(env, 32);
(*env)->SetByteArrayRegion(env, res, 0, 32, out32);
return res;
}
```
`FUN_180001e4c(input, len, out32)`:
1. `FUN_180001f1c(&DAT_180010e90, 0x20, input, len, out32)` - **стандартный HMAC-SHA256**
(внутри явные SHA-IV `0x6a09e667…`, копирование ключа в ipad/opad);
2. затем антиотладка, при срабатывании - `out32 ^= 0xA5…`:
- `IsDebuggerPresent()`;
- `CheckRemoteDebuggerPresent()`;
- загружены модули `jdwp.dll`, `dt_socket.dll`, `dt_shmem.dll` (транспорты JDWP);
- `JAVA_TOOL_OPTIONS` / `_JAVA_OPTIONS` содержат `jdwp`/`agentlib`.
**Ключ HMAC (32 байта, `.rdata` @ `0x180010e90`):**
```
70ea2f4857869bdc8606a29a6c8cdcedbe92c18bef1aad106024db104a8f25d1
```
Ключи `evoguard.dll` и `java.dll` **разные** - это два независимых шейра.
Константы-маски: `0xA5` - антиотладка (натив), `0x3C` - «чужой агент» из HotSpot,
`0x5A` - Java-уровень (см. §4).
---
## 3. jvm.dll - экспорт `JVM_EvoForeignAgentPresent`
`mod/evojdk_x/bin/server/jvm.dll` (15,4 МБ) - тот же HotSpot, что и в `java.dll`, но с
добавленным экспортом (в таблице экспортов: `[4281] +base[4282] 10b9 JVM_EvoForeignAgentPresent`).
`java.dll` импортирует его и вызывает в `computeRaw`.
Адрес: `0x1803a2340`. Декомпиляция: `reverse/out/jvm_focus.c` (функция + доступы к полю `0x53`),
`reverse/out/jvm_more.c` (итератор), `reverse/out/jvm_list.c` (источник списка),
`reverse/out/jvm_agent.c` (конструктор элемента).
### Что делает
```c
undefined1 JVM_EvoForeignAgentPresent(void) {
int instrument_cnt = 0; // iVar2
int native_cnt = 0; // iVar5
build_iterator(&it, &DAT_180e2c160, mode=3); // копия глобального списка с фильтром
while (hasNext(&it)) {
void* e = pop(&it);
if (byte_at(e, 0x53)) instrument_cnt++; else native_cnt++;
}
destroy_iterator(&it);
if (instrument_cnt < 2 && native_cnt == 0) return 0;
return 1;
}
```
### Откуда берётся список и что такое `+0x53`
- `DAT_180e2c160` - **глава глобального связного списка JVMTI-агентов** (stock-механизм HotSpot,
`+0x20` = `next`, `+0x58` размер элемента). Доказательство: рядом лежит функция с литералом
`"Disabled all JVMTI agents during -XX:AOTMode=create"`, которая делает `DAT_180e2c160 = 0`;
push-функция `FUN_1805f40e0(name, path, is_absolute)` аллоцирует `0x58` байт, инициализирует
и lock-free пушит в голову; `FUN_1805f44b0` выдаёт
`"Dynamic agent loading is only permitted in the live phase"`.
- Конструктор элемента `FUN_1805f30d0` (см. `jvm_agent.c`):
```c
*(bool *)(obj + 0x53) = (strcmp(name, "instrument") == 0); // «это javaagent/instrument»
*(obj + 0x54) = param_5; // is_dynamic
*(obj + 0x55) = 0; // фильтр итератора (mode 3 берёт только == 0)
```
- Фильтр итератора (`FUN_1805f3880`) = `byte[e + 0x55] == 0`, `next` = `*(e + 0x20)`.
### Итоговая семантика
`JVM_EvoForeignAgentPresent()` возвращает `1` («чужой агент присутствует»), если в
JVMTI-списке есть **хотя бы один не-instrument агент** (`-agentlib:`/`-agentpath:`, в т.ч.
`-agentlib:jdwp`), **либо два и более instrument-агента** (второй `-javaagent:`).
Единственный свой `-javaagent:evo-agent.jar` считается нормой.
Вызов из `EvoShare.computeRaw` → `XOR 0x3C` на HMAC-шейре (см. §2, §5).
Также в `jvm_focus.c` - две функции, содержащие доступы к полю `0x53`
(`FUN_180268640`, `FUN_1804a7920`), - это чтения в фильтрах классов/загрузчика, к
логике агентов отношения не имеют. Прямой записи `mov byte [reg+0x53]` в программе нет:
поле инициализируется в конструкторе широкой записью через `strcmp`-результат.
---
## 4. `java.lang.EvoShare` (Java-сторона, живёт в патченном JDK)
```java
public static byte[] compute(byte[] input) {
byte[] out = computeRaw(input); // натив, §2
if (out != null && out.length == 32 && agentMismatch())
for (int i = 0; i < out.length; i++)
out[i] = (byte) (out[i] ^ 90); // 0x5A - третий уровень порчи
return out;
}
```
`agentMismatch()` - сканирует `VM.getRuntimeArguments()`:
- аргумент `-agentpath:` или `-agentlib:` (любой нативный агент, в т.ч. `-agentlib:jdwp`) → `true`;
- `-javaagent:<jar>[=opts]`: берёт путь jar, читает файл, считает SHA-256 и сравнивает с
`EXPECTED_AGENT_SHA256`; если не совпало → `true`;
- `Throwable` → `false` (не портит).
Константы:
```
SHARE_LENGTH = 32
AGENT_CORRUPT_MASK = 90 // 0x5A
EXPECTED_AGENT_SHA256 = 374ace507bd0cc2d0ce6dbbe07e50ddb3173ff819e92470271e2bf0fccff2038
```
Проверено: `sha256sum mod/evo-agent.jar` = `374ace50…` - **пиннинг на свой агент
`evo-agent.jar`**. То есть JVM должен быть запущен либо вообще без `-javaagent`,
либо именно с их агентом; чужой агент тихо ломает шейр.
Потребитель - `evo.agent.key.NativeJvmShare` (в `evo-agent.jar`):
```java
Method compute = Class.forName("java.lang.EvoShare").getMethod("compute", byte[].class);
byte[] share = (byte[]) compute.invoke(null, hwid.getBytes(UTF_8));
// ClassNotFoundException → KeyProviderException("not running on the EvoVis custom JVM")
```
То есть **payload-ключ требует их кастомного JDK** - на стоковом `java` шейр получить нельзя.
---
## 5. Сводка: три независимых уровня порчи ключа
| уровень | где | условие | порча |
|---|---|---|---|
| натив evoguard | `NativeGuard.nativeShare` | debugger / `JAVA_TOOL_OPTIONS`/`_JAVA_OPTIONS` (`jdwp`,`agentlib`) | `XOR 0xA5` |
| натив JDK | `EvoShare.computeRaw` → `FUN_180001e4c` | debugger / модули `jdwp.dll`,`dt_socket.dll`,`dt_shmem.dll` / те же env | `XOR 0xA5` |
| натив JDK | `EvoShare.computeRaw` | `JVM_EvoForeignAgentPresent() == 1`: есть `-agentlib:`/`-agentpath:` агент либо ≥2 `-javaagent:` | `XOR 0x3C` |
| Java JDK | `EvoShare.compute` | `-agentpath:`/`-agentlib:` или `-javaagent:` с чужим sha256 | `XOR 0x5A` |
Во всех случаях результат остаётся валидным `byte[32]` - ошибка не сигнализируется,
ключ просто собирается неверно (в `alm.java` дальше идёт `combine` Шамира и `alz.wipe`).
Две разные схемы HMAC (разные ключи) дают два разных шейра:
- `NativeGuard.share(hwid)` - в сборку ключа мода (`alm.java`, 4-of-4 Шамир);
- `EvoShare.compute(hwid)` - в цепочку ключа payload'а агента (`NativeJvmShare`, `EvoAgent`).
---
## 6. `evovis-audiotap` (macOS, Swift)
`mod/jar/assets/evolution/native/macos/evovis-audiotap` - **Mach-O universal (x86_64 + arm64),
174 848 байт, исполняемый файл** (`_main` + `__mh_execute_header`, не dylib).
Факты по `strings` / `r2`:
- написан на **Swift** (секции `__swift5_typeref/capture/entry/reflstr/fieldmd/types`),
класс `main.AudioTap` (`_TtC4main8AudioTap`);
- зависимости: **ScreenCaptureKit**, CoreMedia, AVFoundation, CoreAudio (swiftCoreAudio),
Foundation, IOKit - то есть захват системного звука/видеопотока через ScreenCaptureKit;
- делегат `stream(_:didOutputSampleBuffer:of:)` (`SCStream` + `CMSampleBuffer`) и
`stream(_:didStopWithError:)`; поля `output: NSFileHandle`, `mono: [Float]`;
- логи/метки `evovis.audiotap.audio`, `evovis.audiotap.screen`;
- **сетевых строк нет** (ни `http`, ни сокетов, ни доменов) - данные пишутся в
`NSFileHandle` (pipe/stdout), то есть процесс-потребитель мод запускает локально.
Вывод: это локальный helper для тапа аудио (и экрана) на macOS, результат уходит пайпом
в родительский процесс; в аудио/сеть наружу ничего не отдаётся.
Декомпиляция Ghidra для него не запускалась (предмет - Swift + macOS-фреймворки, на
уровне строк/символов картина уже ясна); запускается по необходимости:
```bash
/opt/ghidra/support/analyzeHeadless "$PWD/proj" EvoTap \
-import "$PWD/../mod/jar/assets/evolution/native/macos/evovis-audiotap" \
-postScript DumpDecomp.java "$PWD/out/audiotap.c" -scriptPath "$PWD"
```