x86 Reverse Engineering for Games
A practical workflow for applying the x86 knowledge from x86 Assembly Fundamentals, x86 Instructions and the Stack, and Reading Compiled Code Patterns to real game binaries: tools, how to find code you care about, patching techniques, and a worked crackme walkthrough. For live memory inspection (finding a health value while the game runs, pointer chains, trainers) see the companion article Cheat Engine and Memory Scanning.
A note on scope: everything here is aimed at offline, single-player analysis and modding — crackmes, DRM-free/offline games, your own builds. Reverse engineering an online multiplayer game’s client to cheat against other players is against essentially every game’s ToS and, depending on jurisdiction and what protections you bypass, can carry legal risk (e.g. DMCA anti-circumvention in the US); anti-cheat internals specifically are also one of the few RE targets that routinely fight back (kernel-mode anti-tamper, VM detection) in ways that can be genuinely unsafe to poke at without knowing what you’re doing. Everything below is the same skill set either way — where you point it is on you.
Tools
- Disassembler/decompiler — Ghidra (free, NSA-developed) or IDA Free/IDA Pro. Both disassemble to Intel-syntax asm and decompile to readable C-like pseudocode; Ghidra’s decompiler is genuinely competitive with IDA’s paid Hex-Rays decompiler and is the reasonable default to start with. Use these for static analysis — the binary at rest, no running process.
- Debugger — x64dbg (Windows, free, x86/x64) or IDA’s built-in debugger. Attaches to or launches a running process, sets breakpoints, single-steps, inspects registers/stack/memory live. Use this for dynamic analysis — watching the actual execution.
- Cheat Engine — memory scanner plus a capable debugger and disassembler of its own; the standard tool specifically for game memory work. Covered in depth in Cheat Engine and Memory Scanning.
- API Monitor / Process Monitor (Windows) — trace which Windows API calls a process makes and with what arguments; useful for finding, e.g., where a game reads a save file or checks a license, without reading any assembly at all first.
Static vs. dynamic analysis, and when to reach for each
Static analysis (reading the disassembly/decompilation cold) is best for understanding structure — what functions exist, how they call each other, what a vtable looks like — without the overhead of getting the game into a specific state. Dynamic analysis (breakpoints, stepping, watching registers change) is what actually resolves ambiguity fast: rather than statically tracing what a confusing function does, run it, breakpoint on entry, and read the real values as they flow through. In practice you constantly switch between the two — static analysis to find a function worth investigating, a breakpoint to confirm what it does with real data, back to static analysis to understand the surrounding logic once you know what to look for.
Finding functions of interest
A stripped, unfamiliar binary with thousands of functions is useless to search linearly. In rough order of how fast each technique narrows things down:
-
String references. Nearly every disassembler cross-references string constants back to the code that uses them. Search for a string you saw in the game’s UI (“Invalid serial”, “You have died”, “Save complete”) and follow the xref straight to the relevant function — usually the single fastest way in.
-
Imported API calls. A function that calls
CreateFileW/ReadFileis doing file I/O; one that callsRegOpenKeyExWis touching the registry;recv/sendmeans networking. Ghidra/IDA both let you jump to an import and list every caller. Combine with API Monitor if you’d rather observe this dynamically first and then jump to the calling address statically. -
Cross-references (XRefs) in general. From any function or global you’ve identified, “who calls this” / “what does this call” lets you walk the call graph outward from a known-good anchor point.
-
Pattern/signature scanning. Once you know the byte pattern of an instruction sequence (from one game version, or from a public cheat-engine table/trainer source), you can scan a different build or a patched binary for the same bytes even without symbols. A signature is written with wildcards for the parts expected to change (addresses, immediates):
48 8B 05 ?? ?? ?? ?? 48 85 C0 74 ?? ; mov rax, [rip+????] ; test rax, rax ; je short ????This is exactly how trainers keep working across game patches: instead of hardcoding an address that shifts every build, they scan for the surrounding instruction bytes and compute the real address relative to the match. Cheat Engine’s “Auto Assemble” / AOB (array-of-bytes) injection feature (see Cheat Engine and Memory Scanning) builds this directly.
-
Breakpoint on effect, not cause. If you know what changes (a health value drops, a counter increments) but not what code changes it, a debugger’s “break on memory write/access” at that address gets you straight to the responsible instruction with no static searching at all — this is the primary technique covered in depth in Cheat Engine and Memory Scanning’s “find out what writes to this address.”
Patching techniques
- NOP-ing. Overwrite an instruction with
0x90(NOP) bytes, one per byte of the original instruction’s encoding. Used to remove a check entirely — e.g. NOP-ing out acall CheckLicenseso it’s simply never invoked. Simple, but changes the file size of nothing (same number of bytes) while leaving an obvious gap of no-ops that’s trivial to spot if anyone re-checks the binary — that’s fine for personal use, not for staying stealthy. - Flipping a conditional jump. The single most common crackme/license-check patch: change
je(0x74) tojne(0x75) (orjmp, unconditional), inverting or removing a branch’s condition entirely. Almost always a 1-byte edit for the short-form encodings. Worked example below. - JMP redirects / detours / trampolines. To run your own code instead of patching in place (when the fix needs more bytes than the space available, or needs to actually compute something), overwrite the start of a function with a
jmpto a code cave — a chunk of unused/newly-allocated executable memory — that runs your replacement logic and then eitherjmps back into the original function past the overwritten bytes, or fully re-implements and returns. This is also literally how DLL-injection-based game trainers and mods hook functions at runtime, as opposed to editing the file on disk. - Code caves. Executable sections often have unused padding bytes (alignment padding between functions, or
0xCCfiller) large enough to hold a short patch; for anything bigger,VirtualAlloc/mmapa fresh executable page at runtime instead of hunting for space.
Anti-debug and anti-tamper basics
Worth recognizing even if you’re not trying to defeat them: IsDebuggerPresent/CheckRemoteDebuggerPresent API checks, manually reading the PEB’s BeingDebugged flag, timing checks around a code region (a debugger single-stepping through it takes drastically longer than real execution, so an abnormally large elapsed-time delta implies a debugger), and int 3/hardware-breakpoint self-detection (scanning your own code for unexpected 0xCC bytes, or reading debug registers dr0–dr3). Commercial anti-cheat (EAC, BattlEye, Vanguard) escalates this dramatically with kernel-mode drivers, virtualized/obfuscated checks, and server-side heuristics — outside the scope of what this article covers, and, per the scope note above, not something to be poking at against a live online service.
Worked example: a crackme serial check
A “crackme” is a small program written specifically for RE practice — usually a fake serial-number/license check with no real stakes, the standard entry point into this whole skill (sites like crackmes.one host thousands, MIT/GPL-style, made explicitly to be broken). The shape below is representative of the simplest, most common category.
Say static analysis (string reference to "Invalid serial!", per the technique above) lands you on this decompiled/disassembled function:
// Ghidra-style decompiled pseudocode
int check_serial(char *input) {
int sum = 0;
for (int i = 0; input[i] != 0; i++) {
sum += input[i];
}
if (sum == 1337) {
return 1; // valid
}
return 0; // invalid
}check_serial:
xor eax, eax ; sum = 0
xor ecx, ecx ; i = 0
loop_top:
movzx edx, byte [rdi+rcx]
test dl, dl
je check_sum
add eax, edx
inc ecx
jmp loop_top
check_sum:
cmp eax, 1337
jne fail
mov eax, 1
ret
fail:
xor eax, eax
retTwo ways to “solve” it, both standard RE approaches:
- Keygen approach — understand the algorithm (sum of character byte values) and construct an input that satisfies it, e.g.
"AAAA..."repeated/adjusted until the byte sum hits exactly 1337. This is the only approach a real license-key algorithm forces you into if it’s cryptographically sound (checks against a signature rather than a simple invertible sum), and is the more interesting/harder skill to practice. - Patch approach — skip understanding the algorithm and just remove the check. The whole function reduces to one decision point:
cmp eax, 1337/jne fail. Flipjne(opcode0x75, short form) toje’s opposite — i.e. simply overwrite it withjmp fail’s inverse, which in practice means changing the single conditional-jump byte so the branch is never taken, or more simply NOP-ing thejne failentirely (2 bytes →90 90) so execution always falls through intomov eax, 1 / ret. Either edit makescheck_serialreturn “valid” for every input, no matter what’s typed.
In x64dbg: set a breakpoint at check_serial’s address (found via the string-reference xref), run the program, trigger the check by entering any serial in the crackme’s UI, single-step until you reach cmp eax, 1337, confirm in the register view that eax holds your computed sum, then either right-click the jne fail instruction → “Assemble” and type nop, or use “Patch” to write the bytes directly. x64dbg’s “Patch File” saves the change back to the binary on disk so it persists without the debugger attached.
This same reasoning — find the deciding compare, understand or bypass it — is the whole method behind DRM removal, trial-period bypasses, and license checks in general; the crackme is just a version with no legal ambiguity to practice on.
See also
- x86 Assembly Fundamentals, x86 Instructions and the Stack, Reading Compiled Code Patterns — the prerequisite reading for everything above.
- Cheat Engine and Memory Scanning — live memory scanning, pointer chains, and a trainer-style memory-edit walkthrough (health/ammo values, freezing memory, AOB injection).
- All Articles