paavohtl Claude Opus 4.6 commited on
Commit
beab230
·
1 Parent(s): 8372d88

feat(validator): add function hooking via MinHook and vtable swapping

Browse files

Two-layer hooking system replaces the failed retour-rs approach:
- VTable hooks: swap function pointers in .rdata for CTask virtual methods
- MinHook inline hooks: FrontendChangeScreen, CTask::Constructor,
CGameTask::Constructor — handles SEH prologues correctly

Also adds .cargo/config.toml to default target to i686-pc-windows-msvc.

Tested in-game: all hooks fire correctly, game runs normally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

.cargo/config.toml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [build]
2
+ target = "i686-pc-windows-msvc"
Cargo.lock CHANGED
@@ -2,12 +2,50 @@
2
  # It is not intended for manual editing.
3
  version = 4
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  [[package]]
6
  name = "libc"
7
  version = "0.2.182"
8
  source = "registry+https://github.com/rust-lang/crates.io-index"
9
  checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  [[package]]
12
  name = "openwa-harness"
13
  version = "0.1.0"
@@ -25,9 +63,90 @@ version = "0.1.0"
25
  name = "openwa-validator"
26
  version = "0.1.0"
27
  dependencies = [
 
28
  "openwa-types",
 
29
  ]
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  [[package]]
32
  name = "windows-sys"
33
  version = "0.59.0"
 
2
  # It is not intended for manual editing.
3
  version = 4
4
 
5
+ [[package]]
6
+ name = "cc"
7
+ version = "1.2.56"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
10
+ dependencies = [
11
+ "find-msvc-tools",
12
+ "shlex",
13
+ ]
14
+
15
+ [[package]]
16
+ name = "find-msvc-tools"
17
+ version = "0.1.9"
18
+ source = "registry+https://github.com/rust-lang/crates.io-index"
19
+ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
20
+
21
  [[package]]
22
  name = "libc"
23
  version = "0.2.182"
24
  source = "registry+https://github.com/rust-lang/crates.io-index"
25
  checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
26
 
27
+ [[package]]
28
+ name = "log"
29
+ version = "0.4.29"
30
+ source = "registry+https://github.com/rust-lang/crates.io-index"
31
+ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
32
+
33
+ [[package]]
34
+ name = "minhook"
35
+ version = "0.9.0"
36
+ source = "registry+https://github.com/rust-lang/crates.io-index"
37
+ checksum = "455e088cb1600ffa5f1524c9702c9eeb0ba3604e0f4932efa165b99973272a8d"
38
+ dependencies = [
39
+ "cc",
40
+ "tracing",
41
+ ]
42
+
43
+ [[package]]
44
+ name = "once_cell"
45
+ version = "1.21.3"
46
+ source = "registry+https://github.com/rust-lang/crates.io-index"
47
+ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
48
+
49
  [[package]]
50
  name = "openwa-harness"
51
  version = "0.1.0"
 
63
  name = "openwa-validator"
64
  version = "0.1.0"
65
  dependencies = [
66
+ "minhook",
67
  "openwa-types",
68
+ "windows-sys",
69
  ]
70
 
71
+ [[package]]
72
+ name = "pin-project-lite"
73
+ version = "0.2.17"
74
+ source = "registry+https://github.com/rust-lang/crates.io-index"
75
+ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
76
+
77
+ [[package]]
78
+ name = "proc-macro2"
79
+ version = "1.0.106"
80
+ source = "registry+https://github.com/rust-lang/crates.io-index"
81
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
82
+ dependencies = [
83
+ "unicode-ident",
84
+ ]
85
+
86
+ [[package]]
87
+ name = "quote"
88
+ version = "1.0.45"
89
+ source = "registry+https://github.com/rust-lang/crates.io-index"
90
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
91
+ dependencies = [
92
+ "proc-macro2",
93
+ ]
94
+
95
+ [[package]]
96
+ name = "shlex"
97
+ version = "1.3.0"
98
+ source = "registry+https://github.com/rust-lang/crates.io-index"
99
+ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
100
+
101
+ [[package]]
102
+ name = "syn"
103
+ version = "2.0.117"
104
+ source = "registry+https://github.com/rust-lang/crates.io-index"
105
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
106
+ dependencies = [
107
+ "proc-macro2",
108
+ "quote",
109
+ "unicode-ident",
110
+ ]
111
+
112
+ [[package]]
113
+ name = "tracing"
114
+ version = "0.1.44"
115
+ source = "registry+https://github.com/rust-lang/crates.io-index"
116
+ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
117
+ dependencies = [
118
+ "log",
119
+ "pin-project-lite",
120
+ "tracing-attributes",
121
+ "tracing-core",
122
+ ]
123
+
124
+ [[package]]
125
+ name = "tracing-attributes"
126
+ version = "0.1.31"
127
+ source = "registry+https://github.com/rust-lang/crates.io-index"
128
+ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
129
+ dependencies = [
130
+ "proc-macro2",
131
+ "quote",
132
+ "syn",
133
+ ]
134
+
135
+ [[package]]
136
+ name = "tracing-core"
137
+ version = "0.1.36"
138
+ source = "registry+https://github.com/rust-lang/crates.io-index"
139
+ checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
140
+ dependencies = [
141
+ "once_cell",
142
+ ]
143
+
144
+ [[package]]
145
+ name = "unicode-ident"
146
+ version = "1.0.24"
147
+ source = "registry+https://github.com/rust-lang/crates.io-index"
148
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
149
+
150
  [[package]]
151
  name = "windows-sys"
152
  version = "0.59.0"
crates/openwa-validator/Cargo.toml CHANGED
@@ -9,3 +9,7 @@ crate-type = ["cdylib"]
9
 
10
  [dependencies]
11
  openwa-types = { path = "../openwa-types" }
 
 
 
 
 
9
 
10
  [dependencies]
11
  openwa-types = { path = "../openwa-types" }
12
+ minhook = "0.9"
13
+
14
+ [target.'cfg(target_os = "windows")'.dependencies]
15
+ windows-sys = { version = "0.59", features = ["Win32_System_Memory"] }
crates/openwa-validator/src/hooks/detour.rs ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! MinHook-based inline hooking for arbitrary functions.
2
+ //!
3
+ //! Uses MinHook to create trampolines that correctly handle SEH prologues
4
+ //! (which retour-rs could not). This allows hooking constructors and free
5
+ //! functions that aren't in vtables.
6
+
7
+ use std::ffi::c_void;
8
+ use std::sync::atomic::{AtomicU32, Ordering};
9
+
10
+ use minhook::MinHook;
11
+
12
+ use crate::{log_line, rb};
13
+ use openwa_types::address::va;
14
+
15
+ /// Install an inline hook at a Ghidra address, redirecting to `detour_fn`.
16
+ /// Returns the trampoline pointer for calling the original.
17
+ unsafe fn install_hook(ghidra_addr: u32, detour_fn: *const (), name: &str) -> Result<u32, String> {
18
+ let target = rb(ghidra_addr) as *mut c_void;
19
+ let detour = detour_fn as *mut c_void;
20
+
21
+ let trampoline = MinHook::create_hook(target, detour)
22
+ .map_err(|e| format!("MinHook create_hook failed for {name}: {e}"))?;
23
+
24
+ MinHook::enable_hook(target)
25
+ .map_err(|e| format!("MinHook enable_hook failed for {name}: {e}"))?;
26
+
27
+ let trampoline_addr = trampoline as u32;
28
+ let _ = log_line(&format!(
29
+ " [MH] {name}: target 0x{:08X} (ghidra 0x{ghidra_addr:08X}), trampoline 0x{trampoline_addr:08X}",
30
+ target as u32
31
+ ));
32
+
33
+ Ok(trampoline_addr)
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // FrontendChangeScreen hook
38
+ // ---------------------------------------------------------------------------
39
+
40
+ static ORIG_FRONTEND_CHANGE_SCREEN: AtomicU32 = AtomicU32::new(0);
41
+
42
+ /// Hook for FrontendChangeScreen (0x447A20).
43
+ /// Called on every menu screen transition. Logs the screen ID.
44
+ ///
45
+ /// RET 0x4 → stdcall with 1 param (screen_id). ESI = dialog this (implicit).
46
+ unsafe extern "stdcall" fn hook_frontend_change_screen(screen_id: u32) {
47
+ let _ = log_line(&format!("[HOOK] FrontendChangeScreen(screen_id={screen_id})"));
48
+ let orig: unsafe extern "stdcall" fn(u32) =
49
+ core::mem::transmute(ORIG_FRONTEND_CHANGE_SCREEN.load(Ordering::Relaxed));
50
+ orig(screen_id);
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // CTask::Constructor hook
55
+ // ---------------------------------------------------------------------------
56
+
57
+ static ORIG_CTASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
58
+
59
+ /// Hook for CTask::Constructor (0x5625A0).
60
+ /// stdcall(this, parent, class_type) -> this
61
+ unsafe extern "stdcall" fn hook_ctask_constructor(this: u32, parent: u32, class_type: u32) -> u32 {
62
+ let orig: unsafe extern "stdcall" fn(u32, u32, u32) -> u32 =
63
+ core::mem::transmute(ORIG_CTASK_CONSTRUCTOR.load(Ordering::Relaxed));
64
+ let result = orig(this, parent, class_type);
65
+ // Log after calling original (so vtable is set)
66
+ let _ = log_line(&format!(
67
+ "[HOOK] CTask::ctor(this=0x{this:08X}, parent=0x{parent:08X}, type={class_type}) -> 0x{result:08X}"
68
+ ));
69
+ result
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // CGameTask::Constructor hook
74
+ // ---------------------------------------------------------------------------
75
+
76
+ static ORIG_CGAMETASK_CONSTRUCTOR: AtomicU32 = AtomicU32::new(0);
77
+
78
+ /// Hook for CGameTask::Constructor (0x4FED50).
79
+ /// RET 0x10 → stdcall with 4 params: (this, parent, param3, param4)
80
+ /// param3/param4 are stored at this+0x30/this+0x38 (likely initial position)
81
+ unsafe extern "stdcall" fn hook_cgametask_constructor(this: u32, parent: u32, param3: u32, param4: u32) -> u32 {
82
+ let orig: unsafe extern "stdcall" fn(u32, u32, u32, u32) -> u32 =
83
+ core::mem::transmute(ORIG_CGAMETASK_CONSTRUCTOR.load(Ordering::Relaxed));
84
+ let result = orig(this, parent, param3, param4);
85
+ let _ = log_line(&format!(
86
+ "[HOOK] CGameTask::ctor(this=0x{this:08X}, parent=0x{parent:08X}) -> 0x{result:08X}"
87
+ ));
88
+ result
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Installation
93
+ // ---------------------------------------------------------------------------
94
+
95
+ pub fn install() -> Result<(), String> {
96
+ unsafe {
97
+ // FrontendChangeScreen — called on every menu navigation
98
+ let trampoline = install_hook(
99
+ va::FRONTEND_CHANGE_SCREEN,
100
+ hook_frontend_change_screen as *const (),
101
+ "FrontendChangeScreen",
102
+ )?;
103
+ ORIG_FRONTEND_CHANGE_SCREEN.store(trampoline, Ordering::Relaxed);
104
+
105
+ // CTask::Constructor — base class constructor
106
+ let trampoline = install_hook(
107
+ va::CTASK_CONSTRUCTOR,
108
+ hook_ctask_constructor as *const (),
109
+ "CTask::Constructor",
110
+ )?;
111
+ ORIG_CTASK_CONSTRUCTOR.store(trampoline, Ordering::Relaxed);
112
+
113
+ // CGameTask::Constructor — game entity constructor
114
+ let trampoline = install_hook(
115
+ va::CGAMETASK_CONSTRUCTOR,
116
+ hook_cgametask_constructor as *const (),
117
+ "CGameTask::Constructor",
118
+ )?;
119
+ ORIG_CGAMETASK_CONSTRUCTOR.store(trampoline, Ordering::Relaxed);
120
+ }
121
+
122
+ let _ = log_line(" 3 inline hooks installed via MinHook");
123
+ Ok(())
124
+ }
crates/openwa-validator/src/hooks/mod.rs ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pub mod vtable;
2
+ pub mod detour;
3
+
4
+ use crate::log_line;
5
+
6
+ /// Install all hooks. Called from run_validation() after rebase init.
7
+ pub fn install_all() -> Result<(), String> {
8
+ let _ = log_line("");
9
+ let _ = log_line("--- Installing Hooks ---");
10
+
11
+ // VTable hooks (pure Rust, no dependencies)
12
+ vtable::install()?;
13
+
14
+ // Inline hooks via MinHook (for constructors and free functions)
15
+ detour::install()?;
16
+
17
+ let _ = log_line(" All hooks installed.");
18
+ Ok(())
19
+ }
crates/openwa-validator/src/hooks/vtable.rs ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! VTable hooking — swap function pointers in vtable memory.
2
+ //!
3
+ //! This is the simplest hooking approach: write a new function pointer
4
+ //! into a vtable slot and save the original. No trampoline, no prologue
5
+ //! analysis, no external dependencies.
6
+
7
+ use std::sync::atomic::{AtomicU32, Ordering};
8
+
9
+ use crate::{log_line, rb};
10
+ use openwa_types::address::va;
11
+
12
+ #[cfg(target_os = "windows")]
13
+ use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_READWRITE};
14
+
15
+ /// A single vtable hook — remembers the slot address and original pointer.
16
+ pub struct VtableHook {
17
+ #[allow(dead_code)]
18
+ slot_addr: *mut u32,
19
+ original_fn: u32,
20
+ #[allow(dead_code)]
21
+ name: &'static str,
22
+ }
23
+
24
+ // VtableHook contains a raw pointer but is only used from the main thread
25
+ unsafe impl Send for VtableHook {}
26
+ unsafe impl Sync for VtableHook {}
27
+
28
+ impl VtableHook {
29
+ /// Install a vtable hook at `vtable_ghidra_addr + slot_index * 4`.
30
+ ///
31
+ /// # Safety
32
+ /// - `vtable_ghidra_addr` must be a valid vtable base in .rdata
33
+ /// - `hook_fn` must match the calling convention of the original method
34
+ #[cfg(target_os = "windows")]
35
+ pub unsafe fn install(
36
+ vtable_ghidra_addr: u32,
37
+ slot_index: usize,
38
+ hook_fn: u32,
39
+ name: &'static str,
40
+ ) -> Result<Self, String> {
41
+ let slot_addr = rb(vtable_ghidra_addr + (slot_index as u32) * 4) as *mut u32;
42
+
43
+ // Make the vtable slot writable
44
+ let mut old_protect: u32 = 0;
45
+ let ok = VirtualProtect(slot_addr as *mut _, 4, PAGE_READWRITE, &mut old_protect);
46
+ if ok == 0 {
47
+ return Err(format!("VirtualProtect failed for {name} at 0x{:08X}", slot_addr as u32));
48
+ }
49
+
50
+ // Read original and write hook
51
+ let original_fn = *slot_addr;
52
+ *slot_addr = hook_fn;
53
+
54
+ // Restore protection
55
+ VirtualProtect(slot_addr as *mut _, 4, old_protect, &mut old_protect);
56
+
57
+ let _ = log_line(&format!(
58
+ " [VT] {name}: slot 0x{:08X}, orig 0x{original_fn:08X} -> hook 0x{hook_fn:08X}",
59
+ slot_addr as u32
60
+ ));
61
+
62
+ Ok(VtableHook { slot_addr, original_fn, name })
63
+ }
64
+
65
+ /// Get the original function pointer (for calling the original from the hook).
66
+ pub fn original(&self) -> u32 {
67
+ self.original_fn
68
+ }
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Installed hooks — stored globally so originals can be called
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /// Original CTask::ProcessFrame function pointer (thiscall: ECX = this)
76
+ static ORIG_CTASK_PROCESS_FRAME: AtomicU32 = AtomicU32::new(0);
77
+
78
+ /// Hook for CTask::ProcessFrame (vtable slot 7).
79
+ /// thiscall convention: ECX = this. We use fastcall (ECX = arg1, EDX = arg2).
80
+ unsafe extern "fastcall" fn hook_ctask_process_frame(this: u32, _edx: u32) -> u32 {
81
+ // Call original
82
+ let orig: unsafe extern "fastcall" fn(u32, u32) -> u32 =
83
+ core::mem::transmute(ORIG_CTASK_PROCESS_FRAME.load(Ordering::Relaxed));
84
+ orig(this, 0)
85
+ }
86
+
87
+ /// Original CTask::HandleMessage function pointer
88
+ static ORIG_CTASK_HANDLE_MESSAGE: AtomicU32 = AtomicU32::new(0);
89
+
90
+ /// Hook for CTask::HandleMessage (vtable slot 2).
91
+ /// thiscall(this, msg_id, param) — ECX = this, two stack params
92
+ unsafe extern "fastcall" fn hook_ctask_handle_message(this: u32, _edx: u32, msg_id: u32, param: u32) -> u32 {
93
+ let orig: unsafe extern "fastcall" fn(u32, u32, u32, u32) -> u32 =
94
+ core::mem::transmute(ORIG_CTASK_HANDLE_MESSAGE.load(Ordering::Relaxed));
95
+ orig(this, 0, msg_id, param)
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Installation
100
+ // ---------------------------------------------------------------------------
101
+
102
+ pub fn install() -> Result<(), String> {
103
+ unsafe {
104
+ // CTask::ProcessFrame — slot 7
105
+ let hook = VtableHook::install(
106
+ va::CTASK_VTABLE,
107
+ 7,
108
+ hook_ctask_process_frame as *const () as u32,
109
+ "CTask::ProcessFrame",
110
+ )?;
111
+ ORIG_CTASK_PROCESS_FRAME.store(hook.original(), Ordering::Relaxed);
112
+
113
+ // CTask::HandleMessage — slot 2
114
+ let hook = VtableHook::install(
115
+ va::CTASK_VTABLE,
116
+ 2,
117
+ hook_ctask_handle_message as *const () as u32,
118
+ "CTask::HandleMessage",
119
+ )?;
120
+ ORIG_CTASK_HANDLE_MESSAGE.store(hook.original(), Ordering::Relaxed);
121
+
122
+ let _ = log_line(" 2 vtable hooks installed");
123
+ }
124
+
125
+ Ok(())
126
+ }
crates/openwa-validator/src/lib.rs CHANGED
@@ -3,6 +3,8 @@
3
  use std::ffi::c_void;
4
  use std::sync::atomic::{AtomicU32, Ordering};
5
 
 
 
6
  use openwa_types::address::va;
7
  use openwa_types::task::{CTask, CGameTask};
8
  use openwa_types::ddgame::DDGame;
@@ -18,7 +20,7 @@ static REBASE_DELTA: AtomicU32 = AtomicU32::new(0);
18
 
19
  /// Rebase a Ghidra VA to the actual runtime address.
20
  #[inline]
21
- fn rb(ghidra_addr: u32) -> u32 {
22
  ghidra_addr.wrapping_add(REBASE_DELTA.load(Ordering::Relaxed))
23
  }
24
 
@@ -59,7 +61,7 @@ unsafe extern "system" fn DllMain(
59
  // Logging
60
  // ---------------------------------------------------------------------------
61
 
62
- fn log_line(msg: &str) -> std::io::Result<()> {
63
  use std::io::Write;
64
  let mut f = std::fs::OpenOptions::new()
65
  .create(true)
@@ -504,8 +506,13 @@ fn run_validation() -> Result<(), Box<dyn std::error::Error>> {
504
  let _ = log_line("--- Static Validation Summary ---");
505
  let _ = log_line(&format!(" {}", result.summary_line()));
506
 
507
- // 6. Hooks disabled — retour trampoline corrupts SEH prologue on these constructors
508
- // Instead, use a polling thread to find DDGameWrapper after init completes
 
 
 
 
 
509
  let _ = log_line("");
510
  let _ = log_line("--- Deferred Validation (polling) ---");
511
  std::thread::spawn(move || {
 
3
  use std::ffi::c_void;
4
  use std::sync::atomic::{AtomicU32, Ordering};
5
 
6
+ mod hooks;
7
+
8
  use openwa_types::address::va;
9
  use openwa_types::task::{CTask, CGameTask};
10
  use openwa_types::ddgame::DDGame;
 
20
 
21
  /// Rebase a Ghidra VA to the actual runtime address.
22
  #[inline]
23
+ pub(crate) fn rb(ghidra_addr: u32) -> u32 {
24
  ghidra_addr.wrapping_add(REBASE_DELTA.load(Ordering::Relaxed))
25
  }
26
 
 
61
  // Logging
62
  // ---------------------------------------------------------------------------
63
 
64
+ pub(crate) fn log_line(msg: &str) -> std::io::Result<()> {
65
  use std::io::Write;
66
  let mut f = std::fs::OpenOptions::new()
67
  .create(true)
 
506
  let _ = log_line("--- Static Validation Summary ---");
507
  let _ = log_line(&format!(" {}", result.summary_line()));
508
 
509
+ // 6. Install hooks (vtable + inline)
510
+ match hooks::install_all() {
511
+ Ok(()) => {}
512
+ Err(e) => { let _ = log_line(&format!("[ERROR] Hook installation failed: {}", e)); }
513
+ }
514
+
515
+ // 7. Deferred validation via polling (for DDGameWrapper — hooks don't cover this yet)
516
  let _ = log_line("");
517
  let _ = log_line("--- Deferred Validation (polling) ---");
518
  std::thread::spawn(move || {