Injection
Grave ships with two injectors per platform: a friendly one for everyday use, and a raw one for users who want stealth.
Windows
On Windows, Grave is distributed as two binaries that load the same payload in different ways.
The user-friendly path. Run the executable and it reflectively loads the client into a target Minecraft process, with no extra files and no setup. This is what almost everyone should use.
For users who want to drive injection themselves. The .bin file is
position-independent - raw bytes that execute correctly at any
address. There is no PE header and no entry point in the usual sense.
You read it, allocate executable memory in a host process,
copy it in, and jump to its first byte.
The minimal example below shows the technique. It spawns a suspended copy of itself as a sacrificial host, writes the shellcode in, flips the page to RX, and starts a remote thread at the base of the allocation:
#define WIN32_LEAN_AND_MEAN #include <windows.h> #include <cstdio> #include <cstdlib> int main() { // 1. Read the position-independent shellcode. HANDLE f = CreateFileA("GraveInjector.bin", GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); DWORD size = GetFileSize(f, nullptr); auto* bin = (BYTE*)malloc(size); DWORD read = 0; ReadFile(f, bin, size, &read, nullptr); CloseHandle(f); // 2. Spawn a suspended copy of ourselves to host the payload. char self[MAX_PATH]; GetModuleFileNameA(nullptr, self, sizeof(self)); STARTUPINFOA si{ sizeof(si) }; PROCESS_INFORMATION pi{}; CreateProcessA(self, nullptr, nullptr, nullptr, FALSE, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi); // 3. Allocate, write, and flip to executable in the host. void* base = VirtualAllocEx(pi.hProcess, nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); WriteProcessMemory(pi.hProcess, base, bin, size, nullptr); DWORD old; VirtualProtectEx(pi.hProcess, base, size, PAGE_EXECUTE_READWRITE, &old); free(bin); // 4. Run the shellcode from its first byte. HANDLE th = CreateRemoteThread(pi.hProcess, nullptr, 0, (LPTHREAD_START_ROUTINE)base, nullptr, 0, nullptr); WaitForSingleObject(th, INFINITE); // 5. Cleanup. The shellcode encodes its leftover bootstrap region in the // thread exit code: 0 = clean, 1 = failure, else (size << 16) | (base & 0xFFFF). DWORD ec = 0; GetExitCodeThread(th, &ec); if (ec != 0 && ec != 1) { unsigned stub_size = ec >> 16; unsigned long long stub_base = ((unsigned long long)base & ~0xFFFFULL) | (ec & 0xFFFF); BYTE zeros[4096] = {0}; for (unsigned o = 0; o < stub_size; ) { SIZE_T chunk = (stub_size - o) > sizeof(zeros) ? sizeof(zeros) : (stub_size - o); WriteProcessMemory(pi.hProcess, (BYTE*)stub_base + o, zeros, chunk, nullptr); o += (unsigned)chunk; } VirtualFreeEx(pi.hProcess, (void*)stub_base, 0, MEM_RELEASE); } CloseHandle(th); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; }
The host process here is just somewhere for the bytes to run. It does not need to be Minecraft, and the loader does not need to know anything about Minecraft. Once the shellcode is executing, it locates the Minecraft process on its own and performs the actual injection from there. The host's only job is to give the injector a live thread.
For more advanced users: what step 5 actually does
By the time WaitForSingleObject returns, the shellcode has already torn
down almost everything it placed in the host. The injector's own working pages,
where the API resolver, decompression scratch buffers, and decoded string table
lived, have been zeroed and freed. The DLL image the injector mapped has been wiped
in place. Every cached function pointer, every decrypted constant, every transient
buffer is gone before the loader thread exits.
What the shellcode cannot clean up is the small bootstrap stub it is currently
executing from. You cannot zero the page your instruction pointer is on without
crashing the thread, so the shellcode leaves that stub intact, packs its address
and size into a single 32-bit value, and returns it as the thread exit code. The
loader (this program) reads the exit code, decodes the region, walks it writing
zeros, then releases the pages with VirtualFreeEx.
What this means for detection. Once that final
VirtualFreeEx returns, every region the injector touched — its own
working pages, the mapped DLL image, and the bootstrap stub it ran from — is
gone. A memory scanner sweeping the process at that point sees the same anonymous,
uncommitted regions it would see for any clean instance.
Once you're injected, browse modules in the sidebar to see what's available.