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

15 KiB
Raw Permalink Blame History

Реверс нативных библиотек EvoVis (Ghidra headless)

Инструменты: Ghidra 12.1.2 (/opt/ghidra/support/analyzeHeadless), radare2, objdump, javap. Скрипты: reverse/DumpDecomp.java (дамп всех функций), reverse/FocusDecomp.java (точечная декомпиляция: точка входа + её вызываемые + все функции, пишущие в поле со смещением 0x53).

Воспроизведение:

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]

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 сборку Шамира:

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).

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 (конструктор элемента).

Что делает

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):
*(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)

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):

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-фреймворки, на уровне строк/символов картина уже ясна); запускается по необходимости:

/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"