added files.
This commit is contained in:
commit
e908ab2355
353 files changed
+130515
No files matched your search
@@ -0,0 +1,43 @@
|
||||
#include <windows.h>
|
||||
#include "Logger.hpp"
|
||||
#include "JvmManager.hpp"
|
||||
#include "MinecraftDetector.hpp"
|
||||
#include "PayloadLoader.hpp"
|
||||
|
||||
static HMODULE g_hModule = nullptr;
|
||||
|
||||
DWORD WINAPI SlinkyNativeWorkerThread(LPVOID lpParam) {
|
||||
HMODULE hModule = (HMODULE)lpParam;
|
||||
|
||||
SlinkyNative::Logger::Initialize(hModule);
|
||||
|
||||
if (!SlinkyNative::JvmManager::Initialize()) {
|
||||
SlinkyNative::Logger::Log(L"FINAL STATUS: FAILED");
|
||||
return 1;
|
||||
}
|
||||
|
||||
SlinkyNative::MinecraftInfo mcInfo;
|
||||
if (!SlinkyNative::MinecraftDetector::Detect(mcInfo)) {
|
||||
SlinkyNative::Logger::Log(L"FINAL STATUS: FAILED");
|
||||
return 1;
|
||||
}
|
||||
|
||||
SlinkyNative::InjectionStatus status = SlinkyNative::PayloadLoader::LoadAndStart(hModule, mcInfo);
|
||||
|
||||
return (status == SlinkyNative::InjectionStatus::Success || status == SlinkyNative::InjectionStatus::AlreadyRunning) ? 0 : 1;
|
||||
}
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
|
||||
switch (fdwReason) {
|
||||
case DLL_PROCESS_ATTACH:
|
||||
g_hModule = (HMODULE)hinstDLL;
|
||||
DisableThreadLibraryCalls(hinstDLL);
|
||||
CreateThread(nullptr, 0, SlinkyNativeWorkerThread, (LPVOID)hinstDLL, 0, nullptr);
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
SlinkyNative::JvmManager::Shutdown();
|
||||
SlinkyNative::Logger::Shutdown();
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
#include "JvmManager.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
typedef jint(JNICALL* pfn_JNI_GetCreatedJavaVMs)(JavaVM** vmBuf, jsize bufLen, jsize* nVMs);
|
||||
|
||||
namespace SlinkyNative {
|
||||
|
||||
JavaVM* JvmManager::s_JavaVM = nullptr;
|
||||
JNIEnv* JvmManager::s_JniEnv = nullptr;
|
||||
jvmtiEnv* JvmManager::s_JvmtiEnv = nullptr;
|
||||
|
||||
std::mutex JvmManager::s_HookMutex;
|
||||
jclass JvmManager::s_CaptureTarget = nullptr;
|
||||
std::vector<unsigned char> JvmManager::s_CapturedBytes;
|
||||
|
||||
jclass JvmManager::s_ReplacementTarget = nullptr;
|
||||
const unsigned char* JvmManager::s_ReplacementData = nullptr;
|
||||
jint JvmManager::s_ReplacementLength = 0;
|
||||
bool JvmManager::s_ReplacementApplied = false;
|
||||
|
||||
bool JvmManager::Initialize() {
|
||||
Logger::Log(L"SlinkyNative loaded into PID %lu", GetCurrentProcessId());
|
||||
|
||||
HMODULE hJvm = nullptr;
|
||||
for (int i = 0; i < 60; ++i) {
|
||||
hJvm = GetModuleHandleW(L"jvm.dll");
|
||||
if (hJvm) break;
|
||||
Sleep(100);
|
||||
}
|
||||
|
||||
if (!hJvm) {
|
||||
Logger::Log(L"jvm.dll / JNI_GetCreatedJavaVMs not found; is this a Java process?");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto pJNI_GetCreatedJavaVMs = (pfn_JNI_GetCreatedJavaVMs)GetProcAddress(hJvm, "JNI_GetCreatedJavaVMs");
|
||||
if (!pJNI_GetCreatedJavaVMs) {
|
||||
Logger::Log(L"JNI_GetCreatedJavaVMs export not found in jvm.dll");
|
||||
return false;
|
||||
}
|
||||
|
||||
jsize nVMs = 0;
|
||||
jint res = 0;
|
||||
for (int i = 0; i < 60; ++i) {
|
||||
res = pJNI_GetCreatedJavaVMs(&s_JavaVM, 1, &nVMs);
|
||||
if (res == JNI_OK && nVMs > 0 && s_JavaVM) break;
|
||||
Sleep(100);
|
||||
}
|
||||
|
||||
if (res != JNI_OK || nVMs == 0 || !s_JavaVM) {
|
||||
Logger::Log(L"could not attach to the JVM");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Attach current thread
|
||||
res = s_JavaVM->AttachCurrentThread((void**)&s_JniEnv, nullptr);
|
||||
if (res != JNI_OK || !s_JniEnv) {
|
||||
Logger::Log(L"could not attach to the JVM");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get JVMTI 1.2
|
||||
res = s_JavaVM->GetEnv((void**)&s_JvmtiEnv, JVMTI_VERSION_1_2);
|
||||
if (res != JNI_OK || !s_JvmtiEnv) {
|
||||
Logger::Log(L"JVMTI 1.2 is unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add capabilities
|
||||
jvmtiCapabilities potentialCaps = { 0 };
|
||||
jvmtiError err = s_JvmtiEnv->GetPotentialCapabilities(&potentialCaps);
|
||||
if (err != JVMTI_ERROR_NONE) {
|
||||
Logger::Log(L"GetPotentialCapabilities failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!potentialCaps.can_retransform_classes) {
|
||||
Logger::Log(L"JVM cannot retransform classes");
|
||||
return false;
|
||||
}
|
||||
|
||||
jvmtiCapabilities neededCaps = { 0 };
|
||||
neededCaps.can_retransform_classes = 1;
|
||||
neededCaps.can_retransform_any_class = potentialCaps.can_retransform_any_class;
|
||||
neededCaps.can_get_bytecodes = potentialCaps.can_get_bytecodes;
|
||||
neededCaps.can_generate_all_class_hook_events = potentialCaps.can_generate_all_class_hook_events;
|
||||
|
||||
err = s_JvmtiEnv->AddCapabilities(&neededCaps);
|
||||
if (err != JVMTI_ERROR_NONE) {
|
||||
Logger::Log(L"AddCapabilities failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set callbacks
|
||||
jvmtiEventCallbacks callbacks = { 0 };
|
||||
callbacks.ClassFileLoadHook = &OnClassFileLoadHook;
|
||||
err = s_JvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
|
||||
if (err != JVMTI_ERROR_NONE) {
|
||||
Logger::Log(L"SetEventCallbacks failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void JvmManager::Shutdown() {
|
||||
if (s_JvmtiEnv) {
|
||||
// Disable class file load hook event
|
||||
s_JvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, nullptr);
|
||||
s_JvmtiEnv = nullptr;
|
||||
}
|
||||
|
||||
if (s_JavaVM) {
|
||||
s_JavaVM->DetachCurrentThread();
|
||||
s_JavaVM = nullptr;
|
||||
}
|
||||
|
||||
s_JniEnv = nullptr;
|
||||
}
|
||||
|
||||
JavaVM* JvmManager::GetJavaVM() { return s_JavaVM; }
|
||||
JNIEnv* JvmManager::GetEnv() { return s_JniEnv; }
|
||||
jvmtiEnv* JvmManager::GetJvmti() { return s_JvmtiEnv; }
|
||||
|
||||
void JvmManager::SetCaptureTarget(jclass targetClass) {
|
||||
std::lock_guard<std::mutex> lock(s_HookMutex);
|
||||
s_CaptureTarget = targetClass;
|
||||
s_CapturedBytes.clear();
|
||||
}
|
||||
|
||||
std::vector<unsigned char> JvmManager::GetCapturedBytes() {
|
||||
std::lock_guard<std::mutex> lock(s_HookMutex);
|
||||
return s_CapturedBytes;
|
||||
}
|
||||
|
||||
void JvmManager::ClearCapturedBytes() {
|
||||
std::lock_guard<std::mutex> lock(s_HookMutex);
|
||||
s_CaptureTarget = nullptr;
|
||||
s_CapturedBytes.clear();
|
||||
}
|
||||
|
||||
void JvmManager::SetReplacementData(jclass targetClass, const unsigned char* data, jint length) {
|
||||
std::lock_guard<std::mutex> lock(s_HookMutex);
|
||||
s_ReplacementTarget = targetClass;
|
||||
s_ReplacementData = data;
|
||||
s_ReplacementLength = length;
|
||||
s_ReplacementApplied = false;
|
||||
}
|
||||
|
||||
bool JvmManager::WasReplacementApplied() {
|
||||
std::lock_guard<std::mutex> lock(s_HookMutex);
|
||||
return s_ReplacementApplied;
|
||||
}
|
||||
|
||||
void JvmManager::ClearReplacementData() {
|
||||
std::lock_guard<std::mutex> lock(s_HookMutex);
|
||||
s_ReplacementTarget = nullptr;
|
||||
s_ReplacementData = nullptr;
|
||||
s_ReplacementLength = 0;
|
||||
s_ReplacementApplied = false;
|
||||
}
|
||||
|
||||
void JNICALL JvmManager::OnClassFileLoadHook(
|
||||
jvmtiEnv* jvmti_env,
|
||||
JNIEnv* jni_env,
|
||||
jclass class_being_redefined,
|
||||
jobject loader,
|
||||
const char* name,
|
||||
jobject protection_domain,
|
||||
jint class_data_len,
|
||||
const unsigned char* class_data,
|
||||
jint* new_class_data_len,
|
||||
unsigned char** new_class_data)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_HookMutex);
|
||||
|
||||
// If capturing bytecode for getClassBytes
|
||||
if (s_CaptureTarget && class_being_redefined && jni_env->IsSameObject(s_CaptureTarget, class_being_redefined)) {
|
||||
if (class_data && class_data_len > 0) {
|
||||
s_CapturedBytes.assign(class_data, class_data + class_data_len);
|
||||
}
|
||||
}
|
||||
|
||||
// If applying replacement bytecode for retransformClass
|
||||
if (s_ReplacementTarget && class_being_redefined && jni_env->IsSameObject(s_ReplacementTarget, class_being_redefined)) {
|
||||
if (s_ReplacementData && s_ReplacementLength > 0) {
|
||||
unsigned char* jvmMemory = nullptr;
|
||||
jvmtiError err = jvmti_env->Allocate(s_ReplacementLength, &jvmMemory);
|
||||
if (err == JVMTI_ERROR_NONE && jvmMemory) {
|
||||
memcpy(jvmMemory, s_ReplacementData, s_ReplacementLength);
|
||||
*new_class_data_len = s_ReplacementLength;
|
||||
*new_class_data = jvmMemory;
|
||||
s_ReplacementApplied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace SlinkyNative
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "Logger.hpp"
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
namespace SlinkyNative {
|
||||
|
||||
CRITICAL_SECTION Logger::s_CriticalSection;
|
||||
std::wstring Logger::s_LogFilePath;
|
||||
bool Logger::s_Initialized = false;
|
||||
|
||||
void Logger::Initialize(HMODULE hModule) {
|
||||
if (s_Initialized) return;
|
||||
InitializeCriticalSection(&s_CriticalSection);
|
||||
|
||||
wchar_t modulePath[MAX_PATH] = { 0 };
|
||||
if (GetModuleFileNameW(hModule, modulePath, MAX_PATH) > 0) {
|
||||
wchar_t* lastSlash = wcsrchr(modulePath, L'\\');
|
||||
if (lastSlash) {
|
||||
*lastSlash = L'\0';
|
||||
s_LogFilePath = std::wstring(modulePath) + L"\\slinky-native.log";
|
||||
}
|
||||
}
|
||||
|
||||
if (s_LogFilePath.empty()) {
|
||||
wchar_t tempPath[MAX_PATH] = { 0 };
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
s_LogFilePath = std::wstring(tempPath) + L"slinky-native.log";
|
||||
}
|
||||
|
||||
s_Initialized = true;
|
||||
}
|
||||
|
||||
void Logger::Shutdown() {
|
||||
if (!s_Initialized) return;
|
||||
DeleteCriticalSection(&s_CriticalSection);
|
||||
s_Initialized = false;
|
||||
}
|
||||
|
||||
const std::wstring& Logger::GetLogFilePath() {
|
||||
return s_LogFilePath;
|
||||
}
|
||||
|
||||
void Logger::Log(const wchar_t* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
LogV(format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void Logger::LogV(const wchar_t* format, va_list args) {
|
||||
if (!s_Initialized) return;
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
|
||||
wchar_t messageBuf[2048] = { 0 };
|
||||
vswprintf_s(messageBuf, format, args);
|
||||
|
||||
wchar_t lineBuf[2560] = { 0 };
|
||||
swprintf_s(lineBuf, L"[%04u-%02u-%02u %02u:%02u:%02u.%03u] %ls\n",
|
||||
st.wYear, st.wMonth, st.wDay,
|
||||
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds,
|
||||
messageBuf);
|
||||
|
||||
EnterCriticalSection(&s_CriticalSection);
|
||||
|
||||
// Also output to debug string for debugger attach
|
||||
OutputDebugStringW(lineBuf);
|
||||
|
||||
// Write to slinky-native.log
|
||||
FILE* fp = nullptr;
|
||||
if (_wfopen_s(&fp, s_LogFilePath.c_str(), L"a, ccs=UTF-8") == 0 && fp) {
|
||||
fputws(lineBuf, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
LeaveCriticalSection(&s_CriticalSection);
|
||||
}
|
||||
|
||||
void Logger::LogA(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
|
||||
char charBuf[2048] = { 0 };
|
||||
vsprintf_s(charBuf, format, args);
|
||||
va_end(args);
|
||||
|
||||
wchar_t wideBuf[2048] = { 0 };
|
||||
MultiByteToWideChar(CP_UTF8, 0, charBuf, -1, wideBuf, 2048);
|
||||
Log(L"%ls", wideBuf);
|
||||
}
|
||||
|
||||
} // namespace SlinkyNative
|
||||
@@ -0,0 +1,121 @@
|
||||
#include "MinecraftDetector.hpp"
|
||||
#include "JvmManager.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
namespace SlinkyNative {
|
||||
|
||||
bool MinecraftDetector::Detect(MinecraftInfo& outInfo) {
|
||||
JNIEnv* env = JvmManager::GetEnv();
|
||||
jvmtiEnv* jvmti = JvmManager::GetJvmti();
|
||||
|
||||
if (!env || !jvmti) return false;
|
||||
|
||||
// Wait up to 2 minutes for Minecraft 1.8.9 to finish starting
|
||||
ULONGLONG startTime = GetTickCount64();
|
||||
const ULONGLONG timeoutMs = 120000; // 2 minutes
|
||||
|
||||
while (GetTickCount64() - startTime < timeoutMs) {
|
||||
jint classCount = 0;
|
||||
jclass* classes = nullptr;
|
||||
jvmtiError err = jvmti->GetLoadedClasses(&classCount, &classes);
|
||||
|
||||
if (err == JVMTI_ERROR_NONE && classes) {
|
||||
jclass candidateMinecraft = nullptr;
|
||||
jclass candidateAve = nullptr;
|
||||
|
||||
for (jint i = 0; i < classCount; ++i) {
|
||||
char* signature = nullptr;
|
||||
char* generic = nullptr;
|
||||
if (jvmti->GetClassSignature(classes[i], &signature, &generic) == JVMTI_ERROR_NONE && signature) {
|
||||
if (strcmp(signature, "Lnet/minecraft/client/Minecraft;") == 0) {
|
||||
candidateMinecraft = classes[i];
|
||||
} else if (strcmp(signature, "Lave;") == 0) {
|
||||
candidateAve = classes[i];
|
||||
}
|
||||
jvmti->Deallocate((unsigned char*)signature);
|
||||
if (generic) jvmti->Deallocate((unsigned char*)generic);
|
||||
}
|
||||
}
|
||||
|
||||
// Check candidateMinecraft (MCP or SRG)
|
||||
if (candidateMinecraft) {
|
||||
// Check MCP: theMinecraft (Lnet/minecraft/client/Minecraft;)
|
||||
jfieldID fId = env->GetStaticFieldID(candidateMinecraft, "theMinecraft", "Lnet/minecraft/client/Minecraft;");
|
||||
if (fId) {
|
||||
outInfo.mapping = MappingType::Named;
|
||||
outInfo.mappingName = "named";
|
||||
outInfo.resourceId = 101;
|
||||
outInfo.minecraftClass = (jclass)env->NewGlobalRef(candidateMinecraft);
|
||||
|
||||
jvmti->GetClassLoader(candidateMinecraft, &outInfo.classLoader);
|
||||
if (outInfo.classLoader) {
|
||||
outInfo.classLoader = env->NewGlobalRef(outInfo.classLoader);
|
||||
}
|
||||
|
||||
jvmti->Deallocate((unsigned char*)classes);
|
||||
Logger::Log(L"detected Minecraft 1.8.9 with %hs names", outInfo.mappingName.c_str());
|
||||
return true;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
|
||||
// Check SRG: field_71432_P (Lnet/minecraft/client/Minecraft;)
|
||||
fId = env->GetStaticFieldID(candidateMinecraft, "field_71432_P", "Lnet/minecraft/client/Minecraft;");
|
||||
if (fId) {
|
||||
outInfo.mapping = MappingType::Srg;
|
||||
outInfo.mappingName = "srg";
|
||||
outInfo.resourceId = 102;
|
||||
outInfo.minecraftClass = (jclass)env->NewGlobalRef(candidateMinecraft);
|
||||
|
||||
jvmti->GetClassLoader(candidateMinecraft, &outInfo.classLoader);
|
||||
if (outInfo.classLoader) {
|
||||
outInfo.classLoader = env->NewGlobalRef(outInfo.classLoader);
|
||||
}
|
||||
|
||||
jvmti->Deallocate((unsigned char*)classes);
|
||||
Logger::Log(L"detected Minecraft 1.8.9 with %hs names", outInfo.mappingName.c_str());
|
||||
return true;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
|
||||
Logger::Log(L"net.minecraft.client.Minecraft has neither MCP nor SRG members; unsupported runtime");
|
||||
jvmti->Deallocate((unsigned char*)classes);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check candidateAve (Official / Notch)
|
||||
if (candidateAve) {
|
||||
// Check official: static field S (Lave;)
|
||||
jfieldID fId = env->GetStaticFieldID(candidateAve, "S", "Lave;");
|
||||
if (fId) {
|
||||
outInfo.mapping = MappingType::Official;
|
||||
outInfo.mappingName = "official";
|
||||
outInfo.resourceId = 103;
|
||||
outInfo.minecraftClass = (jclass)env->NewGlobalRef(candidateAve);
|
||||
|
||||
jvmti->GetClassLoader(candidateAve, &outInfo.classLoader);
|
||||
if (outInfo.classLoader) {
|
||||
outInfo.classLoader = env->NewGlobalRef(outInfo.classLoader);
|
||||
}
|
||||
|
||||
jvmti->Deallocate((unsigned char*)classes);
|
||||
Logger::Log(L"detected Minecraft 1.8.9 with %hs names", outInfo.mappingName.c_str());
|
||||
return true;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
|
||||
Logger::Log(L"class ave is not Minecraft 1.8.9; unsupported runtime");
|
||||
jvmti->Deallocate((unsigned char*)classes);
|
||||
return false;
|
||||
}
|
||||
|
||||
jvmti->Deallocate((unsigned char*)classes);
|
||||
}
|
||||
|
||||
Sleep(500);
|
||||
}
|
||||
|
||||
Logger::Log(L"Minecraft 1.8.9 did not finish starting within 2 minutes");
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace SlinkyNative
|
||||
@@ -0,0 +1,135 @@
|
||||
#include "NativeBridge.hpp"
|
||||
#include "JvmManager.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
namespace SlinkyNative {
|
||||
|
||||
bool NativeBridge::Register(JNIEnv* env, jobject classLoader) {
|
||||
jclass bridgeClass = nullptr;
|
||||
|
||||
// First try ClassLoader.loadClass("dev.slinky.inject.NativeBridge")
|
||||
if (classLoader) {
|
||||
jclass clClass = env->GetObjectClass(classLoader);
|
||||
jmethodID loadClassMethod = env->GetMethodID(clClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
if (loadClassMethod) {
|
||||
jstring className = env->NewStringUTF("dev.slinky.inject.NativeBridge");
|
||||
bridgeClass = (jclass)env->CallObjectMethod(classLoader, loadClassMethod, className);
|
||||
env->DeleteLocalRef(className);
|
||||
env->ExceptionClear();
|
||||
}
|
||||
env->DeleteLocalRef(clClass);
|
||||
}
|
||||
|
||||
if (!bridgeClass) {
|
||||
bridgeClass = env->FindClass("dev/slinky/inject/NativeBridge");
|
||||
env->ExceptionClear();
|
||||
}
|
||||
|
||||
if (!bridgeClass) {
|
||||
Logger::Log(L"could not find class dev.slinky.inject.NativeBridge");
|
||||
return false;
|
||||
}
|
||||
|
||||
JNINativeMethod methods[] = {
|
||||
{ (char*)"getClassBytes", (char*)"(Ljava/lang/Class;)[B", (void*)&GetClassBytes },
|
||||
{ (char*)"retransformClass", (char*)"(Ljava/lang/Class;[B)I", (void*)&RetransformClass },
|
||||
{ (char*)"log", (char*)"(Ljava/lang/String;)V", (void*)&Log }
|
||||
};
|
||||
|
||||
jint res = env->RegisterNatives(bridgeClass, methods, sizeof(methods) / sizeof(methods[0]));
|
||||
if (res != JNI_OK) {
|
||||
Logger::Log(L"RegisterNatives dev.slinky.inject.NativeBridge failed: %d", res);
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::Log(L"RegisterNatives dev.slinky.inject.NativeBridge succeeded");
|
||||
return true;
|
||||
}
|
||||
|
||||
jbyteArray JNICALL NativeBridge::GetClassBytes(JNIEnv* env, jclass clazz, jclass targetClass) {
|
||||
if (!targetClass) return nullptr;
|
||||
|
||||
jvmtiEnv* jvmti = JvmManager::GetJvmti();
|
||||
if (!jvmti) return nullptr;
|
||||
|
||||
JvmManager::SetCaptureTarget(targetClass);
|
||||
|
||||
// Enable ClassFileLoadHook
|
||||
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, nullptr);
|
||||
|
||||
// Trigger ClassFileLoadHook via RetransformClasses
|
||||
jvmtiError err = jvmti->RetransformClasses(1, &targetClass);
|
||||
|
||||
// Disable ClassFileLoadHook
|
||||
jvmti->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, nullptr);
|
||||
|
||||
std::vector<unsigned char> bytes = JvmManager::GetCapturedBytes();
|
||||
JvmManager::ClearCapturedBytes();
|
||||
|
||||
if (err != JVMTI_ERROR_NONE || bytes.empty()) {
|
||||
Logger::Log(L"RetransformClasses failed: jvmti=%d, target=%p", err, targetClass);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jbyteArray result = env->NewByteArray((jsize)bytes.size());
|
||||
if (result) {
|
||||
env->SetByteArrayRegion(result, 0, (jsize)bytes.size(), (const jbyte*)bytes.data());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
jint JNICALL NativeBridge::RetransformClass(JNIEnv* env, jclass clazz, jclass targetClass, jbyteArray newBytecode) {
|
||||
if (!targetClass || !newBytecode) return 0x74; // JVMTI_ERROR_NULL_POINTER (0x74 = 116)
|
||||
|
||||
jvmtiEnv* jvmti = JvmManager::GetJvmti();
|
||||
if (!jvmti) return 0x6e; // JVMTI_ERROR_UNATTACHED_THREAD
|
||||
|
||||
jsize len = env->GetArrayLength(newBytecode);
|
||||
jbyte* bytes = env->GetByteArrayElements(newBytecode, nullptr);
|
||||
if (!bytes) return 0x6e;
|
||||
|
||||
JvmManager::SetReplacementData(targetClass, (const unsigned char*)bytes, len);
|
||||
|
||||
// Enable ClassFileLoadHook
|
||||
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, nullptr);
|
||||
|
||||
// Trigger retransformation
|
||||
jvmtiError err = jvmti->RetransformClasses(1, &targetClass);
|
||||
|
||||
// Disable ClassFileLoadHook
|
||||
jvmti->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, nullptr);
|
||||
|
||||
bool applied = JvmManager::WasReplacementApplied();
|
||||
JvmManager::ClearReplacementData();
|
||||
env->ReleaseByteArrayElements(newBytecode, bytes, JNI_ABORT);
|
||||
|
||||
if (err != JVMTI_ERROR_NONE) {
|
||||
Logger::Log(L"RetransformClasses(apply) failed: jvmti=%d", err);
|
||||
return (jint)err;
|
||||
}
|
||||
|
||||
if (!applied) {
|
||||
Logger::Log(L"RetransformClasses(apply): load hook never matched the target; class unchanged");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void JNICALL NativeBridge::Log(JNIEnv* env, jclass clazz, jstring message) {
|
||||
if (!message) return;
|
||||
|
||||
const char* utfChars = env->GetStringUTFChars(message, nullptr);
|
||||
if (utfChars) {
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, utfChars, -1, nullptr, 0);
|
||||
if (wlen > 0) {
|
||||
std::vector<wchar_t> wbuf(wlen);
|
||||
MultiByteToWideChar(CP_UTF8, 0, utfChars, -1, wbuf.data(), wlen);
|
||||
Logger::Log(L"[java] %ls", wbuf.data());
|
||||
}
|
||||
env->ReleaseStringUTFChars(message, utfChars);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace SlinkyNative
|
||||
@@ -0,0 +1,485 @@
|
||||
#include "PayloadLoader.hpp"
|
||||
#include "JvmManager.hpp"
|
||||
#include "NativeBridge.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace SlinkyNative {
|
||||
|
||||
InjectionStatus PayloadLoader::LoadAndStart(HMODULE hModule, const MinecraftInfo& mcInfo) {
|
||||
JNIEnv* env = JvmManager::GetEnv();
|
||||
if (!env) {
|
||||
Logger::Log(L"FINAL STATUS: FAILED");
|
||||
return InjectionStatus::Failed;
|
||||
}
|
||||
|
||||
bool alreadyRunning = false;
|
||||
if (CheckPreviousState(env, mcInfo.classLoader, alreadyRunning)) {
|
||||
if (alreadyRunning) {
|
||||
Logger::Log(L"Slinky is already active; no classes or native methods were replaced");
|
||||
Logger::Log(L"FINAL STATUS: ALREADY_RUNNING");
|
||||
return InjectionStatus::AlreadyRunning;
|
||||
} else {
|
||||
Logger::Log(L"atomic payload replacement (class schema must be compatible)");
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring jarPath;
|
||||
if (!ExtractPayloadJar(hModule, mcInfo, jarPath)) {
|
||||
Logger::Log(L"FINAL STATUS: FAILED");
|
||||
return InjectionStatus::Failed;
|
||||
}
|
||||
|
||||
// Add JAR to ClassLoader
|
||||
if (!AddJarToClassLoader(env, mcInfo.classLoader, jarPath)) {
|
||||
DeleteFileW(jarPath.c_str());
|
||||
Logger::Log(L"FINAL STATUS: FAILED");
|
||||
return InjectionStatus::Failed;
|
||||
}
|
||||
|
||||
// Read and define dev.slinky classes
|
||||
DefinePayloadClasses(env, mcInfo.classLoader, jarPath);
|
||||
|
||||
// Delete temporary jar immediately after loading
|
||||
DeleteFileW(jarPath.c_str());
|
||||
|
||||
// Register native bridge methods
|
||||
if (!NativeBridge::Register(env, mcInfo.classLoader)) {
|
||||
Logger::Log(L"FINAL STATUS: FAILED");
|
||||
return InjectionStatus::Failed;
|
||||
}
|
||||
|
||||
// Launch Bootstrap
|
||||
if (!LaunchBootstrap(env, mcInfo.classLoader, mcInfo, hModule)) {
|
||||
Logger::Log(L"bootstrap failed; unloading");
|
||||
Logger::Log(L"FINAL STATUS: FAILED");
|
||||
return InjectionStatus::Failed;
|
||||
}
|
||||
|
||||
// Set reload markers / system properties
|
||||
jclass systemClass = env->FindClass("java/lang/System");
|
||||
if (systemClass) {
|
||||
jmethodID setPropertyMethod = env->GetStaticMethodID(systemClass, "setProperty", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
|
||||
if (setPropertyMethod) {
|
||||
jstring key1 = env->NewStringUTF("slinky.injected");
|
||||
jstring val1 = env->NewStringUTF("true");
|
||||
env->CallStaticObjectMethod(systemClass, setPropertyMethod, key1, val1);
|
||||
env->DeleteLocalRef(key1);
|
||||
env->DeleteLocalRef(val1);
|
||||
|
||||
jstring key2 = env->NewStringUTF("slinky.patches.installed");
|
||||
jstring val2 = env->NewStringUTF("true");
|
||||
env->CallStaticObjectMethod(systemClass, setPropertyMethod, key2, val2);
|
||||
env->DeleteLocalRef(key2);
|
||||
env->DeleteLocalRef(val2);
|
||||
}
|
||||
env->DeleteLocalRef(systemClass);
|
||||
}
|
||||
env->ExceptionClear();
|
||||
|
||||
Logger::Log(L"Slinky is running");
|
||||
Logger::Log(L"FINAL STATUS: RUNNING");
|
||||
return InjectionStatus::Success;
|
||||
}
|
||||
|
||||
bool PayloadLoader::CheckPreviousState(JNIEnv* env, jobject classLoader, bool& outAlreadyRunning) {
|
||||
outAlreadyRunning = false;
|
||||
|
||||
// Check if dev.slinky.inject.Bootstrap was already loaded
|
||||
jclass bootstrapClass = nullptr;
|
||||
if (classLoader) {
|
||||
jclass clClass = env->GetObjectClass(classLoader);
|
||||
jmethodID findLoadedClassMethod = env->GetMethodID(clClass, "findLoadedClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
if (findLoadedClassMethod) {
|
||||
jstring className = env->NewStringUTF("dev.slinky.inject.Bootstrap");
|
||||
bootstrapClass = (jclass)env->CallObjectMethod(classLoader, findLoadedClassMethod, className);
|
||||
env->DeleteLocalRef(className);
|
||||
env->ExceptionClear();
|
||||
}
|
||||
env->DeleteLocalRef(clClass);
|
||||
}
|
||||
|
||||
if (!bootstrapClass) {
|
||||
bootstrapClass = env->FindClass("dev/slinky/inject/Bootstrap");
|
||||
env->ExceptionClear();
|
||||
}
|
||||
|
||||
if (!bootstrapClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::Log(L"inspect previous Bootstrap");
|
||||
|
||||
// Check dev/slinky/client/hook/GameHooks
|
||||
jclass hooksClass = env->FindClass("dev/slinky/client/hook/GameHooks");
|
||||
env->ExceptionClear();
|
||||
if (hooksClass) {
|
||||
Logger::Log(L"inspect previous GameHooks");
|
||||
jfieldID modulesField = env->GetStaticFieldID(hooksClass, "modules", "Ldev/slinky/module/ModuleManager;");
|
||||
env->ExceptionClear();
|
||||
if (modulesField) {
|
||||
Logger::Log(L"inspect previous module manager");
|
||||
jobject moduleManager = env->GetStaticObjectField(hooksClass, modulesField);
|
||||
if (moduleManager) {
|
||||
jclass mmClass = env->GetObjectClass(moduleManager);
|
||||
jfieldID shutdownField = env->GetFieldID(mmClass, "shutdown", "Ljava/util/concurrent/atomic/AtomicBoolean;");
|
||||
env->ExceptionClear();
|
||||
if (shutdownField) {
|
||||
Logger::Log(L"read previous shutdown state");
|
||||
jobject shutdownObj = env->GetObjectField(moduleManager, shutdownField);
|
||||
if (shutdownObj) {
|
||||
jclass abClass = env->GetObjectClass(shutdownObj);
|
||||
jmethodID getMethod = env->GetMethodID(abClass, "get", "()Z");
|
||||
if (getMethod) {
|
||||
jboolean isShutdown = env->CallBooleanMethod(shutdownObj, getMethod);
|
||||
if (!isShutdown) {
|
||||
outAlreadyRunning = true;
|
||||
}
|
||||
}
|
||||
env->DeleteLocalRef(abClass);
|
||||
env->DeleteLocalRef(shutdownObj);
|
||||
}
|
||||
}
|
||||
env->DeleteLocalRef(mmClass);
|
||||
env->DeleteLocalRef(moduleManager);
|
||||
}
|
||||
}
|
||||
env->DeleteLocalRef(hooksClass);
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(bootstrapClass);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PayloadLoader::ExtractPayloadJar(HMODULE hModule, const MinecraftInfo& mcInfo, std::wstring& outJarPath) {
|
||||
wchar_t dllDir[MAX_PATH] = { 0 };
|
||||
if (GetModuleFileNameW(hModule, dllDir, MAX_PATH) > 0) {
|
||||
wchar_t* lastSlash = wcsrchr(dllDir, L'\\');
|
||||
if (lastSlash) *lastSlash = L'\0';
|
||||
}
|
||||
|
||||
// Format temp jar path: slinky-%hs-%lu-%llu.jar
|
||||
wchar_t jarFileName[256] = { 0 };
|
||||
swprintf_s(jarFileName, L"slinky-%hs-%lu-%llu.jar",
|
||||
mcInfo.mappingName.c_str(),
|
||||
GetCurrentProcessId(),
|
||||
GetTickCount64());
|
||||
|
||||
outJarPath = std::wstring(dllDir) + L"\\" + jarFileName;
|
||||
|
||||
// First attempt: embedded resource
|
||||
HRSRC hRes = FindResourceW(hModule, MAKEINTRESOURCEW(mcInfo.resourceId), (LPCWSTR)10); // RT_RCDATA = 10
|
||||
if (hRes) {
|
||||
DWORD resSize = SizeofResource(hModule, hRes);
|
||||
HGLOBAL hLoad = LoadResource(hModule, hRes);
|
||||
if (hLoad && resSize >= 4) {
|
||||
const unsigned char* pData = (const unsigned char*)LockResource(hLoad);
|
||||
if (pData && pData[0] == 0x50 && pData[1] == 0x4B) { // PK magic
|
||||
HANDLE hFile = CreateFileW(outJarPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile != INVALID_HANDLE_VALUE) {
|
||||
DWORD written = 0;
|
||||
WriteFile(hFile, pData, resSize, &written, nullptr);
|
||||
CloseHandle(hFile);
|
||||
|
||||
Logger::Log(L"payload %hs written to %ls (%lu bytes)", mcInfo.mappingName.c_str(), outJarPath.c_str(), written);
|
||||
return true;
|
||||
} else {
|
||||
Logger::Log(L"cannot write %ls: %lu", outJarPath.c_str(), GetLastError());
|
||||
}
|
||||
} else {
|
||||
Logger::Log(L"embedded payload %d is invalid", mcInfo.resourceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger::Log(L"embedded payload %d is missing", mcInfo.resourceId);
|
||||
|
||||
// Fallback: check for standalone JAR next to the DLL (slinky-named.jar, slinky-srg.jar, slinky-official.jar, or slinky.jar)
|
||||
std::wstring candidates[] = {
|
||||
std::wstring(dllDir) + L"\\slinky-" + std::wstring(mcInfo.mappingName.begin(), mcInfo.mappingName.end()) + L".jar",
|
||||
std::wstring(dllDir) + L"\\slinky.jar",
|
||||
std::wstring(dllDir) + L"\\Slinky-0.5.0-settings.jar"
|
||||
};
|
||||
|
||||
for (const auto& candidate : candidates) {
|
||||
if (GetFileAttributesW(candidate.c_str()) != INVALID_FILE_ATTRIBUTES) {
|
||||
// Copy candidate to outJarPath
|
||||
if (CopyFileW(candidate.c_str(), outJarPath.c_str(), FALSE)) {
|
||||
Logger::Log(L"payload %hs loaded from disk %ls", mcInfo.mappingName.c_str(), candidate.c_str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger::Log(L"payload contains no dev.slinky classes");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PayloadLoader::AddJarToClassLoader(JNIEnv* env, jobject classLoader, const std::wstring& jarPath) {
|
||||
if (!classLoader) {
|
||||
Logger::Log(L"game class loader unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
jclass fileClass = env->FindClass("java/io/File");
|
||||
jclass uriClass = env->FindClass("java/net/URI");
|
||||
if (!fileClass || !uriClass) return false;
|
||||
|
||||
jmethodID fileInit = env->GetMethodID(fileClass, "<init>", "(Ljava/lang/String;)V");
|
||||
jmethodID fileToURI = env->GetMethodID(fileClass, "toURI", "()Ljava/net/URI;");
|
||||
jmethodID uriToURL = env->GetMethodID(uriClass, "toURL", "()Ljava/net/URL;");
|
||||
|
||||
int u8Len = WideCharToMultiByte(CP_UTF8, 0, jarPath.c_str(), -1, nullptr, 0, nullptr, nullptr);
|
||||
std::vector<char> u8Path(u8Len);
|
||||
WideCharToMultiByte(CP_UTF8, 0, jarPath.c_str(), -1, u8Path.data(), u8Len, nullptr, nullptr);
|
||||
|
||||
jstring jPath = env->NewStringUTF(u8Path.data());
|
||||
jobject fileObj = env->NewObject(fileClass, fileInit, jPath);
|
||||
env->DeleteLocalRef(jPath);
|
||||
|
||||
if (!fileObj) return false;
|
||||
|
||||
jobject uriObj = env->CallObjectMethod(fileObj, fileToURI);
|
||||
jobject urlObj = env->CallObjectMethod(uriObj, uriToURL);
|
||||
|
||||
env->DeleteLocalRef(fileObj);
|
||||
env->DeleteLocalRef(uriObj);
|
||||
env->DeleteLocalRef(fileClass);
|
||||
env->DeleteLocalRef(uriClass);
|
||||
|
||||
if (!urlObj) return false;
|
||||
|
||||
// Try URLClassLoader.addURL(URL) on game classloader or system classloader
|
||||
jclass uclClass = env->FindClass("java/net/URLClassLoader");
|
||||
if (uclClass) {
|
||||
jmethodID addURLMethod = env->GetMethodID(uclClass, "addURL", "(Ljava/net/URL;)V");
|
||||
if (addURLMethod) {
|
||||
env->CallVoidMethod(classLoader, addURLMethod, urlObj);
|
||||
if (!env->ExceptionCheck()) {
|
||||
Logger::Log(L"payload added to the game class loader (addURL)");
|
||||
env->DeleteLocalRef(urlObj);
|
||||
env->DeleteLocalRef(uclClass);
|
||||
return true;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
}
|
||||
env->DeleteLocalRef(uclClass);
|
||||
}
|
||||
|
||||
// Fallback: add to SystemClassLoader
|
||||
jclass sysClass = env->FindClass("java/lang/ClassLoader");
|
||||
if (sysClass) {
|
||||
jmethodID getSysClMethod = env->GetStaticMethodID(sysClass, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
|
||||
if (getSysClMethod) {
|
||||
jobject sysCl = env->CallStaticObjectMethod(sysClass, getSysClMethod);
|
||||
if (sysCl) {
|
||||
jclass sysUcl = env->FindClass("java/net/URLClassLoader");
|
||||
if (sysUcl) {
|
||||
jmethodID addURL = env->GetMethodID(sysUcl, "addURL", "(Ljava/net/URL;)V");
|
||||
if (addURL) {
|
||||
env->CallVoidMethod(sysCl, addURL, urlObj);
|
||||
if (!env->ExceptionCheck()) {
|
||||
Logger::Log(L"payload appended to the system class loader search");
|
||||
env->DeleteLocalRef(urlObj);
|
||||
env->DeleteLocalRef(sysCl);
|
||||
env->DeleteLocalRef(sysUcl);
|
||||
env->DeleteLocalRef(sysClass);
|
||||
return true;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
}
|
||||
env->DeleteLocalRef(sysUcl);
|
||||
}
|
||||
env->DeleteLocalRef(sysCl);
|
||||
}
|
||||
}
|
||||
env->DeleteLocalRef(sysClass);
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(urlObj);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PayloadLoader::DefinePayloadClasses(JNIEnv* env, jobject classLoader, const std::wstring& jarPath) {
|
||||
jclass jarFileClass = env->FindClass("java/util/jar/JarFile");
|
||||
jclass zipEntryClass = env->FindClass("java/util/zip/ZipEntry");
|
||||
jclass enumerationClass = env->FindClass("java/util/Enumeration");
|
||||
jclass isClass = env->FindClass("java/io/InputStream");
|
||||
jclass baosClass = env->FindClass("java/io/ByteArrayOutputStream");
|
||||
|
||||
if (!jarFileClass || !zipEntryClass || !enumerationClass || !isClass || !baosClass) {
|
||||
Logger::Log(L"resolve JAR reader failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
jmethodID jarInit = env->GetMethodID(jarFileClass, "<init>", "(Ljava/lang/String;)V");
|
||||
jmethodID jarEntries = env->GetMethodID(jarFileClass, "entries", "()Ljava/util/Enumeration;");
|
||||
jmethodID jarGetInputStream = env->GetMethodID(jarFileClass, "getInputStream", "(Ljava/util/zip/ZipEntry;)Ljava/io/InputStream;");
|
||||
jmethodID jarClose = env->GetMethodID(jarFileClass, "close", "()V");
|
||||
|
||||
jmethodID entryGetName = env->GetMethodID(zipEntryClass, "getName", "()Ljava/lang/String;");
|
||||
jmethodID enumHasMore = env->GetMethodID(enumerationClass, "hasMoreElements", "()Z");
|
||||
jmethodID enumNext = env->GetMethodID(enumerationClass, "nextElement", "()Ljava/lang/Object;");
|
||||
|
||||
jmethodID isRead = env->GetMethodID(isClass, "read", "([B)I");
|
||||
jmethodID baosInit = env->GetMethodID(baosClass, "<init>", "()V");
|
||||
jmethodID baosWrite = env->GetMethodID(baosClass, "write", "([BII)V");
|
||||
jmethodID baosToByteArray = env->GetMethodID(baosClass, "toByteArray", "()[B");
|
||||
|
||||
int u8Len = WideCharToMultiByte(CP_UTF8, 0, jarPath.c_str(), -1, nullptr, 0, nullptr, nullptr);
|
||||
std::vector<char> u8Path(u8Len);
|
||||
WideCharToMultiByte(CP_UTF8, 0, jarPath.c_str(), -1, u8Path.data(), u8Len, nullptr, nullptr);
|
||||
|
||||
jstring jPath = env->NewStringUTF(u8Path.data());
|
||||
jobject jarObj = env->NewObject(jarFileClass, jarInit, jPath);
|
||||
env->DeleteLocalRef(jPath);
|
||||
|
||||
if (!jarObj || env->ExceptionCheck()) {
|
||||
env->ExceptionClear();
|
||||
Logger::Log(L"open payload jar failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
jobject entries = env->CallObjectMethod(jarObj, jarEntries);
|
||||
size_t definedCount = 0;
|
||||
size_t unresolvedCount = 0;
|
||||
|
||||
jbyteArray buffer = env->NewByteArray(4096);
|
||||
|
||||
while (entries && env->CallBooleanMethod(entries, enumHasMore)) {
|
||||
jobject entry = env->CallObjectMethod(entries, enumNext);
|
||||
if (!entry) continue;
|
||||
|
||||
jstring nameStr = (jstring)env->CallObjectMethod(entry, entryGetName);
|
||||
if (nameStr) {
|
||||
const char* nameChars = env->GetStringUTFChars(nameStr, nullptr);
|
||||
if (nameChars) {
|
||||
std::string entryName = nameChars;
|
||||
env->ReleaseStringUTFChars(nameStr, nameChars);
|
||||
|
||||
if (entryName.rfind("dev/slinky/", 0) == 0 && entryName.size() > 6 && entryName.substr(entryName.size() - 6) == ".class") {
|
||||
// Convert "dev/slinky/Foo.class" -> "dev.slinky.Foo"
|
||||
std::string className = entryName.substr(0, entryName.size() - 6);
|
||||
for (char& c : className) {
|
||||
if (c == '/') c = '.';
|
||||
}
|
||||
|
||||
// Read bytes
|
||||
jobject inputStream = env->CallObjectMethod(jarObj, jarGetInputStream, entry);
|
||||
if (inputStream) {
|
||||
jobject baos = env->NewObject(baosClass, baosInit);
|
||||
jint bytesRead = 0;
|
||||
while ((bytesRead = env->CallIntMethod(inputStream, isRead, buffer)) > 0) {
|
||||
env->CallVoidMethod(baos, baosWrite, buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
jbyteArray classBytesArr = (jbyteArray)env->CallObjectMethod(baos, baosToByteArray);
|
||||
if (classBytesArr) {
|
||||
jsize byteLen = env->GetArrayLength(classBytesArr);
|
||||
jbyte* rawBytes = env->GetByteArrayElements(classBytesArr, nullptr);
|
||||
if (rawBytes) {
|
||||
jclass definedClass = env->DefineClass(className.c_str(), classLoader, rawBytes, byteLen);
|
||||
if (definedClass) {
|
||||
definedCount++;
|
||||
env->DeleteLocalRef(definedClass);
|
||||
} else {
|
||||
unresolvedCount++;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
env->ReleaseByteArrayElements(classBytesArr, rawBytes, JNI_ABORT);
|
||||
}
|
||||
env->DeleteLocalRef(classBytesArr);
|
||||
}
|
||||
env->DeleteLocalRef(baos);
|
||||
env->DeleteLocalRef(inputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
env->DeleteLocalRef(nameStr);
|
||||
}
|
||||
env->DeleteLocalRef(entry);
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(buffer);
|
||||
env->CallVoidMethod(jarObj, jarClose);
|
||||
env->DeleteLocalRef(jarObj);
|
||||
|
||||
Logger::Log(L"defined %zu new payload classes in the game class loader; %zu unresolved", definedCount, unresolvedCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PayloadLoader::LaunchBootstrap(JNIEnv* env, jobject classLoader, const MinecraftInfo& mcInfo, HMODULE hModule) {
|
||||
jclass bootstrapClass = nullptr;
|
||||
|
||||
if (classLoader) {
|
||||
jclass clClass = env->GetObjectClass(classLoader);
|
||||
jmethodID loadClassMethod = env->GetMethodID(clClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
if (loadClassMethod) {
|
||||
jstring className = env->NewStringUTF("dev.slinky.inject.Bootstrap");
|
||||
bootstrapClass = (jclass)env->CallObjectMethod(classLoader, loadClassMethod, className);
|
||||
env->DeleteLocalRef(className);
|
||||
env->ExceptionClear();
|
||||
}
|
||||
env->DeleteLocalRef(clClass);
|
||||
}
|
||||
|
||||
if (!bootstrapClass) {
|
||||
bootstrapClass = env->FindClass("dev/slinky/inject/Bootstrap");
|
||||
env->ExceptionClear();
|
||||
}
|
||||
|
||||
if (!bootstrapClass) {
|
||||
Logger::Log(L"resolve Bootstrap.start failed: class not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
jmethodID startMethod = env->GetStaticMethodID(bootstrapClass, "start", "(Ljava/lang/String;Ljava/lang/String;)V");
|
||||
if (!startMethod) {
|
||||
Logger::Log(L"resolve Bootstrap.start failed: method not found");
|
||||
env->DeleteLocalRef(bootstrapClass);
|
||||
return false;
|
||||
}
|
||||
|
||||
wchar_t dllPath[MAX_PATH] = { 0 };
|
||||
GetModuleFileNameW(hModule, dllPath, MAX_PATH);
|
||||
int u8Len = WideCharToMultiByte(CP_UTF8, 0, dllPath, -1, nullptr, 0, nullptr, nullptr);
|
||||
std::vector<char> u8DllPath(u8Len);
|
||||
WideCharToMultiByte(CP_UTF8, 0, dllPath, -1, u8DllPath.data(), u8Len, nullptr, nullptr);
|
||||
|
||||
jstring jMapping = env->NewStringUTF(mcInfo.mappingName.c_str());
|
||||
jstring jDllPath = env->NewStringUTF(u8DllPath.data());
|
||||
|
||||
Logger::Log(L"Bootstrap.start calling with mapping=%hs", mcInfo.mappingName.c_str());
|
||||
env->CallStaticVoidMethod(bootstrapClass, startMethod, jMapping, jDllPath);
|
||||
|
||||
env->DeleteLocalRef(jMapping);
|
||||
env->DeleteLocalRef(jDllPath);
|
||||
env->DeleteLocalRef(bootstrapClass);
|
||||
|
||||
if (env->ExceptionCheck()) {
|
||||
jthrowable ex = env->ExceptionOccurred();
|
||||
env->ExceptionClear();
|
||||
if (ex) {
|
||||
jclass exClass = env->GetObjectClass(ex);
|
||||
jmethodID toString = env->GetMethodID(exClass, "toString", "()Ljava/lang/String;");
|
||||
if (toString) {
|
||||
jstring msg = (jstring)env->CallObjectMethod(ex, toString);
|
||||
if (msg) {
|
||||
const char* chars = env->GetStringUTFChars(msg, nullptr);
|
||||
if (chars) {
|
||||
Logger::Log(L"Bootstrap.start failed: %hs", chars);
|
||||
env->ReleaseStringUTFChars(msg, chars);
|
||||
}
|
||||
env->DeleteLocalRef(msg);
|
||||
}
|
||||
}
|
||||
env->DeleteLocalRef(exClass);
|
||||
env->DeleteLocalRef(ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace SlinkyNative
|
||||
Reference in new issue
Block a user