From 239c872b65703f16621e6d85b933ea63f662470c Mon Sep 17 00:00:00 2001 From: a2x <45197573+a2x@users.noreply.github.com> Date: Thu, 26 Oct 2023 15:41:34 +1000 Subject: [PATCH] Release 1.1.4 --- .gitattributes | 1 + .gitignore | 1 + Cargo.toml | 8 +- README.md | 17 +- src/builder/cpp_file_builder.rs | 46 ++-- src/builder/csharp_file_builder.rs | 37 ++- src/builder/file_builder.rs | 64 ++++- src/builder/json_file_builder.rs | 23 +- src/builder/mod.rs | 20 +- src/builder/python_file_builder.rs | 29 +- src/builder/rust_file_builder.rs | 41 ++- src/config.rs | 51 +++- src/dumper/interfaces.rs | 149 +++++++++++ src/dumper/mod.rs | 154 +++++++++++ src/{dumpers => dumper}/offsets.rs | 253 ++++++++++-------- src/dumper/schemas.rs | 80 ++++++ src/dumpers/interfaces.rs | 69 ----- src/dumpers/mod.rs | 109 -------- src/dumpers/schemas.rs | 51 ---- src/main.rs | 79 ++++-- src/mem/address.rs | 114 -------- src/mem/mod.rs | 3 - src/remote/module.rs | 254 ------------------ src/remote/process.rs | 285 -------------------- src/sdk/interface.rs | 43 --- src/sdk/mod.rs | 2 - src/sdk/schema_class_field_data.rs | 65 +++-- src/sdk/schema_class_info.rs | 88 ++++-- src/sdk/schema_system.rs | 48 ++-- src/sdk/schema_system_type_scope.rs | 62 +++-- src/sdk/schema_type.rs | 50 +++- src/sdk/schema_type_declared_class.rs | 39 ++- src/sdk/utl_ts_hash.rs | 138 ++++++++-- src/util/address.rs | 211 +++++++++++++++ src/{remote => util}/mod.rs | 2 + src/util/module.rs | 156 +++++++++++ src/util/process.rs | 371 ++++++++++++++++++++++++++ 37 files changed, 1905 insertions(+), 1308 deletions(-) create mode 100644 src/dumper/interfaces.rs create mode 100644 src/dumper/mod.rs rename src/{dumpers => dumper}/offsets.rs (54%) create mode 100644 src/dumper/schemas.rs delete mode 100644 src/dumpers/interfaces.rs delete mode 100644 src/dumpers/mod.rs delete mode 100644 src/dumpers/schemas.rs delete mode 100644 src/mem/address.rs delete mode 100644 src/mem/mod.rs delete mode 100644 src/remote/module.rs delete mode 100644 src/remote/process.rs delete mode 100644 src/sdk/interface.rs create mode 100644 src/util/address.rs rename src/{remote => util}/mod.rs (66%) create mode 100644 src/util/module.rs create mode 100644 src/util/process.rs diff --git a/.gitattributes b/.gitattributes index 6a0e8fe..4458b85 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ *.cs linguist-detectable=false *.hpp linguist-detectable=false *.json linguist-detectable=false +*.py linguist-detectable=false *.rs linguist-detectable=true diff --git a/.gitignore b/.gitignore index 1db65aa..9ac52c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ +.vscode/ Cargo.lock target/ diff --git a/Cargo.toml b/Cargo.toml index 121a792..632ec58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cs2-dumper" -version = "1.1.3" +version = "1.1.4" authors = ["a2x"] edition = "2021" readme = "README.md" @@ -11,13 +11,13 @@ license = "MIT" anyhow = "1.0" chrono = "0.4" clap = { version = "4.4", features = ["derive"] } -convert_case = "0.6" +goblin = { git = "https://github.com/m4b/goblin" } lazy_static = "1.4" log = "0.4" -regex = "1.9" +regex = "1.10" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -simple_logger = "4.2" +simplelog = { version = "0.12", features = ["paris"] } [dependencies.windows] version = "0.51" diff --git a/README.md b/README.md index 57080b7..d64f85c 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,24 @@ # cs2-dumper -External offsets/interfaces dumper for Counter-Strike: 2, written in Rust. +An external offsets/interfaces dumper for Counter-Strike 2, written in Rust. + +# Usage + +You can either download the latest release from [Releases](https://github.com/a2x/cs2-dumper/releases) or build it yourself. Note that building it yourself requires Rust's nightly toolchain. + +If you want to see more detailed runtime messages, you can pass the `--verbose` flag. + +For a complete list of all available flags, use `--help`. # Generated Files -Generated files are stored in the `generated` directory. +By default, processed files are stored in the `generated` directory. However, this can be modified by specifying your desired output folder using the `--output` flag. 📂 [Pre-generated Files](./generated) # Running Tests - -`cargo test -- --nocapture` +To run tests, use the following command: `cargo test -- --nocapture`. # License -Please refer to the [LICENSE](./LICENSE) file for more details. +Please refer to the [LICENSE](./LICENSE) file for more details. \ No newline at end of file diff --git a/src/builder/cpp_file_builder.rs b/src/builder/cpp_file_builder.rs index 2139614..8efd846 100644 --- a/src/builder/cpp_file_builder.rs +++ b/src/builder/cpp_file_builder.rs @@ -1,8 +1,9 @@ -use std::io::{Result, Write}; - use super::FileBuilder; -/// Represents a C++ header file builder. +use std::io::{Result, Write}; + +/// A structure representing a builder for C++ header files. +/// The builder implements the `FileBuilder` trait. #[derive(Debug, PartialEq)] pub struct CppFileBuilder; @@ -12,10 +13,7 @@ impl FileBuilder for CppFileBuilder { } fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()> { - write!(output, "#pragma once\n\n")?; - write!(output, "#include \n\n")?; - - Ok(()) + write!(output, "#pragma once\n\n#include \n\n") } fn write_namespace( @@ -24,13 +22,9 @@ impl FileBuilder for CppFileBuilder { name: &str, comment: Option<&str>, ) -> Result<()> { - if let Some(comment) = comment { - write!(output, "namespace {} {{ // {}\n", name, comment)?; - } else { - write!(output, "namespace {} {{\n", name)?; - } + let comment = comment.map_or(String::new(), |c| format!("// {}", c)); - Ok(()) + write!(output, "namespace {} {{ {}\n", name, comment) } fn write_variable( @@ -39,24 +33,20 @@ impl FileBuilder for CppFileBuilder { name: &str, value: usize, comment: Option<&str>, + indentation: Option, ) -> Result<()> { - match comment { - Some(comment) => write!( - output, - " constexpr std::ptrdiff_t {} = {:#X}; // {}\n", - name, value, comment - ), - None => write!( - output, - " constexpr std::ptrdiff_t {} = {:#X};\n", - name, value - ), - } + let indentation = " ".repeat(indentation.unwrap_or(4)); + + let comment = comment.map_or(String::new(), |c| format!("// {}", c)); + + write!( + output, + "{}constexpr std::ptrdiff_t {} = {:#X}; {}\n", + indentation, name, value, comment + ) } fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> { - write!(output, "{}", if eof { "}" } else { "}\n\n" })?; - - Ok(()) + write!(output, "{}", if eof { "}" } else { "}\n\n" }) } } diff --git a/src/builder/csharp_file_builder.rs b/src/builder/csharp_file_builder.rs index affa4d8..db8e0e0 100644 --- a/src/builder/csharp_file_builder.rs +++ b/src/builder/csharp_file_builder.rs @@ -1,8 +1,9 @@ -use std::io::{Result, Write}; - use super::FileBuilder; -/// Represents a C# file builder. +use std::io::{Result, Write}; + +/// A structure representing a builder for C# files. +/// The builder implements the `FileBuilder` trait. #[derive(Debug, PartialEq)] pub struct CSharpFileBuilder; @@ -21,13 +22,9 @@ impl FileBuilder for CSharpFileBuilder { name: &str, comment: Option<&str>, ) -> Result<()> { - if let Some(comment) = comment { - write!(output, "public static class {} {{ // {}\n", name, comment)?; - } else { - write!(output, "public static class {} {{\n", name)?; - } + let comment = comment.map_or(String::new(), |c| format!("// {}", c)); - Ok(()) + write!(output, "public static class {} {{ {}\n", name, comment) } fn write_variable( @@ -36,20 +33,20 @@ impl FileBuilder for CSharpFileBuilder { name: &str, value: usize, comment: Option<&str>, + indentation: Option, ) -> Result<()> { - match comment { - Some(comment) => write!( - output, - " public const nint {} = {:#X}; // {}\n", - name, value, comment - ), - None => write!(output, " public const nint {} = {:#X};\n", name, value), - } + let indentation = " ".repeat(indentation.unwrap_or(4)); + + let comment = comment.map_or(String::new(), |c| format!("// {}", c)); + + write!( + output, + "{}public const nint {} = {:#X}; {}\n", + indentation, name, value, comment + ) } fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> { - write!(output, "{}", if eof { "}" } else { "}\n\n" })?; - - Ok(()) + write!(output, "{}", if eof { "}" } else { "}\n\n" }) } } diff --git a/src/builder/file_builder.rs b/src/builder/file_builder.rs index a31395b..23384a5 100644 --- a/src/builder/file_builder.rs +++ b/src/builder/file_builder.rs @@ -1,14 +1,42 @@ use std::io::{Result, Write}; -/// Represents a file builder. +/// A trait that defines the file builder operations. pub trait FileBuilder { - /// Returns the file extension. + /// Returns the extension of the file. + /// + /// # Arguments + /// + /// * `&mut self` - A mutable reference to the `FileBuilder` struct. + /// + /// # Returns + /// + /// * `&str` - A string slice containing the extension of the file. fn extension(&mut self) -> &str; - /// Writes the top level of the file. + /// Write to the top level of the file. The output destination is `output`. + /// + /// # Arguments + /// + /// * `&mut self` - A mutable reference to the `FileBuilder` struct. + /// * `output` - An object implementing Write trait where the top level will be written. + /// + /// # Returns + /// + /// * `Result<()>` - A generic Result type indicating the operations outcome. fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()>; - /// Writes a namespace. + /// Write a namespace to the output. + /// + /// # Arguments + /// + /// * `&mut self` - A mutable reference to the `FileBuilder` struct. + /// * `output` - An object implementing Write trait where the namespace will be written. + /// * `name` - The name of the namespace. + /// * `comment` - An optional comment. If present, this comment will be included in the output. + /// + /// # Returns + /// + /// * `Result<()>` - A generic Result type indicating the operations outcome. fn write_namespace( &mut self, output: &mut dyn Write, @@ -16,15 +44,39 @@ pub trait FileBuilder { comment: Option<&str>, ) -> Result<()>; - /// Writes a variable. + /// Write a variable to the output. + /// + /// # Arguments + /// + /// * `&mut self` - A mutable reference to the `FileBuilder` struct. + /// * `output` - An object implementing Write trait where the variable will be written. + /// * `name` - The name of the variable. + /// * `value` - The value of the variable. + /// * `comment` - An optional comment. If present, this comment will be included in the output. + /// * `indentation` - An optional indentation value. If present, the variable will be written with the specified indentation. + /// + /// # Returns + /// + /// * `Result<()>` - A generic Result type indicating the operations outcome. fn write_variable( &mut self, output: &mut dyn Write, name: &str, value: usize, comment: Option<&str>, + indentation: Option, ) -> Result<()>; - /// Writes a closure. + /// Writes a closure to the output. + /// + /// # Arguments + /// + /// * `&mut self` - A mutable reference to the `FileBuilder` struct. + /// * `output` - An object implementing Write trait where the closure will be written. + /// * `eof` - A boolean value, if true, indicates that this is the last element to write to the output. + /// + /// # Returns + /// + /// * `Result<()>` - A generic Result type indicating the operations outcome. fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()>; } diff --git a/src/builder/json_file_builder.rs b/src/builder/json_file_builder.rs index 4df89dd..9ca568e 100644 --- a/src/builder/json_file_builder.rs +++ b/src/builder/json_file_builder.rs @@ -1,27 +1,27 @@ -use std::{ - collections::BTreeMap, - io::{Result, Write}, -}; +use super::FileBuilder; use serde::Serialize; -use super::FileBuilder; +use std::collections::BTreeMap; +use std::io::{Result, Write}; -/// Represents an offset value in JSON format. +/// Represents a JSON offset value with an optional comment. #[derive(Debug, PartialEq, Default, Serialize)] struct JsonOffsetValue { value: usize, comment: Option, } -/// Represents a module in JSON format. +/// Represents a JSON module, which contains data in the form of a `BTreeMap` of string keys and +/// `JsonOffsetValue` values, as well as an optional comment. #[derive(Debug, PartialEq, Default, Serialize)] struct JsonModule { data: BTreeMap, comment: Option, } -/// Represents a JSON file builder. +/// A structure representing a builder for JSON files. +/// The builder implements the `FileBuilder` trait. #[derive(Debug, PartialEq, Default)] pub struct JsonFileBuilder { data: BTreeMap, @@ -43,8 +43,8 @@ impl FileBuilder for JsonFileBuilder { name: &str, comment: Option<&str>, ) -> Result<()> { - self.current_namespace = name.to_string(); self.data.entry(name.to_string()).or_default().comment = comment.map(str::to_string); + self.current_namespace = name.to_string(); Ok(()) } @@ -55,6 +55,7 @@ impl FileBuilder for JsonFileBuilder { name: &str, value: usize, comment: Option<&str>, + _indentation: Option, ) -> Result<()> { self.data .entry(self.current_namespace.clone()) @@ -63,7 +64,7 @@ impl FileBuilder for JsonFileBuilder { .insert( name.to_string(), JsonOffsetValue { - value: value, + value, comment: comment.map(str::to_string), }, ); @@ -75,7 +76,7 @@ impl FileBuilder for JsonFileBuilder { if eof { write!(output, "{}", serde_json::to_string_pretty(&self.data)?)?; - self.data = BTreeMap::new(); + self.data.clear(); } Ok(()) diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 5e99164..4bc4358 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -1,5 +1,3 @@ -pub use std::io::{Result, Write}; - pub use cpp_file_builder::CppFileBuilder; pub use csharp_file_builder::CSharpFileBuilder; pub use file_builder::FileBuilder; @@ -7,6 +5,8 @@ pub use json_file_builder::JsonFileBuilder; pub use python_file_builder::PythonFileBuilder; pub use rust_file_builder::RustFileBuilder; +pub use std::io::{Result, Write}; + pub mod cpp_file_builder; pub mod csharp_file_builder; pub mod file_builder; @@ -14,13 +14,23 @@ pub mod json_file_builder; pub mod python_file_builder; pub mod rust_file_builder; -/// Represents a file builder enum. +/// `FileBuilder` is an enum that defines different kinds of file builders. +/// Each variant corresponds to a builder for a particular type of file. #[derive(Debug, PartialEq)] pub enum FileBuilderEnum { + /// Represents a builder for C++ header files. CppFileBuilder(CppFileBuilder), + + /// Represents a builder for C# files. CSharpFileBuilder(CSharpFileBuilder), + + /// Represents a builder for JSON files. JsonFileBuilder(JsonFileBuilder), + + /// Represents a builder for Python files. PythonFileBuilder(PythonFileBuilder), + + /// Represents a builder for Rust files. RustFileBuilder(RustFileBuilder), } @@ -48,8 +58,10 @@ impl FileBuilder for FileBuilderEnum { name: &str, value: usize, comment: Option<&str>, + indentation: Option, ) -> Result<()> { - self.as_mut().write_variable(output, name, value, comment) + self.as_mut() + .write_variable(output, name, value, comment, indentation) } fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> { diff --git a/src/builder/python_file_builder.rs b/src/builder/python_file_builder.rs index aaebeda..b10e7ce 100644 --- a/src/builder/python_file_builder.rs +++ b/src/builder/python_file_builder.rs @@ -1,8 +1,9 @@ -use std::io::{Result, Write}; - use super::FileBuilder; -/// Represents a Python file builder. +use std::io::{Result, Write}; + +/// A structure representing a builder for Python files. +/// The builder implements the `FileBuilder` trait. #[derive(Debug, PartialEq)] pub struct PythonFileBuilder; @@ -21,13 +22,9 @@ impl FileBuilder for PythonFileBuilder { name: &str, comment: Option<&str>, ) -> Result<()> { - if let Some(comment) = comment { - write!(output, "class {}: # {}\n", name, comment)?; - } else { - write!(output, "class {}:\n", name)?; - } + let comment = comment.map_or(String::new(), |c| format!("# {}", c)); - Ok(()) + write!(output, "class {}: {}\n", name, comment) } fn write_variable( @@ -36,11 +33,17 @@ impl FileBuilder for PythonFileBuilder { name: &str, value: usize, comment: Option<&str>, + indentation: Option, ) -> Result<()> { - match comment { - Some(comment) => write!(output, " {} = {:#X} # {}\n", name, value, comment), - None => write!(output, " {} = {:#X}\n", name, value), - } + let indentation = " ".repeat(indentation.unwrap_or(4)); + + let comment = comment.map_or(String::new(), |c| format!("# {}", c)); + + write!( + output, + "{}{} = {:#X} {}\n", + indentation, name, value, comment + ) } fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> { diff --git a/src/builder/rust_file_builder.rs b/src/builder/rust_file_builder.rs index ff508fa..ece519c 100644 --- a/src/builder/rust_file_builder.rs +++ b/src/builder/rust_file_builder.rs @@ -1,8 +1,9 @@ -use std::io::{Result, Write}; - use super::FileBuilder; -/// Represents a Rust file builder. +use std::io::{Result, Write}; + +/// A structure representing a builder for Rust files. +/// The builder implements the `FileBuilder` trait. #[derive(Debug, Default, PartialEq)] pub struct RustFileBuilder; @@ -15,9 +16,7 @@ impl FileBuilder for RustFileBuilder { write!( output, "#![allow(non_snake_case, non_upper_case_globals)]\n\n" - )?; - - Ok(()) + ) } fn write_namespace( @@ -26,13 +25,9 @@ impl FileBuilder for RustFileBuilder { name: &str, comment: Option<&str>, ) -> Result<()> { - if let Some(comment) = comment { - write!(output, "pub mod {} {{ // {}\n", name, comment)?; - } else { - write!(output, "pub mod {} {{\n", name)?; - } + let comment = comment.map_or(String::new(), |c| format!("// {}", c)); - Ok(()) + write!(output, "pub mod {} {{ {}\n", name, comment) } fn write_variable( @@ -41,20 +36,20 @@ impl FileBuilder for RustFileBuilder { name: &str, value: usize, comment: Option<&str>, + indentation: Option, ) -> Result<()> { - match comment { - Some(comment) => write!( - output, - " pub const {}: usize = {:#X}; // {}\n", - name, value, comment - ), - None => write!(output, " pub const {}: usize = {:#X};\n", name, value), - } + let indentation = " ".repeat(indentation.unwrap_or(4)); + + let comment = comment.map_or(String::new(), |c| format!("// {}", c)); + + write!( + output, + "{}pub const {}: usize = {:#X}; {}\n", + indentation, name, value, comment + ) } fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> { - write!(output, "{}", if eof { "}" } else { "}\n\n" })?; - - Ok(()) + write!(output, "{}", if eof { "}" } else { "}\n\n" }) } } diff --git a/src/config.rs b/src/config.rs index 3ac9186..cbde55e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,40 +1,71 @@ use serde::{Deserialize, Serialize}; +/// Represents an operation that can be performed on a memory address. #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Operation { - Add { - value: usize, - }, + /// Represents an "add" operation with a given value. + /// + /// `value` is the value to add. + Add { value: usize }, + + /// Represents a dereference operation with optional parameters for the number of times to dereference + /// and the size of the resulting value. + /// + /// `times` is the number of times to dereference the address. If `None`, the number of times will be `1`. + /// `size` is the size of the resulting value. If `None`, the size will be `8`. Dereference { times: Option, size: Option, }, + + /// Represents a jump instruction with an optional offset and length. + /// + /// `offset` is the offset of the displacement value. If `None`, the offset will be `0x1`. + /// `length` is the length of the instruction. If `None`, the length will be `0x5`. Jmp { offset: Option, length: Option, }, + + /// Represents a relative instruction pointer (RIP) with an optional offset and length. + /// + /// `offset` is the offset of the displacement value. If `None`, the offset will be `0x3`. + /// `length` is the length of the instruction. If `None`, the length will be `0x7`. RipRelative { offset: Option, length: Option, }, - Slice { - start: usize, - end: usize, - }, - Subtract { - value: usize, - }, + + /// Represents a slice operation with a start and end index. + /// + /// `start` is the start index of the slice. + /// `end` is the end index of the slice. + Slice { start: usize, end: usize }, + + /// Represents a subtract operation with a given value. + /// + /// `value` is the value to subtract. + Subtract { value: usize }, } +/// Represents a signature for a specific module. #[derive(Debug, Deserialize, Serialize)] pub struct Signature { + /// The name of the signature. pub name: String, + + /// The name of the module. pub module: String, + + /// The pattern of the signature. pub pattern: String, + + /// The list of operations to perform on the signature. pub operations: Vec, } +/// Configuration struct that holds a vector of `Signature` structs. #[derive(Debug, Deserialize, Serialize)] pub struct Config { pub signatures: Vec, diff --git a/src/dumper/interfaces.rs b/src/dumper/interfaces.rs new file mode 100644 index 0000000..c517efc --- /dev/null +++ b/src/dumper/interfaces.rs @@ -0,0 +1,149 @@ +use super::{generate_files, Entries, Entry}; + +use crate::builder::FileBuilderEnum; +use crate::util::{Address, Process}; + +use anyhow::Result; + +use simplelog::{debug, info}; + +use std::ffi::c_char; +use std::mem::offset_of; + +/// Represents a node in a linked list of interfaces. +#[derive(Debug)] +#[repr(C)] +struct InterfaceNode { + /// Function pointer used to instantiate an instance of the interface. + pub create_fn: *const (), + + /// Pointer to the name of the interface. + pub name: *const c_char, + + /// Pointer to the next entry in the linked list. + pub next: *mut InterfaceNode, +} + +impl InterfaceNode { + /// Returns the instance of the interface. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `InterfaceNode` struct. + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result
` - A `Result` containing the instance of the interface if successful, or an error if the memory read fails. + fn instance(&self, process: &Process) -> Result
{ + process + .read_memory::( + (self as *const _ as usize + offset_of!(InterfaceNode, create_fn)).into(), + ) + .map(|ptr| ptr.into()) + } + + /// Returns the name of the interface. + /// + /// E.g. "Source2Client002". + /// + /// # Arguments + /// + /// * `&self` - A reference to the `InterfaceNode` struct. + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result` - A `Result` containing the name of the interface if successful, or an error if the memory read fails. + fn name(&self, process: &Process) -> Result { + let name_ptr = process.read_memory::( + (self as *const _ as usize + offset_of!(InterfaceNode, name)).into(), + )?; + + process.read_string(name_ptr.into()) + } + + /// Returns a pointer to the next `InterfaceNode` in the linked list. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `InterfaceNode` struct. + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result<*mut InterfaceNode>` - A `Result` containing a pointer to the next `InterfaceNode` if successful, or an error if the memory read fails. + fn next(&self, process: &Process) -> Result<*mut InterfaceNode> { + process.read_memory::<*mut InterfaceNode>( + (self as *const _ as usize + offset_of!(InterfaceNode, next)).into(), + ) + } +} + +/// Dumps all interfaces and writes the results to a file. +/// +/// # Arguments +/// +/// * `process` - A reference to the `Process` struct. +/// * `builders` - A mutable reference to a vector of `FileBuilderEnum`. +/// * `file_path` - A string slice representing the path to the file to write the results to. +/// * `indent` - The number of spaces to use for indentation in the output file. +/// +/// # Returns +/// +/// * `Result<()>` - A `Result` indicating the outcome of the operation. +pub fn dump_interfaces( + process: &Process, + builders: &mut Vec, + file_path: &str, + indent: usize, +) -> Result<()> { + let mut entries = Entries::new(); + + for module in process + .modules()? + .iter() + .filter(|m| m.name != "crashhandler64.dll") + { + if let Some(create_interface_export) = module.get_export_by_name("CreateInterface") { + info!("Dumping interfaces in {}...", module.name); + + let create_interface_address = + process.resolve_rip(create_interface_export, 0x3, 0x7)?; + + if let Ok(mut node) = + process.read_memory::<*mut InterfaceNode>(create_interface_address) + { + while !node.is_null() { + let instance = unsafe { (*node).instance(process) }?; + let name = unsafe { (*node).name(process) }?; + + debug!( + "Found {} @ {:#X} ({} + {:#X})", + name, + instance, + module.name, + instance - module.base() + ); + + let container = entries.entry(module.name.replace(".", "_")).or_default(); + + container.comment = Some(module.name.to_string()); + + container.data.push(Entry { + name, + value: (instance - module.base()).into(), + comment: None, + indent: Some(indent), + }); + + node = unsafe { (*node).next(process) }?; + } + } + } + } + + generate_files(builders, &entries, file_path, "interfaces")?; + + Ok(()) +} diff --git a/src/dumper/mod.rs b/src/dumper/mod.rs new file mode 100644 index 0000000..34c61c3 --- /dev/null +++ b/src/dumper/mod.rs @@ -0,0 +1,154 @@ +use crate::builder::{FileBuilder, FileBuilderEnum}; + +use anyhow::Result; + +use chrono::Utc; + +pub use interfaces::dump_interfaces; +pub use offsets::dump_offsets; +pub use schemas::dump_schemas; + +use std::collections::BTreeMap; +use std::fs::File; +use std::io::Write; + +pub mod interfaces; +pub mod offsets; +pub mod schemas; + +/// Represents an entry in the generated file. +#[derive(Debug, PartialEq)] +pub struct Entry { + /// The name of the entry. + pub name: String, + + /// The value of the entry. + pub value: usize, + + /// An optional comment associated with the entry. + pub comment: Option, + + /// An optional indentation level for the entry. + pub indent: Option, +} + +/// A container for entries, which consists of data and an optional comment. +#[derive(Default)] +pub struct EntriesContainer { + /// The data associated with the container. + pub data: Vec, + + /// An optional comment associated with the container. + pub comment: Option, +} + +/// A type alias for a `BTreeMap` that maps `String` keys to `EntriesContainer` values. +pub type Entries = BTreeMap; + +/// Generates a file using the given `builder`, `entries`, `file_path`, and `file_name`. +/// +/// # Arguments +/// +/// * `builder` - A mutable reference to the `FileBuilderEnum`. +/// * `entries` - A reference to the `Entries` struct. +/// * `file_path` - A string slice representing the path to the file. +/// * `file_name` - A string slice representing the name of the file. +/// +/// # Returns +/// +/// * `Result<()>` - A `Result` indicating the outcome of the operation. +pub fn generate_file( + builder: &mut FileBuilderEnum, + entries: &Entries, + file_path: &str, + file_name: &str, +) -> Result<()> { + if entries.is_empty() { + return Ok(()); + } + + let file_path = format!("{}/{}.{}", file_path, file_name, builder.extension()); + + let mut file = File::create(file_path)?; + + write_banner_to_file(&mut file, builder.extension())?; + + builder.write_top_level(&mut file)?; + + let len = entries.len(); + + for (i, pair) in entries.iter().enumerate() { + builder.write_namespace(&mut file, pair.0, pair.1.comment.as_deref())?; + + pair.1.data.iter().try_for_each(|entry| { + builder.write_variable( + &mut file, + &entry.name, + entry.value, + entry.comment.as_deref(), + entry.indent, + ) + })?; + + builder.write_closure(&mut file, i == len - 1)?; + } + + Ok(()) +} + +/// Generate files using the given `builders`, `entries`, `file_path`, and `file_name`. +/// +/// # Arguments +/// +/// * `builders` - A mutable slice of `FileBuilderEnum` objects. +/// * `entries` - A reference to the `Entries` struct. +/// * `file_path` - A string slice representing the path to the file. +/// * `file_name` - A string slice representing the name of the file. +/// +/// # Returns +/// +/// * `Result<()>` - A `Result` indicating the outcome of the operation. +pub fn generate_files( + builders: &mut [FileBuilderEnum], + entries: &Entries, + file_path: &str, + file_name: &str, +) -> Result<()> { + builders + .iter_mut() + .try_for_each(|builder| generate_file(builder, entries, file_path, file_name)) +} + +/// Writes the banner to the given file based on the file extension. +/// +/// # Arguments +/// +/// * `file` - A mutable reference to the file to write the banner to. +/// * `file_extension` - A string slice representing the file extension of the file. +/// +/// # Returns +/// +/// * `Result<()>` - A `Result` indicating the outcome of the operation. +fn write_banner_to_file(file: &mut File, file_extension: &str) -> Result<()> { + const REPO_URL: &str = "https://github.com/a2x/cs2-dumper"; + + let time_now = Utc::now().to_rfc2822(); + + let banner = match file_extension { + "json" => None, + "py" => Some(format!( + "'''\nCreated using {}\n{}\n'''\n\n", + REPO_URL, time_now + )), + _ => Some(format!( + "/*\n * Created using {}\n * {}\n */\n\n", + REPO_URL, time_now + )), + }; + + if let Some(banner) = banner { + write!(file, "{}", banner)?; + } + + Ok(()) +} diff --git a/src/dumpers/offsets.rs b/src/dumper/offsets.rs similarity index 54% rename from src/dumpers/offsets.rs rename to src/dumper/offsets.rs index 13628cd..7051bfe 100644 --- a/src/dumpers/offsets.rs +++ b/src/dumper/offsets.rs @@ -1,15 +1,141 @@ -use std::fs::File; +use super::{generate_files, Entries, Entry}; + +use crate::builder::FileBuilderEnum; +use crate::config::Config; +use crate::config::Operation::*; +use crate::util::Process; use anyhow::Result; -use convert_case::{Case, Casing}; +use simplelog::{debug, error, info}; -use crate::builder::FileBuilderEnum; -use crate::config::{Config, Operation::*}; -use crate::dumpers::Entry; -use crate::remote::Process; +use std::fs::File; -use super::{generate_files, Entries}; +// Dumps all offsets specified in the `config.json` file and writes the results to a file. +/// +/// # Arguments +/// +/// * `process` - A reference to the `Process` struct. +/// * `builders` - A mutable reference to a vector of `FileBuilderEnum`. +/// * `file_path` - A string slice representing the path to the file to write the results to. +/// * `indent` - The number of spaces to use for indentation in the output file. +/// +/// # Returns +/// +/// * `Result<()>` - A `Result` indicating the outcome of the operation. +pub fn dump_offsets( + process: &Process, + builders: &mut Vec, + file_path: &str, + indent: usize, +) -> Result<()> { + let file = File::open("config.json")?; + + let config: Config = serde_json::from_reader(file)?; + + info!("Dumping offsets..."); + + let mut entries = Entries::new(); + + for signature in config.signatures { + debug!( + "Searching for {}...", + signature.name + ); + + let module = process + .get_module_by_name(&signature.module) + .expect(&format!("Failed to find module {}.", signature.module)); + + let mut address = match process.find_pattern(&signature.module, &signature.pattern) { + Some(a) => a, + None => { + error!( + "Failed to find pattern for {}.", + signature.name + ); + + continue; + } + }; + + for operation in signature.operations { + match operation { + Add { value } => address += value, + Dereference { times, size } => { + let times = times.unwrap_or(1); + let size = size.unwrap_or(8); + + for _ in 0..times { + process.read_memory_raw( + address, + &mut address.0 as *mut _ as *mut _, + size, + )?; + } + } + Jmp { offset, length } => { + address = process + .resolve_jmp(address, offset.unwrap_or(0x1), length.unwrap_or(0x5))? + .into() + } + RipRelative { offset, length } => { + address = process + .resolve_rip(address, offset.unwrap_or(0x3), length.unwrap_or(0x7))? + .into() + } + Slice { start, end } => { + let mut result: usize = 0; + + process.read_memory_raw( + address.add(start), + &mut result as *mut _ as *mut _, + end - start, + )?; + + address = result.into(); + } + Subtract { value } => address -= value, + } + } + + let (name, value) = if address < module.base() { + debug!( + "Found {} @ {:#X}", + signature.name, address + ); + + (signature.name, address.0) + } else { + debug!( + "Found {} @ {:#X} ({} + {:#X})", + signature.name, + address, + signature.module, + address.sub(module.base().0) + ); + + (signature.name, address.sub(module.base().0).0) + }; + + let container = entries + .entry(signature.module.replace(".", "_")) + .or_default(); + + container.comment = Some(signature.module); + + container.data.push(Entry { + name, + value, + comment: None, + indent: Some(indent), + }); + } + + generate_files(builders, &entries, file_path, "offsets")?; + + Ok(()) +} #[cfg(test)] mod tests { @@ -22,7 +148,10 @@ mod tests { fn build_number() -> Result<()> { let process = Process::new("cs2.exe")?; - let engine_base = process.get_module_by_name("engine2.dll")?.base_address(); + let engine_base = process + .get_module_by_name("engine2.dll") + .expect("Failed to find engine2.dll") + .base(); let build_number = process.read_memory::(engine_base + 0x487514)?; @@ -72,7 +201,10 @@ mod tests { let process = Process::new("cs2.exe")?; - let client_base = process.get_module_by_name("client.dll")?.base_address(); + let client_base = process + .get_module_by_name("client.dll") + .expect("Failed to find client.dll") + .base(); let global_vars = process.read_memory::<*const GlobalVarsBase>(client_base + 0x1696F40)?; @@ -91,7 +223,10 @@ mod tests { fn local_player() -> Result<()> { let process = Process::new("cs2.exe")?; - let client_base = process.get_module_by_name("client.dll")?.base_address(); + let client_base = process + .get_module_by_name("client.dll") + .expect("Failed to find client.dll") + .base(); let local_player_controller = process.read_memory::(client_base + 0x17E27C8)?; @@ -106,7 +241,10 @@ mod tests { fn window_size() -> Result<()> { let process = Process::new("cs2.exe")?; - let engine_base = process.get_module_by_name("engine2.dll")?.base_address(); + let engine_base = process + .get_module_by_name("engine2.dll") + .expect("Failed to find engine2.dll") + .base(); let window_width = process.read_memory::(engine_base + 0x5386D0)?; let window_height = process.read_memory::(engine_base + 0x5386D4)?; @@ -116,96 +254,3 @@ mod tests { Ok(()) } } - -pub fn dump_offsets(builders: &mut Vec, process: &Process) -> Result<()> { - let file = File::open("config.json")?; - - let config: Config = serde_json::from_reader(file)?; - - let mut entries = Entries::new(); - - log::info!("Dumping offsets..."); - - for signature in config.signatures { - log::info!("Searching for {}...", signature.name); - - let module = process.get_module_by_name(&signature.module)?; - - let mut addr = match process.find_pattern(&signature.module, &signature.pattern) { - Ok(a) => a, - Err(_) => { - log::error!("Failed to find pattern for {}.", signature.name); - - continue; - } - }; - - for op in signature.operations { - match op { - Add { value } => addr += value, - Dereference { times, size } => { - let times = times.unwrap_or(1); - let size = size.unwrap_or(8); - - for _ in 0..times { - process.read_memory_raw(addr, &mut addr.0 as *mut _ as *mut _, size)?; - } - } - Jmp { offset, length } => addr = process.resolve_jmp(addr, offset, length)?.into(), - RipRelative { offset, length } => { - addr = process.resolve_rip(addr, offset, length)?.into() - } - Slice { start, end } => { - let mut result: usize = 0; - - process.read_memory_raw( - addr.add(start), - &mut result as *mut _ as *mut _, - end - start, - )?; - - addr = result.into(); - } - Subtract { value } => addr -= value, - } - } - - let (name, value) = if addr < module.base_address() { - log::debug!(" └─ {} @ {:#X}", signature.name, addr.0); - - (signature.name, addr.0) - } else { - log::debug!( - " └─ {} @ {:#X} ({} + {:#X})", - signature.name, - addr, - signature.module, - addr.sub(module.base_address().0) - ); - - (signature.name, addr.sub(module.base_address().0).0) - }; - - let container = entries - .entry( - signature - .module - .replace(".", "_") - .to_case(Case::Snake) - .to_case(Case::Pascal), - ) - .or_default(); - - container.comment = Some(signature.module); - - container.data.push(Entry { - name, - value, - comment: None, - }); - } - - generate_files(builders, &entries, "offsets")?; - - Ok(()) -} diff --git a/src/dumper/schemas.rs b/src/dumper/schemas.rs new file mode 100644 index 0000000..35dff80 --- /dev/null +++ b/src/dumper/schemas.rs @@ -0,0 +1,80 @@ +use super::{generate_files, Entries, Entry}; + +use crate::builder::FileBuilderEnum; +use crate::sdk::SchemaSystem; +use crate::util::Process; + +use anyhow::Result; + +use simplelog::{debug, info}; + +// Dumps all schema system defined classes and writes the results to a file. +/// +/// # Arguments +/// +/// * `process` - A reference to the `Process` struct. +/// * `builders` - A mutable reference to a vector of `FileBuilderEnum`. +/// * `file_path` - A string slice representing the path to the file to write the results to. +/// * `indent` - The number of spaces to use for indentation in the output file. +/// +/// # Returns +/// +/// * `Result<()>` - A `Result` indicating the outcome of the operation. +pub fn dump_schemas( + process: &Process, + builders: &mut Vec, + file_path: &str, + indent: usize, +) -> Result<()> { + let schema_system = SchemaSystem::new(&process)?; + + for type_scope in schema_system.type_scopes()? { + let module_name = type_scope.module_name()?; + + info!("Generating files for {}...", module_name); + + let mut entries = Entries::new(); + + for class in type_scope.classes()? { + let parent_name = match class.parent()?.map(|p| p.name().to_string()) { + Some(name) => name, + None => continue, + }; + + debug!( + "{} : {}", + class.name(), + parent_name + ); + + let container = entries.entry(class.name().replace("::", "_")).or_default(); + + container.comment = Some(parent_name); + + for field in class.fields()? { + let name = field.name()?; + let offset = field.offset()?; + let type_name = field.r#type()?.name()?; + + debug!( + "{}{} = {:#X} // {}", + " ".repeat(indent), + name, + offset, + type_name + ); + + container.data.push(Entry { + name, + value: offset as usize, + comment: Some(type_name), + indent: Some(indent), + }); + } + } + + generate_files(builders, &entries, file_path, &module_name)?; + } + + Ok(()) +} diff --git a/src/dumpers/interfaces.rs b/src/dumpers/interfaces.rs deleted file mode 100644 index 4d3e73f..0000000 --- a/src/dumpers/interfaces.rs +++ /dev/null @@ -1,69 +0,0 @@ -use anyhow::Result; - -use convert_case::{Case, Casing}; - -use crate::builder::FileBuilderEnum; -use crate::dumpers::Entry; -use crate::remote::Process; -use crate::sdk::InterfaceReg; - -use super::{generate_files, Entries}; - -pub fn dump_interfaces(builders: &mut Vec, process: &Process) -> Result<()> { - let mut entries = Entries::new(); - - for module_name in process - .get_loaded_modules()? - .into_iter() - .filter(|module_name| *module_name != "crashhandler64.dll") - { - let module = process.get_module_by_name(&module_name)?; - - if let Some(create_interface_export) = module.export("CreateInterface") { - log::info!("Dumping interfaces in {}...", module_name); - - let create_interface_addr = - process.resolve_rip(create_interface_export.va.into(), None, None)?; - - if let Ok(mut interface_reg) = - process.read_memory::<*mut InterfaceReg>(create_interface_addr) - { - while !interface_reg.is_null() { - let ptr = unsafe { (*interface_reg).pointer(process) }?; - let name = unsafe { (*interface_reg).name(process) }?; - - log::debug!( - " └─ {} @ {:#X} ({} + {:#X})", - name, - ptr, - module_name, - ptr - module.base_address() - ); - - let container = entries - .entry( - module_name - .replace(".", "_") - .to_case(Case::Snake) - .to_case(Case::Pascal), - ) - .or_default(); - - container.comment = Some(module_name.clone()); - - container.data.push(Entry { - name: name.clone(), - value: (ptr - module.base_address()).into(), - comment: None, - }); - - interface_reg = unsafe { (*interface_reg).next(process) }?; - } - } - } - } - - generate_files(builders, &entries, "interfaces")?; - - Ok(()) -} diff --git a/src/dumpers/mod.rs b/src/dumpers/mod.rs deleted file mode 100644 index dc6aaf4..0000000 --- a/src/dumpers/mod.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::collections::BTreeMap; -use std::fs::File; -use std::io::Write; - -use anyhow::Result; - -use chrono::Utc; - -use crate::builder::{FileBuilder, FileBuilderEnum}; - -pub use interfaces::dump_interfaces; -pub use offsets::dump_offsets; -pub use schemas::dump_schemas; - -pub mod interfaces; -pub mod offsets; -pub mod schemas; - -/// A single entry. -#[derive(Debug, PartialEq)] -pub struct Entry { - pub name: String, - pub value: usize, - pub comment: Option, -} - -/// A container for entries. -#[derive(Default)] -pub struct EntriesContainer { - pub data: Vec, - pub comment: Option, -} - -/// A map of entries. -pub type Entries = BTreeMap; - -/// Generate a file with the given builder. -pub fn generate_file( - builder: &mut FileBuilderEnum, - entries: &Entries, - file_name: &str, -) -> Result<()> { - if entries.is_empty() { - return Ok(()); - } - - let file_path = format!("generated/{}.{}", file_name, builder.extension()); - - let mut file = File::create(file_path)?; - - write_banner_to_file(&mut file, builder.extension())?; - - builder.write_top_level(&mut file)?; - - let len = entries.len(); - - for (i, pair) in entries.iter().enumerate() { - builder.write_namespace(&mut file, pair.0, pair.1.comment.as_deref())?; - - pair.1.data.iter().try_for_each(|entry| { - builder.write_variable( - &mut file, - &entry.name, - entry.value, - entry.comment.as_deref(), - ) - })?; - - builder.write_closure(&mut file, i == len - 1)?; - } - - Ok(()) -} - -/// Generate files with the given builders. -pub fn generate_files( - builders: &mut [FileBuilderEnum], - entries: &Entries, - file_name: &str, -) -> Result<()> { - builders - .iter_mut() - .try_for_each(|builder| generate_file(builder, entries, file_name)) -} - -/// Writes the banner to the given file. -fn write_banner_to_file(file: &mut File, file_ext: &str) -> Result<()> { - const REPO_URL: &str = "https://github.com/a2x/cs2-dumper"; - - let now_utc = Utc::now(); - - let banner = match file_ext { - "json" => None, - "py" => Some(format!( - "'''\nCreated using {}\n{}\n'''\n\n", - REPO_URL, now_utc - )), - _ => Some(format!( - "/*\n * Created using {}\n * {}\n */\n\n", - REPO_URL, now_utc - )), - }; - - if let Some(banner) = banner { - write!(file, "{}", banner)?; - } - - Ok(()) -} diff --git a/src/dumpers/schemas.rs b/src/dumpers/schemas.rs deleted file mode 100644 index 4f31993..0000000 --- a/src/dumpers/schemas.rs +++ /dev/null @@ -1,51 +0,0 @@ -use anyhow::Result; - -use crate::builder::FileBuilderEnum; -use crate::dumpers::Entry; -use crate::remote::Process; -use crate::sdk::SchemaSystem; - -use super::{generate_files, Entries}; - -pub fn dump_schemas(builders: &mut Vec, process: &Process) -> Result<()> { - let schema_system = SchemaSystem::new(&process)?; - - for type_scope in schema_system.type_scopes()? { - let module_name = type_scope.module_name()?; - - log::info!("Generating files for {}...", module_name); - - let mut entries = Entries::new(); - - for class in type_scope.classes()? { - log::debug!(" {}", class.name()); - - let container = entries.entry(class.name().replace("::", "_")).or_default(); - - container.comment = class.parent()?.map(|p| p.name().to_string()); - - for field in class.fields()? { - let field_name = field.name()?; - let field_offset = field.offset()?; - let field_type_name = field.r#type()?.name()?; - - log::debug!( - " └─ {} = {:#X} // {}", - field_name, - field_offset, - field_type_name - ); - - container.data.push(Entry { - name: field_name, - value: field_offset as usize, - comment: Some(field_type_name), - }); - } - } - - generate_files(builders, &entries, &module_name)?; - } - - Ok(()) -} diff --git a/src/main.rs b/src/main.rs index 6c0e1dc..c433e49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,40 +1,58 @@ #![allow(dead_code)] #![feature(offset_of)] +use anyhow::Result; + +use builder::*; + +use clap::Parser; + +use dumper::{dump_interfaces, dump_offsets, dump_schemas}; + +use log::LevelFilter; + +use simplelog::{info, ColorChoice, ConfigBuilder, TermLogger, TerminalMode}; + use std::fs; use std::time::Instant; -use anyhow::Result; - -use clap::Parser; - -use simple_logger::SimpleLogger; - -use builder::*; -use dumpers::*; -use remote::Process; +use util::Process; mod builder; mod config; -mod dumpers; -mod mem; -mod remote; +mod dumper; mod sdk; +mod util; +/// Command line arguments for the program. #[derive(Debug, Parser)] -#[command(author, version, about, long_about = None)] +#[command(name = "cs2-dumper")] +#[command(author = "a2x")] +#[command(version = "1.1.4")] struct Args { + /// Dump interfaces. #[arg(short, long)] interfaces: bool, + /// Dump offsets. #[arg(short, long)] offsets: bool, + /// Dump schemas. #[arg(short, long)] schemas: bool, + /// Enable verbose output. #[arg(short, long)] verbose: bool, + + /// Output folder. + #[arg(short, long, default_value = "generated")] + output: String, + + /// Indentation level. + #[arg(long, default_value = "4")] + indent: usize, } fn main() -> Result<()> { @@ -43,25 +61,27 @@ fn main() -> Result<()> { offsets, schemas, verbose, + output, + indent, } = Args::parse(); let log_level = if verbose { - log::LevelFilter::Debug + LevelFilter::Debug } else { - log::LevelFilter::Info + LevelFilter::Info }; - SimpleLogger::new() - .with_level(log_level) - .without_timestamps() - .init() - .unwrap(); + let config = ConfigBuilder::new().add_filter_ignore_str("goblin").build(); - let start_time = Instant::now(); + TermLogger::init(log_level, config, TerminalMode::Mixed, ColorChoice::Auto)?; - let process = Process::new("cs2.exe")?; + let now = Instant::now(); - fs::create_dir_all("generated")?; + fs::create_dir_all(&output)?; + + let mut process = Process::new("cs2.exe")?; + + process.initialize()?; let mut builders: Vec = vec![ FileBuilderEnum::CppFileBuilder(CppFileBuilder), @@ -74,20 +94,21 @@ fn main() -> Result<()> { let all = !(interfaces || offsets || schemas); if schemas || all { - dump_schemas(&mut builders, &process)?; + dump_schemas(&mut process, &mut builders, &output, indent)?; } if interfaces || all { - dump_interfaces(&mut builders, &process)?; + dump_interfaces(&mut process, &mut builders, &output, indent)?; } if offsets || all { - dump_offsets(&mut builders, &process)?; + dump_offsets(&mut process, &mut builders, &output, indent)?; } - let duration = start_time.elapsed(); - - log::info!("Done! Time elapsed: {:?}", duration); + info!( + "Done! Time elapsed: {:?}", + now.elapsed() + ); Ok(()) } diff --git a/src/mem/address.rs b/src/mem/address.rs deleted file mode 100644 index ab7d847..0000000 --- a/src/mem/address.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::fmt::{LowerHex, UpperHex}; -use std::ops::{Add, AddAssign, Sub, SubAssign}; - -/// Represents a memory address. -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)] -#[repr(transparent)] -pub struct Address(pub usize); - -impl Address { - /// Add the given value to the address. - pub fn add(&self, value: usize) -> Self { - Self(self.0 + value) - } - - /// Returns `true` if the address is zero. - pub fn is_zero(&self) -> bool { - self.0 == 0 - } - - /// Subtract the given value from the address. - pub fn sub(&self, value: usize) -> Self { - Self(self.0 - value) - } - - /// Get the address as a pointer. - pub fn as_ptr(&self) -> *const T { - self.0 as *const T - } - - /// Get the address as a mutable pointer. - pub fn as_mut_ptr(&self) -> *mut T { - self.0 as *mut T - } -} - -impl From for Address { - fn from(value: usize) -> Self { - Self(value) - } -} - -impl From
for usize { - fn from(value: Address) -> Self { - value.0 - } -} - -impl Add for Address { - type Output = Self; - - fn add(self, rhs: usize) -> Self::Output { - Self(self.0 + rhs) - } -} - -impl Add
for Address { - type Output = Self; - - fn add(self, rhs: Address) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl AddAssign for Address { - fn add_assign(&mut self, rhs: usize) { - self.0 += rhs; - } -} - -impl AddAssign
for Address { - fn add_assign(&mut self, rhs: Address) { - self.0 += rhs.0; - } -} - -impl Sub for Address { - type Output = Self; - - fn sub(self, rhs: usize) -> Self::Output { - Self(self.0 - rhs) - } -} - -impl Sub
for Address { - type Output = Self; - - fn sub(self, rhs: Address) -> Self::Output { - Self(self.0 - rhs.0) - } -} - -impl SubAssign for Address { - fn sub_assign(&mut self, rhs: usize) { - self.0 -= rhs; - } -} - -impl SubAssign
for Address { - fn sub_assign(&mut self, rhs: Address) { - self.0 -= rhs.0; - } -} - -impl UpperHex for Address { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:#X}", self.0) - } -} - -impl LowerHex for Address { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:#x}", self.0) - } -} diff --git a/src/mem/mod.rs b/src/mem/mod.rs deleted file mode 100644 index c6e6424..0000000 --- a/src/mem/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use address::Address; - -pub mod address; diff --git a/src/remote/module.rs b/src/remote/module.rs deleted file mode 100644 index 3dc6ae7..0000000 --- a/src/remote/module.rs +++ /dev/null @@ -1,254 +0,0 @@ -use std::ffi::CStr; -use std::mem; -use std::slice; - -use anyhow::{bail, Result}; - -use windows::Win32::System::Diagnostics::Debug::*; -use windows::Win32::System::SystemServices::*; - -use crate::mem::Address; - -use super::Process; - -/// Represents an export in a module. -#[derive(Debug)] -pub struct Export { - /// Name of the export. - pub name: String, - - /// Address of the export. - pub va: usize, -} - -/// Represents a section in a module. -#[derive(Debug)] -pub struct Section { - /// Name of the section. - pub name: String, - - /// Base address of the section. - pub start_va: usize, - - /// End address of the section. - pub end_va: usize, - - /// Size of the section. - pub size: usize, -} - -/// Represents a module in a process. -pub struct Module<'a> { - /// Base address of the module. - base_addr: Address, - - /// DOS header. - dos_hdr: &'a IMAGE_DOS_HEADER, - - /// NT header. - nt_hdr: &'a IMAGE_NT_HEADERS64, - - /// Size of the module. - size: u32, - - /// List of exports. - exports: Vec, - - /// List of sections. - sections: Vec
, -} - -impl<'a> Module<'a> { - pub fn new(process: &'a Process, base_addr: Address) -> Result { - let mut buf: [u8; 0x1000] = [0; 0x1000]; - - process.read_memory_raw(base_addr, buf.as_mut_ptr() as *mut _, buf.len())?; - - if buf.len() < mem::size_of::() { - bail!( - "Buffer size mismatch. Expected {} bytes, got {} bytes", - mem::size_of::(), - buf.len() - ); - } - - let dos_hdr = unsafe { &*(buf.as_ptr() as *const IMAGE_DOS_HEADER) }; - - if dos_hdr.e_magic != IMAGE_DOS_SIGNATURE { - bail!( - "Invalid DOS signature. Expected 0x{:X}, got 0x{:X}", - IMAGE_DOS_SIGNATURE, - dos_hdr.e_magic - ); - } - - let nt_hdr = unsafe { - &*(buf.as_ptr().offset(dos_hdr.e_lfanew as isize) as *const IMAGE_NT_HEADERS64) - }; - - if nt_hdr.Signature != IMAGE_NT_SIGNATURE { - bail!( - "Invalid NT signature. Expected 0x{:X}, got 0x{:X}", - IMAGE_NT_SIGNATURE, - nt_hdr.Signature - ); - } - - let size = nt_hdr.OptionalHeader.SizeOfImage; - - let exports = unsafe { Self::parse_exports(process, base_addr, size, nt_hdr)? }; - let sections = unsafe { Self::parse_sections(base_addr, nt_hdr) }; - - Ok(Self { - base_addr, - dos_hdr, - nt_hdr, - size, - exports, - sections, - }) - } - - /// Returns the base address of the module. - #[inline] - pub fn base_address(&self) -> Address { - self.base_addr - } - - /// Returns the exports of the module. - #[inline] - pub fn exports(&self) -> &Vec { - &self.exports - } - - /// Returns the sections of the module. - #[inline] - pub fn sections(&self) -> &Vec
{ - &self.sections - } - - /// Returns the export with the given name. - #[inline] - pub fn export(&self, name: &str) -> Option<&Export> { - self.exports.iter().find(|export| export.name == name) - } - - /// Returns the section with the given name. - #[inline] - pub fn section(&self, name: &str) -> Option<&Section> { - self.sections.iter().find(|section| section.name == name) - } - - /// Returns the size of the module. - #[inline] - pub fn size(&self) -> u32 { - self.size - } - - /// Parses the exports of the module. - unsafe fn parse_exports( - process: &Process, - base_addr: Address, - size: u32, - nt_hdr: &IMAGE_NT_HEADERS64, - ) -> Result> { - let export_data_dir = - nt_hdr.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT.0 as usize]; - - let export_dir_start = base_addr + export_data_dir.VirtualAddress as usize; - let export_dir_end = export_dir_start + export_data_dir.Size as usize; - - let mut buf: Vec = vec![0; export_data_dir.Size as usize]; - - process.read_memory_raw(export_dir_start, buf.as_mut_ptr() as *mut _, buf.len())?; - - if buf.len() < mem::size_of::() { - bail!( - "Buffer size mismatch. Expected {} bytes, got {} bytes", - mem::size_of::(), - buf.len() - ); - } - - let export_dir = &*(buf.as_ptr() as *const IMAGE_EXPORT_DIRECTORY); - - let delta = export_dir as *const _ as usize - export_data_dir.VirtualAddress as usize; - - let name_table = (delta + export_dir.AddressOfNames as usize) as *const u32; - let ordinal_table = (delta + export_dir.AddressOfNameOrdinals as usize) as *const u16; - let function_table = (delta + export_dir.AddressOfFunctions as usize) as *const u32; - - let mut exports: Vec = Vec::with_capacity(export_dir.NumberOfNames as usize); - - for i in 0..export_dir.NumberOfNames { - let target_va = ordinal_table as usize + i as usize * mem::size_of::(); - - if target_va > base_addr.add(size as usize).0 || target_va < ordinal_table as usize { - continue; - } - - let func_ordinal = *ordinal_table.offset(i as isize); - - if func_ordinal as usize > export_dir.NumberOfFunctions as usize { - continue; - } - - let func_va = base_addr.add(*function_table.offset(func_ordinal as isize) as usize); - - // Skip forwarded exports. - if func_va >= export_dir_start && func_va <= export_dir_end { - continue; - } - - let name_ptr = delta.wrapping_add(*name_table.offset(i as isize) as usize) as *const i8; - - if name_ptr.is_null() { - continue; - } - - let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned(); - - exports.push(Export { - name, - va: func_va.0, - }); - } - - Ok(exports) - } - - /// Parses the sections of the module. - unsafe fn parse_sections(base_addr: Address, nt_hdr: &IMAGE_NT_HEADERS64) -> Vec
{ - let optional_hdr = &nt_hdr.OptionalHeader as *const _ as *const u8; - - let section_hdr = optional_hdr.offset(nt_hdr.FileHeader.SizeOfOptionalHeader as isize) - as *const IMAGE_SECTION_HEADER; - - let sections_raw = - slice::from_raw_parts(section_hdr, nt_hdr.FileHeader.NumberOfSections as usize); - - sections_raw - .iter() - .map(|section| { - let name = CStr::from_ptr(section.Name.as_ptr() as *const _) - .to_string_lossy() - .into_owned(); - - let start_rva = section.VirtualAddress as usize; - let end_rva = start_rva + section.Misc.VirtualSize as usize; - - let start_va = base_addr + start_rva; - let end_va = base_addr + end_rva; - - let size = section.SizeOfRawData as usize; - - Section { - name, - start_va: start_va.0, - end_va: end_va.0, - size, - } - }) - .collect() - } -} diff --git a/src/remote/process.rs b/src/remote/process.rs deleted file mode 100644 index 72aae6b..0000000 --- a/src/remote/process.rs +++ /dev/null @@ -1,285 +0,0 @@ -use std::ffi::{c_void, CStr}; -use std::mem; -use std::ptr; - -use anyhow::{bail, Result}; - -use windows::Win32::Foundation::*; -use windows::Win32::System::Diagnostics::Debug::*; -use windows::Win32::System::Diagnostics::ToolHelp::*; -use windows::Win32::System::Threading::*; - -use crate::mem::Address; - -use super::Module; - -/// Represents a Win32 process. -#[derive(Debug)] -pub struct Process { - process_id: u32, - process_handle: HANDLE, -} - -impl Process { - pub fn new(process_name: &str) -> Result { - let process_id = Self::get_process_id_by_name(process_name)?; - - let process_handle = unsafe { OpenProcess(PROCESS_ALL_ACCESS, false, process_id) }?; - - Ok(Self { - process_id, - process_handle, - }) - } - - /// Finds an sequence of bytes in memory. - pub fn find_pattern(&self, module_name: &str, pattern: &str) -> Result
{ - let module = self.get_module_by_name(module_name)?; - - let mut module_data: Vec = vec![0; module.size() as usize]; - - self.read_memory_raw( - module.base_address(), - module_data.as_mut_ptr() as *mut _, - module_data.len(), - )?; - - let pat_bytes = Self::pattern_to_bytes(pattern); - - for i in 0..module.size() as usize - pattern.len() { - let mut found = true; - - for j in 0..pat_bytes.len() { - if module_data[i + j] != pat_bytes[j] as u8 && pat_bytes[j] != -1 { - found = false; - - break; - } - } - - if found { - return Ok(module.base_address() + i); - } - } - - bail!("Pattern not found: {}", pattern) - } - - /// Returns a list of loaded modules. - pub fn get_loaded_modules(&self) -> Result> { - let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, self.process_id) }?; - - let mut entry = MODULEENTRY32 { - dwSize: mem::size_of::() as u32, - ..Default::default() - }; - - let mut modules = Vec::new(); - - unsafe { - Module32First(snapshot, &mut entry)?; - - while Module32Next(snapshot, &mut entry).is_ok() { - let name = CStr::from_ptr(&entry.szModule as *const _ as *const _) - .to_string_lossy() - .into_owned(); - - modules.push(name); - } - } - - Ok(modules) - } - - /// Returns a module by name. - pub fn get_module_by_name(&self, module_name: &str) -> Result { - let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, self.process_id) }?; - - let mut entry = MODULEENTRY32 { - dwSize: mem::size_of::() as u32, - ..Default::default() - }; - - unsafe { - Module32First(snapshot, &mut entry)?; - - while Module32Next(snapshot, &mut entry).is_ok() { - let name = CStr::from_ptr(&entry.szModule as *const _ as *const _) - .to_string_lossy() - .into_owned(); - - if name == module_name { - return Module::new(self, Address::from(entry.modBaseAddr as usize)); - } - } - } - - bail!("Module not found: {}", module_name) - } - - /// Reads raw data from memory. - pub fn read_memory_raw(&self, addr: Address, buf: *mut c_void, size: usize) -> Result<()> { - unsafe { - ReadProcessMemory( - self.process_handle, - addr.as_ptr(), - buf, - size, - Some(ptr::null_mut()), - ) - } - .map_err(Into::into) - } - - /// Writes raw data to memory. - pub fn write_memory_raw(&self, addr: Address, buf: *const c_void, size: usize) -> Result<()> { - unsafe { - WriteProcessMemory( - self.process_handle, - addr.as_ptr(), - buf, - size, - Some(ptr::null_mut()), - ) - } - .map_err(Into::into) - } - - /// Reads a value from memory. - pub fn read_memory(&self, addr: Address) -> Result { - let mut buf: T = unsafe { mem::zeroed() }; - - self.read_memory_raw(addr, &mut buf as *const _ as *mut _, mem::size_of::())?; - - Ok(buf) - } - - /// Writes a value to memory. - pub fn write_memory(&self, addr: Address, val: T) -> Result<()> { - self.write_memory_raw(addr, &val as *const _ as *const _, mem::size_of::()) - } - - /// Reads a string. - pub fn read_string(&self, addr: Address) -> Result { - let mut buf = Vec::new(); - - for i in 0.. { - match self.read_memory::(addr + i) { - Ok(byte) if byte != 0 => buf.push(byte), - _ => break, - } - } - - Ok(String::from_utf8(buf)?) - } - - /// Reads a string with a specified length. - pub fn read_string_len(&self, addr: Address, len: usize) -> Result { - let mut buf: Vec = vec![0; len]; - - self.read_memory_raw(addr, buf.as_mut_ptr() as *mut _, len)?; - - if let Some(end) = buf.iter().position(|&x| x == 0) { - buf.truncate(end); - } - - Ok(String::from_utf8(buf)?) - } - - /// Resolves a JMP/CALL instruction. - pub fn resolve_jmp( - &self, - addr: Address, - offset: Option, - len: Option, - ) -> Result
{ - let disp = self.read_memory::(addr + offset.unwrap_or(0x1))?; - - Ok(((addr.add(len.unwrap_or(0x5)).0 as isize + disp as isize) as usize).into()) - } - - /// Resolves a RIP-relative address. - pub fn resolve_rip( - &self, - addr: Address, - offset: Option, - len: Option, - ) -> Result
{ - let disp = self.read_memory::(addr + offset.unwrap_or(0x3))?; - - Ok(((addr.add(len.unwrap_or(0x7)).0 as isize + disp as isize) as usize).into()) - } - - /// Returns the process ID of a process by name. - fn get_process_id_by_name(process_name: &str) -> Result { - let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }?; - - let mut entry = PROCESSENTRY32 { - dwSize: mem::size_of::() as u32, - ..Default::default() - }; - - unsafe { - Process32First(snapshot, &mut entry)?; - - while Process32Next(snapshot, &mut entry).is_ok() { - let name = CStr::from_ptr(&entry.szExeFile as *const _ as *const _) - .to_string_lossy() - .into_owned(); - - if name == process_name { - return Ok(entry.th32ProcessID); - } - } - } - - bail!("Process not found: {}", process_name) - } - - /// Converts a pattern to a list of bytes. - fn pattern_to_bytes(pattern: &str) -> Vec { - let mut bytes = Vec::new(); - - let chars: Vec = pattern.chars().collect(); - - let mut i = 0; - - while i < chars.len() { - if chars[i] == ' ' { - i += 1; - - continue; - } - - if chars[i] == '?' { - bytes.push(-1); - - i += 1; - - continue; - } - - if i + 1 < chars.len() { - match i32::from_str_radix(&pattern[i..i + 2], 16) { - Ok(v) => bytes.push(v), - Err(_) => {} - } - - i += 1; - } - - i += 1; - } - - bytes - } -} - -impl Drop for Process { - /// Closes the process handle. - fn drop(&mut self) { - if !self.process_handle.is_invalid() { - unsafe { CloseHandle(self.process_handle).unwrap() } - } - } -} diff --git a/src/sdk/interface.rs b/src/sdk/interface.rs deleted file mode 100644 index 7c24d97..0000000 --- a/src/sdk/interface.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::ffi::c_char; -use std::mem::offset_of; - -use anyhow::Result; - -use crate::mem::Address; -use crate::remote::Process; - -#[derive(Debug)] -#[repr(C)] -pub struct InterfaceReg { - pub create_fn: *const (), // 0x0000 - pub name: *const c_char, // 0x0008 - pub next: *mut InterfaceReg, // 0x0010 -} - -impl InterfaceReg { - /// Returns the pointer of the interface. - pub fn pointer(&self, process: &Process) -> Result
{ - process - .read_memory::( - (self as *const _ as usize + offset_of!(InterfaceReg, create_fn)).into(), - ) - .map(|ptr| ptr.into()) - } - - /// Returns the name of the interface. - /// E.g. "Source2Client002" - pub fn name(&self, process: &Process) -> Result { - let name_ptr = process.read_memory::( - (self as *const _ as usize + offset_of!(InterfaceReg, name)).into(), - )?; - - process.read_string(name_ptr.into()) - } - - /// Returns the next interface in the list. - pub fn next(&self, process: &Process) -> Result<*mut InterfaceReg> { - process.read_memory::<*mut InterfaceReg>( - (self as *const _ as usize + offset_of!(InterfaceReg, next)).into(), - ) - } -} diff --git a/src/sdk/mod.rs b/src/sdk/mod.rs index 71817ea..47039f5 100644 --- a/src/sdk/mod.rs +++ b/src/sdk/mod.rs @@ -1,4 +1,3 @@ -pub use interface::InterfaceReg; pub use schema_class_field_data::SchemaClassFieldData; pub use schema_class_info::SchemaClassInfo; pub use schema_system::SchemaSystem; @@ -7,7 +6,6 @@ pub use schema_type::SchemaType; pub use schema_type_declared_class::SchemaTypeDeclaredClass; pub use utl_ts_hash::UtlTsHash; -pub mod interface; pub mod schema_class_field_data; pub mod schema_class_info; pub mod schema_system; diff --git a/src/sdk/schema_class_field_data.rs b/src/sdk/schema_class_field_data.rs index 2005b37..efed0ac 100644 --- a/src/sdk/schema_class_field_data.rs +++ b/src/sdk/schema_class_field_data.rs @@ -1,40 +1,73 @@ -use anyhow::Result; - -use crate::mem::Address; -use crate::remote::Process; - use super::SchemaType; -/// Represents a class field in a schema. +use crate::util::{Address, Process}; + +use anyhow::Result; + +/// Represents data for a field in a schema class. pub struct SchemaClassFieldData<'a> { process: &'a Process, - - /// Address of the class field. - addr: Address, + address: Address, } impl<'a> SchemaClassFieldData<'a> { - pub fn new(process: &'a Process, addr: Address) -> Self { - Self { process, addr } + /// Creates a new `SchemaClassFieldData` instance. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// * `address` - The address of the `SchemaClassFieldData` instance. + /// + /// # Returns + /// + /// * `SchemaClassFieldData` - The new `SchemaClassFieldData` instance. + pub fn new(process: &'a Process, address: Address) -> Self { + Self { process, address } } /// Returns the name of the field. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaClassFieldData` struct. + /// + /// # Returns + /// + /// * `Result` - The name of the field. pub fn name(&self) -> Result { - let name_ptr = self.process.read_memory::(self.addr + 0x0)?; + let name_ptr = self.process.read_memory::(self.address)?; - self.process.read_string_len(name_ptr.into(), 64) + self.process.read_string_length(name_ptr.into(), 64) } - /// Returns the type of the field. + /// Returns the `SchemaType` of the field. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaClassFieldData` struct. + /// + /// # Returns + /// + /// * `Result` - The `SchemaType` of the field. pub fn r#type(&self) -> Result { Ok(SchemaType::new( self.process, - self.process.read_memory::(self.addr + 0x8)?.into(), + self.process + .read_memory::(self.address + 0x8)? + .into(), )) } /// Returns the offset of the field. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaClassFieldData` struct. + /// + /// # Returns + /// + /// * `Result` - The offset of the field. pub fn offset(&self) -> Result { - self.process.read_memory::(self.addr + 0x10) + self.process.read_memory::(self.address + 0x10) } } diff --git a/src/sdk/schema_class_info.rs b/src/sdk/schema_class_info.rs index cd9242a..4d63e4e 100644 --- a/src/sdk/schema_class_info.rs +++ b/src/sdk/schema_class_info.rs @@ -1,47 +1,70 @@ -use anyhow::Result; - -use crate::mem::Address; -use crate::remote::Process; - use super::SchemaClassFieldData; -/// Represents a class in a schema. +use crate::util::{Address, Process}; + +use anyhow::Result; + +/// Represents information about a schema class. pub struct SchemaClassInfo<'a> { process: &'a Process, - - /// Address of the class. - addr: Address, - - /// Name of the class. + address: Address, class_name: String, } impl<'a> SchemaClassInfo<'a> { - pub fn new(process: &'a Process, addr: Address, class_name: &str) -> Self { + /// Creates a new `SchemaClassInfo` instance. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// * `address` - The address of the `SchemaClassInfo` instance. + /// * `class_name` - The name of the class. + /// + /// # Returns + /// + /// * `SchemaClassInfo` - The new `SchemaClassInfo` instance. + pub fn new(process: &'a Process, address: Address, class_name: &str) -> Self { Self { process, - addr, + address, class_name: class_name.to_string(), } } - /// Returns the name of the class. + /// Returns a reference to the name of the class. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaClassInfo` struct. + /// + /// # Returns + /// + /// * `&str` - A string slice containing the name of the class. #[inline] pub fn name(&self) -> &str { &self.class_name } - /// Returns a list of fields in the class. + /// Returns a vector of `SchemaClassFieldData` representing the fields of the schema class. + /// If the address of the schema class is null, an empty vector is returned. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaClassInfo` struct. + /// + /// # Returns + /// + /// * `Result>` - A vector of `SchemaClassFieldData` representing the fields of the schema class. pub fn fields(&self) -> Result> { - let addr = self.process.read_memory::(self.addr + 0x28)?; + let address = self.process.read_memory::(self.address + 0x28)?; - if addr == 0 { + if address == 0 { return Ok(Vec::new()); } let count = self.fields_count()?; - let fields: Vec = (addr..addr + count as usize * 0x20) + let fields: Vec = (address..address + count as usize * 0x20) .step_by(0x20) .map(|address| SchemaClassFieldData::new(self.process, address.into())) .collect(); @@ -50,21 +73,38 @@ impl<'a> SchemaClassInfo<'a> { } /// Returns the number of fields in the class. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaClassInfo` struct. + /// + /// # Returns + /// + /// * `Result` - The number of fields in the class. pub fn fields_count(&self) -> Result { - self.process.read_memory::(self.addr + 0x1C) + self.process.read_memory::(self.address + 0x1C) } - /// Returns the parent class. + /// Returns the parent `SchemaClassInfo` of the current `SchemaClassInfo` instance. + /// If the parent is not found, returns `Ok(None)`. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaClassInfo` struct. + /// + /// # Returns + /// + /// * `Result>` - The parent `SchemaClassInfo` of the current `SchemaClassInfo` instance. pub fn parent(&self) -> Result> { - let addr = self.process.read_memory::(self.addr + 0x38)?; + let address = Address::from(self.process.read_memory::(self.address + 0x38)?); - if addr == 0 { + if address.is_zero() { return Ok(None); } - let parent = self.process.read_memory::((addr + 0x8).into())?; + let parent = Address::from(self.process.read_memory::(address + 0x8)?); - let name_ptr = self.process.read_memory::((parent + 0x8).into())?; + let name_ptr = self.process.read_memory::(parent + 0x8)?; let name = self.process.read_string(name_ptr.into())?; Ok(Some(SchemaClassInfo::new( diff --git a/src/sdk/schema_system.rs b/src/sdk/schema_system.rs index 233a8e7..b757456 100644 --- a/src/sdk/schema_system.rs +++ b/src/sdk/schema_system.rs @@ -1,43 +1,57 @@ -use std::mem; +use super::SchemaSystemTypeScope; + +use crate::util::{Address, Process}; use anyhow::{bail, Result}; -use crate::mem::Address; -use crate::remote::Process; - -use super::SchemaSystemTypeScope; +use std::mem; /// Represents the schema system. pub struct SchemaSystem<'a> { process: &'a Process, - - /// Address of the schema system. - addr: Address, + address: Address, } impl<'a> SchemaSystem<'a> { + /// Creates a new `SchemaSystem` instance. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result` - The new `SchemaSystem` instance. pub fn new(process: &'a Process) -> Result { - let mut addr = process.find_pattern( + let mut address = process.find_pattern( "schemasystem.dll", "48 8D 0D ? ? ? ? E9 ? ? ? ? CC CC CC CC 48 8D 0D ? ? ? ? E9 ? ? ? ? CC CC CC CC 48 83 EC 28" - )?; + ).expect("Failed to find SchemaSystem pattern"); - addr = process.resolve_rip(addr, None, None)?; + address = process.resolve_rip(address, 0x3, 0x7)?; - Ok(Self { process, addr }) + Ok(Self { process, address }) } - /// Returns a list of type scopes. + /// Returns a vector of `SchemaSystemTypeScope` objects. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaSystem` struct. + /// + /// # Returns + /// + /// * `Result>` - A vector of `SchemaSystemTypeScope` objects. pub fn type_scopes(&self) -> Result> { - let size = self.process.read_memory::(self.addr + 0x190)?; + let size = self.process.read_memory::(self.address + 0x190)?; if size == 0 { bail!("Type scopes size is 0"); } - let data = self.process.read_memory::(self.addr + 0x198)?; + let data = self.process.read_memory::(self.address + 0x198)?; - let mut addresses: Vec = vec![0; size as usize]; + let mut addresses = vec![0; size as usize]; self.process.read_memory_raw( data.into(), @@ -47,7 +61,7 @@ impl<'a> SchemaSystem<'a> { let type_scopes: Vec = addresses .iter() - .map(|&addr| SchemaSystemTypeScope::new(self.process, addr.into())) + .map(|&address| SchemaSystemTypeScope::new(self.process, address.into())) .collect(); Ok(type_scopes) diff --git a/src/sdk/schema_system_type_scope.rs b/src/sdk/schema_system_type_scope.rs index f354d26..824cdef 100644 --- a/src/sdk/schema_system_type_scope.rs +++ b/src/sdk/schema_system_type_scope.rs @@ -1,49 +1,73 @@ -use anyhow::Result; - -use crate::mem::Address; -use crate::remote::Process; - use super::{SchemaClassInfo, SchemaTypeDeclaredClass, UtlTsHash}; -/// Represents a schema system type scope. +use crate::util::{Address, Process}; + +use anyhow::Result; + +/// Represents a system type scope in the schema. pub struct SchemaSystemTypeScope<'a> { process: &'a Process, - - /// Address of the schema system type scope. - addr: Address, + address: Address, } impl<'a> SchemaSystemTypeScope<'a> { - pub fn new(process: &'a Process, addr: Address) -> Self { - Self { process, addr } + /// Creates a new `SchemaSystemTypeScope` instance. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// * `address` - The address of the `SchemaSystemTypeScope` instance. + /// * `class_name` - The name of the class. + /// + /// # Returns + /// + /// * `SchemaSystemTypeScope` - The new `SchemaSystemTypeScope` instance. + pub fn new(process: &'a Process, address: Address) -> Self { + Self { process, address } } - /// Returns a list of classes in the type scope. + /// Returns a vector of `SchemaClassInfo` containing information about all the classes declared in the current scope. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaSystemTypeScope` struct. + /// + /// # Returns + /// + /// * `Result>` - A vector of `SchemaClassInfo` containing information about all the classes declared in the current scope. pub fn classes(&self) -> Result> { let declared_classes = self .process - .read_memory::>(self.addr + 0x588)?; + .read_memory::>(self.address + 0x588)?; let classes: Vec = declared_classes .elements(self.process)? .iter() - .filter_map(|&addr| { - let addr = Address::from(addr as usize); + .filter_map(|&a| { + let address = Address::from(a as usize); - let declared_class = SchemaTypeDeclaredClass::new(self.process, addr); + let declared_class = SchemaTypeDeclaredClass::new(self.process, address); declared_class .name() .ok() - .map(|name| SchemaClassInfo::new(self.process, addr, &name)) + .map(|name| SchemaClassInfo::new(self.process, address, &name)) }) .collect(); Ok(classes) } - /// Returns the name of the module that the type scope belongs to. + /// Returns the name of the module associated with the current `SchemaSystemTypeScope` instance. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaSystemTypeScope` struct. + /// + /// # Returns + /// + /// * `Result` - The name of the module associated with the current `SchemaSystemTypeScope` instance. pub fn module_name(&self) -> Result { - self.process.read_string_len(self.addr + 0x8, 256) + self.process.read_string_length(self.address + 0x8, 256) } } diff --git a/src/sdk/schema_type.rs b/src/sdk/schema_type.rs index e72ffd2..040981a 100644 --- a/src/sdk/schema_type.rs +++ b/src/sdk/schema_type.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use crate::util::{Address, Process}; use anyhow::Result; @@ -6,10 +6,9 @@ use lazy_static::lazy_static; use regex::Regex; -use crate::mem::Address; -use crate::remote::Process; +use std::collections::HashMap; -/// Map of type names to their C equivalents. +/// Map of type names to their `C` equivalents. const TYPE_MAP: &[(&'static str, &'static str)] = &[ ("uint8", "uint8_t"), ("uint16", "uint16_t"), @@ -24,6 +23,8 @@ const TYPE_MAP: &[(&'static str, &'static str)] = &[ ]; lazy_static! { + /// A static HashMap that maps a string to a Regex pattern. + /// The Regex pattern is created by wrapping the string with word boundaries `(\b)`. static ref REGEX_MAP: HashMap<&'static str, Regex> = { let mut map = HashMap::with_capacity(TYPE_MAP.len()); @@ -35,22 +36,38 @@ lazy_static! { }; } -/// Represents a schema type. +/// Represents a type in the schema. pub struct SchemaType<'a> { process: &'a Process, - - /// Address of the schema type. - addr: Address, + address: Address, } impl<'a> SchemaType<'a> { - pub fn new(process: &'a Process, addr: Address) -> Self { - Self { process, addr } + /// Creates a new `SchemaType` instance. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// * `address` - The address of the `SchemaType` instance. + /// + /// # Returns + /// + /// * `SchemaType` - The new `SchemaType` instance. + pub fn new(process: &'a Process, address: Address) -> Self { + Self { process, address } } - /// Returns the name of the type. + /// Returns the name of the schema type. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaType` instance. + /// + /// # Returns + /// + /// * `Result` - The name of the schema type, wrapped in a `Result` object. pub fn name(&self) -> Result { - let name_ptr = self.process.read_memory::(self.addr + 0x8)?; + let name_ptr = self.process.read_memory::(self.address + 0x8)?; let name = self .process @@ -61,6 +78,15 @@ impl<'a> SchemaType<'a> { Ok(Self::convert_type_name(&name)) } + /// Converts a schema type name to its `C` equivalent. + /// + /// # Arguments + /// + /// * `type_name` - A string slice that holds the name of the schema type. + /// + /// # Returns + /// + /// * `String` - The `C` equivalent of the schema type name. fn convert_type_name(type_name: &str) -> String { let mut result = type_name.to_string(); diff --git a/src/sdk/schema_type_declared_class.rs b/src/sdk/schema_type_declared_class.rs index ac0f7c0..a50d0d2 100644 --- a/src/sdk/schema_type_declared_class.rs +++ b/src/sdk/schema_type_declared_class.rs @@ -1,25 +1,40 @@ +use crate::util::{Address, Process}; + use anyhow::Result; -use crate::mem::Address; -use crate::remote::Process; - -/// Represents a schema type declared class. +/// Represents a declared class type in the schema. pub struct SchemaTypeDeclaredClass<'a> { process: &'a Process, - - /// Address of the schema type declared class. - addr: Address, + address: Address, } impl<'a> SchemaTypeDeclaredClass<'a> { - pub fn new(process: &'a Process, addr: Address) -> Self { - Self { process, addr } + /// Creates a new `SchemaTypeDeclaredClass` instance. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// * `address` - The address of the `SchemaTypeDeclaredClass` instance. + /// + /// # Returns + /// + /// * `SchemaTypeDeclaredClass` - The new `SchemaTypeDeclaredClass` instance. + pub fn new(process: &'a Process, address: Address) -> Self { + Self { process, address } } - /// Returns the name of the class. + /// Returns the name of the declared class. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `SchemaTypeDeclaredClass` struct. + /// + /// # Returns + /// + /// * `Result` - The name of the declared class. pub fn name(&self) -> Result { - let name_ptr = self.process.read_memory::(self.addr + 0x8)?; + let name_ptr = self.process.read_memory::(self.address + 0x8)?; - self.process.read_string_len(name_ptr.into(), 64) + self.process.read_string_length(name_ptr.into(), 64) } } diff --git a/src/sdk/utl_ts_hash.rs b/src/sdk/utl_ts_hash.rs index 175fcb6..14b1ccd 100644 --- a/src/sdk/utl_ts_hash.rs +++ b/src/sdk/utl_ts_hash.rs @@ -1,44 +1,63 @@ +use crate::util::Process; + use anyhow::Result; use std::mem::offset_of; -use crate::remote::Process; - -/// Represents a hash bucket. +/// Represents the internal data of a hash table. #[derive(Debug)] #[repr(C)] -pub struct HashFixedDataInternal { +struct HashFixedDataInternal { ui_key: K, // 0x0010 next: *mut HashFixedDataInternal, // 0x0010 data: T, // 0x0010 } +/// Implementation of HashFixedDataInternal struct with methods for reading the next element in the hash table. impl HashFixedDataInternal { - pub fn next(&self, process: &Process) -> Result<*mut HashFixedDataInternal> { + /// Reads the next element in the hash table. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// Returns a Result containing a pointer to the next element in the hash table if successful, or an error if unsuccessful. + fn next(&self, process: &Process) -> Result<*mut HashFixedDataInternal> { process.read_memory::<*mut HashFixedDataInternal>( (self as *const _ as usize + offset_of!(HashFixedDataInternal, next)).into(), ) } } -/// Represents a hash bucket. +/// Represents the internal data of a hash bucket. #[derive(Debug)] #[repr(C)] -pub struct HashBucketDataInternal { +struct HashBucketDataInternal { data: T, // 0x0000 next: *mut HashFixedDataInternal, // 0x0008 ui_key: K, // 0x0010 } impl HashBucketDataInternal { - pub fn next(&self, process: &Process) -> Result<*mut HashFixedDataInternal> { + /// Reads the next element in the hash table. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// Returns a Result containing a pointer to the next element in the hash table if successful, or an error if unsuccessful. + fn next(&self, process: &Process) -> Result<*mut HashFixedDataInternal> { process.read_memory::<*mut HashFixedDataInternal>( (self as *const _ as usize + offset_of!(HashBucketDataInternal, next)).into(), ) } } -/// Represents a hash table. +/// Represents allocated data in a hash table. #[derive(Debug)] #[repr(C)] pub struct HashAllocatedData { @@ -47,17 +66,26 @@ pub struct HashAllocatedData { } impl HashAllocatedData { - pub fn list(&self, process: &Process) -> Result<[HashFixedDataInternal; 128]> { + /// Reads the list of elements in the hash table. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// Returns a Result containing a list of elements in the hash table if successful, or an error if unsuccessful. + fn list(&self, process: &Process) -> Result<[HashFixedDataInternal; 128]> { process.read_memory::<[HashFixedDataInternal; 128]>( (self as *const _ as usize + offset_of!(HashAllocatedData, list)).into(), ) } } -/// Represents a hash table. +/// A struct representing unallocated data in a hash table. #[derive(Debug)] #[repr(C)] -pub struct HashUnallocatedData { +struct HashUnallocatedData { next: *mut HashUnallocatedData, // 0x0000 unknown_1: K, // 0x0008 ui_key: K, // 0x0010 @@ -66,19 +94,46 @@ pub struct HashUnallocatedData { } impl HashUnallocatedData { - pub fn next(&self, process: &Process) -> Result<*mut HashUnallocatedData> { + /// Reads the next `HashUnallocatedData` element in memory. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result<*mut HashUnallocatedData>` - A Result containing a pointer to the next `HashUnallocatedData` element in memory. + fn next(&self, process: &Process) -> Result<*mut HashUnallocatedData> { process.read_memory::<*mut HashUnallocatedData>( (self as *const _ as usize + offset_of!(HashUnallocatedData, next)).into(), ) } - pub fn ui_key(&self, process: &Process) -> Result { + /// Reads the UI key of the `HashUnallocatedData` element in memory. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result` - A Result containing the UI key of the `HashUnallocatedData` element in memory. + fn ui_key(&self, process: &Process) -> Result { process.read_memory::( (self as *const _ as usize + offset_of!(HashUnallocatedData, ui_key)).into(), ) } - pub fn block_list(&self, process: &Process) -> Result<[HashBucketDataInternal; 256]> { + /// Reads the block list of the `HashUnallocatedData` element in memory. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result<[HashBucketDataInternal; 256]>` - A Result containing the block list of the `HashUnallocatedData` element in memory. + fn block_list(&self, process: &Process) -> Result<[HashBucketDataInternal; 256]> { process.read_memory::<[HashBucketDataInternal; 256]>( (self as *const _ as usize + offset_of!(HashUnallocatedData, block_list)).into(), ) @@ -88,15 +143,16 @@ impl HashUnallocatedData { /// Represents a hash bucket. #[derive(Debug)] #[repr(C)] -pub struct HashBucket { +struct HashBucket { pad_0: [u8; 0x10], // 0x0000 allocated_data: *const HashAllocatedData, // 0x0010 unallocated_data: *const HashUnallocatedData, // 0x0018 } +/// Represents a memory pool used by the `UtlTsHash` class. #[derive(Debug)] #[repr(C)] -pub struct UtlMemoryPool { +struct UtlMemoryPool { block_size: i32, // 0x0000 blocks_per_blob: i32, // 0x0004 grow_mode: i32, // 0x0008 @@ -107,14 +163,30 @@ pub struct UtlMemoryPool { impl UtlMemoryPool { /// Returns the number of blocks per blob. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `UtlMemoryPool` struct. + /// + /// # Returns + /// + /// * `i32` - The number of blocks per blob. #[inline] - pub fn block_size(&self) -> i32 { + fn block_size(&self) -> i32 { self.blocks_per_blob } /// Returns the number of blocks allocated. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `UtlMemoryPool` struct. + /// + /// # Returns + /// + /// * `i32` - The number of blocks allocated. #[inline] - pub fn count(&self) -> i32 { + fn count(&self) -> i32 { self.block_allocated_size } } @@ -132,25 +204,49 @@ where T: Copy, { /// Returns the number of blocks per blob. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `UtlTsHash` struct. + /// + /// # Returns + /// + /// * `i32` - The number of blocks per blob. #[inline] pub fn block_size(&self) -> i32 { self.entry_memory.block_size() } /// Returns the number of blocks allocated. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `UtlTsHash` struct. + /// + /// # Returns + /// + /// * `i32` - The number of blocks allocated. #[inline] pub fn count(&self) -> i32 { self.entry_memory.count() } /// Returns a list of elements in the hash table. + /// + /// # Arguments + /// + /// * `process` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result>` - A Result containing a list of elements in the hash table if successful, or an error if unsuccessful. pub fn elements(&self, process: &Process) -> Result> { + let mut address = self.buckets.unallocated_data; + let min_size = (self.block_size() as usize).min(self.count() as usize); let mut list = Vec::with_capacity(min_size); - let mut address = self.buckets.unallocated_data; - while !address.is_null() { let block_list = unsafe { (*address).block_list(process) }?; diff --git a/src/util/address.rs b/src/util/address.rs new file mode 100644 index 0000000..3ddd41c --- /dev/null +++ b/src/util/address.rs @@ -0,0 +1,211 @@ +use std::fmt::{LowerHex, UpperHex}; +use std::ops::{Add, AddAssign, Sub, SubAssign}; + +/// Represents a memory address. +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)] +#[repr(transparent)] +pub struct Address(pub usize); + +impl Address { + /// Adds the given value to the current address and returns a new `Address`. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Address` struct. + /// * `value` - The value to add to the current address. + /// + /// # Returns + /// + /// * `Address` - A new `Address` struct with the value of the current address plus the given value. + #[inline] + pub fn add(&self, value: usize) -> Self { + Self(self.0 + value) + } + + /// Returns true if the value of the address is zero. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Address` struct. + /// + /// # Returns + /// + /// * `bool` - True if the value of the address is zero, false otherwise. + #[inline] + pub fn is_zero(&self) -> bool { + self.0 == 0 + } + + /// Subtracts a value from the current address and returns a new `Address`. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Address` struct. + /// * `value` - The value to subtract from the current address. + /// + /// # Returns + /// + /// * `Address` - A new `Address` struct with the value of the current address minus the given value. + #[inline] + pub fn sub(&self, value: usize) -> Self { + Self(self.0 - value) + } + + /// Returns a raw pointer to the underlying data as a `*const T`. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Address` struct. + /// + /// # Returns + /// + /// * `*const T` - A raw pointer to the underlying data as a `*const T`. + #[inline] + pub fn as_ptr(&self) -> *const T { + self.0 as *const T + } + + /// Returns a mutable pointer to the underlying data. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Address` struct. + /// + /// # Returns + /// + /// * `*mut T` - A mutable pointer to the underlying data. + #[inline] + pub fn as_mut_ptr(&self) -> *mut T { + self.0 as *mut T + } +} + +/// Converts a `usize` value to an `Address` struct. +impl From for Address { + fn from(value: usize) -> Self { + Self(value) + } +} + +/// Converts a raw pointer to a `usize` value and creates an `Address` instance from it. +impl From<*const u8> for Address { + fn from(value: *const u8) -> Self { + Self(value as usize) + } +} + +/// Converts a raw pointer to a `usize` value and wraps it in an `Address` struct. +impl From<*mut u8> for Address { + fn from(value: *mut u8) -> Self { + Self(value as usize) + } +} + +/// Converts an `Address` struct to a `usize` value. +impl From
for usize { + fn from(value: Address) -> Self { + value.0 + } +} + +/// Converts an `Address` struct to a raw pointer to an unsigned 8-bit integer. +impl From
for *const u8 { + fn from(value: Address) -> Self { + value.0 as *const u8 + } +} + +/// Converts an `Address` struct to a raw pointer to a mutable unsigned 8-bit integer. +impl From
for *mut u8 { + fn from(value: Address) -> Self { + value.0 as *mut u8 + } +} + +/// Implements the addition of a `usize` value to an `Address` value. +impl Add for Address { + type Output = Self; + + /// Adds a `usize` value to an `Address` value and returns the result. + fn add(self, rhs: usize) -> Self::Output { + Self(self.0 + rhs) + } +} + +/// Implements the addition of two `Address` instances. +impl Add
for Address { + type Output = Self; + + /// Adds two `Address` instances and returns a new `Address`. + fn add(self, rhs: Address) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +/// Implements the `AddAssign` trait for `Address` struct. +impl AddAssign for Address { + /// Adds the given `rhs` value to the `Address` struct. + fn add_assign(&mut self, rhs: usize) { + self.0 += rhs; + } +} + +/// Implements the `AddAssign` trait for `Address`. +impl AddAssign
for Address { + /// Adds the given `rhs` value to the `Address` struct. + fn add_assign(&mut self, rhs: Address) { + self.0 += rhs.0; + } +} + +/// Implements the subtraction of a `usize` from an `Address`. +impl Sub for Address { + type Output = Self; + + /// Subtracts the given `rhs` from `&self`. + fn sub(self, rhs: usize) -> Self::Output { + Self(self.0 - rhs) + } +} + +/// Implements the subtraction operation for `Address`. +impl Sub
for Address { + type Output = Self; + + /// Subtracts the given `rhs` from `&self`. + fn sub(self, rhs: Address) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +/// Implements the subtraction assignment operation for `Address` and `usize`. +impl SubAssign for Address { + /// Subtracts the given `rhs` from the `Address`. + fn sub_assign(&mut self, rhs: usize) { + self.0 -= rhs; + } +} + +/// Implements the subtraction assignment operator for `Address`. +impl SubAssign
for Address { + /// Subtracts the value of `rhs` from `&self`. + fn sub_assign(&mut self, rhs: Address) { + self.0 -= rhs.0; + } +} + +/// Implements the `UpperHex` trait for the `Address` struct. +impl UpperHex for Address { + /// Formats the value of `&self` using the `write!` macro and the `UpperHex` format specifier. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:#X}", self.0) + } +} + +/// Implements the `LowerHex` trait for the `Address` struct. +impl LowerHex for Address { + /// Formats the value of `&self` using the `write!` macro and the `LowerHex` format specifier. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:#x}", self.0) + } +} diff --git a/src/remote/mod.rs b/src/util/mod.rs similarity index 66% rename from src/remote/mod.rs rename to src/util/mod.rs index fa3b87b..08a23a3 100644 --- a/src/remote/mod.rs +++ b/src/util/mod.rs @@ -1,5 +1,7 @@ +pub use address::Address; pub use module::Module; pub use process::Process; +pub mod address; pub mod module; pub mod process; diff --git a/src/util/module.rs b/src/util/module.rs new file mode 100644 index 0000000..fb886f3 --- /dev/null +++ b/src/util/module.rs @@ -0,0 +1,156 @@ +use super::Address; + +use anyhow::Result; + +use goblin::pe::export::Export; +use goblin::pe::import::Import; +use goblin::pe::options::ParseOptions; +use goblin::pe::section_table::SectionTable; +use goblin::pe::PE; + +/// Represents a module loaded into the process. +pub struct Module<'a> { + /// The name of the module. + pub name: &'a str, + + /// A reference to a slice of bytes containing the module data. + pub data: &'a [u8], + + /// The PE file format representation of the module. + pub pe: PE<'a>, +} + +impl<'a> Module<'a> { + /// Parses the given module name and data and returns a `Result` containing a `Module` struct. + /// + /// # Arguments + /// + /// * `name` - A string slice that holds the name of the module. + /// * `data` - A byte slice that holds the data of the module. + /// + /// # Returns + /// + /// * `Result` - A `Result` containing a `Module` instance if successful, or an error if the module could not be parsed. + pub fn parse(name: &'a str, data: &'a [u8]) -> Result { + let pe = PE::parse_with_opts( + data, + &ParseOptions { + parse_attribute_certificates: false, + resolve_rva: false, + }, + )?; + + Ok(Self { name, data, pe }) + } + + /// Returns the base address of the module. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Module` struct. + /// + /// # Returns + /// + /// * `Address` - The base address of the module. + #[inline] + pub fn base(&self) -> Address { + self.pe.image_base.into() + } + + /// Returns a slice of `Export` structs representing the exports of the module. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Module` struct. + /// + /// # Returns + /// + /// * `&[Export]` - A slice of `Export` structs representing the exports of the module. + #[inline] + pub fn exports(&self) -> &'a [Export] { + self.pe.exports.as_slice() + } + + /// Returns the address of the export with the given name, if it exists. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Module` struct. + /// + /// # Returns + /// + /// * `Option
` - The address of the export with the given name, if it exists. + #[inline] + pub fn get_export_by_name(&self, name: &str) -> Option
{ + self.pe + .exports + .iter() + .find(|e| e.name.unwrap() == name) + .map(|e| (self.pe.image_base + e.rva).into()) + } + + /// Returns the address of the imported function with the given name, if it exists. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Module` struct. + /// + /// # Returns + /// + /// * `Option
` - The address of the imported function with the given name, if it exists. + #[inline] + pub fn get_import_by_name(&self, name: &str) -> Option
{ + self.pe + .imports + .iter() + .find(|i| i.name.to_string() == name) + .map(|i| (self.pe.image_base + i.rva).into()) + } + + /// Returns a slice of the imported functions of the module. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Module` struct. + /// + /// # Returns + /// + /// * `&[Import]` - A slice of `Import` structs representing the imported functions of the module. + #[inline] + pub fn imports(&self) -> &'a [Import] { + self.pe.imports.as_slice() + } + + /// Returns a slice of the section table entries in the module's PE header. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Module` struct. + /// + /// # Returns + /// + /// * `&[SectionTable]` - A slice of `SectionTable` structs representing the section table entries in the module's PE header. + #[inline] + pub fn sections(&self) -> &[SectionTable] { + self.pe.sections.as_slice() + } + + /// Returns the size of the module. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Module` struct. + /// + /// # Returns + /// + /// * `u32` - The size of the module. + #[inline] + pub fn size(&self) -> u32 { + self.pe + .header + .optional_header + .unwrap() + .windows_fields + .size_of_image + } +} diff --git a/src/util/process.rs b/src/util/process.rs new file mode 100644 index 0000000..485d96f --- /dev/null +++ b/src/util/process.rs @@ -0,0 +1,371 @@ +use super::{Address, Module}; + +use anyhow::{bail, Result}; + +use std::collections::HashMap; +use std::ffi::{c_void, CStr}; +use std::mem; +use std::ptr; + +use windows::Win32::Foundation::{CloseHandle, HANDLE}; +use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; +use windows::Win32::System::Diagnostics::ToolHelp::*; +use windows::Win32::System::Threading::{OpenProcess, PROCESS_ALL_ACCESS}; + +/// Represents a Windows process. +#[derive(Debug)] +pub struct Process { + /// ID of the process. + id: u32, + + /// Handle to the process. + handle: HANDLE, + + /// A HashMap containing the name of each module and its corresponding raw data. + modules: HashMap>, +} + +impl Process { + /// Creates a new `Process` instance with the given name. + /// + /// # Arguments + /// + /// * `name` - A string slice that holds the name of the process. + /// + /// # Returns + /// + /// * `Result` - A `Result` containing the `Process` instance if successful, or an error if the process could not be found. + pub fn new(name: &str) -> Result { + let id = Self::get_process_id_by_name(name)?; + + let handle = unsafe { OpenProcess(PROCESS_ALL_ACCESS, false, id) }?; + + Ok(Self { + id, + handle, + modules: HashMap::new(), + }) + } + + /// Initializes the process by parsing the loaded modules. + /// + /// # Arguments + /// + /// * `&self` - A mutable reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result<()>` - A `Result` indicating the outcome of the operation. + pub fn initialize(&mut self) -> Result<()> { + self.parse_loaded_modules() + } + + /// Searches for a pattern in the memory of a specified module and returns the address of the first occurrence. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Process` struct. + /// * `module_name` - A string slice that holds the name of the module to search in. + /// * `pattern` - A string slice that holds the pattern to search for. + /// + /// # Returns + /// + /// * `Option
` - The address of the first occurrence of the pattern if found, or `None` if the pattern was not found. + pub fn find_pattern(&self, module_name: &str, pattern: &str) -> Option
{ + let module = self.get_module_by_name(module_name)?; + + let pattern_bytes = Self::pattern_to_bytes(pattern); + + for (i, window) in module.data.windows(pattern_bytes.len()).enumerate() { + if window + .iter() + .zip(&pattern_bytes) + .all(|(&x, &y)| x == y as u8 || y == -1) + { + return Some(module.base() + i); + } + } + + None + } + + /// Returns an optional `Module` instance by its name. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Process` struct. + /// * `name` - A string slice representing the name of the module to retrieve. + /// + /// # Returns + /// + /// * `Option` - An optional `Module` instance if the module was found, or `None` if the module was not found. + pub fn get_module_by_name<'a>(&'a self, name: &'a str) -> Option> { + self.modules + .get(name) + .map(|data| Module::parse(name, data).unwrap()) + } + + /// Returns a vector of `Module` instances parsed from the process's loaded modules. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result>` - A `Result` containing a vector of `Module` instances if successful, or an error if the modules could not be parsed. + pub fn modules(&self) -> Result> { + let mut modules = Vec::new(); + + for (name, data) in &self.modules { + modules.push(Module::parse(name, data)?); + } + + Ok(modules) + } + + /// Reads the memory at the specified address and returns the value as type T. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Process` struct. + /// * `address` - The address to read from. + /// + /// # Examples + /// + /// ``` + /// let process = Process::new(pid)?; + /// let value: i32 = process.read_memory(address)?; + /// ``` + /// + /// # Returns + /// + /// * `Result` - A `Result` containing the value if successful, or an error if the memory read fails. + pub fn read_memory(&self, address: Address) -> Result { + let mut buffer: T = unsafe { mem::zeroed() }; + + self.read_memory_raw( + address, + &mut buffer as *const _ as *mut _, + mem::size_of::(), + )?; + + Ok(buffer) + } + + /// Reads the memory of a process and stores it in a buffer. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Process` struct. + /// * `address` - The address to start reading from. + /// * `buffer` - A pointer to the buffer where the read data will be stored. + /// * `size` - The number of bytes to read. + /// + /// # Returns + /// + /// * `Result<()>` - A `Result` indicating the outcome of the operation. + pub fn read_memory_raw( + &self, + address: Address, + buffer: *mut c_void, + size: usize, + ) -> Result<()> { + unsafe { + ReadProcessMemory( + self.handle, + address.as_ptr(), + buffer, + size, + Some(ptr::null_mut()), + ) + } + .map_err(|e| e.into()) + } + + /// Reads a null-terminated string from the process memory at the given address. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Process` struct. + /// * `address` - The address in the process memory where the string is located. + /// + /// # Returns + /// + /// * `Result` - A `Result` containing the string read from the process memory if successful, or an error if the memory read fails or if the string contains invalid UTF-8. + pub fn read_string(&self, address: Address) -> Result { + let mut buffer = Vec::new(); + + for i in 0.. { + match self.read_memory::(address + i) { + Ok(byte) if byte != 0 => buffer.push(byte), + _ => break, + } + } + + Ok(String::from_utf8(buffer)?) + } + + /// Reads a string of the specified length from the process memory at the given address. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Process` struct. + /// * `address` - The address to read the string from. + /// * `length` - The length of the string to read. + /// + /// # Returns + /// + /// * `Result` - A `Result` containing the string read from the process memory if successful, or an error if the memory read fails or if the string contains invalid UTF-8. + pub fn read_string_length(&self, address: Address, length: usize) -> Result { + let mut buffer = vec![0; length]; + + self.read_memory_raw(address, buffer.as_mut_ptr() as *mut _, length)?; + + if let Some(end) = buffer.iter().position(|&x| x == 0) { + buffer.truncate(end); + } + + Ok(String::from_utf8(buffer)?) + } + + /// Resolves a jump instruction at the given address by calculating the target address based on the + /// displacement value at the given offset. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Process` struct. + /// * `address` - The address of the jump instruction. + /// * `offset` - The offset of the displacement value. + /// * `length` - The length of the instruction. + /// + /// # Returns + /// + /// * `Result
` - A `Result` containing the absolute address if successful, or an error if the memory read fails. + pub fn resolve_jmp(&self, address: Address, offset: usize, length: usize) -> Result
{ + let displacement = self.read_memory::(address.add(offset))?; + + Ok(((address.add(length).0 as isize + displacement as isize) as usize).into()) + } + + /// Resolves the absolute address of a relative instruction pointer (RIP) address. + /// + /// # Arguments + /// + /// * `&self` - A reference to the `Process` struct. + /// * `address` - The address of the relative instruction pointer (RIP). + /// * `offset` - The offset of the displacement value. + /// * `length` - The length of the instruction. + /// + /// # Returns + /// + /// * `Result
` - A `Result` containing the absolute address if successful, or an error if the memory read fails. + pub fn resolve_rip(&self, address: Address, offset: usize, length: usize) -> Result
{ + let displacement = self.read_memory::(address.add(offset))?; + + Ok(((address.add(length).0 as isize + displacement as isize) as usize).into()) + } + + /// Returns the process ID of the first process with the given name. + /// + /// # Arguments + /// + /// * `process_name` - A string slice that holds the name of the process to search for. + /// + /// # Returns + /// + /// * `Result` - A `Result` containing the process ID if successful, or an error if the process could not be found. + fn get_process_id_by_name(process_name: &str) -> Result { + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }?; + + let mut entry = PROCESSENTRY32 { + dwSize: mem::size_of::() as u32, + ..Default::default() + }; + + unsafe { + Process32First(snapshot, &mut entry)?; + + while Process32Next(snapshot, &mut entry).is_ok() { + let name = CStr::from_ptr(&entry.szExeFile as *const _ as *const _).to_str()?; + + if name == process_name { + return Ok(entry.th32ProcessID); + } + } + } + + bail!("Process not found: {}", process_name) + } + + /// Parses the loaded modules of a process and stores them in a HashMap with the module name as the key and the module data as the value. + /// + /// # Arguments + /// + /// * `&self` - A mutable reference to the `Process` struct. + /// + /// # Returns + /// + /// * `Result<()>` - A `Result` indicating the outcome of the operation. + fn parse_loaded_modules(&mut self) -> Result<()> { + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, self.id) }?; + + let mut entry = MODULEENTRY32 { + dwSize: mem::size_of::() as u32, + ..Default::default() + }; + + unsafe { + Module32First(snapshot, &mut entry)?; + + while Module32Next(snapshot, &mut entry).is_ok() { + let name = CStr::from_ptr(&entry.szModule as *const _ as *const _).to_str()?; + + let mut data = vec![0; entry.modBaseSize as usize]; + + self.read_memory_raw( + entry.modBaseAddr.into(), + data.as_mut_ptr() as *mut _, + data.len(), + )?; + + self.modules.insert(name.to_string(), data); + } + } + + Ok(()) + } + + /// Converts a pattern string to a vector of bytes. + /// + /// # Arguments + /// + /// * `pattern` - A string slice that represents the pattern to be converted. + /// + /// # Returns + /// + /// * `Vec` - A vector of bytes representing the pattern. + fn pattern_to_bytes(pattern: &str) -> Vec { + pattern + .split_whitespace() + .map(|s| { + if s == "?" { + -1 + } else { + i32::from_str_radix(s, 16).unwrap_or(0) + } + }) + .collect() + } +} + +/// Implements the `Drop` trait for the `Process` struct. +/// +/// When a `Process` instance goes out of scope, this implementation will automatically close the process handle if it is not invalid. +impl Drop for Process { + fn drop(&mut self) { + if !self.handle.is_invalid() { + unsafe { CloseHandle(self.handle).unwrap() } + } + } +}