How Detours Change Control Flow
Understand how a small detour can run extra instructions and still return the game to its original path.
Cheatsheet for Lesson 2.9
A one-page summary of “How Detours Change Control Flow” you can keep beside your notes. Plain Markdown.
Lesson 2.9 — How Detours Change Control Flow: cheatsheet
- Remember: A detour has five parts: a hook site, saved bytes, a cave, the extra logic, and a return address. Get one wrong and the game may crash.
- Also useful: Think of cutting a sentence to paste in a sticker: cut between words, never in the middle of one, or the leftover letters read as nonsense.
Key terms
- code cave: A code cave is a spare executable area used for extra code.
extra_logic:extra_logicis a placeholder, not a ready-to-install hook.- lifecycle code versus boundary code: The key distinction is lifecycle code versus boundary code.
Review
- Q: Where should a code-cave detour return after replaying displaced behavior? A: The first complete original instruction after the overwritten region
- Q: A jump needs five bytes, but the fifth byte lands in the middle of the next CPU instruction. What should the patcher do? A: Cover enough whole instructions, save them, and replay their meaning before jumping back
The problem with replacing code
Section titled “The problem with replacing code”The pointer path gives us a repeatable way to find data, but it cannot add
behavior. Replacing the gold-changing instruction with nop was useful for
one quick test, but it could only remove behavior. What if we want to observe
values, add a check, or run several instructions and then resume the game?
A code cave is a spare executable area used for extra code. A detour redirects execution there and then returns.
The road-diversion image behind the name is worth taking literally, because every part of it maps onto something you have to get right. You put a sign at one junction sending traffic down a side road that rejoins the main road further along. You have not demolished the main road, so traffic can be restored by removing the sign. The side road must rejoin — a diversion that just stops leaves everyone stranded, which is what a detour with no return jump does. And the sign has to replace a whole junction, not half of one, which is the instruction-boundary problem below.
A detour needs five parts
Section titled “A detour needs five parts”A responsible detour has:
- hook site — the instructions being replaced;
- saved bytes — an exact copy of those original instructions;
- cave — memory where the new code can execute;
- extra logic — the observation or change;
- return address — the first untouched instruction after the hook.
If any one is wrong, the game may crash or behave strangely.
Instructions are not all the same size
Section titled “Instructions are not all the same size”On x86, one instruction may be 1 byte and another may be 7 bytes. A typical near jump needs 5 bytes. You must replace whole instructions until you have enough room.
The reason is that the CPU keeps no map of where instructions begin. It starts decoding at whatever address it is handed and reads forward, letting each opcode announce how many bytes it consumes. Start it one byte late and every instruction from that point on is decoded from the wrong position.
Take the six-byte span this lesson hooks:
mov eax, [ecx]lea esi, [esi]A five-byte jump written over it covers 8B 01 8D 74 26 and strands the final
00. When the cave later returns to 0x1005, the CPU does not see “the
leftover tail of a lea.” It sees a fresh byte stream beginning with 00,
which starts a completely different instruction that swallows whatever follows
as its operands. Execution continues confidently into nonsense, and the crash
usually surfaces somewhere unrelated a few instructions later — which is what
makes this mistake so hard to diagnose after the fact.
jmp to the cave0x1005, and the CPU starts a new instruction at the orphaned 00.jmp to the cavenopnop. The cave returns to 0x1006, the first untouched byte.So you round up to the next instruction boundary: cover all six bytes and fill
the spare sixth byte with a nop. The return address is the hook address plus
the total number of whole bytes replaced.
Make room without cutting an instruction. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
sizes = [2, 4]
covered = 0
take the next whole instruction
repeat until covered >= jump_size
padding = covered - jump_size
resume = hook_start + coveredResult: cover 6 bytes and resume at 0x1006, a complete instruction boundary.
Final values: jump = 5; covered = 6; hook = 0x1000; padding = 1; resume = 0x1006.
Read every step of the lesson’s example
Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.
jump = 5.
Step 2, line 2. Begin at illustrative hook address 0x1000.
jump = 5; covered = 0; hook = 0x1000.
Step 3, line 3. Cover the complete first instruction, which is two bytes.
jump = 5; covered = 2; hook = 0x1000.
Memory: 0x1000 = 8B; +1 = 01; +2 = 8D; +3 = 74; +4 = 26; +5 = 00.
Step 4, line 4. The first instruction is too short, so also cover the entire four-byte second instruction.
jump = 5; covered = 6; hook = 0x1000.
Memory: 0x1000 = 8B; +1 = 01; +2 = 8D; +3 = 74; +4 = 26; +5 = 00.
Step 5, line 5. The replacement fits, leaving 1 byte(s) of padding.
jump = 5; covered = 6; hook = 0x1000; padding = 1.
Memory: 0x1000 = 8B; +1 = 01; +2 = 8D; +3 = 74; +4 = 26; +5 = 00.
Step 6, line 6. Result: cover 6 bytes and resume at 0x1006, a complete instruction boundary.
jump = 5; covered = 6; hook = 0x1000; padding = 1; resume = 0x1006.
Memory: 0x1000 = 8B; +1 = 01; +2 = 8D; +3 = 74; +4 = 26; +5 = 00.
Follow the detour before writing a DLL
Section titled “Follow the detour before writing a DLL”For the pinned Wesnoth 1.14.9 build, the Terrain Description menu reaches a
six-byte span at 0x00CC_AF8A: 8B 01 8D 74 26 00. The first two bytes
encode mov eax,[ecx]; the other four encode an alignment lea. A jump
needs five bytes, so it must cover both whole instructions and resume at
0x00CC_AF90, the first untouched byte.
This short x86 sketch puts code beside the flow diagram above. The two instructions near the end replay the behavior removed from the hook site; the final jump lands after all six covered bytes:
pushfd ; save the interrupted flagspushad ; save the 32-bit general registerscall extra_logic ; observe or change somethingpopadpopfdmov eax, [ecx] ; replay the first covered instructionlea esi, [esi] ; replay the second covered instructionjmp 0x00CCAF90 ; first untouched Wesnoth instruction1 At 0x00CCAF8A: jump to the cave
Hook site, 6 bytes0x00CCAF8A0x00CCAF8FE9 rel32jmp cave90nopThe game reaches the hook site. Its six bytes are now a five-byte jump and a nop.
2 In the cave: extra work, then the saved bytes
Cave: the added worksaveextra logicrestoreCave: the saved bytes8B 01mov eax,[ecx]8D 74 26 00lea esi,[esi]jmp backThe extra logic runs between save and restore. Then the two whole instructions the jump covered run here, once.
3 Back at 0x00CCAF90
Game code againjmp backfrom the cave0x00CCAF90first untouched instructionjmp back lands on 0x00CCAF8A + 6 = 0x00CCAF90, the first byte the patch did not touch.
The six bytes at 0x00CCAF8A now hold a jump and a nop. The two instructions they held run in the cave instead, exactly once.
extra_logic is a placeholder, not a ready-to-install hook. The exact
implementation also has to verify the original bytes, encode a reachable
relative jump, and restore the patch when disabled.
Lesson 2.10 uses x64dbg to trace that route with breakpoints before adding behavior. Lesson 8.3 then builds the full DLL version: checked gold pointer path, naked x86 cave, relative jump bytes, saved registers and flags, injector commands, F1 toggle, and restoration. The exact code and stack diagram live beside that runnable implementation.
Why assembly is still required
Section titled “Why assembly is still required”A detour begins and ends at machine-code boundaries. The surrounding program can:
- allocate and own buffers;
- calculate and verify addresses;
- decode instructions with a library;
- build a patch plan;
- restore original bytes automatically;
- expose a small safe API.
The final jump bytes and an in-process hook still require unsafe code and a
small amount of assembly. Typed code manages the larger, error-prone lifecycle:
struct Patch { address: usize, original: Vec<u8>, replacement: Vec<u8>,}
impl Patch { fn is_compatible_with(&self, found: &[u8]) -> bool { found == self.original }}Before writing, compare the bytes in memory with original. If they differ, stop. The target may be a different version or another tool may already have changed it.
Restore is part of the design
Section titled “Restore is part of the design”A patch is not complete until it has a cleanup path. Plan restoration before applying the detour:
- store the exact original bytes;
- restore them when the feature is disabled;
- restore them when the tool exits normally;
- make repeated enable/disable operations safe.
This habit will shape the wrappers we build later.
The key distinction is lifecycle code versus boundary code. The manager owns the patch, validates the target, reports failures, and restores state. Assembly describes the few CPU instructions that must run at the exact hijacked boundary.
Return safely
Where should a code-cave detour return after replaying displaced behavior?
Returning at a verified instruction boundary preserves the decoder's byte stream and original control flow.
Finished Lesson 2.9?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.