Skip to content

Commit

Permalink
Merge pull request #8 from rkr35/hook_process_event
Browse files Browse the repository at this point in the history
Hook process event
  • Loading branch information
rkr35 authored Jun 28, 2020
2 parents 32f0899 + 09e7086 commit 230ae52
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 82 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/target
Cargo.lock
.vs
src/sdk.rs
src/hook/sdk.rs
9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
37 changes: 34 additions & 3 deletions journal/20200627.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
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.
55 changes: 55 additions & 0 deletions journal/20200628.txt
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 12 additions & 3 deletions src/dump/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()");

Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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<Item = Option<&str>> {
self.variants.iter().map(|n| n.name())
}
}

let name = helper::resolve_duplicate(object)?;

if name.starts_with("Default__") {
Expand Down
21 changes: 0 additions & 21 deletions src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,6 @@ pub unsafe fn cast<To>(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()
Expand Down Expand Up @@ -223,12 +208,6 @@ pub struct Enum {
pub variants: Array<NameIndex>,
}

impl Enum {
pub unsafe fn variants(&self) -> impl Iterator<Item = Option<&str>> {
self.variants.iter().map(|n| n.name())
}
}

impl Deref for Enum {
type Target = Field;

Expand Down
14 changes: 14 additions & 0 deletions src/hook/bitfield.rs
Original file line number Diff line number Diff line change
@@ -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;
}
}
107 changes: 107 additions & 0 deletions src/hook/mod.rs
Original file line number Diff line number Diff line change
@@ -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, Error> {
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<HashSet<String>> = 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);
}
Loading

0 comments on commit 230ae52

Please sign in to comment.