Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
211 lines (193 loc) · 7.4 KB Raw
// 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;
}