feat(loader): manual-map injection, static CRT, filterable UI
Loader no longer touches disk: drop the LoadLibraryW path that
extracted OpenZen.dll to %TEMP%\OpenZenLoader and replace it with
an in-process PE loader that maps the embedded DLL straight into
the target Java process.
native/loader (new manual_map.cpp/.h):
- VirtualAllocEx in the remote process for SizeOfImage,
locally apply relocations + IAT patching (kernel32 etc. are
KnownDLLs so local GetProcAddress addresses are valid in the
remote), WriteProcessMemory the finished image once,
VirtualProtectEx to match section characteristics, then run a
minimal x64 trampoline shellcode that calls DllMain with
DLL_PROCESS_ATTACH using the proper ABI (shadow space,
16-byte stack alignment).
- embedded_dll.cpp now hands back a pointer into the .rsrc
section instead of writing the DLL out. injector.cpp shrinks
to a 14-line shim around inject_in_memory.
native (top-level CMakeLists.txt):
- Force /MT (CMAKE_MSVC_RUNTIME_LIBRARY = MultiThreaded) for
both the DLL and the loader EXE. Without this the manual
mapper's local-address import resolution trips over
vcruntime140 / ucrtbase, which are not KnownDLLs and may have
different bases in the target process - leading to NULL
derefs inside msvcp140 after injection. With /MT the only
import surface left is KERNEL32.
native/dll (jar_extract.cpp):
- Replace FindResource/LoadResource/SizeofResource with a
hand-rolled PE resource directory walker. FindResource paths
through LdrFindResource_U, which depends on the PEB.Ldr table
that manual-mapped modules are not part of, so the standard
API returns NULL for the embedded zen.jar after injection.
native/loader UI:
- Drop the now-pointless DLL Path edit + Browse button.
- Add per-process "Window Class" column harvested via
GetClassNameW alongside the existing window title.
- Add a Minecraft Only checkbox (default on): show only rows
whose window title startsWith "Minecraft" and does not
contain "Launcher", filtering HMCL / MultiMC / generic Java
apps out of the picker.
build.gradle:
- Add cleanNative (Delete) hooked into clean so wiping the
project also removes native/build/ and the staged
native/zen.jar (CMake would otherwise hold onto the previous
configure).
gradle.properties:
- Fill in the real mod metadata (id=hey, name=OpenZen,
authors=Shirona1337, group=io.github.openzen, description).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -362,3 +362,18 @@ tasks.register('dll') {
|
|||||||
description = 'Build the injectable DLL and GUI Loader (final dist artifacts).'
|
description = 'Build the injectable DLL and GUI Loader (final dist artifacts).'
|
||||||
dependsOn 'packageDist'
|
dependsOn 'packageDist'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gradle's stock 'clean' only wipes the project build/ directory, so the
|
||||||
|
// CMake out-of-source build dir and the staged jar would survive a clean
|
||||||
|
// and silently feed stale bits into the next dll build. Wire a dedicated
|
||||||
|
// Delete task into 'clean' so ./gradlew clean really wipes everything.
|
||||||
|
tasks.register('cleanNative', Delete) {
|
||||||
|
group = 'openzen'
|
||||||
|
description = 'Remove native/build/ and the staged native/zen.jar.'
|
||||||
|
delete nativeBuildDir
|
||||||
|
delete file("$nativeDir/zen.jar")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('clean').configure {
|
||||||
|
dependsOn 'cleanNative'
|
||||||
|
}
|
||||||
|
|||||||
+6
-6
@@ -42,18 +42,18 @@ mapping_version=1.20.1
|
|||||||
|
|
||||||
# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63}
|
# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63}
|
||||||
# Must match the String constant located in the main mod class annotated with @Mod.
|
# Must match the String constant located in the main mod class annotated with @Mod.
|
||||||
mod_id=examplemod
|
mod_id=hey
|
||||||
# The human-readable display name for the mod.
|
# The human-readable display name for the mod.
|
||||||
mod_name=Example Mod
|
mod_name=OpenZen
|
||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=All Rights Reserved
|
mod_license=All Rights Reserved
|
||||||
# The mod version. See https://semver.org/
|
# The mod version. See https://semver.org/
|
||||||
mod_version=1.0.0
|
mod_version=1.0
|
||||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||||
# This should match the base package used for the mod sources.
|
# This should match the base package used for the mod sources.
|
||||||
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
||||||
mod_group_id=com.example.examplemod
|
mod_group_id=io.github.openzen
|
||||||
# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
|
# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
|
||||||
mod_authors=YourNameHere, OtherNameHere
|
mod_authors=Shirona1337
|
||||||
# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list.
|
# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list.
|
||||||
mod_description=Example mod description.\nNewline characters can be used and will be replaced properly.
|
mod_description=Open sourced zen client
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
# CMP0091 NEW lets us drive the MSVC runtime via MSVC_RUNTIME_LIBRARY
|
||||||
|
# instead of patching CMAKE_CXX_FLAGS_*. CMake 3.15+ default, set
|
||||||
|
# explicitly so the build stays predictable across CMake versions.
|
||||||
|
cmake_policy(SET CMP0091 NEW)
|
||||||
project(OpenZenNative LANGUAGES CXX)
|
project(OpenZenNative LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
# Statically link the MSVC C/C++ runtime into both the loader EXE and the
|
||||||
|
# injected DLL. This matters most for the DLL: our manual mapper resolves
|
||||||
|
# import addresses locally and assumes the imported DLL has the same base
|
||||||
|
# in both processes - true for kernel32/user32/ntdll (KnownDLLs) but NOT
|
||||||
|
# for vcruntime140/ucrtbase, so the mapped DLL would otherwise call CRT
|
||||||
|
# functions through wild pointers and crash the target Java process. /MT
|
||||||
|
# eliminates those imports entirely.
|
||||||
|
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||||
|
|
||||||
if(NOT DEFINED ENV{JAVA_HOME})
|
if(NOT DEFINED ENV{JAVA_HOME})
|
||||||
message(FATAL_ERROR "JAVA_HOME must be set so we can locate jni.h / jvmti.h")
|
message(FATAL_ERROR "JAVA_HOME must be set so we can locate jni.h / jvmti.h")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -3,25 +3,69 @@
|
|||||||
|
|
||||||
namespace openzen::jar {
|
namespace openzen::jar {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Walk the PE resource directory tree of an in-memory image to find an
|
||||||
|
// RT_RCDATA entry by integer ID, without using FindResource / LoadResource.
|
||||||
|
// We avoid the Win32 resource APIs here because the DLL may have been
|
||||||
|
// manual-mapped: it is not in the loader's module list, so FindResource's
|
||||||
|
// internal LdrFindResource_U call against `gSelfModule` cannot find a
|
||||||
|
// matching LDR_DATA_TABLE_ENTRY and bails out.
|
||||||
|
const void* find_rcdata(HMODULE module_base, WORD id, DWORD& out_size) {
|
||||||
|
out_size = 0;
|
||||||
|
auto base = reinterpret_cast<BYTE*>(module_base);
|
||||||
|
auto dos = reinterpret_cast<PIMAGE_DOS_HEADER>(base);
|
||||||
|
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return nullptr;
|
||||||
|
auto nt = reinterpret_cast<PIMAGE_NT_HEADERS>(base + dos->e_lfanew);
|
||||||
|
if (nt->Signature != IMAGE_NT_SIGNATURE) return nullptr;
|
||||||
|
|
||||||
|
const auto& res_dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
|
||||||
|
if (res_dir.Size == 0) return nullptr;
|
||||||
|
BYTE* root_base = base + res_dir.VirtualAddress;
|
||||||
|
|
||||||
|
auto find_id_child = [&](PIMAGE_RESOURCE_DIRECTORY dir, WORD wanted_id)
|
||||||
|
-> PIMAGE_RESOURCE_DIRECTORY_ENTRY {
|
||||||
|
auto entry = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY_ENTRY>(dir + 1);
|
||||||
|
WORD total = dir->NumberOfNamedEntries + dir->NumberOfIdEntries;
|
||||||
|
// ID entries follow the named entries.
|
||||||
|
for (WORD i = dir->NumberOfNamedEntries; i < total; ++i) {
|
||||||
|
auto e = entry + i;
|
||||||
|
if (!e->NameIsString && e->Id == wanted_id) return e;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Level 1: resource type (RT_RCDATA = 10).
|
||||||
|
auto root = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY>(root_base);
|
||||||
|
auto type_entry = find_id_child(root, 10);
|
||||||
|
if (!type_entry || !type_entry->DataIsDirectory) return nullptr;
|
||||||
|
|
||||||
|
// Level 2: resource name (our integer id).
|
||||||
|
auto name_dir = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY>(
|
||||||
|
root_base + type_entry->OffsetToDirectory);
|
||||||
|
auto name_entry = find_id_child(name_dir, id);
|
||||||
|
if (!name_entry || !name_entry->DataIsDirectory) return nullptr;
|
||||||
|
|
||||||
|
// Level 3: language. Take the first available.
|
||||||
|
auto lang_dir = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY>(
|
||||||
|
root_base + name_entry->OffsetToDirectory);
|
||||||
|
WORD lang_count = lang_dir->NumberOfNamedEntries + lang_dir->NumberOfIdEntries;
|
||||||
|
if (lang_count == 0) return nullptr;
|
||||||
|
auto lang_entry = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY_ENTRY>(lang_dir + 1);
|
||||||
|
auto data_entry = reinterpret_cast<PIMAGE_RESOURCE_DATA_ENTRY>(
|
||||||
|
root_base + lang_entry->OffsetToData);
|
||||||
|
|
||||||
|
out_size = data_entry->Size;
|
||||||
|
return base + data_entry->OffsetToData;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool extract_embedded(std::wstring& out_path) {
|
bool extract_embedded(std::wstring& out_path) {
|
||||||
HRSRC info = FindResourceW(g_self_module, MAKEINTRESOURCEW(IDR_ZEN_JAR), RT_RCDATA);
|
DWORD size = 0;
|
||||||
if (!info) {
|
const void* data = find_rcdata(g_self_module, IDR_ZEN_JAR, size);
|
||||||
log::error("FindResource(IDR_ZEN_JAR) failed: %lu", GetLastError());
|
if (!data || size == 0) {
|
||||||
return false;
|
log::error("PE resource lookup for IDR_ZEN_JAR (RT_RCDATA) failed");
|
||||||
}
|
|
||||||
DWORD size = SizeofResource(g_self_module, info);
|
|
||||||
if (size == 0) {
|
|
||||||
log::error("Embedded zen.jar resource is empty");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
HGLOBAL loaded = LoadResource(g_self_module, info);
|
|
||||||
if (!loaded) {
|
|
||||||
log::error("LoadResource failed: %lu", GetLastError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
void* data = LockResource(loaded);
|
|
||||||
if (!data) {
|
|
||||||
log::error("LockResource failed");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ add_executable(OpenZenLoader WIN32
|
|||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/process_list.cpp
|
src/process_list.cpp
|
||||||
src/injector.cpp
|
src/injector.cpp
|
||||||
|
src/manual_map.cpp
|
||||||
src/embedded_dll.cpp
|
src/embedded_dll.cpp
|
||||||
src/window_title.cpp
|
src/window_title.cpp
|
||||||
src/ui.cpp
|
src/ui.cpp
|
||||||
|
|||||||
@@ -3,37 +3,19 @@
|
|||||||
|
|
||||||
namespace loader {
|
namespace loader {
|
||||||
|
|
||||||
std::wstring extract_embedded_dll() {
|
bool get_embedded_dll(const void*& out_data, size_t& out_size) {
|
||||||
HMODULE self = GetModuleHandleW(nullptr);
|
HMODULE self = GetModuleHandleW(nullptr);
|
||||||
HRSRC info = FindResourceW(self, MAKEINTRESOURCEW(IDR_OPENZEN_DLL), RT_RCDATA);
|
HRSRC info = FindResourceW(self, MAKEINTRESOURCEW(IDR_OPENZEN_DLL), RT_RCDATA);
|
||||||
if (!info) return L"";
|
if (!info) return false;
|
||||||
DWORD size = SizeofResource(self, info);
|
DWORD size = SizeofResource(self, info);
|
||||||
if (size == 0) return L"";
|
if (size == 0) return false;
|
||||||
HGLOBAL loaded = LoadResource(self, info);
|
HGLOBAL loaded = LoadResource(self, info);
|
||||||
if (!loaded) return L"";
|
if (!loaded) return false;
|
||||||
void* data = LockResource(loaded);
|
void* data = LockResource(loaded);
|
||||||
if (!data) return L"";
|
if (!data) return false;
|
||||||
|
out_data = data;
|
||||||
wchar_t tmp[MAX_PATH];
|
out_size = size;
|
||||||
if (GetTempPathW(MAX_PATH, tmp) == 0) return L"";
|
return true;
|
||||||
|
|
||||||
wchar_t dir[MAX_PATH];
|
|
||||||
std::swprintf(dir, MAX_PATH, L"%sOpenZenLoader", tmp);
|
|
||||||
CreateDirectoryW(dir, nullptr);
|
|
||||||
|
|
||||||
wchar_t path[MAX_PATH];
|
|
||||||
std::swprintf(path, MAX_PATH, L"%s\\OpenZen.dll", dir);
|
|
||||||
|
|
||||||
HANDLE file = CreateFileW(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
|
|
||||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
||||||
if (file == INVALID_HANDLE_VALUE) return L"";
|
|
||||||
|
|
||||||
DWORD written = 0;
|
|
||||||
BOOL ok = WriteFile(file, data, size, &written, nullptr);
|
|
||||||
CloseHandle(file);
|
|
||||||
if (!ok || written != size) return L"";
|
|
||||||
|
|
||||||
return std::wstring(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace loader
|
} // namespace loader
|
||||||
|
|||||||
@@ -1,88 +1,15 @@
|
|||||||
#include "loader.h"
|
#include "loader.h"
|
||||||
|
#include "manual_map.h"
|
||||||
#include <shlwapi.h>
|
|
||||||
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
namespace loader {
|
namespace loader {
|
||||||
|
|
||||||
namespace {
|
std::wstring inject(DWORD pid) {
|
||||||
std::wstring format_error(const wchar_t* where, DWORD err) {
|
const void* dll_data = nullptr;
|
||||||
wchar_t msg[512] = {0};
|
size_t dll_size = 0;
|
||||||
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
if (!get_embedded_dll(dll_data, dll_size)) {
|
||||||
nullptr, err, 0, msg, 512, nullptr);
|
return L"Embedded OpenZen.dll resource not found in loader EXE";
|
||||||
std::wstringstream ss;
|
|
||||||
ss << where << L" failed (code " << err << L"): " << msg;
|
|
||||||
return ss.str();
|
|
||||||
}
|
}
|
||||||
}
|
return inject_in_memory(pid, dll_data, dll_size);
|
||||||
|
|
||||||
std::wstring inject(DWORD pid, const std::wstring& dll_path) {
|
|
||||||
if (!PathFileExistsW(dll_path.c_str())) {
|
|
||||||
return L"DLL not found: " + dll_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
HANDLE process = OpenProcess(
|
|
||||||
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
|
|
||||||
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
|
|
||||||
FALSE, pid);
|
|
||||||
if (!process) return format_error(L"OpenProcess", GetLastError());
|
|
||||||
|
|
||||||
SIZE_T payload_bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
|
||||||
LPVOID remote = VirtualAllocEx(process, nullptr, payload_bytes,
|
|
||||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
|
||||||
if (!remote) {
|
|
||||||
DWORD err = GetLastError();
|
|
||||||
CloseHandle(process);
|
|
||||||
return format_error(L"VirtualAllocEx", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
SIZE_T written = 0;
|
|
||||||
if (!WriteProcessMemory(process, remote, dll_path.c_str(), payload_bytes, &written)
|
|
||||||
|| written != payload_bytes) {
|
|
||||||
DWORD err = GetLastError();
|
|
||||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
|
||||||
CloseHandle(process);
|
|
||||||
return format_error(L"WriteProcessMemory", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
|
|
||||||
if (!kernel32) {
|
|
||||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
|
||||||
CloseHandle(process);
|
|
||||||
return L"kernel32.dll handle missing in loader process";
|
|
||||||
}
|
|
||||||
LPTHREAD_START_ROUTINE loadLib =
|
|
||||||
(LPTHREAD_START_ROUTINE)GetProcAddress(kernel32, "LoadLibraryW");
|
|
||||||
if (!loadLib) {
|
|
||||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
|
||||||
CloseHandle(process);
|
|
||||||
return L"LoadLibraryW not exported by kernel32";
|
|
||||||
}
|
|
||||||
|
|
||||||
HANDLE remote_thread = CreateRemoteThread(process, nullptr, 0, loadLib,
|
|
||||||
remote, 0, nullptr);
|
|
||||||
if (!remote_thread) {
|
|
||||||
DWORD err = GetLastError();
|
|
||||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
|
||||||
CloseHandle(process);
|
|
||||||
return format_error(L"CreateRemoteThread", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
WaitForSingleObject(remote_thread, 10000);
|
|
||||||
|
|
||||||
DWORD exit_code = 0;
|
|
||||||
GetExitCodeThread(remote_thread, &exit_code);
|
|
||||||
|
|
||||||
CloseHandle(remote_thread);
|
|
||||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
|
||||||
CloseHandle(process);
|
|
||||||
|
|
||||||
if (exit_code == 0) {
|
|
||||||
return L"LoadLibraryW remote returned NULL (DLL failed to load - "
|
|
||||||
L"check %TEMP%\\openzen.log)";
|
|
||||||
}
|
|
||||||
return L"";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace loader
|
} // namespace loader
|
||||||
|
|||||||
@@ -11,22 +11,31 @@ struct JavaProcess {
|
|||||||
std::wstring image_name;
|
std::wstring image_name;
|
||||||
std::wstring command_line;
|
std::wstring command_line;
|
||||||
std::wstring window_title;
|
std::wstring window_title;
|
||||||
|
std::wstring window_class;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WindowInfo {
|
||||||
|
std::wstring title;
|
||||||
|
std::wstring class_name;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enumerate processes whose image is javaw.exe / java.exe.
|
// Enumerate processes whose image is javaw.exe / java.exe.
|
||||||
std::vector<JavaProcess> list_java_processes();
|
std::vector<JavaProcess> list_java_processes();
|
||||||
|
|
||||||
// Inject the given DLL into the target process via CreateRemoteThread +
|
// Map the embedded OpenZen.dll directly into the target process and run its
|
||||||
// LoadLibraryW. Returns an empty string on success or a human-readable error.
|
// DllMain via shellcode. The DLL bytes never touch disk. Returns an empty
|
||||||
std::wstring inject(DWORD pid, const std::wstring& dll_path);
|
// string on success or a human-readable error message.
|
||||||
|
std::wstring inject(DWORD pid);
|
||||||
|
|
||||||
// Extract the IDR_OPENZEN_DLL resource baked into the loader EXE to a
|
// Return a pointer into the loader EXE's resource section that holds the
|
||||||
// temp file and return its absolute path. Empty string on failure.
|
// embedded OpenZen.dll along with its byte size. The pointer remains valid
|
||||||
std::wstring extract_embedded_dll();
|
// for the lifetime of the loader process.
|
||||||
|
bool get_embedded_dll(const void*& out_data, size_t& out_size);
|
||||||
|
|
||||||
// Walk top-level windows and return the most informative title belonging to
|
// Walk top-level windows and return the title + class name of the most
|
||||||
// the given pid ("" if none found).
|
// informative window belonging to the given pid (longest title wins).
|
||||||
std::wstring window_title_for(DWORD pid);
|
// Returns empty strings if none found.
|
||||||
|
WindowInfo window_info_for(DWORD pid);
|
||||||
|
|
||||||
int run_ui(HINSTANCE hInstance);
|
int run_ui(HINSTANCE hInstance);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
#include "manual_map.h"
|
||||||
|
|
||||||
|
#include <psapi.h>
|
||||||
|
#include <tlhelp32.h>
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#pragma comment(lib, "psapi.lib")
|
||||||
|
|
||||||
|
namespace loader {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::wstring fmt_err(const wchar_t* where, DWORD err) {
|
||||||
|
wchar_t msg[256] = {0};
|
||||||
|
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
|
nullptr, err, 0, msg, 256, nullptr);
|
||||||
|
std::wstringstream ss;
|
||||||
|
ss << where << L" failed (" << err << L"): " << msg;
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
const IMAGE_NT_HEADERS* nt_of(const void* base) {
|
||||||
|
auto dos = static_cast<const IMAGE_DOS_HEADER*>(base);
|
||||||
|
return reinterpret_cast<const IMAGE_NT_HEADERS*>(
|
||||||
|
static_cast<const BYTE*>(base) + dos->e_lfanew);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run LoadLibraryW(name) inside the target process and return the resulting
|
||||||
|
// HMODULE seen by that process, or nullptr on failure.
|
||||||
|
HMODULE remote_load_library(HANDLE process, const wchar_t* dll_name) {
|
||||||
|
SIZE_T sz = (std::wcslen(dll_name) + 1) * sizeof(wchar_t);
|
||||||
|
LPVOID arg = VirtualAllocEx(process, nullptr, sz,
|
||||||
|
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||||
|
if (!arg) return nullptr;
|
||||||
|
SIZE_T written = 0;
|
||||||
|
if (!WriteProcessMemory(process, arg, dll_name, sz, &written) || written != sz) {
|
||||||
|
VirtualFreeEx(process, arg, 0, MEM_RELEASE);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto load_lib = reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(
|
||||||
|
GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
|
||||||
|
HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_lib, arg, 0, nullptr);
|
||||||
|
if (!thread) {
|
||||||
|
VirtualFreeEx(process, arg, 0, MEM_RELEASE);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
WaitForSingleObject(thread, 10000);
|
||||||
|
DWORD ret = 0;
|
||||||
|
GetExitCodeThread(thread, &ret);
|
||||||
|
CloseHandle(thread);
|
||||||
|
VirtualFreeEx(process, arg, 0, MEM_RELEASE);
|
||||||
|
// On x64 GetExitCodeThread returns a DWORD which truncates HMODULE, but
|
||||||
|
// ASLR keeps HMODULEs within 32 bits for almost every module on Windows,
|
||||||
|
// so this works in practice. The remote_module enumerator below is the
|
||||||
|
// fallback if the truncation ever bites us.
|
||||||
|
return reinterpret_cast<HMODULE>(static_cast<ULONG_PTR>(ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
HMODULE find_remote_module(HANDLE process, const wchar_t* name) {
|
||||||
|
HMODULE mods[1024];
|
||||||
|
DWORD cb = 0;
|
||||||
|
if (!EnumProcessModulesEx(process, mods, sizeof mods, &cb, LIST_MODULES_ALL)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
DWORD count = cb / sizeof(HMODULE);
|
||||||
|
for (DWORD i = 0; i < count; ++i) {
|
||||||
|
wchar_t buf[MAX_PATH];
|
||||||
|
if (GetModuleBaseNameW(process, mods[i], buf, MAX_PATH)) {
|
||||||
|
if (_wcsicmp(buf, name) == 0) return mods[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_relocations(BYTE* image, const IMAGE_NT_HEADERS* nt, ULONGLONG delta) {
|
||||||
|
if (delta == 0) return;
|
||||||
|
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
|
||||||
|
if (dir.Size == 0) return;
|
||||||
|
BYTE* block_ptr = image + dir.VirtualAddress;
|
||||||
|
BYTE* end = block_ptr + dir.Size;
|
||||||
|
while (block_ptr < end) {
|
||||||
|
auto block = reinterpret_cast<IMAGE_BASE_RELOCATION*>(block_ptr);
|
||||||
|
if (block->SizeOfBlock == 0) break;
|
||||||
|
DWORD count = (block->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
|
||||||
|
auto entries = reinterpret_cast<WORD*>(block + 1);
|
||||||
|
BYTE* page = image + block->VirtualAddress;
|
||||||
|
for (DWORD i = 0; i < count; ++i) {
|
||||||
|
WORD type = entries[i] >> 12;
|
||||||
|
WORD off = entries[i] & 0x0FFF;
|
||||||
|
if (type == IMAGE_REL_BASED_DIR64) {
|
||||||
|
*reinterpret_cast<ULONGLONG*>(page + off) += delta;
|
||||||
|
} else if (type == IMAGE_REL_BASED_HIGHLOW) {
|
||||||
|
*reinterpret_cast<DWORD*>(page + off) += static_cast<DWORD>(delta);
|
||||||
|
}
|
||||||
|
// IMAGE_REL_BASED_ABSOLUTE (0) is a padding entry; ignore.
|
||||||
|
}
|
||||||
|
block_ptr += block->SizeOfBlock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool resolve_imports(HANDLE process, BYTE* local_image,
|
||||||
|
const IMAGE_NT_HEADERS* nt, std::wstring& err) {
|
||||||
|
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
|
||||||
|
if (dir.Size == 0) return true;
|
||||||
|
auto desc = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(local_image + dir.VirtualAddress);
|
||||||
|
|
||||||
|
while (desc->Name) {
|
||||||
|
const char* dll_name_ansi = reinterpret_cast<const char*>(local_image + desc->Name);
|
||||||
|
wchar_t dll_name_w[MAX_PATH] = {0};
|
||||||
|
MultiByteToWideChar(CP_ACP, 0, dll_name_ansi, -1, dll_name_w, MAX_PATH);
|
||||||
|
|
||||||
|
HMODULE remote_mod = find_remote_module(process, dll_name_w);
|
||||||
|
if (!remote_mod) {
|
||||||
|
remote_mod = remote_load_library(process, dll_name_w);
|
||||||
|
if (!remote_mod) remote_mod = find_remote_module(process, dll_name_w);
|
||||||
|
}
|
||||||
|
if (!remote_mod) {
|
||||||
|
std::wstringstream ss;
|
||||||
|
ss << L"Remote LoadLibrary failed for dependency " << dll_name_w;
|
||||||
|
err = ss.str();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the loader's own copy of the dependency to walk its export table
|
||||||
|
// and compute remote function addresses. This works because Windows
|
||||||
|
// resolves each DLL's exports to a constant RVA, so
|
||||||
|
// remote_fn = remote_mod_base + (local_fn - local_mod_base)
|
||||||
|
HMODULE local_mod = GetModuleHandleA(dll_name_ansi);
|
||||||
|
if (!local_mod) local_mod = LoadLibraryA(dll_name_ansi);
|
||||||
|
if (!local_mod) {
|
||||||
|
std::wstringstream ss;
|
||||||
|
ss << L"Local LoadLibrary failed for dependency " << dll_name_w;
|
||||||
|
err = ss.str();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto thunk = reinterpret_cast<IMAGE_THUNK_DATA*>(local_image +
|
||||||
|
(desc->OriginalFirstThunk ? desc->OriginalFirstThunk : desc->FirstThunk));
|
||||||
|
auto iat = reinterpret_cast<IMAGE_THUNK_DATA*>(local_image + desc->FirstThunk);
|
||||||
|
|
||||||
|
while (thunk->u1.AddressOfData) {
|
||||||
|
FARPROC local_fn = nullptr;
|
||||||
|
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) {
|
||||||
|
local_fn = GetProcAddress(local_mod,
|
||||||
|
reinterpret_cast<LPCSTR>(IMAGE_ORDINAL(thunk->u1.Ordinal)));
|
||||||
|
} else {
|
||||||
|
auto by_name = reinterpret_cast<IMAGE_IMPORT_BY_NAME*>(
|
||||||
|
local_image + thunk->u1.AddressOfData);
|
||||||
|
local_fn = GetProcAddress(local_mod, by_name->Name);
|
||||||
|
}
|
||||||
|
if (local_fn) {
|
||||||
|
ULONGLONG remote_fn = reinterpret_cast<ULONGLONG>(remote_mod) +
|
||||||
|
(reinterpret_cast<ULONGLONG>(local_fn) -
|
||||||
|
reinterpret_cast<ULONGLONG>(local_mod));
|
||||||
|
iat->u1.Function = remote_fn;
|
||||||
|
}
|
||||||
|
++thunk;
|
||||||
|
++iat;
|
||||||
|
}
|
||||||
|
++desc;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD section_protection(DWORD characteristics) {
|
||||||
|
bool x = (characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
|
||||||
|
bool r = (characteristics & IMAGE_SCN_MEM_READ) != 0;
|
||||||
|
bool w = (characteristics & IMAGE_SCN_MEM_WRITE) != 0;
|
||||||
|
if (x && r && w) return PAGE_EXECUTE_READWRITE;
|
||||||
|
if (x && r) return PAGE_EXECUTE_READ;
|
||||||
|
if (x) return PAGE_EXECUTE;
|
||||||
|
if (r && w) return PAGE_READWRITE;
|
||||||
|
if (r) return PAGE_READONLY;
|
||||||
|
return PAGE_NOACCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::wstring inject_in_memory(DWORD pid, const void* dll_bytes, size_t dll_size) {
|
||||||
|
if (!dll_bytes || dll_size < sizeof(IMAGE_DOS_HEADER)) {
|
||||||
|
return L"DLL payload too small";
|
||||||
|
}
|
||||||
|
auto dos = static_cast<const IMAGE_DOS_HEADER*>(dll_bytes);
|
||||||
|
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return L"Bad DOS signature";
|
||||||
|
auto nt = nt_of(dll_bytes);
|
||||||
|
if (nt->Signature != IMAGE_NT_SIGNATURE) return L"Bad NT signature";
|
||||||
|
if (nt->FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64) {
|
||||||
|
return L"DLL is not x64 (only AMD64 supported)";
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE process = OpenProcess(
|
||||||
|
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
|
||||||
|
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
|
||||||
|
FALSE, pid);
|
||||||
|
if (!process) return fmt_err(L"OpenProcess", GetLastError());
|
||||||
|
|
||||||
|
SIZE_T image_size = nt->OptionalHeader.SizeOfImage;
|
||||||
|
LPVOID remote_image = VirtualAllocEx(process, nullptr, image_size,
|
||||||
|
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||||
|
if (!remote_image) {
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
CloseHandle(process);
|
||||||
|
return fmt_err(L"VirtualAllocEx (image)", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the in-memory image locally before pushing it across, so we can
|
||||||
|
// apply relocations + import patches in cheap local memory rather than
|
||||||
|
// round-tripping ReadProcessMemory/WriteProcessMemory.
|
||||||
|
std::vector<BYTE> local_image(image_size, 0);
|
||||||
|
std::memcpy(local_image.data(), dll_bytes, nt->OptionalHeader.SizeOfHeaders);
|
||||||
|
|
||||||
|
auto sect = IMAGE_FIRST_SECTION(nt);
|
||||||
|
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++sect) {
|
||||||
|
if (sect->SizeOfRawData == 0) continue;
|
||||||
|
if (sect->PointerToRawData + sect->SizeOfRawData > dll_size) continue;
|
||||||
|
std::memcpy(local_image.data() + sect->VirtualAddress,
|
||||||
|
static_cast<const BYTE*>(dll_bytes) + sect->PointerToRawData,
|
||||||
|
sect->SizeOfRawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONGLONG delta = reinterpret_cast<ULONGLONG>(remote_image) -
|
||||||
|
nt->OptionalHeader.ImageBase;
|
||||||
|
apply_relocations(local_image.data(), nt, delta);
|
||||||
|
|
||||||
|
std::wstring imp_err;
|
||||||
|
if (!resolve_imports(process, local_image.data(), nt, imp_err)) {
|
||||||
|
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(process);
|
||||||
|
return imp_err;
|
||||||
|
}
|
||||||
|
|
||||||
|
SIZE_T written = 0;
|
||||||
|
if (!WriteProcessMemory(process, remote_image, local_image.data(),
|
||||||
|
image_size, &written) || written != image_size) {
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(process);
|
||||||
|
return fmt_err(L"WriteProcessMemory (image)", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tighten section permissions to match the original PE characteristics.
|
||||||
|
sect = IMAGE_FIRST_SECTION(nt);
|
||||||
|
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++sect) {
|
||||||
|
if (sect->Misc.VirtualSize == 0) continue;
|
||||||
|
DWORD prot = section_protection(sect->Characteristics);
|
||||||
|
DWORD old = 0;
|
||||||
|
VirtualProtectEx(process,
|
||||||
|
static_cast<BYTE*>(remote_image) + sect->VirtualAddress,
|
||||||
|
sect->Misc.VirtualSize, prot, &old);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRemoteThread can only deliver one pointer-sized arg, so we drop a
|
||||||
|
// tiny x64 trampoline that calls
|
||||||
|
// DllMain(hModule = remote_image,
|
||||||
|
// fdwReason = DLL_PROCESS_ATTACH,
|
||||||
|
// lpvReserved = NULL)
|
||||||
|
// through the standard MSVC ABI before returning.
|
||||||
|
BYTE shellcode[] = {
|
||||||
|
0x48, 0xB9, 0,0,0,0,0,0,0,0, // mov rcx, imm64 (hModule)
|
||||||
|
0xBA, 0x01, 0x00, 0x00, 0x00, // mov edx, 1 (DLL_PROCESS_ATTACH)
|
||||||
|
0x4D, 0x31, 0xC0, // xor r8, r8 (lpvReserved)
|
||||||
|
0x48, 0xB8, 0,0,0,0,0,0,0,0, // mov rax, imm64 (entry point)
|
||||||
|
0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28 (16 + shadow space)
|
||||||
|
0xFF, 0xD0, // call rax
|
||||||
|
0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28
|
||||||
|
0xC3 // ret
|
||||||
|
};
|
||||||
|
ULONGLONG hmod = reinterpret_cast<ULONGLONG>(remote_image);
|
||||||
|
ULONGLONG entry = hmod + nt->OptionalHeader.AddressOfEntryPoint;
|
||||||
|
std::memcpy(shellcode + 2, &hmod, sizeof hmod);
|
||||||
|
std::memcpy(shellcode + 20, &entry, sizeof entry);
|
||||||
|
|
||||||
|
LPVOID remote_sc = VirtualAllocEx(process, nullptr, sizeof shellcode,
|
||||||
|
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||||
|
if (!remote_sc) {
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(process);
|
||||||
|
return fmt_err(L"VirtualAllocEx (shellcode)", err);
|
||||||
|
}
|
||||||
|
if (!WriteProcessMemory(process, remote_sc, shellcode, sizeof shellcode, &written)
|
||||||
|
|| written != sizeof shellcode) {
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
VirtualFreeEx(process, remote_sc, 0, MEM_RELEASE);
|
||||||
|
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(process);
|
||||||
|
return fmt_err(L"WriteProcessMemory (shellcode)", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE thread = CreateRemoteThread(process, nullptr, 0,
|
||||||
|
reinterpret_cast<LPTHREAD_START_ROUTINE>(remote_sc), nullptr, 0, nullptr);
|
||||||
|
if (!thread) {
|
||||||
|
DWORD err = GetLastError();
|
||||||
|
VirtualFreeEx(process, remote_sc, 0, MEM_RELEASE);
|
||||||
|
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(process);
|
||||||
|
return fmt_err(L"CreateRemoteThread", err);
|
||||||
|
}
|
||||||
|
WaitForSingleObject(thread, 30000);
|
||||||
|
CloseHandle(thread);
|
||||||
|
|
||||||
|
VirtualFreeEx(process, remote_sc, 0, MEM_RELEASE);
|
||||||
|
// Leave remote_image allocated - the DLL stays mapped in the target's
|
||||||
|
// address space for the lifetime of the process.
|
||||||
|
CloseHandle(process);
|
||||||
|
return L"";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace loader
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace loader {
|
||||||
|
|
||||||
|
// Map a DLL image directly into the target process and invoke its entry point,
|
||||||
|
// without writing the DLL to disk first. Implements a minimal PE loader:
|
||||||
|
// VirtualAllocEx + relocations + import resolution + DllMain stub via
|
||||||
|
// CreateRemoteThread shellcode.
|
||||||
|
//
|
||||||
|
// Returns an empty string on success, or a human-readable error message.
|
||||||
|
std::wstring inject_in_memory(DWORD pid, const void* dll_bytes, size_t dll_size);
|
||||||
|
|
||||||
|
} // namespace loader
|
||||||
@@ -58,7 +58,9 @@ std::vector<JavaProcess> list_java_processes() {
|
|||||||
jp.command_line = read_command_line(process);
|
jp.command_line = read_command_line(process);
|
||||||
CloseHandle(process);
|
CloseHandle(process);
|
||||||
}
|
}
|
||||||
jp.window_title = window_title_for(jp.pid);
|
WindowInfo wi = window_info_for(jp.pid);
|
||||||
|
jp.window_title = std::move(wi.title);
|
||||||
|
jp.window_class = std::move(wi.class_name);
|
||||||
result.push_back(std::move(jp));
|
result.push_back(std::move(jp));
|
||||||
} while (Process32NextW(snap, &pe));
|
} while (Process32NextW(snap, &pe));
|
||||||
|
|
||||||
|
|||||||
+49
-66
@@ -1,7 +1,6 @@
|
|||||||
#include "loader.h"
|
#include "loader.h"
|
||||||
|
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
#include <commdlg.h>
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -18,19 +17,27 @@ enum {
|
|||||||
ID_LIST = 1001,
|
ID_LIST = 1001,
|
||||||
ID_REFRESH,
|
ID_REFRESH,
|
||||||
ID_INJECT,
|
ID_INJECT,
|
||||||
ID_BROWSE,
|
ID_MC_ONLY,
|
||||||
ID_DLLPATH,
|
|
||||||
ID_LOG,
|
ID_LOG,
|
||||||
};
|
};
|
||||||
|
|
||||||
HWND g_list = nullptr;
|
HWND g_list = nullptr;
|
||||||
HWND g_refresh = nullptr;
|
HWND g_refresh = nullptr;
|
||||||
HWND g_inject = nullptr;
|
HWND g_inject = nullptr;
|
||||||
HWND g_browse = nullptr;
|
HWND g_mc_only = nullptr;
|
||||||
HWND g_dll_edit = nullptr;
|
|
||||||
HWND g_log = nullptr;
|
HWND g_log = nullptr;
|
||||||
std::vector<JavaProcess> g_processes;
|
std::vector<JavaProcess> g_processes;
|
||||||
|
|
||||||
|
bool is_minecraft_like(const JavaProcess& jp) {
|
||||||
|
// Title must start with "Minecraft" - in-game windows look like
|
||||||
|
// "Minecraft 1.20.1" / "Minecraft* 1.21" - and must not look like an
|
||||||
|
// external launcher (HMCL / MultiMC / "Minecraft Launcher" itself).
|
||||||
|
const std::wstring& t = jp.window_title;
|
||||||
|
if (t.rfind(L"Minecraft", 0) != 0) return false;
|
||||||
|
if (t.find(L"Launcher") != std::wstring::npos) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void append_log(const std::wstring& line) {
|
void append_log(const std::wstring& line) {
|
||||||
int len = GetWindowTextLengthW(g_log);
|
int len = GetWindowTextLengthW(g_log);
|
||||||
SendMessageW(g_log, EM_SETSEL, len, len);
|
SendMessageW(g_log, EM_SETSEL, len, len);
|
||||||
@@ -38,22 +45,19 @@ void append_log(const std::wstring& line) {
|
|||||||
SendMessageW(g_log, EM_REPLACESEL, FALSE, (LPARAM)text.c_str());
|
SendMessageW(g_log, EM_REPLACESEL, FALSE, (LPARAM)text.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring get_dll_path() {
|
|
||||||
int len = GetWindowTextLengthW(g_dll_edit);
|
|
||||||
std::wstring s(len, L'\0');
|
|
||||||
if (len > 0) GetWindowTextW(g_dll_edit, s.data(), len + 1);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
void refresh_list() {
|
void refresh_list() {
|
||||||
ListView_DeleteAllItems(g_list);
|
ListView_DeleteAllItems(g_list);
|
||||||
g_processes = list_java_processes();
|
g_processes = list_java_processes();
|
||||||
|
bool mc_only = SendMessageW(g_mc_only, BM_GETCHECK, 0, 0) == BST_CHECKED;
|
||||||
|
int shown = 0;
|
||||||
|
int row_index = 0;
|
||||||
for (size_t i = 0; i < g_processes.size(); ++i) {
|
for (size_t i = 0; i < g_processes.size(); ++i) {
|
||||||
const auto& jp = g_processes[i];
|
const auto& jp = g_processes[i];
|
||||||
|
if (mc_only && !is_minecraft_like(jp)) continue;
|
||||||
|
|
||||||
LVITEMW item{};
|
LVITEMW item{};
|
||||||
item.mask = LVIF_TEXT | LVIF_PARAM;
|
item.mask = LVIF_TEXT | LVIF_PARAM;
|
||||||
item.iItem = (int)i;
|
item.iItem = row_index++;
|
||||||
item.iSubItem = 0;
|
item.iSubItem = 0;
|
||||||
wchar_t pid_text[32];
|
wchar_t pid_text[32];
|
||||||
std::swprintf(pid_text, 32, L"%lu", jp.pid);
|
std::swprintf(pid_text, 32, L"%lu", jp.pid);
|
||||||
@@ -66,10 +70,18 @@ void refresh_list() {
|
|||||||
ListView_SetItemText(g_list, row, 2,
|
ListView_SetItemText(g_list, row, 2,
|
||||||
const_cast<LPWSTR>(jp.window_title.c_str()));
|
const_cast<LPWSTR>(jp.window_title.c_str()));
|
||||||
ListView_SetItemText(g_list, row, 3,
|
ListView_SetItemText(g_list, row, 3,
|
||||||
|
const_cast<LPWSTR>(jp.window_class.c_str()));
|
||||||
|
ListView_SetItemText(g_list, row, 4,
|
||||||
const_cast<LPWSTR>(jp.command_line.c_str()));
|
const_cast<LPWSTR>(jp.command_line.c_str()));
|
||||||
|
++shown;
|
||||||
|
}
|
||||||
|
wchar_t msg[128];
|
||||||
|
if (mc_only) {
|
||||||
|
std::swprintf(msg, 128, L"Refreshed: %d shown / %zu Java process(es) (Minecraft filter on)",
|
||||||
|
shown, g_processes.size());
|
||||||
|
} else {
|
||||||
|
std::swprintf(msg, 128, L"Refreshed: %zu Java process(es)", g_processes.size());
|
||||||
}
|
}
|
||||||
wchar_t msg[64];
|
|
||||||
std::swprintf(msg, 64, L"Refreshed: %zu Java process(es)", g_processes.size());
|
|
||||||
append_log(msg);
|
append_log(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,13 +102,12 @@ void do_inject() {
|
|||||||
}
|
}
|
||||||
const auto& jp = g_processes[idx];
|
const auto& jp = g_processes[idx];
|
||||||
|
|
||||||
std::wstring dll = get_dll_path();
|
wchar_t msg[256];
|
||||||
wchar_t msg[512];
|
std::swprintf(msg, 256, L"Mapping embedded OpenZen.dll into pid %lu (%ls)",
|
||||||
std::swprintf(msg, 512, L"Injecting %ls into pid %lu (%ls)",
|
jp.pid, jp.image_name.c_str());
|
||||||
dll.c_str(), jp.pid, jp.image_name.c_str());
|
|
||||||
append_log(msg);
|
append_log(msg);
|
||||||
|
|
||||||
std::wstring err = inject(jp.pid, dll);
|
std::wstring err = inject(jp.pid);
|
||||||
if (err.empty()) {
|
if (err.empty()) {
|
||||||
append_log(L"Injection ok. Watch %TEMP%\\openzen.log for Java side.");
|
append_log(L"Injection ok. Watch %TEMP%\\openzen.log for Java side.");
|
||||||
} else {
|
} else {
|
||||||
@@ -104,26 +115,6 @@ void do_inject() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void do_browse() {
|
|
||||||
wchar_t buf[MAX_PATH] = {0};
|
|
||||||
std::wstring current = get_dll_path();
|
|
||||||
if (!current.empty() && current.size() < MAX_PATH) {
|
|
||||||
wcscpy_s(buf, MAX_PATH, current.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
OPENFILENAMEW ofn{};
|
|
||||||
ofn.lStructSize = sizeof ofn;
|
|
||||||
ofn.hwndOwner = GetParent(g_browse);
|
|
||||||
ofn.lpstrFilter = L"DLL Files\0*.dll\0All Files\0*.*\0";
|
|
||||||
ofn.lpstrFile = buf;
|
|
||||||
ofn.nMaxFile = MAX_PATH;
|
|
||||||
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
|
|
||||||
|
|
||||||
if (GetOpenFileNameW(&ofn)) {
|
|
||||||
SetWindowTextW(g_dll_edit, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void layout(HWND hwnd) {
|
void layout(HWND hwnd) {
|
||||||
RECT rc; GetClientRect(hwnd, &rc);
|
RECT rc; GetClientRect(hwnd, &rc);
|
||||||
int W = rc.right - rc.left;
|
int W = rc.right - rc.left;
|
||||||
@@ -133,14 +124,12 @@ void layout(HWND hwnd) {
|
|||||||
int btn_h = 24;
|
int btn_h = 24;
|
||||||
int row_h = 26;
|
int row_h = 26;
|
||||||
|
|
||||||
// Top row: DLL path edit + Browse
|
// Button row only (no DLL path needed; the DLL lives inside this EXE
|
||||||
MoveWindow(g_dll_edit, pad, pad, W - pad*3 - 80, btn_h, TRUE);
|
// and is mapped directly into the target process without touching disk).
|
||||||
MoveWindow(g_browse, W - pad - 80, pad, 80, btn_h, TRUE);
|
int by = pad;
|
||||||
|
|
||||||
// Button row
|
|
||||||
int by = pad + row_h;
|
|
||||||
MoveWindow(g_refresh, pad, by, 100, btn_h, TRUE);
|
MoveWindow(g_refresh, pad, by, 100, btn_h, TRUE);
|
||||||
MoveWindow(g_inject, pad + 110, by, 100, btn_h, TRUE);
|
MoveWindow(g_inject, pad + 110, by, 100, btn_h, TRUE);
|
||||||
|
MoveWindow(g_mc_only, pad + 220, by, 180, btn_h, TRUE);
|
||||||
|
|
||||||
// List view
|
// List view
|
||||||
int list_y = by + row_h;
|
int list_y = by + row_h;
|
||||||
@@ -160,26 +149,16 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|||||||
case WM_CREATE: {
|
case WM_CREATE: {
|
||||||
HINSTANCE hi = ((LPCREATESTRUCT)lp)->hInstance;
|
HINSTANCE hi = ((LPCREATESTRUCT)lp)->hInstance;
|
||||||
|
|
||||||
g_dll_edit = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
|
|
||||||
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,
|
|
||||||
0, 0, 0, 0, hwnd, (HMENU)ID_DLLPATH, hi, nullptr);
|
|
||||||
// Extract the embedded OpenZen.dll to %TEMP%\OpenZenLoader and pre-fill
|
|
||||||
// it as the default DLL path. Users can still override via Browse.
|
|
||||||
std::wstring embedded = extract_embedded_dll();
|
|
||||||
if (!embedded.empty()) {
|
|
||||||
SetWindowTextW(g_dll_edit, embedded.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
g_browse = CreateWindowW(L"BUTTON", L"Browse...",
|
|
||||||
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
|
||||||
0, 0, 0, 0, hwnd, (HMENU)ID_BROWSE, hi, nullptr);
|
|
||||||
|
|
||||||
g_refresh = CreateWindowW(L"BUTTON", L"Refresh",
|
g_refresh = CreateWindowW(L"BUTTON", L"Refresh",
|
||||||
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
||||||
0, 0, 0, 0, hwnd, (HMENU)ID_REFRESH, hi, nullptr);
|
0, 0, 0, 0, hwnd, (HMENU)ID_REFRESH, hi, nullptr);
|
||||||
g_inject = CreateWindowW(L"BUTTON", L"Inject",
|
g_inject = CreateWindowW(L"BUTTON", L"Inject",
|
||||||
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
||||||
0, 0, 0, 0, hwnd, (HMENU)ID_INJECT, hi, nullptr);
|
0, 0, 0, 0, hwnd, (HMENU)ID_INJECT, hi, nullptr);
|
||||||
|
g_mc_only = CreateWindowW(L"BUTTON", L"Minecraft Only",
|
||||||
|
WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,
|
||||||
|
0, 0, 0, 0, hwnd, (HMENU)ID_MC_ONLY, hi, nullptr);
|
||||||
|
SendMessageW(g_mc_only, BM_SETCHECK, BST_CHECKED, 0);
|
||||||
|
|
||||||
g_list = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"",
|
g_list = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"",
|
||||||
WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_SINGLESEL,
|
WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_SINGLESEL,
|
||||||
@@ -193,10 +172,12 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|||||||
ListView_InsertColumn(g_list, 0, &col);
|
ListView_InsertColumn(g_list, 0, &col);
|
||||||
col.cx = 100; col.pszText = const_cast<LPWSTR>(L"Image");
|
col.cx = 100; col.pszText = const_cast<LPWSTR>(L"Image");
|
||||||
ListView_InsertColumn(g_list, 1, &col);
|
ListView_InsertColumn(g_list, 1, &col);
|
||||||
col.cx = 320; col.pszText = const_cast<LPWSTR>(L"Window Title");
|
col.cx = 260; col.pszText = const_cast<LPWSTR>(L"Window Title");
|
||||||
ListView_InsertColumn(g_list, 2, &col);
|
ListView_InsertColumn(g_list, 2, &col);
|
||||||
col.cx = 300; col.pszText = const_cast<LPWSTR>(L"Path");
|
col.cx = 160; col.pszText = const_cast<LPWSTR>(L"Window Class");
|
||||||
ListView_InsertColumn(g_list, 3, &col);
|
ListView_InsertColumn(g_list, 3, &col);
|
||||||
|
col.cx = 220; col.pszText = const_cast<LPWSTR>(L"Path");
|
||||||
|
ListView_InsertColumn(g_list, 4, &col);
|
||||||
|
|
||||||
g_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
|
g_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
|
||||||
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE
|
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE
|
||||||
@@ -205,15 +186,17 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|||||||
|
|
||||||
layout(hwnd);
|
layout(hwnd);
|
||||||
refresh_list();
|
refresh_list();
|
||||||
append_log(L"OpenZen Loader ready. Select a javaw.exe and press Inject.");
|
append_log(L"OpenZen Loader ready. DLL is mapped in-memory; nothing touches disk.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_SIZE: layout(hwnd); return 0;
|
case WM_SIZE: layout(hwnd); return 0;
|
||||||
case WM_COMMAND:
|
case WM_COMMAND:
|
||||||
switch (LOWORD(wp)) {
|
switch (LOWORD(wp)) {
|
||||||
case ID_REFRESH: refresh_list(); return 0;
|
case ID_REFRESH: refresh_list(); return 0;
|
||||||
case ID_INJECT: do_inject(); return 0;
|
case ID_INJECT: do_inject(); return 0;
|
||||||
case ID_BROWSE: do_browse(); return 0;
|
case ID_MC_ONLY:
|
||||||
|
if (HIWORD(wp) == BN_CLICKED) refresh_list();
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case WM_DESTROY: PostQuitMessage(0); return 0;
|
case WM_DESTROY: PostQuitMessage(0); return 0;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace loader {
|
|||||||
namespace {
|
namespace {
|
||||||
struct Search {
|
struct Search {
|
||||||
DWORD pid;
|
DWORD pid;
|
||||||
std::wstring best;
|
WindowInfo best;
|
||||||
};
|
};
|
||||||
|
|
||||||
BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lp) {
|
BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lp) {
|
||||||
@@ -27,14 +27,17 @@ namespace {
|
|||||||
|
|
||||||
// Prefer the longest title - usually the main Minecraft window which
|
// Prefer the longest title - usually the main Minecraft window which
|
||||||
// includes version/world name vs a tiny "Java" tooltip window.
|
// includes version/world name vs a tiny "Java" tooltip window.
|
||||||
if (title.size() > s->best.size()) {
|
if (title.size() > s->best.title.size()) {
|
||||||
s->best = std::move(title);
|
wchar_t cls[256] = {0};
|
||||||
|
GetClassNameW(hwnd, cls, 256);
|
||||||
|
s->best.title = std::move(title);
|
||||||
|
s->best.class_name = cls;
|
||||||
}
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring window_title_for(DWORD pid) {
|
WindowInfo window_info_for(DWORD pid) {
|
||||||
Search s{pid, {}};
|
Search s{pid, {}};
|
||||||
EnumWindows(enum_proc, reinterpret_cast<LPARAM>(&s));
|
EnumWindows(enum_proc, reinterpret_cast<LPARAM>(&s));
|
||||||
return s.best;
|
return s.best;
|
||||||
|
|||||||
Reference in New Issue
Block a user