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.
15 KiB
Реверс нативных библиотек 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):
FUN_180001f1c(&DAT_180010e90, 0x20, input, len, out32)- стандартный HMAC-SHA256 (внутри явные SHA-IV0x6a09e667…, копирование ключа в ipad/opad);- затем антиотладка, при срабатывании -
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"