What a Working Detour Must Preserve
Build a temporary debugger-only detour, prove every hop, and restore the target.
Cheatsheet for Lesson 2.10
A one-page summary of “What a Working Detour Must Preserve” you can keep beside your notes. Plain Markdown.
Lesson 2.10 — What a Working Detour Must Preserve: cheatsheet
- Remember: The cave must leave the interrupted code as it found it: save state, run your logic, restore state, replay the removed instructions once, then jump back.
- Also useful: Think of test-driving a new road with an empty truck first. Only when the trip works do you load the cargo, meaning the new logic.
Key terms
- hook site: A hook site is the address where normal execution is redirected.
- detour: A detour is that redirection.
- code cave: A code cave is executable storage holding the added instructions.
- return address: A return address is the first untouched instruction where normal execution resumes.
Review
- Q: A code cave adds one instruction that changes
eaxand the flags, then replays the displaced instructions and jumps back. Why must it save and restoreeaxand the flags around its own work? A: The hooked function was mid-calculation, so later instructions may still need the values ineaxand the flags - Q: A hook sits at 0x00A1B07C and the displaced span is 7 bytes. What is the return address? A: 0x00A1B083, since 0x7C is 124 and 124 + 7 = 131, which is 0x83
This lesson follows the Terrain Description menu in offline, 32-bit Wesnoth 1.14.9. A six-byte instruction span in that path will become a temporary detour. Use x64dbg to trace every hop before automating its lifecycle in Lesson 8.3.
Detour vocabulary before touching bytes
Section titled “Detour vocabulary before touching bytes”A hook site is the address where normal execution is redirected. A detour is that redirection. A code cave is executable storage holding the added instructions. A return address is the first untouched instruction where normal execution resumes.
Some tools use the word trampoline for a small block that replays displaced instructions and jumps back. Names vary, but every working detour must answer the same questions:
Where do we leave the original path?Which complete instructions did we remove?Where will the new code live?What machine state must it preserve?Where does normal execution resume?How will every changed byte be restored?The CPU has no concept of “our patch.” It only follows the instruction pointer. A single wrong length or destination sends that pointer into unrelated bytes.
Choose a harmless trigger
Section titled “Choose a harmless trigger”Right-click a tile and open Terrain Description to trigger this path. It gives us an easy local-menu event without changing competitive or networked behavior.

Pause the game and choose a hook site with enough complete instructions for a jump. Building the detour takes five facts about that site. For the exact course build they are:
Hook address: 0x00CC_AF8AOriginal instructions: mov eax,[ecx] / alignment leaOriginal bytes: 8B 01 8D 74 26 00Total byte length: 6Return address: 0x00CC_AF90The byte count comes from instruction decoding, not subtraction by guesswork. 8B 01 is the two-byte mov eax,[ecx], and 8D 74 26 00 is a four-byte lea that changes nothing and exists only as alignment padding, so the two instructions cover 2 + 4 = 6 bytes. The return address is the first byte after them: 0x00CCAF8A + 6 = 0x00CCAF90, because 0x8A is 8 × 16 + 10 = 138, and 138 + 6 = 144 = 9 × 16 = 0x90.
Do not continue until the return address points to the first untouched instruction.
x86 instructions have different lengths. A five-byte jump cannot safely replace
the first five bytes of a six-byte instruction. The overwrite must end exactly
on an instruction boundary, even when that means replacing six, eight, or more
bytes and filling unused space with nop.
Find a suitable cave
Section titled “Find a suitable cave”For this historical debugger exercise, locate executable padding inside the controlled module.

“Looks empty” is enough for this one debugger experiment, not for a repeatable hook installer. A detour installed whenever the game starts should allocate its own cave, request the needed page protection, verify the hook bytes, and release or restore everything during shutdown. Here we use existing padding so you can see the control-flow idea before building that machinery in chapter 6.
Build the smallest detour
Section titled “Build the smallest detour”Start with no extra behavior:
; cave; replay the complete instructions removed from the hookjmp return_addressAt the hook site, write a jump to the cave and fill any remaining whole bytes with nop.

Set breakpoints at:
- the hook;
- the first cave instruction;
- the return address.
Trigger the menu once. You should see the breakpoints in that order. This proves the path before new logic complicates it.
Preserve the interrupted calculation
Section titled “Preserve the interrupted calculation”The hook interrupts a function in the middle of its work. Registers, flags, and stack entries may already carry values needed by later instructions.
The cave must normally:
- save any register or flag it will change but the original code expects;
- run the added behavior;
- restore that machine state;
- replay the complete displaced instructions exactly once;
- jump to the first untouched instruction.
Replaying an instruction twice is just as wrong as skipping it. Changing the
stack pointer without undoing the change can make a later ret jump to garbage.
These delayed failures are why a hook may appear to work once and crash later.
Predict first
The game reaches the hook with ecx = 0x0A3C1000, the object that the displaced mov eax,[ecx] reads. The cave's extra logic sets ecx to 1 for its own work.
The cave forgets to restore ecx, then replays mov eax,[ecx]. What happens?
Answer: It reads from address 0x00000001 and faults. mov eax,[ecx] uses the current ecx, which is 1. Nothing is mapped at address 1, so the read faults. Saving ecx before the extra logic and restoring it afterwards would have put 0x0A3C1000 back.
One trip through a detour. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
hook: jmp cave ; 6 bytes: jmp + nop padding
cave: push ecx ; save what we will change
mov ecx, 1 ; added behaviour borrows ecx
pop ecx ; restore it
mov eax, [ecx] ; replay displaced instruction
lea esi, [esi+0] ; replay displaced instruction
jmp 0x00CCAF90 ; first untouched instructionJump to 0x00CCAF90, which is 0x00CCAF8A + 6, the first byte after the displaced instructions. The game continues with ecx, eax and the stack exactly as if the detour had never been there.
Final values: ip = 0x00CCAF90; ecx = 0x0A3C1000; eax = 0x00000007; stack = empty.
Read every step of the lesson’s example
Step 1, line 1. The game reaches 0x00CCAF8A. The six bytes there used to be 8B 01 8D 74 26 00, which is mov eax,[ecx] (2 bytes) plus an alignment lea (4 bytes). Now they hold a jump to the cave and a nop, so the CPU goes to the cave instead.
ip = 0x00CCAF8A; ecx = 0x0A3C1000; eax = 0x00000000 (old value); stack = empty.
Memory: …AF8A = E9 (jmp); …AF8B = .. (to cave); …AF8C = .. (to cave); …AF8D = .. (to cave); …AF8E = .. (to cave); …AF8F = 90 (nop).
Step 2, line 2. The cave first saves ecx on the stack, because the added code is about to change it and the game still needs it.
ip = in the cave; ecx = 0x0A3C1000; eax = 0x00000000 (old value); stack = ecx (0x0A3C1000).
Memory: …AF8A = E9 (jmp); …AF8B = .. (to cave); …AF8C = .. (to cave); …AF8D = .. (to cave); …AF8E = .. (to cave); …AF8F = 90 (nop).
Step 3, line 3. The added behaviour uses ecx as scratch space and sets it to 1. This is the "something our code changes" the lesson warns about.
ip = in the cave; ecx = 0x00000001; eax = 0x00000000 (old value); stack = ecx (0x0A3C1000).
Memory: …AF8A = E9 (jmp); …AF8B = .. (to cave); …AF8C = .. (to cave); …AF8D = .. (to cave); …AF8E = .. (to cave); …AF8F = 90 (nop).
Step 4, line 4. Restore ecx from the stack. The stack is empty again, so the game's stack pointer is back where it was.
ip = in the cave; ecx = 0x0A3C1000; eax = 0x00000000 (old value); stack = empty.
Memory: …AF8A = E9 (jmp); …AF8B = .. (to cave); …AF8C = .. (to cave); …AF8D = .. (to cave); …AF8E = .. (to cave); …AF8F = 90 (nop).
Step 5, line 5. Replay the first displaced instruction exactly once: mov eax,[ecx] reads the value at 0x0A3C1000, which is 7, into eax. The game gets the value it would have got without the detour.
ip = in the cave; ecx = 0x0A3C1000; eax = 0x00000007; stack = empty.
Memory: …AF8A = E9 (jmp); …AF8B = .. (to cave); …AF8C = .. (to cave); …AF8D = .. (to cave); …AF8E = .. (to cave); …AF8F = 90 (nop).
Step 6, line 6. Replay the second displaced instruction. This lea only pads alignment and changes nothing, but it must still be replayed so the byte count adds up.
ip = in the cave; ecx = 0x0A3C1000; eax = 0x00000007; stack = empty.
Memory: …AF8A = E9 (jmp); …AF8B = .. (to cave); …AF8C = .. (to cave); …AF8D = .. (to cave); …AF8E = .. (to cave); …AF8F = 90 (nop).
Step 7, line 7. Jump to 0x00CCAF90, which is 0x00CCAF8A + 6, the first byte after the displaced instructions. The game continues with ecx, eax and the stack exactly as if the detour had never been there.
ip = 0x00CCAF90; ecx = 0x0A3C1000; eax = 0x00000007; stack = empty.
Memory: …AF8A = E9 (jmp); …AF8B = .. (to cave); …AF8C = .. (to cave); …AF8D = .. (to cave); …AF8E = .. (to cave); …AF8F = 90 (nop).

Add one observable change
Section titled “Add one observable change”Only after the round trip works, add a tiny change such as writing a known value to the already-identified gold field.

Trigger once, observe once, and stop. If the wrong action triggers it, your hook is too broad or your model is wrong.
Diagnose by symptom
Section titled “Diagnose by symptom”Detour symptoms
| Symptom | Likely place to check |
|---|---|
| Crash before cave | Hook bytes or jump destination |
| Cave never runs | Wrong trigger or hook not reached |
| Crash after cave | Saved instruction or return address |
| Game acts oddly later | Register, flag, or stack state was changed |
| Runs more than once | Trigger path or re-entry assumption |
Change one variable per retry.
Restore the target
Section titled “Restore the target”Before closing the debugger:
- pause the game;
- restore the hook’s original bytes;
- remove breakpoints;
- verify the original behavior;
- restore the VM snapshot if anything is uncertain.
The lasting result is your map of the detour, not the temporary patch.
After this debugger-only round trip, Lesson 8.3
implements the same map in a DLL. It verifies the six bytes, replaces them
with E9 rel32 90, runs a naked x86 cave, sets Wesnoth gold to 888, replays
the removed behavior, jumps to 0x00CCAF90, and restores the bytes when
disabled. The complete source is also in
game_hooks.rs.
game_hooks.rs
Show the codeHide the code
//! Exact x86 hooks used by the version-pinned course targets.
#![cfg(target_arch = "x86")]
use std::{
ptr,
sync::atomic::{AtomicBool, Ordering},
};
use anyhow::{Context, Result};
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_MOUSE, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEINPUT, SendInput,
};
use super::{LocalPatch, input_structure_size, near_jump};
const WESNOTH_PLAYER_ROOT: usize = 0x017E_ED18;
const WESNOTH_GAME_OFFSET: usize = 0x0A90;
const WESNOTH_GOLD_OFFSET: usize = 0x0004;
const WESNOTH_TERRAIN_HOOK: usize = 0x00CC_AF8A;
const WESNOTH_TERRAIN_RESUME: usize = 0x00CC_AF90;
const AC_TRIGGER_HOOK: usize = 0x0040_AD9D;
const AC_TRIGGER_ORIGINAL: usize = 0x0046_07C0;
const AC_TRIGGER_RESUME: usize = 0x0040_ADA2;
const AC_RECOIL_STORE: usize = 0x0045_BAAD;
const AC_RADAR_FILTER: usize = 0x0040_9FB3;
const URT_RENDER_HOOK: usize = 0x0052_D2FD;
const URT_RENDER_RESUME: usize = 0x0052_D303;
pub static TARGET_UNDER_CROSSHAIR: AtomicBool = AtomicBool::new(false);
/// Resolves Wesnoth's two pointers and writes the live gold field.
///
/// # Safety
/// Call only inside 32-bit Wesnoth 1.14.9 after a local match has started.
unsafe fn set_wesnoth_gold(amount: u32) -> Result<()> {
// SAFETY: the caller verified the target build; volatile access makes the
// actual memory operation visible at this exact point.
let player = unsafe { ptr::read_volatile(WESNOTH_PLAYER_ROOT as *const u32) } as usize;
anyhow::ensure!(
player != 0,
"start a Wesnoth match before using the terrain hook"
);
let side_pointer_address = player
.checked_add(WESNOTH_GAME_OFFSET)
.context("Wesnoth side-pointer address overflowed")?;
// SAFETY: the verified player structure owns a 32-bit pointer at +0xA90.
let side = unsafe { ptr::read_volatile(side_pointer_address as *const u32) } as usize;
anyhow::ensure!(side != 0, "Wesnoth side pointer is null");
let gold_address = side
.checked_add(WESNOTH_GOLD_OFFSET)
.context("Wesnoth gold address overflowed")?;
// SAFETY: the verified side structure owns its four-byte gold field at +4.
unsafe { ptr::write_volatile(gold_address as *mut u32, amount) };
Ok(())
}
pub fn write_wesnoth_gold(amount: u32) -> Result<()> {
// SAFETY: the DLL worker calls this only after matching wesnoth.exe. The
// pointer resolver still rejects the normal "no active match" null state.
unsafe { set_wesnoth_gold(amount) }
}
extern "C" fn wesnoth_cave_body() {
// A naked cave cannot return `Result`, so the worker verifies the target and
// this tiny body simply leaves the game unchanged if the match is not ready.
// SAFETY: the cave is installed only after the exact Wesnoth hook bytes
// match; the helper checks every nullable pointer in the live chain.
let _ = unsafe { set_wesnoth_gold(888) };
}
#[unsafe(naked)]
unsafe extern "C" fn wesnoth_terrain_cave() {
core::arch::naked_asm!(
"pushfd",
"pushad",
"call {body}",
"popad",
"popfd",
// Replay the behavior of the six bytes replaced at the hook site.
"mov eax, dword ptr [ecx]",
"lea esi, [esi]",
"jmp {resume}",
body = sym wesnoth_cave_body,
resume = const WESNOTH_TERRAIN_RESUME,
);
}
extern "C" fn record_crosshair_target(value: u32) {
TARGET_UNDER_CROSSHAIR.store(value != 0, Ordering::Release);
}
#[unsafe(naked)]
unsafe extern "C" fn assaultcube_trigger_cave() {
core::arch::naked_asm!(
// The removed instruction was this call. Run it first so EAX contains
// the game's real target-under-crosshair result.
"call {original}",
"pushfd",
"pushad",
"push eax",
"call {record}",
"add esp, 4",
"popad",
"popfd",
"jmp {resume}",
original = const AC_TRIGGER_ORIGINAL,
record = sym record_crosshair_target,
resume = const AC_TRIGGER_RESUME,
);
}
#[unsafe(naked)]
unsafe extern "C" fn urban_terror_render_cave() {
core::arch::naked_asm!(
"pushfd",
"pushad",
"mov dword ptr [ebx + 4], 0x0D",
"popad",
"popfd",
// Replay the complete instruction that the detour replaced.
"mov dword ptr [0x0102AE98], ebx",
"jmp {resume}",
resume = const URT_RENDER_RESUME,
);
}
fn six_byte_detour(hook: usize, cave: usize) -> Result<[u8; 6]> {
let jump = near_jump(hook, cave)?;
let mut replacement = [0x90_u8; 6];
replacement[..5].copy_from_slice(&jump);
Ok(replacement)
}
pub fn install_wesnoth_terrain_hook() -> Result<LocalPatch> {
// mov eax,[ecx] ; lea esi,[esi+eiz]
const ORIGINAL: [u8; 6] = [0x8B, 0x01, 0x8D, 0x74, 0x26, 0x00];
let cave = wesnoth_terrain_cave as *const () as usize;
let replacement = six_byte_detour(WESNOTH_TERRAIN_HOOK, cave)?;
LocalPatch::apply(WESNOTH_TERRAIN_HOOK, &ORIGINAL, &replacement)
}
pub fn install_assaultcube_trigger_hook() -> Result<LocalPatch> {
// call 0x004607C0, encoded relative to the end of this instruction.
const ORIGINAL: [u8; 5] = [0xE8, 0x1E, 0x5A, 0x05, 0x00];
let cave = assaultcube_trigger_cave as *const () as usize;
let replacement = near_jump(AC_TRIGGER_HOOK, cave)?;
LocalPatch::apply(AC_TRIGGER_HOOK, &ORIGINAL, &replacement)
}
pub fn install_urban_terror_memory_wallhook() -> Result<LocalPatch> {
// mov dword ptr [0x0102AE98],ebx
const ORIGINAL: [u8; 6] = [0x89, 0x1D, 0x98, 0xAE, 0x02, 0x01];
let cave = urban_terror_render_cave as *const () as usize;
let replacement = six_byte_detour(URT_RENDER_HOOK, cave)?;
LocalPatch::apply(URT_RENDER_HOOK, &ORIGINAL, &replacement)
}
pub fn enable_assaultcube_no_recoil() -> Result<LocalPatch> {
// fstp dword ptr [ebx+0x44] -> fstp st(0), nop
LocalPatch::apply(AC_RECOIL_STORE, &[0xD9, 0x5B, 0x44], &[0xDD, 0xD8, 0x90])
}
pub fn enable_assaultcube_show_all_radar() -> Result<LocalPatch> {
// 0F 85 is a six-byte near JNE. Capture its four-byte destination exactly,
// NOP the full branch, and let LocalPatch restore all six captured bytes.
LocalPatch::apply_masked(
AC_RADAR_FILTER,
&[Some(0x0F), Some(0x85), None, None, None, None],
&[0x90; 6],
)
}
pub fn send_left_mouse(pressed: bool) -> Result<()> {
let flags = if pressed {
MOUSEEVENTF_LEFTDOWN
} else {
MOUSEEVENTF_LEFTUP
};
let input = INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: 0,
dy: 0,
mouseData: 0,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
},
},
};
// SAFETY: `input` is a fully initialized INPUT value and the structure size
// is the one expected by this build of Windows.
let sent = unsafe { SendInput(&[input], input_structure_size()) };
anyhow::ensure!(sent == 1, "SendInput sent {sent} of 1 mouse event");
Ok(())
}Chapter 3 uses the verified addresses and types from this chapter to build an external memory tool. The DLL hook remains a later application of this same control-flow model.
Leave the machine as you found it
A code cave adds one instruction that changes eax and the flags, then replays the displaced instructions and jumps back. Why must it save and restore eax and the flags around its own work?
The hook interrupts a function in the middle of its work, so registers, flags, and stack entries may already hold values its later instructions depend on. If the cave leaves eax or the flags changed, the original code keeps running with the wrong inputs, and the failure may only show up later, which is why a hook can seem to work once and then crash.
Finished Lesson 2.10?
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.