CompanionCube32: DLL Injection, VTable Hooking, and an Overlay

CompanionCube32 is a modding tool for Portal (2007) that injects a DLL into the game process and renders a DX9 overlay via vtable hooking. It’s two components: a command-line injector that gets the DLL into the target, and the DLL itself that intercepts the render pipeline and draws with Dear ImGui.

DLL Injection

The injector uses CreateToolhelp32Snapshot to walk the process list and find the target by name. Once located, it opens the process with PROCESS_ALL_ACCESS, writes the DLL path into remote memory via VirtualAllocEx and WriteProcessMemory, then calls CreateRemoteThread with LoadLibraryA as the entry point.

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
LPVOID pRemotePath = VirtualAllocEx(hProcess, NULL, pathLen,
                                     MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, pRemotePath, dllPath, pathLen, NULL);
CreateRemoteThread(hProcess, NULL, 0,
    (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("kernel32"),
                                           "LoadLibraryA"),
    pRemotePath, 0, NULL);

The thread handle is waited on and the remote allocation is freed after the DLL loads. Error handling covers the obvious failure modes — process not found, access denied, injection timeout.

VTable Hooking With MinHook

Once the DLL lives inside Portal, it needs to intercept the DirectX 9 render pipeline. The approach is vtable patching: every D3D9 object’s first field is a pointer to its virtual function table. By swapping entries in that table we can redirect calls to our own functions.

The setup happens on DLL_PROCESS_ATTACH. A dedicated thread initialises MinHook, creates a temporary IDirect3D9 object to retrieve the vtable, and hooks IDirect3D9::CreateDevice (vtable index 16).

void *createDeviceVtable = *(void**)d3d9;
MH_CreateHook((void*)((DWORD_PTR*)createDeviceVtable)[16],
              hkCreateDevice, (void**)&oCreateDevice);
MH_EnableHook(MH_ALL_HOOKS);

When Portal calls CreateDevice, the hook saves the device pointer and chains a second hook onto IDirect3DDevice9::EndScene (vtable index 42). Every frame, the EndScene hook renders the overlay.

HRESULT WINAPI hkEndScene(IDirect3DDevice9 *device) {
    if (!initialised) {
        IMGUI_CHECKVERSION();
        ImGui::CreateContext();
        ImGui_ImplWin32_Init(FindWindow(NULL, L"Portal"));
        ImGui_ImplDX9_Init(device);
        initialised = true;
    }
    ImGui_ImplDX9_NewFrame();
    ImGui_ImplWin32_NewFrame();
    ImGui::NewFrame();
    ImGui::Begin("CC-Trainer");
    ImGui::Text("Hello from CC-Trainer!");
    ImGui::End();
    ImGui::EndFrame();
    ImGui::Render();
    ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
    return oEndScene(device);
}

MinHook handles the tricky parts — allocating executable memory near the target for the trampoline, patching the function prologue, and restoring the original bytes when the hook is removed. The library ships its own x86/x64 instruction length disassembler (HDE) to determine where to place the detour.

Build

The injector builds as a static executable, the trainer as a DLL. Both use CMake with MinGW/GCC and C++23. ImGui and MinHook sources are compiled directly into the trainer — no separate library dependencies at runtime.

Results

The injector loads the DLL in under 10ms. The hook chain resolves the vtable in microseconds. The overlay runs at the game’s native framerate with no perceptible overhead. The entire pipeline — inject, hook, render — is about 250 lines of C++ and has held up across repeated Portal sessions with no crashes or desyncs.

Have a comment on this article? Send me an email.

The Segfault Garden

Luna

frgmntedflower@linux.com