feat(validator): add function hooking via MinHook and vtable swapping
Browse filesTwo-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 +2 -0
- Cargo.lock +119 -0
- crates/openwa-validator/Cargo.toml +4 -0
- crates/openwa-validator/src/hooks/detour.rs +124 -0
- crates/openwa-validator/src/hooks/mod.rs +19 -0
- crates/openwa-validator/src/hooks/vtable.rs +126 -0
- crates/openwa-validator/src/lib.rs +11 -4
.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.
|
| 508 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 || {
|