Skip to lesson
Game Hacking Academy
Preparing lesson text…
Exit reader mode
Voice and speed

Preparing read-aloud controls…

Lesson 8.2 Listening edition

D L L, dynamic link library, Loading Across Process Boundaries

Follow the module path, process handle, remote memory, and loader call involved in loading a DLL into a controlled game process.

About 20 minutes

Section: Keep target identity explicit.

Lesson 8.1 built a D L L with a tiny DllMain and an explicit gha_start, but that file is still on disk. To run it inside the course game, the injector must ask the target process to load the exact D L L, wait for loading to finish, and only then call its start function. Every temporary handle and allocation needs an owner along the way.

This injector accepts only the exact target builds used in the book. Put the allowlist in code so a mistyped command cannot select an unrelated process.

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

This chapter covers the classic Windows loader sequence. Protected-process work and anti-cheat bypasses are separate subjects and are outside this tool’s contract.

It is worth knowing that this is one route among several, so you do not mistake it for the only one. The approaches differ along three axes: who allocates the memory in the target, what causes the target to start executing it, and — the important one — whether the Windows loader is involved at all.

This route uses the loader. You hand the target a path and ask it to run LoadLibraryW, and the loader then maps the sections, applies relocations, resolves imports, prepares thread-local storage, runs DllMain, and adds the module to the process’s module list.

That last step is easy to overlook and is precisely why this book takes this route. Because the loader registers the module, your D L L appears in the inventory that Lesson 12.1 builds, GetModuleHandle can find it, and unloading runs through the same bookkeeping in reverse. Every step is observable and reversible.

Routes that avoid the loader have to do the loader’s job themselves — walking the relocation table, resolving each import by hand, and handling TLS — which is where the P E, portable executable, structure from Lesson 5.1 stops being background reading. They exist mainly so that a module does not appear in that list, which is a goal about avoiding detection rather than about understanding the program, and it is not what this book is for.

Section: The classic loader sequence.

Lesson diagram

Every box can fail and must clean up what earlier boxes created.

The copied path is only needed while the DLL loads.

Section: Use a wide absolute path.

Windows LoadLibraryW expects a zero-terminated UTF-16 path:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

A relative path depends on the target’s working directory and is easy to misresolve.

Narration: The injector and game may start in different folders. An apparently correct relative filename can therefore select the wrong file or no file.

Section: Own remote memory.

Model the allocation so cleanup is not optional:

Lesson diagram

RemoteAllocation ties remote-memory release to one Rust owner’s lifetime. It stores the process handle and remote address, then its Drop method releases the address whether the caller finishes normally or returns early with an error.

Alternative: Treat a remote allocation as borrowed workspace with a named custodian, responsible for returning it on every exit path.

Code example. The code’s comments explain: SAFETY: this allocation belongs to this process and is released once.

Allocate and write the path with the actual windows crate calls:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Process::write_exact checks the returned byte count from WriteProcessMemory. RemoteAllocation::drop pairs the successful allocation with VirtualFreeEx even when a later ? returns early.

Section: Verify every boundary.

The high-level loader has several checked steps. One address-sharing assumption in this course version is called out immediately after the code:

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Two things about that sequence deserve an explanation, because both look wrong the first time you read them.

Why does the sample pass a locally resolved LoadLibraryW address to the game? GetProcAddress returns an address in the injector’s process. DLLs often map at the same base in both processes, which makes this common course shortcut work, but that is an assumption to check, not a Windows guarantee. Microsoft documents that a D L L can load at a different base when its preferred one is unavailable, and CreateRemoteThread can even succeed with an invalid start address before the new thread faults. For a reusable injector, find the target process’s actual system-D L L base, resolve or verify the export at that base, and refuse to start the thread if the address is not established. The buildable course injector currently relies on the shared-base assumption in this exact lab. Never apply its locally obtained address to arbitrary game DLLs. See D L L memory protection and CreateRemoteThread.

Why can LoadLibraryW be used as a thread start routine in this 32-bit lab? Both use the Windows system calling convention and take one pointer argument. Here the HMODULE result fits in the thread’s 32-bit DWORD exit code. The remote allocation holding the wide path is the argument. The code requires a nonzero exit code before calculating the D L L-relative gha_start address. This return-value shortcut does not carry over unchanged to a 64-bit injector.

The seven arguments of that CreateRemoteThread call, with the value this lab passes for each:

CreateRemoteThread, argument by argument

hProcesshandlerequired

The game’s process handle, opened with the rights this step needs, including the right to create a thread.

lpThreadAttributesoptionaldefault None

Security settings for the new thread’s handle. None takes the defaults, and the handle cannot be inherited by child processes.

dwStackSizebytesdefault 0

The new thread’s stack size. Zero uses the default of the game’s executable.

lpStartAddressaddress in the gamerequired

Where the thread begins running. Here it is LoadLibraryW, which is why the same-base assumption above matters.

lpParameteraddress in the game

The one pointer handed to the start routine: the remote allocation that holds the wide D L L path.

dwCreationFlagsflagsdefault 0

Zero starts the thread at once. CREATE_SUSPENDED would create it paused.

lpThreadIdoptionaldefault None

Where to store the new thread’s ID. The lab does not need it.

return valuethread handle

The handle of the new thread, which the lab wraps in an owned type so it is always closed. A thread that starts is not a thread that succeeded: the lab still waits for it and reads its exit code.

The wrapper methods are small unsafe boundaries over:

OpenProcess;

VirtualAllocEx / VirtualFreeEx;

WriteProcessMemory;

CreateRemoteThread;

waiting and reading the thread exit code;

CloseHandle.

Section: Architecture must match.

A 32-bit D L L loads into a 32-bit process. A 64-bit D L L loads into a 64-bit process. Check both the loader and target architecture before allocating anything.

In brief: Match the D L L and target architectures before remote loading; a valid path cannot repair an incompatible binary.

For the versions in this book, install the 32-bit compilation target and build the D L L for it:

Terminal window

Code example. The surrounding prose explains this example; the full lesson contains the exact code.

Pass the absolute D L L produced under target\i686-pc-windows-msvc\release\. A 64-bit injector may still need extra care to inspect a 32-bit target, so the simplest course setup builds the injector as 32-bit too.

Section: Loader-lock reminder.

When the D L L loads, Windows calls its DllMain under the loader lock. The course D L L does only one small thing there: DisableThreadLibraryCalls. The injector then finds the exported gha_start address by its D L L-relative offset and starts a second remote thread after LoadLibraryW has returned. That guarantees the real worker and hook installation happen outside DllMain and outside the loader lock.

Lesson diagram

Section: Cleanup order.

On every error:

stop waiting on or close the remote thread handle;

free the remote path allocation;

close the process handle.

RAII types make this happen during early returns.

Section: Run the real game lab.

Build both injector and D L L for i686-pc-windows-msvc.

Start one allowed game in the exact version and enter an offline/local match.

Run the injector with the process name and absolute D L L path. The course code uses the locally resolved LoadLibraryW address under the shared-base assumption described above.

Require a nonzero LoadLibraryW thread result and a zero gha_start thread result. If either check fails, stop instead of trying another address or retrying blindly.

Inspect the target’s module list and confirm that the expected D L L actually loaded. Confirm its log prints the matched target profile and verified original bytes. These independent observations test the result; they do not make the shared-base assumption a general guarantee.

Trigger the visible result: Wesnoth gold becomes 999, AssaultCube recoil disappears, or Urban Terror renders the local wallhack.

Toggle the feature off, verify the original bytes are restored, and exit the game normally.

If the loader thread returns zero, inspect the target P I D, process identifier,, both architectures, the canonical D L L path, and the Windows error. If the target exits or faults before a loader result is available, the remote start address may have been invalid; treat that as a failed experiment and do not rerun it unchanged. Do not suggest “run as administrator” until the earlier facts are correct.

The completed tool performs a real injection into the course games with exact target checks, owned handles, bounded waits, and cleanup. Its local-system-D L L address assumption remains a limitation of this version.

The complete buildable source is injector.rs. It includes the allowlist, architecture check, UTF-16 conversion, allocation, both remote threads, gha_start R V A, relative virtual address, calculation under A S L R, address space layout randomization,, timeout handling, exit-code checks, and cleanup. Its shared process/handle wrappers are in process.rs and handle.rs.