From 31e33ab19659a1a33cac8ca5d3972101a7d55ee1 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sat, 27 Jun 2020 15:03:47 -0400 Subject: [PATCH 01/16] Refactor pattern finding for global names and objects --- src/lib.rs | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 76793c3..65f8c8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,12 +62,8 @@ enum Error { ObjectsNotFound, } -unsafe fn find_globals() -> Result<(), Error> { - let _time = TimeIt::new("find globals"); - - let game = Module::from("BorderlandsPreSequel.exe")?; - - let names_pattern = [ +unsafe fn find_global_names(game: &Module) -> Result<*const Names, Error> { + const PATTERN: [Option; 12] = [ Some(0x66), Some(0x0F), Some(0xEF), @@ -83,14 +79,16 @@ unsafe fn find_globals() -> Result<(), Error> { ]; let global_names = game - .find_pattern(&names_pattern) + .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, @@ -103,11 +101,23 @@ unsafe fn find_globals() -> Result<(), Error> { ]; let global_objects = game - .find_pattern(&objects_pattern) + .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_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); Ok(()) From cd3d38f9333dbb0139a5b8bfb1ddf132b51f127d Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sat, 27 Jun 2020 15:10:57 -0400 Subject: [PATCH 02/16] Update journal: fix typo --- journal/20200627.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/journal/20200627.txt b/journal/20200627.txt index 33a2ae7..7516fa8 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,7 +107,7 @@ 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. From 537c1459d284b19d78cd2f3e3e4bb03470e179c9 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sat, 27 Jun 2020 15:12:39 -0400 Subject: [PATCH 03/16] Find ProcessEvent --- journal/20200627.txt | 5 ++++- src/lib.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/journal/20200627.txt b/journal/20200627.txt index 7516fa8..604a866 100644 --- a/journal/20200627.txt +++ b/journal/20200627.txt @@ -111,4 +111,7 @@ process to the current game instance: = 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. \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 65f8c8c..b80c155 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,7 @@ 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: *const usize = ptr::null(); fn idle() { println!("Idling. Press enter to continue."); @@ -60,6 +61,9 @@ enum Error { #[error("cannot find global objects")] ObjectsNotFound, + + #[error("cannot find ProcessEvent")] + ProcessEventNotFound, } unsafe fn find_global_names(game: &Module) -> Result<*const Names, Error> { @@ -109,6 +113,25 @@ unsafe fn find_global_objects(game: &Module) -> Result<*const Objects, Error> { Ok(global_objects.read_unaligned()) } +unsafe fn find_process_event(game: &Module) -> Result<*const usize, 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 *const usize) +} + unsafe fn find_globals() -> Result<(), Error> { let _time = TimeIt::new("find globals"); @@ -120,6 +143,9 @@ unsafe fn find_globals() -> Result<(), Error> { GLOBAL_OBJECTS = find_global_objects(&game)?; info!("GLOBAL_OBJECTS = {:?}", GLOBAL_OBJECTS); + PROCESS_EVENT = find_process_event(&game)?; + info!("PROCESS_EVENT = {:?}", PROCESS_EVENT); + Ok(()) } From 19569d5a77c6236454a5d8cf2bb97c7bae0844dc Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sat, 27 Jun 2020 16:17:18 -0400 Subject: [PATCH 04/16] Hook ProcessEvent --- Cargo.toml | 1 + journal/20200627.txt | 21 ++++++++++++++++++++- src/lib.rs | 41 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4742490..9ed5149 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ include_sdk = [] [dependencies] codegen = "0.1.3" +detours-sys = { git = "https://github.com/rkr35/detours" } heck = "0.3" log = "0.4" simplelog = "0.8" diff --git a/journal/20200627.txt b/journal/20200627.txt index 604a866..e314b20 100644 --- a/journal/20200627.txt +++ b/journal/20200627.txt @@ -114,4 +114,23 @@ process to the current game instance: 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. \ No newline at end of file +"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 \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index b80c155..5ec5bd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,12 @@ #[cfg(not(all(target_arch = "x86", target_os = "windows")))] compile_error!("You must compile this crate as a 32-bit Windows .DLL."); +use std::ffi::c_void; use std::io::{self, Read}; +use std::mem; use std::ptr; +use detours_sys::{DetourTransactionBegin, DetourUpdateThread, DetourAttach, DetourDetach, DetourTransactionCommit}; use log::{error, info}; use simplelog::{Config, LevelFilter, TermLogger, TerminalMode}; use thiserror::Error; @@ -15,7 +18,7 @@ use winapi::{ um::{ consoleapi::AllocConsole, libloaderapi::{DisableThreadLibraryCalls, FreeLibraryAndExitThread}, - processthreadsapi::CreateThread, + processthreadsapi::{CreateThread, GetCurrentThread}, synchapi::Sleep, wincon::FreeConsole, winnt::DLL_PROCESS_ATTACH, @@ -40,7 +43,7 @@ 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: *const usize = ptr::null(); +pub static mut PROCESS_EVENT: *mut c_void = ptr::null_mut(); fn idle() { println!("Idling. Press enter to continue."); @@ -113,7 +116,7 @@ unsafe fn find_global_objects(game: &Module) -> Result<*const Objects, Error> { Ok(global_objects.read_unaligned()) } -unsafe fn find_process_event(game: &Module) -> Result<*const usize, Error> { +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. @@ -129,7 +132,7 @@ unsafe fn find_process_event(game: &Module) -> Result<*const usize, Error> { let c = b + 4; // 5. The address of ProcessEvent is C + I, where '+' is a wrapping add. - Ok(c.wrapping_add(i) as *const usize) + Ok(c.wrapping_add(i) as *mut _) } unsafe fn find_globals() -> Result<(), Error> { @@ -149,11 +152,41 @@ unsafe fn find_globals() -> Result<(), Error> { Ok(()) } +unsafe fn hook_process_event() { + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + DetourAttach(&mut PROCESS_EVENT, my_process_event as *mut _); + DetourTransactionCommit(); +} + +unsafe fn unhook_process_event() { + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + DetourDetach(&mut PROCESS_EVENT, my_process_event as *mut _); + DetourTransactionCommit(); +} + +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); + + info!("my_process_event"); + + let original = mem::transmute::<*mut c_void, ProcessEvent>(PROCESS_EVENT); + original(this, edx, function, parameters, return_value); +} + unsafe fn run() -> Result<(), Error> { find_globals()?; // dump::names()?; // dump::objects()?; dump::sdk()?; + + hook_process_event(); + + idle(); + + unhook_process_event(); + Ok(()) } From a4a2f5efadd783a4ec30918478a10c644744b99b Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sat, 27 Jun 2020 16:37:51 -0400 Subject: [PATCH 05/16] Print unique events --- src/lib.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5ec5bd8..6f8da10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ use std::mem; use std::ptr; use detours_sys::{DetourTransactionBegin, DetourUpdateThread, DetourAttach, DetourDetach, DetourTransactionCommit}; -use log::{error, info}; +use log::{error, info, warn}; use simplelog::{Config, LevelFilter, TermLogger, TerminalMode}; use thiserror::Error; use winapi::{ @@ -169,7 +169,20 @@ unsafe fn unhook_process_event() { 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); - info!("my_process_event"); + 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); From c987e7169a1c3d941a650a3b2d6334cd133d27d7 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sat, 27 Jun 2020 16:44:41 -0400 Subject: [PATCH 06/16] Update journal: note next step --- journal/20200627.txt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/journal/20200627.txt b/journal/20200627.txt index e314b20..27b362d 100644 --- a/journal/20200627.txt +++ b/journal/20200627.txt @@ -133,4 +133,13 @@ ecx: this, edx: unused, esp+4: function esp+8: parameters -esp+c: return_value \ No newline at end of file +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 From 704a40558c328f9c1196ef3925407bffe77c6583 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 12:41:34 -0400 Subject: [PATCH 07/16] Refactor ProcessEvent detour into hook module --- journal/20200628.txt | 30 +++++++++++++++ src/hook/mod.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 51 ++++--------------------- 3 files changed, 127 insertions(+), 44 deletions(-) create mode 100644 journal/20200628.txt create mode 100644 src/hook/mod.rs diff --git a/journal/20200628.txt b/journal/20200628.txt new file mode 100644 index 0000000..6b0b165 --- /dev/null +++ b/journal/20200628.txt @@ -0,0 +1,30 @@ +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. + +Actually, it looks like I don't need a top-level Error enum for hook just yet. +All I'm doing right now is setting up the detours, and those are infallible. + +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. \ No newline at end of file diff --git a/src/hook/mod.rs b/src/hook/mod.rs new file mode 100644 index 0000000..847db93 --- /dev/null +++ b/src/hook/mod.rs @@ -0,0 +1,90 @@ +use crate::game; +use crate::PROCESS_EVENT; + +use std::ffi::c_void; +use std::mem; + +use detours_sys::{DetourTransactionBegin, DetourUpdateThread, DetourAttach, DetourDetach, DetourTransactionCommit, LONG as DetourErrorCode}; +use log::{error, info, warn}; +use thiserror::Error; +use winapi::um::processthreadsapi::GetCurrentThread; + + +#[derive(Error, Debug)] +pub enum Error { + #[error("detour error: {}", .0)] + Detour(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(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); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 6f8da10..4b7a355 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,8 @@ compile_error!("You must compile this crate as a 32-bit Windows .DLL."); use std::ffi::c_void; use std::io::{self, Read}; -use std::mem; use std::ptr; -use detours_sys::{DetourTransactionBegin, DetourUpdateThread, DetourAttach, DetourDetach, DetourTransactionCommit}; use log::{error, info, warn}; use simplelog::{Config, LevelFilter, TermLogger, TerminalMode}; use thiserror::Error; @@ -18,7 +16,7 @@ use winapi::{ um::{ consoleapi::AllocConsole, libloaderapi::{DisableThreadLibraryCalls, FreeLibraryAndExitThread}, - processthreadsapi::{CreateThread, GetCurrentThread}, + processthreadsapi::CreateThread, synchapi::Sleep, wincon::FreeConsole, winnt::DLL_PROCESS_ATTACH, @@ -30,6 +28,8 @@ mod dump; mod game; use game::{Names, Objects}; +mod hook; + mod macros; mod module; @@ -152,53 +152,16 @@ unsafe fn find_globals() -> Result<(), Error> { Ok(()) } -unsafe fn hook_process_event() { - DetourTransactionBegin(); - DetourUpdateThread(GetCurrentThread()); - DetourAttach(&mut PROCESS_EVENT, my_process_event as *mut _); - DetourTransactionCommit(); -} - -unsafe fn unhook_process_event() { - DetourTransactionBegin(); - DetourUpdateThread(GetCurrentThread()); - DetourDetach(&mut PROCESS_EVENT, my_process_event as *mut _); - DetourTransactionCommit(); -} - -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); -} - unsafe fn run() -> Result<(), Error> { find_globals()?; // dump::names()?; // dump::objects()?; dump::sdk()?; - hook_process_event(); - - idle(); - - unhook_process_event(); + { + let _hook = hook::Hook::new(); + idle(); + } Ok(()) } From 2a37a260efe3557c5087e3ce5d2f7eac41a2f1c4 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 12:42:51 -0400 Subject: [PATCH 08/16] Update journal: remove incorrect assumption --- journal/20200628.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/journal/20200628.txt b/journal/20200628.txt index 6b0b165..a374a35 100644 --- a/journal/20200628.txt +++ b/journal/20200628.txt @@ -23,8 +23,5 @@ 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. -Actually, it looks like I don't need a top-level Error enum for hook just yet. -All I'm doing right now is setting up the detours, and those are infallible. - 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. \ No newline at end of file From f6609cadbbb5a7b4f455cc1c82a5cdd56ce24a98 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 12:52:31 -0400 Subject: [PATCH 09/16] Propagate hook error to lib.rs --- src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4b7a355..3609d38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,6 +56,9 @@ enum Error { #[error("dump error: {0}")] Dump(#[from] dump::Error), + #[error("hook error: {0}")] + Hook(#[from] hook::Error), + #[error("{0}")] Module(#[from] module::Error), @@ -159,7 +162,7 @@ unsafe fn run() -> Result<(), Error> { dump::sdk()?; { - let _hook = hook::Hook::new(); + let _hook = hook::Hook::new()?; idle(); } From ed18f1f4a63a1178a5231eed08e5dc82429803af Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 12:53:18 -0400 Subject: [PATCH 10/16] Include detour func name in error --- src/hook/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hook/mod.rs b/src/hook/mod.rs index 847db93..5f744da 100644 --- a/src/hook/mod.rs +++ b/src/hook/mod.rs @@ -12,8 +12,8 @@ use winapi::um::processthreadsapi::GetCurrentThread; #[derive(Error, Debug)] pub enum Error { - #[error("detour error: {}", .0)] - Detour(DetourErrorCode), + #[error("detour error: {0} returned {1}")] + Detour(&'static str, DetourErrorCode), } /// A helper macro to call Detour functions and wrap any error codes into a @@ -27,7 +27,7 @@ macro_rules! det { if error_code == NO_ERROR { Ok(()) } else { - Err(Error::Detour(error_code)) + Err(Error::Detour(stringify!($call), error_code)) } }} } From d13f08e3102987b92d5b789c2d7a798e9255f2a8 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 14:01:26 -0400 Subject: [PATCH 11/16] Move sdk module into hook module --- .gitignore | 2 +- Cargo.toml | 3 ++- journal/20200628.txt | 19 ++++++++++++++++++- src/dump/mod.rs | 2 +- src/hook/mod.rs | 1 + src/lib.rs | 3 --- 6 files changed, 23 insertions(+), 7 deletions(-) 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 9ed5149..038ccf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,8 @@ lto = "fat" panic = "abort" [features] -include_sdk = [] +hook = [] +dump = [] [dependencies] codegen = "0.1.3" diff --git a/journal/20200628.txt b/journal/20200628.txt index a374a35..531eaae 100644 --- a/journal/20200628.txt +++ b/journal/20200628.txt @@ -24,4 +24,21 @@ 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/src/dump/mod.rs b/src/dump/mod.rs index 70d9755..e97f543 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()"); diff --git a/src/hook/mod.rs b/src/hook/mod.rs index 5f744da..051a718 100644 --- a/src/hook/mod.rs +++ b/src/hook/mod.rs @@ -9,6 +9,7 @@ use log::{error, info, warn}; use thiserror::Error; use winapi::um::processthreadsapi::GetCurrentThread; +mod sdk; #[derive(Error, Debug)] pub enum Error { diff --git a/src/lib.rs b/src/lib.rs index 3609d38..eb89f92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,9 +38,6 @@ 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(); From 68d7ca53164be5e3e79b54c042b661e819ca4d25 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 14:39:15 -0400 Subject: [PATCH 12/16] Move bitfield methods to hook module --- journal/20200628.txt | 7 ++++++- src/dump/mod.rs | 3 ++- src/game.rs | 15 --------------- src/hook/bitfield.rs | 14 ++++++++++++++ src/hook/mod.rs | 1 + 5 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 src/hook/bitfield.rs diff --git a/journal/20200628.txt b/journal/20200628.txt index 531eaae..988beb7 100644 --- a/journal/20200628.txt +++ b/journal/20200628.txt @@ -41,4 +41,9 @@ 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/src/dump/mod.rs b/src/dump/mod.rs index e97f543..e27e01a 100644 --- a/src/dump/mod.rs +++ b/src/dump/mod.rs @@ -136,7 +136,8 @@ 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\ + 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};"); } diff --git a/src/game.rs b/src/game.rs index b3e4766..bbfea76 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() diff --git a/src/hook/bitfield.rs b/src/hook/bitfield.rs new file mode 100644 index 0000000..ac32627 --- /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; + } +} \ No newline at end of file diff --git a/src/hook/mod.rs b/src/hook/mod.rs index 051a718..96320ff 100644 --- a/src/hook/mod.rs +++ b/src/hook/mod.rs @@ -9,6 +9,7 @@ use log::{error, info, warn}; use thiserror::Error; use winapi::um::processthreadsapi::GetCurrentThread; +mod bitfield; mod sdk; #[derive(Error, Debug)] From f7191c59c57a41551cfd601c0711742a4a345529 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 16:06:46 -0400 Subject: [PATCH 13/16] Move `wide_format` macro Only Module uses this macro, so there's no need to have a separate `macros` module. --- src/lib.rs | 2 -- src/macros.rs | 21 --------------------- src/module.rs | 24 ++++++++++++++++++++++-- 3 files changed, 22 insertions(+), 25 deletions(-) delete mode 100644 src/macros.rs diff --git a/src/lib.rs b/src/lib.rs index eb89f92..68f4806 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,8 +30,6 @@ use game::{Names, Objects}; mod hook; -mod macros; - mod module; use module::Module; 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")] From be6b038d432edeb4f8deaf6111a3cb0b4d5b1297 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 16:10:34 -0400 Subject: [PATCH 14/16] Separate `dump` and `hook` features --- Cargo.toml | 11 ++++++----- journal/20200628.txt | 8 +++++++- src/lib.rs | 25 ++++++++++++++++++------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 038ccf3..d712992 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,13 +15,14 @@ lto = "fat" panic = "abort" [features] -hook = [] -dump = [] +default = ["hook"] +dump = ["codegen", "heck"] +hook = ["detours-sys"] [dependencies] -codegen = "0.1.3" -detours-sys = { git = "https://github.com/rkr35/detours" } -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/20200628.txt b/journal/20200628.txt index 988beb7..0baa6c6 100644 --- a/journal/20200628.txt +++ b/journal/20200628.txt @@ -46,4 +46,10 @@ 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. \ No newline at end of file +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/lib.rs b/src/lib.rs index 68f4806..bee6f73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,12 @@ #[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; @@ -23,11 +29,13 @@ use winapi::{ }, }; +#[cfg(feature = "dump")] mod dump; mod game; use game::{Names, Objects}; +#[cfg(feature = "hook")] mod hook; mod module; @@ -49,9 +57,11 @@ 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}")] @@ -152,15 +162,18 @@ unsafe fn find_globals() -> Result<(), Error> { 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(()) } @@ -173,8 +186,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); } From 0552faaa900274ca94028b84a0739c199b410d0b Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 16:13:18 -0400 Subject: [PATCH 15/16] Move `Enum::variants()` to call site I was getting a warning that this method is dead code when compiling with the `hook` feature. --- src/dump/mod.rs | 6 ++++++ src/game.rs | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dump/mod.rs b/src/dump/mod.rs index e27e01a..a0ae7be 100644 --- a/src/dump/mod.rs +++ b/src/dump/mod.rs @@ -179,6 +179,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 bbfea76..33363a7 100644 --- a/src/game.rs +++ b/src/game.rs @@ -208,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; From 09e70869a6443d1ae76265a1ff488e0d583ae089 Mon Sep 17 00:00:00 2001 From: rkr35 Date: Sun, 28 Jun 2020 16:16:29 -0400 Subject: [PATCH 16/16] cargo fmt --- src/dump/mod.rs | 10 ++++++---- src/hook/bitfield.rs | 2 +- src/hook/mod.rs | 25 ++++++++++++++++++++----- src/lib.rs | 42 +++++++++++++++++++++++++++++------------- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/dump/mod.rs b/src/dump/mod.rs index a0ae7be..d66cdd5 100644 --- a/src/dump/mod.rs +++ b/src/dump/mod.rs @@ -136,9 +136,11 @@ fn add_crate_attributes(scope: &mut Scope) { } fn add_imports(scope: &mut Scope) { - 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};"); + 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> { @@ -184,7 +186,7 @@ unsafe fn write_enumeration(sdk: &mut Scope, object: *const Object) -> Result<() self.variants.iter().map(|n| n.name()) } } - + let name = helper::resolve_duplicate(object)?; if name.starts_with("Default__") { diff --git a/src/hook/bitfield.rs b/src/hook/bitfield.rs index ac32627..a940b0f 100644 --- a/src/hook/bitfield.rs +++ b/src/hook/bitfield.rs @@ -11,4 +11,4 @@ pub fn set_bit(bitfield: &mut u32, bit: u8, value: bool) { } else { *bitfield &= !mask; } -} \ No newline at end of file +} diff --git a/src/hook/mod.rs b/src/hook/mod.rs index 96320ff..51c9fe8 100644 --- a/src/hook/mod.rs +++ b/src/hook/mod.rs @@ -4,7 +4,10 @@ use crate::PROCESS_EVENT; use std::ffi::c_void; use std::mem; -use detours_sys::{DetourTransactionBegin, DetourUpdateThread, DetourAttach, DetourDetach, DetourTransactionCommit, LONG as DetourErrorCode}; +use detours_sys::{ + DetourAttach, DetourDetach, DetourTransactionBegin, DetourTransactionCommit, + DetourUpdateThread, LONG as DetourErrorCode, +}; use log::{error, info, warn}; use thiserror::Error; use winapi::um::processthreadsapi::GetCurrentThread; @@ -31,7 +34,7 @@ macro_rules! det { } else { Err(Error::Detour(stringify!($call), error_code)) } - }} + }}; } pub struct Hook; @@ -69,8 +72,20 @@ unsafe fn unhook_process_event() -> Result<(), Error> { 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); +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; @@ -89,4 +104,4 @@ unsafe extern "fastcall" fn my_process_event(this: &game::Object, edx: usize, fu let original = mem::transmute::<*mut c_void, ProcessEvent>(PROCESS_EVENT); original(this, edx, function, parameters, return_value); -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index bee6f73..62cca66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,9 +93,7 @@ unsafe fn find_global_names(game: &Module) -> Result<*const Names, Error> { None, ]; - let global_names = game - .find_pattern(&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; @@ -115,20 +113,36 @@ unsafe fn find_global_objects(game: &Module) -> Result<*const Objects, Error> { Some(0xB9), ]; - let global_objects = game - .find_pattern(&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; - + 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)]; + 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)?; + 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; @@ -162,18 +176,20 @@ unsafe fn find_globals() -> Result<(), Error> { unsafe fn run() -> Result<(), Error> { find_globals()?; - - #[cfg(feature = "dump")] { + + #[cfg(feature = "dump")] + { // dump::names()?; // dump::objects()?; dump::sdk()?; } - #[cfg(feature = "hook")] { + #[cfg(feature = "hook")] + { let _hook = hook::Hook::new()?; idle(); } - + Ok(()) }