1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Loader: maps lodestone_client.dll into the running game.
//
// The classic route — write the DLL path into the target, then run LoadLibraryW
// on it in a remote thread. kernel32 sits at the same base in every process on
// a given boot, so the address we resolve locally is the address the target
// will call.
//
// Two details that are easy to get wrong and cost real time:
//
// * the remote thread's exit code is only the low 32 bits of the returned
// HMODULE, so a module landing on a 4 GB boundary looks like a failure.
// Ask the module list instead;
// * unloading is a request, not a FreeLibrary. The client has patched a
// window procedure and cannot prove nothing still points into its code, so
// it unhooks and goes inert while staying resident. We drop a marker file
// and let it act from its own thread.
#include <windows.h>
#include <psapi.h>
#include <tlhelp32.h>
#include <cstdio>
#include <string>
#include <vector>
namespace {
std::string exe_dir() {
char buf[MAX_PATH * 2] = {};
DWORD n = GetModuleFileNameA(nullptr, buf, sizeof(buf));
std::string p(buf, n);
size_t slash = p.find_last_of("\\/");
return slash == std::string::npos ? std::string(".\\") : p.substr(0, slash + 1);
}
/// Is `needle` loaded in this process?
bool has_module(HANDLE proc, const char* needle) {
std::vector<HMODULE> mods(2048);
DWORD needed = 0;
if (!EnumProcessModulesEx(proc, mods.data(),
static_cast<DWORD>(mods.size() * sizeof(HMODULE)), &needed,
LIST_MODULES_ALL)) {
return false;
}
size_t count = (needed / sizeof(HMODULE)) < mods.size() ? needed / sizeof(HMODULE) : mods.size();
for (size_t i = 0; i < count; ++i) {
char name[MAX_PATH] = {};
if (GetModuleBaseNameA(proc, mods[i], name, sizeof(name)) && _stricmp(name, needle) == 0) {
return true;
}
}
return false;
}
/// The JVM with GLFW loaded is the game. Returns 0 when it is simply not
/// running yet, which is a state to sit in rather than an error.
DWORD find_game(bool* ambiguous) {
*ambiguous = false;
std::vector<DWORD> candidates;
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return 0;
PROCESSENTRY32W e{};
e.dwSize = sizeof(e);
for (BOOL ok = Process32FirstW(snap, &e); ok; ok = Process32NextW(snap, &e)) {
if (_wcsicmp(e.szExeFile, L"javaw.exe") == 0 || _wcsicmp(e.szExeFile, L"java.exe") == 0) {
candidates.push_back(e.th32ProcessID);
}
}
CloseHandle(snap);
std::vector<DWORD> games;
for (DWORD pid : candidates) {
HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!h) continue;
if (has_module(h, "glfw.dll")) games.push_back(pid);
CloseHandle(h);
}
if (games.size() == 1) return games[0];
if (games.size() > 1) *ambiguous = true;
return 0;
}
/// Wait until the game shows up, rewriting one line while it does.
DWORD wait_for_game() {
bool ambiguous = false;
if (DWORD pid = find_game(&ambiguous)) return pid;
if (ambiguous) return 0;
const char spin[] = {'|', '/', '-', '\\'};
for (int i = 0;; ++i) {
std::printf("\rwaiting for Minecraft... %c ", spin[i % 4]);
std::fflush(stdout);
Sleep(400);
if (DWORD pid = find_game(&ambiguous)) {
std::printf("\r \r");
std::fflush(stdout);
// Let the game settle: glfw.dll is loaded a moment before the
// window and GL context actually are.
Sleep(1500);
return pid;
}
if (ambiguous) return 0;
}
}
bool inject(DWORD pid, const std::string& dll) {
HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!proc) {
std::fprintf(stderr, "error: OpenProcess(%lu) failed (run as administrator?)\n", pid);
return false;
}
std::wstring wide;
{
int n = MultiByteToWideChar(CP_UTF8, 0, dll.c_str(), -1, nullptr, 0);
wide.resize(static_cast<size_t>(n));
MultiByteToWideChar(CP_UTF8, 0, dll.c_str(), -1, wide.data(), n);
}
SIZE_T bytes = wide.size() * sizeof(wchar_t);
bool ok = false;
void* remote = VirtualAllocEx(proc, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remote && WriteProcessMemory(proc, remote, wide.c_str(), bytes, nullptr)) {
HMODULE k32 = GetModuleHandleA("kernel32.dll");
auto load = reinterpret_cast<LPTHREAD_START_ROUTINE>(
reinterpret_cast<void*>(GetProcAddress(k32, "LoadLibraryW")));
HANDLE thread = CreateRemoteThread(proc, nullptr, 0, load, remote, 0, nullptr);
if (thread) {
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
// The exit code is truncated; the module list is the real answer.
size_t slash = dll.find_last_of("\\/");
std::string base = slash == std::string::npos ? dll : dll.substr(slash + 1);
ok = has_module(proc, base.c_str());
if (ok) {
std::printf("injected into pid %lu (%s)\n", pid, base.c_str());
} else {
std::fprintf(stderr,
"error: %s did not load into pid %lu — wrong architecture, or a "
"dependency is missing\n",
base.c_str(), pid);
}
} else {
std::fprintf(stderr, "error: CreateRemoteThread failed\n");
}
} else {
std::fprintf(stderr, "error: could not write the path into the target\n");
}
if (remote) VirtualFreeEx(proc, remote, 0, MEM_RELEASE);
CloseHandle(proc);
return ok;
}
/// Ask the client to unload: drop the marker, give it time to act, take it away.
void eject() {
std::string marker = exe_dir() + "unload";
if (FILE* f = std::fopen(marker.c_str(), "w")) {
std::fputs("1", f);
std::fclose(f);
}
Sleep(1200);
DeleteFileA(marker.c_str());
std::printf("asked the client to stop\n");
}
} // namespace
int main(int argc, char** argv) {
std::vector<std::string> args(argv + 1, argv + argc);
auto has = [&](const char* f) {
for (auto& a : args) {
if (a == f) return true;
}
return false;
};
if (has("--eject")) {
eject();
return 0;
}
// An explicit path wins, so a freshly built DLL can be loaded without
// rebuilding the loader around it.
std::string dll;
for (auto& a : args) {
if (a.rfind("--", 0) != 0) {
dll = a;
break;
}
}
if (dll.empty()) dll = exe_dir() + "lodestone_client.dll";
if (GetFileAttributesA(dll.c_str()) == INVALID_FILE_ATTRIBUTES) {
std::fprintf(stderr, "error: %s does not exist\n", dll.c_str());
return 1;
}
// LoadLibraryW in the target resolves relative paths against *its* working
// directory, which is the game's, so hand it an absolute one.
{
char full[MAX_PATH * 2] = {};
if (GetFullPathNameA(dll.c_str(), sizeof(full), full, nullptr)) dll = full;
}
bool ambiguous = false;
DWORD pid = has("--no-wait") ? find_game(&ambiguous) : wait_for_game();
if (!pid) {
std::fprintf(stderr, ambiguous ? "error: more than one Minecraft is running\n"
: "error: Minecraft is not running\n");
return 1;
}
return inject(pid, dll) ? 0 : 1;
}