diff --git a/.gitignore b/.gitignore index aaa7274..96e5853 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target Cargo.lock .vs -src/sdk.rs +src/hook/sdk.rs diff --git a/Cargo.toml b/Cargo.toml index 4742490..d712992 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,14 @@ lto = "fat" panic = "abort" [features] -include_sdk = [] +default = ["hook"] +dump = ["codegen", "heck"] +hook = ["detours-sys"] [dependencies] -codegen = "0.1.3" -heck = "0.3" +codegen = { version = "0.1.3", optional = true } +detours-sys = { git = "https://github.com/rkr35/detours", optional = true } +heck = { version = "0.3", optional = true } log = "0.4" simplelog = "0.8" thiserror = "1.0" diff --git a/journal/20200627.txt b/journal/20200627.txt index 33a2ae7..27b362d 100644 --- a/journal/20200627.txt +++ b/journal/20200627.txt @@ -92,7 +92,7 @@ So here's the entire process for finding the address of ProcessEvent: Call that immediate I. 4. Offset B by four bytes to get the address of the instruction following the CALL instruction. Call that address C. -5. The address of ProcessEvent is B + I, where '+' is a wrapping add. +5. The address of ProcessEvent is C + I, where '+' is a wrapping add. * We need to do an unaligned pointer read of four bytes because there is no guarantee that B is aligned to four bytes; in general, we cannot assume the @@ -107,8 +107,39 @@ process to the current game instance: 3. I = *B = 0xFFFF8E3D. 4. C = B + 4 = 0x1154BAF‬ + 4 = 0x1154BB3.‬ 5. ProcessEvent - = B + I where '+' is wrapping add + = C + I where '+' is wrapping add = 0x1154BB3 + 0xFFFF8E3D where '+' is wrapping add = 0x114D9F0. -Great, so let's put that into code. \ No newline at end of file +Great, so let's put that into code. + +Issue#5 is an imperative to hook ProcessEvent. I'll create a separate +"hook_process_event" branch that I can place into a PR that links #5. + +Okay, the injected .DLL was able to find the ProcessEvent address. I'm going to +write a messy ProcessEvent hook that will print out the unique events that go +through the function. + +I got that hook working, although it's currently only printing +"my_process_event". + +I'm using the `fastcall` calling convention since ProcessEvent is a virtual +function using the `thiscall` calling convention, but Rust doesn't have a stable +`thiscall`, so I need to use `fastcall` to access the `this` pointer (the +UObject that's calling ProcessEvent), which is stored in ecx. + +So my detoured function's arguments are at: +ecx: this, +edx: unused, +esp+4: function +esp+8: parameters +esp+c: return_value + +I added code to print the unique events going through ProcessEvent. That code +assumes that multiple threads won't call ProcessEvent at the same time, which is +an assumption that I haven't verified yet, although I have code from the +Sven Co-op hook that makes that verification easy to do. + +The next step would be to introduce features in Cargo.toml to separate the SDK +generator from the hook. Right now, I'm running the hook right after generating +the SDK. \ No newline at end of file diff --git a/journal/20200628.txt b/journal/20200628.txt new file mode 100644 index 0000000..0baa6c6 --- /dev/null +++ b/journal/20200628.txt @@ -0,0 +1,55 @@ +Today I want to add two features to Cargo.toml, one for generating the SDK, and +one for hooking the game. + +I'll have to refactor a good chunk of the code because the information that the +SDK generator needs is mostly different from the information the hook needs, +although both share some common code, such as a few game structures and +functions. + +I'll say that generating the SDK is called "dumping". I already have a `dump` +module in place that contains most of the generator-specific code. I imagine +I'll have to move things in and out of that `dump` module when I create the +`hook` module. + +To be more specific, here are functionalities that `hook` will need: + 1. Access to the generated SDK. + 2. Access to the helper methods that the SDK relies on. + 3. Access to the handwritten game structures that the hook needs. + 4. Access to the helper methods that the handwritten stuctures rely on. + +So let me create that hook module and start moving things in there. + +I know that module will need at least a top-level Error enum to communicate +hooking errors to lib.rs. I also know that a RAII structure will be useful to +set up the hook without forgetting to clean it up when we unload the .DLL. + +Okay, I got the hook module up, and the printing of unique events through my +detoured ProcessEvent is still working, so the refactor went well. + +Per (1) above, the hook module only needs access to the generated SDK. So let me +make two changes: one, change the (hardcoded) path that the dumper places the +generated SDK so that the sdk.rs is under the hook module; and two, include the +sdk module in the hook module. + +Done. I also had to make changes to remove the old `include_sdk` feature that I +was using to incrementally test whether the generated SDK compiles; to ignore +the sdk.rs under the new path; and to introduce the two new features `dump` and +`hook`. + +While looking at the generated sdk.rs, I noticed that a lot of constants are +duplicated. For example, there are 14 "// WPS_MusicVolume = 107". I'm not going +to dedup those constants, but instead, I'm going to prepend the module and +submodule for each constant. The module and submodule names will provide context +as to where the constant can be used. Let me make an issue on GitHub so I don't +forget. + +There are two functions that the generated SDK uses for querying and modifying +bitfields: is_bit_set() and set_bit(). I placed those functions in game.rs, but +since they're only used in the hook, it makes better sense to place them in a +module under the hook module. + +Okay, now I'm going to try to selectively compile the dump and hook code based +on their respective Cargo.toml features. I also need to add a safeguard to +prevent both features from being enabled at the same time. + +Done. I'm glad I was able to conditionally compile error variants as well. \ No newline at end of file diff --git a/src/dump/mod.rs b/src/dump/mod.rs index 70d9755..d66cdd5 100644 --- a/src/dump/mod.rs +++ b/src/dump/mod.rs @@ -92,7 +92,7 @@ pub unsafe fn _objects() -> Result<(), Error> { } pub unsafe fn sdk() -> Result<(), Error> { - const SDK_PATH: &str = r"C:\Users\Royce\Desktop\repos\blps\src\sdk.rs"; + const SDK_PATH: &str = r"C:\Users\Royce\Desktop\repos\blps\src\hook\sdk.rs"; let _time = TimeIt::new("sdk()"); @@ -136,8 +136,11 @@ fn add_crate_attributes(scope: &mut Scope) { } fn add_imports(scope: &mut Scope) { - scope.raw("use crate::game::{Array, FString, is_bit_set, NameIndex, ScriptDelegate, ScriptInterface, set_bit};\n\ - use std::ops::{Deref, DerefMut};"); + scope.raw( + "use crate::game::{Array, FString, NameIndex, ScriptDelegate, ScriptInterface};\n\ + use crate::hook::bitfield::{is_bit_set, set_bit};\n\ + use std::ops::{Deref, DerefMut};", + ); } unsafe fn write_object(sdk: &mut Scope, object: *const Object) -> Result<(), Error> { @@ -178,6 +181,12 @@ unsafe fn write_constant(sdk: &mut Scope, object: *const Object) -> Result<(), E } unsafe fn write_enumeration(sdk: &mut Scope, object: *const Object) -> Result<(), Error> { + impl Enum { + pub unsafe fn variants(&self) -> impl Iterator> { + self.variants.iter().map(|n| n.name()) + } + } + let name = helper::resolve_duplicate(object)?; if name.starts_with("Default__") { diff --git a/src/game.rs b/src/game.rs index b3e4766..33363a7 100644 --- a/src/game.rs +++ b/src/game.rs @@ -15,21 +15,6 @@ pub unsafe fn cast(from: &Object) -> &To { &*(from as *const Object as *const To) } -pub fn is_bit_set(bitfield: u32, bit: u8) -> bool { - let mask = 1 << bit; - bitfield & mask == mask -} - -pub fn set_bit(bitfield: &mut u32, bit: u8, value: bool) { - let mask = 1 << bit; - - if value { - *bitfield |= mask; - } else { - *bitfield &= !mask; - } -} - impl Objects { pub unsafe fn find(&self, full_name: &str) -> Option<*const Object> { self.iter() @@ -223,12 +208,6 @@ pub struct Enum { pub variants: Array, } -impl Enum { - pub unsafe fn variants(&self) -> impl Iterator> { - self.variants.iter().map(|n| n.name()) - } -} - impl Deref for Enum { type Target = Field; diff --git a/src/hook/bitfield.rs b/src/hook/bitfield.rs new file mode 100644 index 0000000..a940b0f --- /dev/null +++ b/src/hook/bitfield.rs @@ -0,0 +1,14 @@ +pub fn is_bit_set(bitfield: u32, bit: u8) -> bool { + let mask = 1 << bit; + bitfield & mask == mask +} + +pub fn set_bit(bitfield: &mut u32, bit: u8, value: bool) { + let mask = 1 << bit; + + if value { + *bitfield |= mask; + } else { + *bitfield &= !mask; + } +} diff --git a/src/hook/mod.rs b/src/hook/mod.rs new file mode 100644 index 0000000..51c9fe8 --- /dev/null +++ b/src/hook/mod.rs @@ -0,0 +1,107 @@ +use crate::game; +use crate::PROCESS_EVENT; + +use std::ffi::c_void; +use std::mem; + +use detours_sys::{ + DetourAttach, DetourDetach, DetourTransactionBegin, DetourTransactionCommit, + DetourUpdateThread, LONG as DetourErrorCode, +}; +use log::{error, info, warn}; +use thiserror::Error; +use winapi::um::processthreadsapi::GetCurrentThread; + +mod bitfield; +mod sdk; + +#[derive(Error, Debug)] +pub enum Error { + #[error("detour error: {0} returned {1}")] + Detour(&'static str, DetourErrorCode), +} + +/// A helper macro to call Detour functions and wrap any error codes into a +/// variant of the top-level `Error` enum. +macro_rules! det { + ($call:expr) => {{ + const NO_ERROR: DetourErrorCode = 0; + + let error_code = $call; + + if error_code == NO_ERROR { + Ok(()) + } else { + Err(Error::Detour(stringify!($call), error_code)) + } + }}; +} + +pub struct Hook; + +impl Hook { + pub unsafe fn new() -> Result { + hook_process_event()?; + Ok(Hook) + } +} + +impl Drop for Hook { + fn drop(&mut self) { + unsafe { + if let Err(e) = unhook_process_event() { + error!("{}", e); + } + } + } +} + +unsafe fn hook_process_event() -> Result<(), Error> { + det!(DetourTransactionBegin())?; + det!(DetourUpdateThread(GetCurrentThread()))?; + det!(DetourAttach(&mut PROCESS_EVENT, my_process_event as *mut _))?; + det!(DetourTransactionCommit())?; + Ok(()) +} + +unsafe fn unhook_process_event() -> Result<(), Error> { + det!(DetourTransactionBegin())?; + det!(DetourUpdateThread(GetCurrentThread()))?; + det!(DetourDetach(&mut PROCESS_EVENT, my_process_event as *mut _))?; + det!(DetourTransactionCommit())?; + Ok(()) +} + +unsafe extern "fastcall" fn my_process_event( + this: &game::Object, + edx: usize, + function: &game::Function, + parameters: *mut c_void, + return_value: *mut c_void, +) { + type ProcessEvent = unsafe extern "fastcall" fn( + this: &game::Object, + _edx: usize, + function: &game::Function, + parameters: *mut c_void, + return_value: *mut c_void, + ); + + if let Some(full_name) = function.full_name() { + use std::collections::HashSet; + static mut UNIQUE_EVENTS: Option> = None; + + if let Some(set) = UNIQUE_EVENTS.as_mut() { + if set.insert(full_name.clone()) { + info!("{}", full_name); + } + } else { + UNIQUE_EVENTS = Some(HashSet::new()); + } + } else { + warn!("couldn't get full name"); + } + + let original = mem::transmute::<*mut c_void, ProcessEvent>(PROCESS_EVENT); + original(this, edx, function, parameters, return_value); +} diff --git a/src/lib.rs b/src/lib.rs index 76793c3..62cca66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,10 +4,17 @@ #[cfg(not(all(target_arch = "x86", target_os = "windows")))] compile_error!("You must compile this crate as a 32-bit Windows .DLL."); +#[cfg(not(any(feature = "dump", feature = "hook")))] +compile_error!("You must enable exactly one of these features: dump, hook"); + +#[cfg(all(feature = "dump", feature = "hook"))] +compile_error!("You cannot generate an SDK and hook the game at the same time. Disable a feature."); + +use std::ffi::c_void; use std::io::{self, Read}; use std::ptr; -use log::{error, info}; +use log::{error, info, warn}; use simplelog::{Config, LevelFilter, TermLogger, TerminalMode}; use thiserror::Error; use winapi::{ @@ -22,12 +29,14 @@ use winapi::{ }, }; +#[cfg(feature = "dump")] mod dump; mod game; use game::{Names, Objects}; -mod macros; +#[cfg(feature = "hook")] +mod hook; mod module; use module::Module; @@ -35,11 +44,9 @@ use module::Module; mod timeit; use timeit::TimeIt; -#[cfg(feature = "include_sdk")] -mod sdk; - pub static mut GLOBAL_NAMES: *const Names = ptr::null(); pub static mut GLOBAL_OBJECTS: *const Objects = ptr::null(); +pub static mut PROCESS_EVENT: *mut c_void = ptr::null_mut(); fn idle() { println!("Idling. Press enter to continue."); @@ -50,8 +57,13 @@ fn idle() { #[derive(Error, Debug)] enum Error { #[error("dump error: {0}")] + #[cfg(feature = "dump")] Dump(#[from] dump::Error), + #[error("hook error: {0}")] + #[cfg(feature = "hook")] + Hook(#[from] hook::Error), + #[error("{0}")] Module(#[from] module::Error), @@ -60,14 +72,13 @@ enum Error { #[error("cannot find global objects")] ObjectsNotFound, -} - -unsafe fn find_globals() -> Result<(), Error> { - let _time = TimeIt::new("find globals"); - let game = Module::from("BorderlandsPreSequel.exe")?; + #[error("cannot find ProcessEvent")] + ProcessEventNotFound, +} - let names_pattern = [ +unsafe fn find_global_names(game: &Module) -> Result<*const Names, Error> { + const PATTERN: [Option; 12] = [ Some(0x66), Some(0x0F), Some(0xEF), @@ -82,15 +93,15 @@ unsafe fn find_globals() -> Result<(), Error> { None, ]; - let global_names = game - .find_pattern(&names_pattern) - .ok_or(Error::NamesNotFound)?; + let global_names = game.find_pattern(&PATTERN).ok_or(Error::NamesNotFound)?; + let global_names = (global_names + 8) as *const *const Names; - let global_names = global_names.read_unaligned(); - GLOBAL_NAMES = global_names; - info!("GLOBAL_NAMES = {:?}", GLOBAL_NAMES); - let objects_pattern = [ + Ok(global_names.read_unaligned()) +} + +unsafe fn find_global_objects(game: &Module) -> Result<*const Objects, Error> { + const PATTERN: [Option; 9] = [ Some(0x8B), Some(0x0D), None, @@ -102,22 +113,83 @@ unsafe fn find_globals() -> Result<(), Error> { Some(0xB9), ]; - let global_objects = game - .find_pattern(&objects_pattern) - .ok_or(Error::ObjectsNotFound)?; + let global_objects = game.find_pattern(&PATTERN).ok_or(Error::ObjectsNotFound)?; + let global_objects = (global_objects + 2) as *const *const Objects; - let global_objects = global_objects.read_unaligned(); - GLOBAL_OBJECTS = global_objects; + + Ok(global_objects.read_unaligned()) +} + +unsafe fn find_process_event(game: &Module) -> Result<*mut c_void, Error> { + const PATTERN: [Option; 15] = [ + Some(0x50), + Some(0x51), + Some(0x52), + Some(0x8B), + Some(0xCE), + Some(0xE8), + None, + None, + None, + None, + Some(0x5E), + Some(0x5D), + Some(0xC2), + Some(0x0C), + Some(0x00), + ]; + + // 1. Find the first address A that matches the above pattern. + let a = game + .find_pattern(&PATTERN) + .ok_or(Error::ProcessEventNotFound)?; + + // 2. Offset A by six bytes to get the address of the CALL immediate. Call that address B. + let b = a + 6; + + // 3. Do an unaligned* usize pointer read operation on B to get the call immediate. Call that immediate I. + let i = (b as *const usize).read_unaligned(); + + // 4. Offset B by four bytes to get the address of the instruction following the CALL instruction. Call that address C. + let c = b + 4; + + // 5. The address of ProcessEvent is C + I, where '+' is a wrapping add. + Ok(c.wrapping_add(i) as *mut _) +} + +unsafe fn find_globals() -> Result<(), Error> { + let _time = TimeIt::new("find globals"); + + let game = Module::from("BorderlandsPreSequel.exe")?; + + GLOBAL_NAMES = find_global_names(&game)?; + info!("GLOBAL_NAMES = {:?}", GLOBAL_NAMES); + + GLOBAL_OBJECTS = find_global_objects(&game)?; info!("GLOBAL_OBJECTS = {:?}", GLOBAL_OBJECTS); + PROCESS_EVENT = find_process_event(&game)?; + info!("PROCESS_EVENT = {:?}", PROCESS_EVENT); + Ok(()) } unsafe fn run() -> Result<(), Error> { find_globals()?; - // dump::names()?; - // dump::objects()?; - dump::sdk()?; + + #[cfg(feature = "dump")] + { + // dump::names()?; + // dump::objects()?; + dump::sdk()?; + } + + #[cfg(feature = "hook")] + { + let _hook = hook::Hook::new()?; + idle(); + } + Ok(()) } @@ -130,8 +202,6 @@ unsafe extern "system" fn on_attach(dll: LPVOID) -> DWORD { } else { info!("Initialized logger."); - let _time = TimeIt::new("run()"); - if let Err(e) = run() { error!("{}", e); } diff --git a/src/macros.rs b/src/macros.rs deleted file mode 100644 index 32ba35c..0000000 --- a/src/macros.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[macro_export] -macro_rules! wide_format { - ($format:literal, $($arg:tt)*) => {{ - use std::ffi::OsStr; - use std::os::windows::ffi::OsStrExt; - - let mut widened: Vec = OsStr::new(&format!($format, $($arg)*)) - .encode_wide() - .map(|byte| if byte == 0 { - const REPLACEMENT_CHARACTER: u16 = 0xFFFD; - REPLACEMENT_CHARACTER - } else { - byte - }) - .collect(); - - widened.push(0); - - widened - }} -} diff --git a/src/module.rs b/src/module.rs index 7cd81a1..9b3248a 100644 --- a/src/module.rs +++ b/src/module.rs @@ -1,5 +1,3 @@ -use crate::wide_format; - use std::mem::{self, MaybeUninit}; use thiserror::Error; @@ -10,6 +8,28 @@ use winapi::um::{ psapi::{GetModuleInformation, MODULEINFO}, }; +#[macro_export] +macro_rules! wide_format { + ($format:literal, $($arg:tt)*) => {{ + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + + let mut widened: Vec = OsStr::new(&format!($format, $($arg)*)) + .encode_wide() + .map(|byte| if byte == 0 { + const REPLACEMENT_CHARACTER: u16 = 0xFFFD; + REPLACEMENT_CHARACTER + } else { + byte + }) + .collect(); + + widened.push(0); + + widened + }} +} + #[derive(Error, Debug)] pub enum ErrorKind { #[error("failed to get a handle to the module")]