diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da27059 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +uniject_gui +target/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..edda31a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,230 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "either" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "sysinfo" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + +[[package]] +name = "uniject" +version = "0.1.0" +dependencies = [ + "bitflags", + "once_cell", + "sysinfo", + "winapi", +] + +[[package]] +name = "uniject_console" +version = "0.1.0" +dependencies = [ + "uniject", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..973c5e6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["uniject", "uniject_console"] diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..2f7896c --- /dev/null +++ b/Readme.md @@ -0,0 +1,40 @@ +# Uniject + +> [!NOTE] +> Most of the Readme has been taken 1:1 from SharpMonoInjector since Uniject does the exact same! + +Uniject is a tool for injecting assemblies into Mono embedded applications, commonly Unity Engine based games, written in Rust. It is a rewrite of the [SharpMonoInjector](https://github.com/warbler/SharpMonoInjector/) tool, offering the same functionality. + +The target process _usually_ does not have to be restarted in order to inject an updated version of the assembly. Your unload method must destroy all of its resources (such as game objects). + +Uniject works by dynamically generating machine code, writing it to the target process, and executing it using CreateRemoteThread. The code calls functions in the Mono embedded API. The return value is obtained with ReadProcessMemory. + +Both x86 and x64 processes are supported. + +In order for the injector to work, the load/unload methods need to match the following method signature: + +```csharp +static void Method() +``` + +### Upcoming GUI Version + +A GUI version of Uniject is currently in development and will be released very soon. + +### Example Assemblies + +You can find example assemblies to use with Uniject at the SharpMonoInjector Repository: [here](https://github.com/warbler/SharpMonoInjector/tree/master/src/ExampleAssembly) + +These example assemblies demonstrate how to properly structure your code for injection and provide a starting point for creating your own assemblies. + +### Releases + +In the releases section, you will find the console version available for download. + +### Credits + +Uniject was created as a learning project to explore different injection techniques and to provide an improved version of the original SharpMonoInjector tool. + +If you like this project, please also check out the original repo [here](https://github.com/warbler/SharpMonoInjector). + +Credits go to [warbler](https://github.com/warbler). diff --git a/uniject/.gitignore b/uniject/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/uniject/.gitignore @@ -0,0 +1 @@ +/target diff --git a/uniject/Cargo.lock b/uniject/Cargo.lock new file mode 100644 index 0000000..9acb5b6 --- /dev/null +++ b/uniject/Cargo.lock @@ -0,0 +1,223 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "sysinfo" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + +[[package]] +name = "uniject" +version = "0.1.0" +dependencies = [ + "bitflags", + "once_cell", + "sysinfo", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/uniject/Cargo.toml b/uniject/Cargo.toml new file mode 100644 index 0000000..d94f7e2 --- /dev/null +++ b/uniject/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "uniject" +version = "0.1.0" +edition = "2021" + +[dependencies] +winapi = {version = "0.3", features = ["processthreadsapi", "winnt", "memoryapi", "psapi", "winbase", "errhandlingapi"]} +bitflags = "2" +sysinfo = "0.30" +once_cell = "1" \ No newline at end of file diff --git a/uniject/src/assembler.rs b/uniject/src/assembler.rs new file mode 100644 index 0000000..eec0a03 --- /dev/null +++ b/uniject/src/assembler.rs @@ -0,0 +1,103 @@ +use std::collections::VecDeque; + +pub struct Assembler { + asm: VecDeque +} + +impl Assembler { + pub fn new() -> Self { + Assembler { + asm: VecDeque::new() + } + } + + pub fn mov_rax(&mut self, arg: usize) { + self.asm.extend([0x48, 0xB8]); + self.asm.extend(arg.to_le_bytes()); + } + + pub fn mov_rcx(&mut self, arg: usize) { + self.asm.extend([0x48, 0xB9]); + self.asm.extend(arg.to_le_bytes()); + } + + pub fn mov_rdx(&mut self, arg: usize) { + self.asm.extend([0x48, 0xBA]); + self.asm.extend(arg.to_le_bytes()); + } + + pub fn mov_r8(&mut self, arg: usize) { + self.asm.extend([0x49, 0xB8]); + self.asm.extend(arg.to_le_bytes()); + } + + pub fn mov_r9(&mut self, arg: usize) { + self.asm.extend([0x49, 0xB9]); + self.asm.extend(arg.to_le_bytes()); + } + + pub fn sub_rsp(&mut self, arg: u8) { + self.asm.extend([0x48, 0x83, 0xEC]); + self.asm.push_back(arg); + } + + pub fn call_rax(&mut self) { + self.asm.extend([0xFF, 0xD0]); + } + + pub fn add_rsp(&mut self, arg: u8) { + self.asm.extend([0x48, 0x83, 0xC4]); + self.asm.push_back(arg); + } + + pub fn mov_rax_to(&mut self, dest: usize) { + self.asm.extend([0x48, 0xA3]); + self.asm.extend(dest.to_le_bytes()); + } + + pub fn push(&mut self, arg: isize) { + if arg >= -128 && arg <= 127 { + self.asm.push_back(0x6A); + self.asm.push_back(arg as u8); + } else { + self.asm.push_back(0x68); + self.asm.extend((arg as i32).to_le_bytes()); + } + } + + pub fn mov_eax(&mut self, arg: isize) { + self.asm.push_back(0xB8); + self.asm.extend((arg as i32).to_le_bytes()); + } + + pub fn call_eax(&mut self) { + self.asm.extend([0xFF, 0xD0]); + } + + pub fn add_esp(&mut self, arg: u8) { + self.asm.extend([0x83, 0xC4]); + self.asm.push_back(arg); + } + + pub fn mov_eax_to(&mut self, dest: usize) { + self.asm.push_back(0xA3); + self.asm.extend((dest as i32).to_le_bytes()); + } + + pub fn return_(&mut self) { + self.asm.push_back(0xC3); + } + + pub fn to_byte_array(&self) -> Vec { + self.asm.iter().cloned().collect() + } + + + // pub fn get_asm(&self) -> Vec { + // self.asm.iter().cloned().collect() + // } + + // pub fn clear(&mut self) { + // self.asm.clear(); + // } +} diff --git a/uniject/src/exported_functions.rs b/uniject/src/exported_functions.rs new file mode 100644 index 0000000..0bc9ec8 --- /dev/null +++ b/uniject/src/exported_functions.rs @@ -0,0 +1,13 @@ +pub struct ExportedFunction { + pub name: String, + pub address: usize, +} + +impl ExportedFunction { + pub fn new(name: &str, address: usize) -> Self { + ExportedFunction { + name: name.to_string(), + address, + } + } +} \ No newline at end of file diff --git a/uniject/src/injector.rs b/uniject/src/injector.rs new file mode 100644 index 0000000..17d2fa1 --- /dev/null +++ b/uniject/src/injector.rs @@ -0,0 +1,558 @@ +use std::{collections::HashMap, ptr::null_mut}; + +use once_cell::sync::Lazy; +use winapi::{ctypes::c_void, shared::{minwindef::DWORD, ntdef::HANDLE}}; + +use crate::{assembler::Assembler, injector_exceptions::InjectorException, memory::Memory, native::{CloseHandle, CreateRemoteThread, OpenProcess, WaitForSingleObject, WaitResult, ProcessAccessRights}, proc_utils::{find_process_id_by_name, get_exported_functions, get_mono_module, is_64_bit_process}, status::MonoImageOpenStatus}; + +static EXPORTS: Lazy> = Lazy::new(|| { + HashMap::from([ + ("mono_get_root_domain", 0), + ("mono_thread_attach", 0), + ("mono_image_open_from_data", 0), + ("mono_assembly_load_from_full", 0), + ("mono_assembly_get_image", 0), + ("mono_class_from_name", 0), + ("mono_class_get_method_from_name", 0), + ("mono_runtime_invoke", 0), + ("mono_assembly_close", 0), + ("mono_image_strerror", 0), + ("mono_object_get_class", 0), + ("mono_class_get_name", 0), + ]) +}); + + +pub struct Injector { + handle: HANDLE, + memory: Memory, + exports: HashMap<&'static str, usize>, + root_domain: usize, + attach: bool, + mono_module: usize, + pub is_64_bit: bool, +} + + +impl Injector { + const MONO_GET_ROOT_DOMAIN: &'static str = "mono_get_root_domain"; + const MONO_THREAD_ATTACH: &'static str = "mono_thread_attach"; + const MONO_IMAGE_OPEN_FROM_DATA: &'static str = "mono_image_open_from_data"; + const MONO_ASSEMBLY_LOAD_FROM_FULL: &'static str = "mono_assembly_load_from_full"; + const MONO_ASSEMBLY_GET_IMAGE: &'static str = "mono_assembly_get_image"; + const MONO_CLASS_FROM_NAME: &'static str = "mono_class_from_name"; + const MONO_CLASS_GET_METHOD_FROM_NAME: &'static str = "mono_class_get_method_from_name"; + const MONO_RUNTIME_INVOKE: &'static str = "mono_runtime_invoke"; + const MONO_ASSEMBLY_CLOSE: &'static str = "mono_assembly_close"; + const MONO_IMAGE_STRERROR: &'static str = "mono_image_strerror"; + const MONO_OBJECT_GET_CLASS: &'static str = "mono_object_get_class"; + const MONO_CLASS_GET_NAME: &'static str = "mono_class_get_name"; + + pub fn new_by_name(process_name: &str) -> Result { + let process_id = find_process_id_by_name(process_name) + .ok_or_else(|| InjectorException::new(&format!("Could not find a process with the name {}", process_name)))?; + + Self::new(process_id) + } + + pub fn new(process_id: u32) -> Result { + let handle = unsafe { OpenProcess(ProcessAccessRights::ProcessAllAccess as u32 , 0, process_id) }; + if handle == null_mut() { + return Err(InjectorException::new(&format!("Failed to open process with ID {}", process_id))); + } + + let mono_module = match get_mono_module(handle)? { + Some(module) => module, + None => return Err(InjectorException::new("Mono module not found")), + }; + + let is_64_bit = is_64_bit_process(handle)?; + + let memory = Memory::new(handle).map_err(|err| InjectorException::with_inner("Failed to create memory handler", Box::new(err)))?; + + Ok(Injector { + handle, + memory, + exports: EXPORTS.clone(), + root_domain: 0, + attach: false, + mono_module, + is_64_bit, + }) + } + + pub fn new_with_handle( + process_handle: HANDLE, + mono_module: usize, + ) -> Result { + if process_handle == null_mut() { + return Err(InjectorException::new("Argument cannot be zero (processHandle)")); + } + + if mono_module == 0 { + return Err(InjectorException::new("Argument cannot be zero (monoModule)")); + } + + let is_64_bit = is_64_bit_process(process_handle)?; + + let memory = Memory::new(process_handle).map_err(|err| InjectorException::with_inner( + "Failed to create memory handler", + Box::new(err), + ))?; + + Ok(Injector { + handle: process_handle, + memory, + exports : EXPORTS.clone(), + root_domain: 0, + attach: false, + mono_module, + is_64_bit, + }) + } + + pub fn dispose(&mut self) { + // self.memory.clear_allocations(); //drop should be enough + unsafe { + CloseHandle(self.handle); + } + } + + pub fn obtain_mono_exports(&mut self) -> Result<(), InjectorException> { + let exported_functions = get_exported_functions(self.handle, self.mono_module)?; + + for ef in exported_functions { + if let Some(export) = self.exports.get_mut(&*ef.name) { + *export = ef.address; + } + } + + for (name, &address) in &self.exports { + if address == 0 { + return Err(InjectorException::new(&format!( + "Failed to obtain the address of {}()", + name + ))); + } + } + + Ok(()) + } + + pub fn inject( + &mut self, + raw_assembly: &[u8], + namespace: &str, + class_name: &str, + method_name: &str, + ) -> Result { + if raw_assembly.is_empty() { + return Err(InjectorException::new("rawAssembly cannot be empty")); + } + + if class_name.is_empty() { + return Err(InjectorException::new("className cannot be null")); + } + + if method_name.is_empty() { + return Err(InjectorException::new("methodName cannot be null")); + } + + self.obtain_mono_exports()?; + + self.root_domain = self.get_root_domain()?; + let raw_image = self.open_image_from_data(raw_assembly)?; + + self.attach = true; + let assembly = self.open_assembly_from_image(raw_image)?; + let image = self.get_image_from_assembly(assembly)?; + let class = self.get_class_from_name(image, namespace, class_name)?; + let method = self.get_method_from_name(class, method_name)?; + + self.runtime_invoke(method)?; + + Ok(assembly) + } + + pub fn eject( + &mut self, + assembly: usize, + namespace: &str, + class_name: &str, + method_name: &str, + ) -> Result<(), InjectorException> { + if assembly == 0 { + return Err(InjectorException::new("Assembly cannot be zero")); + } + + if class_name.is_empty() { + return Err(InjectorException::new("Class name cannot be null")); + } + + if method_name.is_empty() { + return Err(InjectorException::new("Method name cannot be null")); + } + + self.obtain_mono_exports()?; + self.root_domain = self.get_root_domain()?; + self.attach = true; + + let image = self.get_image_from_assembly(assembly)?; + let class = self.get_class_from_name(image, namespace, class_name)?; + let method = self.get_method_from_name(class, method_name)?; + + self.runtime_invoke(method)?; + self.close_assembly(assembly)?; + + Ok(()) + } + + fn throw_if_null(&self, ptr: usize, method_name: &str) -> Result<(), InjectorException> { + if ptr == 0 { + return Err(InjectorException::new(&format!( + "{}() returned NULL", + method_name + ))); + } + + Ok(()) + } + + pub fn get_root_domain(&mut self) -> Result { + let root_domain = self.execute( + *self + .exports + .get(Self::MONO_GET_ROOT_DOMAIN) + .ok_or_else(|| InjectorException::new("Mono get root domain export not found"))?, + &[], + )?; + self.throw_if_null(root_domain, Self::MONO_GET_ROOT_DOMAIN)?; + + Ok(root_domain) + } + + pub fn open_image_from_data(&mut self, assembly: &[u8]) -> Result { + //allocate space for pointer + let status_ptr = self.memory.allocate(4)?; + + let assembly_data_ptr = self.memory.allocate_and_write(assembly)?; + + //fetch + let mono_image_open_from_data_address = *self + .exports + .get(Self::MONO_IMAGE_OPEN_FROM_DATA) + .ok_or_else(|| InjectorException::new("Mono image open from data export not found"))?; + + //execute with pre alloc + let raw_image = self.execute( + mono_image_open_from_data_address, + &[assembly_data_ptr, assembly.len() as usize, 1, status_ptr], + )?; + + //read status + let status = MonoImageOpenStatus::from(self.memory.read_int(status_ptr)?); + + if status != MonoImageOpenStatus::MonoImageOk { + //get error msg ptr + let mono_image_strerror_address = *self + .exports + .get(Self::MONO_IMAGE_STRERROR) + .ok_or_else(|| InjectorException::new("Mono image strerror export not found"))?; + let message_ptr = self.execute(mono_image_strerror_address, &[status as usize])?; + + //read msg + let message = self.memory.read_string(message_ptr, 256)?; + return Err(InjectorException::new(&format!( + "{}() failed: {}", + Self::MONO_IMAGE_OPEN_FROM_DATA, message + ))); + } + + Ok(raw_image) + } + + pub fn open_assembly_from_image(&mut self, image: usize) -> Result { + let status_ptr = self.memory.allocate(4)?; + let empty_array_ptr = self.memory.allocate_and_write(&[0u8])?; + + let assembly = self.execute( + *self + .exports + .get(Self::MONO_ASSEMBLY_LOAD_FROM_FULL) + .ok_or_else(|| InjectorException::new("Mono assembly load from full export not found"))?, + &[image, empty_array_ptr, status_ptr, 0], + )?; + + let status = MonoImageOpenStatus::from(self.memory.read_int(status_ptr)?); + + if status != MonoImageOpenStatus::MonoImageOk { + let message_ptr = self.execute( + *self + .exports + .get(Self::MONO_IMAGE_STRERROR) + .ok_or_else(|| InjectorException::new("Mono image strerror export not found"))?, + &[status as usize], + )?; + + let message = self.memory.read_string(message_ptr, 256)?; + return Err(InjectorException::new(&format!( + "{}() failed: {}", + Self::MONO_ASSEMBLY_LOAD_FROM_FULL, message + ))); + } + + Ok(assembly) + } + + pub fn get_image_from_assembly(&mut self, assembly: usize) -> Result { + let image = self.execute( + *self + .exports + .get(Self::MONO_ASSEMBLY_GET_IMAGE) + .ok_or_else(|| InjectorException::new("Mono assembly get image export not found"))?, + &[assembly], + )?; + self.throw_if_null(image, Self::MONO_ASSEMBLY_GET_IMAGE)?; + + Ok(image) + } + + pub fn get_class_from_name( + &mut self, + image: usize, + namespace: &str, + class_name: &str, + ) -> Result { + let namespace_ptr = self.memory.allocate_and_write(namespace.as_bytes())?; + let class_name_ptr = self.memory.allocate_and_write(class_name.as_bytes())?; + + let class = self.execute( + *self + .exports + .get(Self::MONO_CLASS_FROM_NAME) + .ok_or_else(|| InjectorException::new("Mono class from name export not found"))?, + &[image, namespace_ptr, class_name_ptr], + )?; + self.throw_if_null(class, Self::MONO_CLASS_FROM_NAME)?; + + Ok(class) + } + + pub fn get_method_from_name( + &mut self, + class: usize, + method_name: &str, + ) -> Result { + let method_name_ptr = self.memory.allocate_and_write(method_name.as_bytes())?; + + let method = self.execute( + *self + .exports + .get(Self::MONO_CLASS_GET_METHOD_FROM_NAME) + .ok_or_else(|| InjectorException::new("Mono class get method from name export not found"))?, + &[class, method_name_ptr, 0], + )?; + self.throw_if_null(method, Self::MONO_CLASS_GET_METHOD_FROM_NAME)?; + + Ok(method) + } + + pub fn get_class_name(&mut self, mono_object: usize) -> Result { + let class_address = self.execute( + *self + .exports + .get(Self::MONO_OBJECT_GET_CLASS) + .ok_or_else(|| InjectorException::new("Mono object get class export not found"))?, + &[mono_object], + )?; + self.throw_if_null(class_address, Self::MONO_OBJECT_GET_CLASS)?; + + let class_name_address = self.execute( + *self + .exports + .get(Self::MONO_CLASS_GET_NAME) + .ok_or_else(|| InjectorException::new("Mono class get name export not found"))?, + &[class_address], + )?; + self.throw_if_null(class_name_address, Self::MONO_CLASS_GET_NAME)?; + + Ok(self.memory.read_string(class_name_address, 256)?) + } + + pub fn read_mono_string(&self, mono_string: usize) -> Result { + let offset = if self.is_64_bit { 0x10 } else { 0x8 }; + let len = self.memory.read_int(mono_string + offset)? as usize; + + let offset_str = if self.is_64_bit { 0x14 } else { 0xC }; + self.memory.read_unicode_string(mono_string + offset_str, len * 2) + } + + pub fn runtime_invoke(&mut self, method: usize) -> Result<(), InjectorException> { + let exc_ptr = if self.is_64_bit { + self.memory.allocate_and_write_long(0)? + } else { + self.memory.allocate_and_write_int(0)? + }; + + //res + self.execute( + *self + .exports + .get(Self::MONO_RUNTIME_INVOKE) + .ok_or_else(|| InjectorException::new("Mono runtime invoke export not found"))?, + &[method, 0, 0, exc_ptr], + )?; + + let exc = self.memory.read_int(exc_ptr)? as usize; + if exc != 0 { + let class_name = self.get_class_name(exc)?; + let message = self.read_mono_string(if self.is_64_bit { + exc + 0x20 + } else { + exc + 0x10 + })?; + return Err(InjectorException::new(&format!( + "The managed method threw an exception: ({}) {}", + class_name, message + ))); + } + + Ok(()) + } + + + pub fn close_assembly(&mut self, assembly: usize) -> Result<(), InjectorException> { + let address = self + .exports + .get(Self::MONO_ASSEMBLY_CLOSE) + .ok_or_else(|| InjectorException::new("Mono assembly close export not found"))?; + + let result = self.execute(*address, &[assembly])?; + self.throw_if_null(result, Self::MONO_ASSEMBLY_CLOSE)?; + + Ok(()) + } + + pub fn execute(&mut self, address: usize, args: &[usize]) -> Result { + let ret_val_ptr = if self.is_64_bit { + self.memory.allocate_and_write_long(0)? + } else { + self.memory.allocate_and_write_int(0)? + }; + + let code = self.assemble(address, ret_val_ptr, args); + let alloc = self.memory.allocate_and_write(&code)?; + + let mut thread_id: DWORD = 0; + let thread: HANDLE = unsafe { + CreateRemoteThread( + self.handle, + null_mut(), + 0, + alloc as *mut c_void, + null_mut(), + 0, + &mut thread_id, + ) + }; + + if thread == null_mut() { + return Err(InjectorException::new("Failed to create a remote thread")); + } + + let wait_result = unsafe { WaitForSingleObject(thread, u32::MAX) }; + if wait_result == WaitResult::WaitFailed as DWORD { + return Err(InjectorException::new("Failed to wait for a remote thread")); + } + + let ret = if self.is_64_bit { + self.memory.read_long(ret_val_ptr)? as usize + } else { + self.memory.read_int(ret_val_ptr)? as usize + }; + + if ret == 0xC0000005 { + let function_name = self + .exports + .iter() + .find(|(_, &addr)| addr == address) + .map(|(name, _)| *name) + .unwrap_or("unknown function"); + + return Err(InjectorException::new(&format!( + "An access violation occurred while executing {}()", + function_name + ))); + } + + Ok(ret) + } + + pub fn assemble(&self, function_ptr: usize, ret_val_ptr: usize, args: &[usize]) -> Vec { + if self.is_64_bit { + self.assemble_64(function_ptr, ret_val_ptr, args) + } else { + self.assemble_86(function_ptr, ret_val_ptr, args) + } + } + + pub fn assemble_86(&self, function_ptr: usize, ret_val_ptr: usize, args: &[usize]) -> Vec { + let mut asm = Assembler::new(); + + if self.attach { + if let Some(&mono_thread_attach) = self.exports.get(Self::MONO_THREAD_ATTACH) { + asm.push(self.root_domain as isize); + asm.mov_eax(mono_thread_attach as isize); + asm.call_eax(); + asm.add_esp(4); + } + } + + for &arg in args.iter().rev() { + asm.push(arg as isize); + } + + asm.mov_eax(function_ptr as isize); + asm.call_eax(); + asm.add_esp((args.len() * 4) as u8); + asm.mov_eax_to(ret_val_ptr as usize); + asm.return_(); + + asm.to_byte_array() + } + + pub fn assemble_64(&self, function_ptr: usize, ret_val_ptr: usize, args: &[usize]) -> Vec { + let mut asm = Assembler::new(); + + asm.sub_rsp(40); + + if self.attach { + if let Some(&mono_thread_attach) = self.exports.get(Self::MONO_THREAD_ATTACH) { + asm.mov_rax(mono_thread_attach); + asm.mov_rcx(self.root_domain); + asm.call_rax(); + } + } + + asm.mov_rax(function_ptr); + + for (i, &arg) in args.iter().enumerate() { + match i { + 0 => asm.mov_rcx(arg), + 1 => asm.mov_rdx(arg), + 2 => asm.mov_r8(arg), + 3 => asm.mov_r9(arg), + _ => break, + } + } + + asm.call_rax(); + asm.add_rsp(40); + asm.mov_rax_to(ret_val_ptr); + asm.return_(); + + asm.to_byte_array() + } + + +} \ No newline at end of file diff --git a/uniject/src/injector_exceptions.rs b/uniject/src/injector_exceptions.rs new file mode 100644 index 0000000..804bf28 --- /dev/null +++ b/uniject/src/injector_exceptions.rs @@ -0,0 +1,40 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub struct InjectorException { + message: String, + inner: Option>, +} + +impl InjectorException { + pub fn new(message: &str) -> Self { + InjectorException { + message: message.to_string(), + inner: None, + } + } + + pub fn with_inner(message: &str, inner: Box) -> Self { + InjectorException { + message: message.to_string(), + inner: Some(inner), + } + } +} + +impl fmt::Display for InjectorException { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "InjectorException: {}", self.message)?; + if let Some(ref inner) = self.inner { + write!(f, "\nCaused by: {}", inner)?; + } + Ok(()) + } +} + +impl Error for InjectorException { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.inner.as_deref() + } +} \ No newline at end of file diff --git a/uniject/src/lib.rs b/uniject/src/lib.rs new file mode 100644 index 0000000..868cd11 --- /dev/null +++ b/uniject/src/lib.rs @@ -0,0 +1,11 @@ +mod assembler; +mod exported_functions; +mod injector_exceptions; +mod injector; +mod status; +mod native; +mod memory; +mod proc_utils; + +pub use injector::*; +pub use assembler::*; \ No newline at end of file diff --git a/uniject/src/memory.rs b/uniject/src/memory.rs new file mode 100644 index 0000000..438f53c --- /dev/null +++ b/uniject/src/memory.rs @@ -0,0 +1,176 @@ +use std::collections::HashMap; +use winapi::ctypes::c_void; +use std::ptr::null_mut; +use winapi::um::winnt::HANDLE; +use crate::native::{AllocationType, MemoryFreeType, MemoryProtection, ReadProcessMemory, VirtualAllocEx, VirtualFreeEx, WriteProcessMemory}; +use crate::injector_exceptions::InjectorException; + +pub struct Memory { + handle: HANDLE, + allocations: HashMap, +} + +impl Memory { + pub fn new(process_handle: HANDLE) -> Result { + if process_handle.is_null() { + Err(InjectorException::new("Invalid process handle")) + } else { + Ok(Memory { + handle: process_handle, + allocations: HashMap::new(), + }) + } + } + + pub fn read_string(&self, address: usize, length: usize) -> Result { + let mut bytes = Vec::new(); + for _ in 0..length { + let read = self.read_bytes(address + bytes.len(), 1)?[0]; + if read == 0x00 { + break; + } + bytes.push(read); + } + + String::from_utf8(bytes).map_err(|e| InjectorException::with_inner("Failed to read string", Box::new(e))) + } + + pub fn read_unicode_string(&self, address: usize, length: usize) -> Result { + let bytes = self.read_bytes(address, length)?; + let utf16_units: Vec = bytes + .chunks_exact(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .collect(); + String::from_utf16(&utf16_units).map_err(|e| InjectorException::with_inner("Failed to read Unicode string", Box::new(e))) + } + + pub fn read_short(&self, address: usize) -> Result { + let bytes = self.read_bytes(address, 2)?; + Ok(i16::from_le_bytes([bytes[0], bytes[1]])) + } + + pub fn read_int(&self, address: usize) -> Result { + let bytes = self.read_bytes(address, 4)?; + Ok(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) + } + + pub fn read_long(&self, address: usize) -> Result { + let bytes = self.read_bytes(address, 8)?; + Ok(i64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + ])) + } + + fn read_bytes(&self, address: usize, size: usize) -> Result, InjectorException> { + let size_u32: u32 = size.try_into().map_err(|_| InjectorException::new("Failed to convert size to u32"))?; + let mut buffer = vec![0u8; size]; + let success = unsafe { + ReadProcessMemory( + self.handle, + address as *mut c_void, + buffer.as_mut_ptr() as *mut c_void, + size_u32, + std::ptr::null_mut(), + ) + } != 0; + + if success { + Ok(buffer) + } else { + Err(InjectorException::new("Failed to read process memory")) + } + } + + pub fn allocate_and_write(&mut self, data: &[u8]) -> Result { + let addr = self.allocate(data.len())?; + self.write(addr, data)?; + Ok(addr) + } + + // pub fn allocate_and_write_string(&mut self, data: &str) -> Result { + // self.allocate_and_write(data.as_bytes()) + // } + + pub fn allocate_and_write_int(&mut self, data: i32) -> Result { + self.allocate_and_write(&data.to_le_bytes()) + } + + pub fn allocate_and_write_long(&mut self, data: i64) -> Result { + self.allocate_and_write(&data.to_le_bytes()) + } + + pub fn allocate(&mut self, size: usize) -> Result { + let size_u32: u32 = size.try_into().map_err(|_| InjectorException::new("Failed to convert size to u32"))?; + let addr = unsafe { + VirtualAllocEx( + self.handle, + null_mut(), + size_u32, + AllocationType::MemCommit as u32 | AllocationType::MemReserve as u32, + MemoryProtection::PAGE_EXECUTE_READWRITE.bits(), + ) + } as usize; + + if addr == 0 { + Err(InjectorException::new("Failed to allocate process memory")) + } else { + self.allocations.insert(addr, size); + Ok(addr) + } + + } + + pub fn write(&self, address: usize, data: &[u8]) -> Result<(), InjectorException> { + let size_u32: u32 = data.len().try_into().map_err(|_| InjectorException::new("Failed to convert size to u32"))?; + let success = unsafe { + WriteProcessMemory( + self.handle, + address as *mut c_void, + data.as_ptr() as *mut c_void, + size_u32, + std::ptr::null_mut(), + ) + } != 0; + + if success { + Ok(()) + } else { + Err(InjectorException::new("Failed to write process memory")) + } + } + + //drop should be enough + // pub fn clear_allocations(&mut self) { + // for (&address, &size) in &self.allocations { + // let size_u32: u32 = size.try_into().unwrap_or(0); + // unsafe { + // VirtualFreeEx( + // self.handle, + // address as *mut c_void, + // size_u32, + // MemoryFreeType::MemDecommit as u32, + // ); + // } + // } + // self.allocations.clear(); + // } + +} + +impl Drop for Memory { + fn drop(&mut self) { + for (&address, &size) in &self.allocations { + let size_u32: u32 = size.try_into().unwrap_or(0); + unsafe { + VirtualFreeEx( + self.handle, + address as *mut c_void, + size_u32, + MemoryFreeType::MemDecommit as u32, + ); + } + } + self.allocations.clear(); + } +} \ No newline at end of file diff --git a/uniject/src/native.rs b/uniject/src/native.rs new file mode 100644 index 0000000..5e7ff0e --- /dev/null +++ b/uniject/src/native.rs @@ -0,0 +1,171 @@ +use winapi::shared::minwindef::{BOOL, DWORD, LPVOID}; +use winapi::um::psapi::MODULEINFO; +use winapi::um::winnt::HANDLE; + +// pub struct ModuleInfo { +// pub lp_base_of_dll: usize, +// pub size_of_image: i32, +// pub entry_point: usize, +// } + +#[repr(u32)] +#[derive(Clone, Copy, Debug)] +pub enum ModuleFilter { + // ListModulesDefault = 0x0, + // ListModules32Bit = 0x01, + // ListModules64Bit = 0x02, + ListModulesAll = 0x03, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug)] +pub enum AllocationType { + MemCommit = 0x00001000, + MemReserve = 0x00002000, + // MemReset = 0x00080000, + // MemResetUndo = 0x1000000, + // MemLargePages = 0x20000000, + // MemPhysical = 0x00400000, + // MemTopDown = 0x00100000, +} + +bitflags::bitflags! { + pub struct MemoryProtection: u32 { + const PAGE_EXECUTE = 0x10; + const PAGE_EXECUTE_READ = 0x20; + const PAGE_EXECUTE_READWRITE = 0x40; + const PAGE_EXECUTE_WRITECOPY = 0x80; + const PAGE_NOACCESS = 0x01; + const PAGE_READONLY = 0x02; + const PAGE_READWRITE = 0x04; + const PAGE_WRITECOPY = 0x08; + const PAGE_TARGETS_INVALID = 0x40000000; + const PAGE_TARGETS_NO_UPDATE = Self::PAGE_TARGETS_INVALID.bits(); + const PAGE_GUARD = 0x100; + const PAGE_NOCACHE = 0x200; + const PAGE_WRITECOMBINE = 0x400; + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug)] +pub enum MemoryFreeType { + MemDecommit = 0x4000, + // MemRelease = 0x8000, +} + +// #[repr(u32)] +// #[derive(Clone, Copy, Debug)] +// pub enum ThreadCreationFlags { +// None = 0, +// CreateSuspended = 0x00000004, +// StackSizeParamIsAReservation = 0x00010000, +// } + +#[repr(u32)] +#[derive(Clone, Copy, Debug)] +pub enum WaitResult { + // WaitAbandoned = 0x00000080, + // WaitObject0 = 0x00000000, + // WaitTimeout = 0x00000102, + WaitFailed = 0xFFFFFFFF, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug)] +pub enum ProcessAccessRights { + ProcessAllAccess = 0x1FFFFF, + // ProcessCreateProcess = 0x0080, + // ProcessCreateThread = 0x0002, + // ProcessDupHandle = 0x0040, + // ProcessQueryInformation = 0x0400, + // ProcessQueryLimitedInformation = 0x1000, + // ProcessSetInformation = 0x0200, + // ProcessSetQuota = 0x0100, + // ProcessSuspendResume = 0x0800, + // ProcessTerminate = 0x0001, + // ProcessVmOperation = 0x0008, + // ProcessVmRead = 0x0010, + // ProcessVmWrite = 0x0020, + // Synchronize = 0x00100000, +} + +extern "system" { + pub fn OpenProcess(dwDesiredAccess: DWORD, bInheritHandle: BOOL, dwProcessId: DWORD) -> HANDLE; + + pub fn CloseHandle(hObject: HANDLE) -> BOOL; + + pub fn IsWow64Process(hProcess: HANDLE, wow64Process: *mut BOOL) -> BOOL; + + pub fn EnumProcessModulesEx( + hProcess: HANDLE, + lphModule: *mut LPVOID, + cb: DWORD, + lpcbNeeded: *mut DWORD, + dwFilterFlag: DWORD, + ) -> BOOL; + + // pub fn GetModuleFileNameEx( + // hProcess: HANDLE, + // hModule: HANDLE, + // lpBaseName: *mut u8, + // nSize: DWORD, + // ) -> DWORD; + + pub fn GetModuleFileNameExA( + hProcess: HANDLE, + hModule: HANDLE, + lpBaseName: *mut i8, + nSize: DWORD, + ) -> DWORD; + + pub fn GetModuleInformation( + hProcess: HANDLE, + hModule: HANDLE, + lpmodinfo: *mut MODULEINFO, + cb: DWORD, + ) -> BOOL; + + pub fn WriteProcessMemory( + hProcess: HANDLE, + lpBaseAddress: LPVOID, + lpBuffer: LPVOID, + nSize: DWORD, + lpNumberOfBytesWritten: *mut DWORD, + ) -> BOOL; + + pub fn ReadProcessMemory( + hProcess: HANDLE, + lpBaseAddress: LPVOID, + lpBuffer: LPVOID, + nSize: DWORD, + lpNumberOfBytesRead: *mut DWORD, + ) -> BOOL; + + pub fn VirtualAllocEx( + hProcess: HANDLE, + lpAddress: LPVOID, + dwSize: DWORD, + flAllocationType: DWORD, + flProtect: DWORD, + ) -> LPVOID; + + pub fn VirtualFreeEx( + hProcess: HANDLE, + lpAddress: LPVOID, + dwSize: DWORD, + dwFreeType: DWORD, + ) -> BOOL; + + pub fn CreateRemoteThread( + hProcess: HANDLE, + lpThreadAttributes: LPVOID, + dwStackSize: DWORD, + lpStartAddress: LPVOID, + lpParameter: LPVOID, + dwCreationFlags: DWORD, + lpThreadId: *mut DWORD, + ) -> HANDLE; + + pub fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) -> DWORD; +} diff --git a/uniject/src/proc_utils.rs b/uniject/src/proc_utils.rs new file mode 100644 index 0000000..7148fc3 --- /dev/null +++ b/uniject/src/proc_utils.rs @@ -0,0 +1,195 @@ +use std::ffi::CStr; +use std::mem; +use std::ptr::null_mut; + +use crate::exported_functions::ExportedFunction; +use crate::injector_exceptions::InjectorException; +use crate::memory::Memory; +use crate::native::{ + EnumProcessModulesEx, GetModuleFileNameExA, GetModuleInformation, IsWow64Process, ModuleFilter, +}; +use sysinfo::System; +use winapi::shared::minwindef::{BOOL, DWORD}; +use winapi::um::psapi::MODULEINFO; +use winapi::um::winnt::HANDLE; + +pub fn find_process_id_by_name(process_name: &str) -> Option { + let mut system = System::new_all(); + system.refresh_all(); + + let x = system + .processes_by_name(process_name) + .next() + .map(|process| process.pid().as_u32()); + x +} + +pub fn get_exported_functions( + handle: HANDLE, + mod_address: usize, +) -> Result, InjectorException> { + let mut exported_functions = Vec::new(); + let is_64_bit = is_64_bit_process(handle)?; + + let memory = Memory::new(handle).map_err(|err| { + InjectorException::with_inner("Failed to create memory handler", Box::new(err)) + })?; + + //nt header offset + let e_lfanew = memory.read_int(mod_address + 0x3C).map_err(|err| { + InjectorException::with_inner("Failed to read NT headers offset", Box::new(err)) + })?; + let nt_headers = mod_address + e_lfanew as usize; + + let optional_header = nt_headers + 0x18; + let data_directory = optional_header + if is_64_bit { 0x70 } else { 0x60 }; + + let export_directory = mod_address + + memory.read_int(data_directory as usize).map_err(|err| { + InjectorException::with_inner("Failed to read export directory", Box::new(err)) + })? as usize; + + let names = mod_address + + memory.read_int(export_directory + 0x20).map_err(|err| { + InjectorException::with_inner("Failed to read names pointer", Box::new(err)) + })? as usize; + let ordinals = mod_address + + memory.read_int(export_directory + 0x24).map_err(|err| { + InjectorException::with_inner("Failed to read ordinals pointer", Box::new(err)) + })? as usize; + let functions = mod_address + + memory.read_int(export_directory + 0x1C).map_err(|err| { + InjectorException::with_inner("Failed to read functions pointer", Box::new(err)) + })? as usize; + let count = memory.read_int(export_directory + 0x18).map_err(|err| { + InjectorException::with_inner("Failed to read functions count", Box::new(err)) + })?; + + for i in 0..count { + let offset = memory.read_int(names + i as usize * 4).map_err(|err| { + InjectorException::with_inner("Failed to read function name offset", Box::new(err)) + })?; + let name = memory + .read_string(mod_address + offset as usize, 32) + .map_err(|err| { + InjectorException::with_inner("Failed to read function name", Box::new(err)) + })?; + let ordinal = memory + .read_short(ordinals + i as usize * 2) + .map_err(|err| { + InjectorException::with_inner("Failed to read function ordinal", Box::new(err)) + })?; + let address = mod_address + + memory + .read_int(functions + ordinal as usize * 4) + .map_err(|err| { + InjectorException::with_inner("Failed to read function address", Box::new(err)) + })? as usize; + + if address != 0 { + exported_functions.push(ExportedFunction::new(&name, address)); + } + } + + Ok(exported_functions) +} + +pub fn get_mono_module(handle: HANDLE) -> Result, InjectorException> { + let is_64_bit = is_64_bit_process(handle)?; + let size = if is_64_bit { 8 } else { 4 }; + + let mut bytes_needed: DWORD = 0; + + //get required buffer size + let success = unsafe { + EnumProcessModulesEx( + handle, + null_mut(), + 0, + &mut bytes_needed, + ModuleFilter::ListModulesAll as DWORD, + ) + }; + + if success == 0 || bytes_needed == 0 { + return Err(InjectorException::new( + "Failed to enumerate process modules", + )); + } + + //resize buffer + let count = bytes_needed as usize / size; + let mut ptrs = vec![null_mut(); count]; + + //call with allocated buffer + let success = unsafe { + EnumProcessModulesEx( + handle, + ptrs.as_mut_ptr(), + bytes_needed, + &mut bytes_needed, + ModuleFilter::ListModulesAll as DWORD, + ) + }; + + if success == 0 { + return Err(InjectorException::new( + "Failed to enumerate process modules", + )) + } + + for &module in ptrs.iter() { + let mut path = vec![0i8; 260]; + unsafe { + GetModuleFileNameExA(handle, module as HANDLE, path.as_mut_ptr(), 260); + } + + let path_str = unsafe { CStr::from_ptr(path.as_ptr()) } + .to_string_lossy() + .to_lowercase(); + + if path_str.contains("mono") { + let mut info: MODULEINFO = MODULEINFO { + lpBaseOfDll: null_mut(), + SizeOfImage: 0, + EntryPoint: null_mut(), + }; + + let success = unsafe { + GetModuleInformation( + handle, + module as HANDLE, + &mut info, + (size * ptrs.len()) as DWORD, + ) + }; + + if success == 0 { + return Err(InjectorException::new(&"Failed to get module information")); + } + + let funcs = get_exported_functions(handle, info.lpBaseOfDll as usize)?; + + if funcs.iter().any(|f| f.name == "mono_get_root_domain") { + return Ok(Some(info.lpBaseOfDll as usize)); + } + } + } + + Ok(None) +} + +pub fn is_64_bit_process(handle: HANDLE) -> Result { + if !cfg!(target_pointer_width = "64") { + return Ok(false); + } + + let mut is_wow64: BOOL = 0; + let success = unsafe { IsWow64Process(handle, &mut is_wow64) }; + + if success == 0 { + Err(InjectorException::new("Failed to query Wow64 status")) + } else { + Ok(is_wow64 == 0 && mem::size_of::() == 8) + } +} diff --git a/uniject/src/status.rs b/uniject/src/status.rs new file mode 100644 index 0000000..bb5f7e5 --- /dev/null +++ b/uniject/src/status.rs @@ -0,0 +1,19 @@ +#[derive(Debug, PartialEq, Eq)] +pub enum MonoImageOpenStatus { + MonoImageOk, + MonoImageErrorErrno, + MonoImageMissingAssemblyRef, + MonoImageInvalid, +} + +impl From for MonoImageOpenStatus { + fn from(value: i32) -> Self { + match value { + 0 => MonoImageOpenStatus::MonoImageOk, + 1 => MonoImageOpenStatus::MonoImageErrorErrno, + 2 => MonoImageOpenStatus::MonoImageMissingAssemblyRef, + 3 => MonoImageOpenStatus::MonoImageInvalid, + _ => MonoImageOpenStatus::MonoImageInvalid, + } + } +} \ No newline at end of file diff --git a/uniject_console/.gitignore b/uniject_console/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/uniject_console/.gitignore @@ -0,0 +1 @@ +/target diff --git a/uniject_console/Cargo.lock b/uniject_console/Cargo.lock new file mode 100644 index 0000000..ce66d39 --- /dev/null +++ b/uniject_console/Cargo.lock @@ -0,0 +1,230 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "sysinfo" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + +[[package]] +name = "uniject" +version = "0.1.0" +dependencies = [ + "bitflags", + "once_cell", + "sysinfo", + "winapi", +] + +[[package]] +name = "uniject_console" +version = "0.1.0" +dependencies = [ + "uniject", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/uniject_console/Cargo.toml b/uniject_console/Cargo.toml new file mode 100644 index 0000000..ccdbf2e --- /dev/null +++ b/uniject_console/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "uniject_console" +version = "0.1.0" +edition = "2021" + +[dependencies] +uniject = {path = "../uniject"} \ No newline at end of file diff --git a/uniject_console/src/args.rs b/uniject_console/src/args.rs new file mode 100644 index 0000000..5a176eb --- /dev/null +++ b/uniject_console/src/args.rs @@ -0,0 +1,57 @@ +use std::fmt::Debug; +use std::str::FromStr; + +pub struct CommandLineArguments<'a> { + args: &'a [String], +} + +impl<'a> CommandLineArguments<'a> { + pub fn new(args: &'a [String]) -> Self { + Self { args } + } + + pub fn is_switch_present(&self, name: &str) -> bool { + self.args.iter().any(|arg| arg == name) + } + + pub fn get_long_arg(&self, name: &str) -> Option { + self.get_arg_value::(name, 16) + } + + pub fn get_int_arg(&self, name: &str) -> Option { + self.get_arg_value::(name, 16) + } + + pub fn get_string_arg(&self, name: &str) -> Option<&str> { + self.get_arg_index(name) + .and_then(|index| self.args.get(index + 1).map(|s| s.as_str())) + } + + fn get_arg_index(&self, name: &str) -> Option { + self.args.iter().position(|arg| arg == name) + } + + fn get_arg_value(&self, name: &str, radix: u32) -> Option + where + T: FromStr + Debug + TryFrom + TryFrom, + ::Err: Debug, + >::Error: Debug, + >::Error: Debug, + { + self.get_string_arg(name).and_then(|str_val| { + let value_str = if str_val.starts_with("0x") { + &str_val[2..] + } else { + str_val + }; + + if radix == 16 { + if let Ok(value) = i64::from_str_radix(value_str, 16) { + return T::try_from(value).ok(); + } + } + + value_str.parse::().ok() + }) + } +} diff --git a/uniject_console/src/main.rs b/uniject_console/src/main.rs new file mode 100644 index 0000000..8dcbcb2 --- /dev/null +++ b/uniject_console/src/main.rs @@ -0,0 +1,193 @@ +use uniject::Injector; + +use std::fmt::Display; +use std::fs; +use std::path::Path; +use std::process::exit; + +mod args; +use args::CommandLineArguments; + +fn main() { + let args: Vec = std::env::args().collect(); + + if args.len() == 1 { + print_help(); + exit(0); + } + + let cla = CommandLineArguments::new(&args[1..]); + + let is_inject = cla.is_switch_present("inject"); + let is_eject = cla.is_switch_present("eject"); + + if !is_inject && !is_eject { + println!("No operation (inject/eject) specified"); + exit(1); + } + + let mut injector = if let Some(pid) = cla.get_int_arg("-p") { + match Injector::new(pid as u32) { + Ok(injector) => injector, + Err(err) => { + println!("Failed to create Injector for process ID {}: {}", pid, err); + exit(1); + } + } + } else if let Some(pname) = cla.get_string_arg("-p") { + match Injector::new_by_name(pname) { + Ok(injector) => injector, + Err(err) => { + println!( + "Failed to create Injector for process name {}: {}", + pname, err + ); + exit(1); + } + } + } else { + println!("No process id/name specified"); + exit(1); + }; + + if is_inject { + inject(&mut injector, &cla); + } else { + eject(&mut injector, &cla); + } +} + +fn print_help() { + let help = r#"Uniject 0.1.0 + +Usage: +uniject_console + +Options: +-p - The id or name of the target process +-a - When injecting, the path of the assembly to inject. When ejecting, the address of the assembly to eject +-n - The namespace in which the loader class resides +-c - The name of the loader class +-m - The name of the method to invoke in the loader class + +Examples: +uniject_console inject -p testgame -a ExampleAssembly.dll -n ExampleAssembly -c Loader -m Load +uniject_console eject -p testgame -a 0x13D23A98 -n ExampleAssembly -c Loader -m Unload +"#; + + println!("{}", help); +} + +fn inject(injector: &mut Injector, args: &CommandLineArguments) { + let assembly_path: String; + let namespace: String; + let class_name: String; + let method_name: String; + + let assembly: Vec; + + if let Some(path) = args.get_string_arg("-a") { + match fs::read(path) { + Ok(content) => assembly = content, + Err(_) => { + println!("Could not read the file {}", path); + return; + } + } + assembly_path = path.to_string(); + } else { + println!("No assembly specified"); + return; + } + + namespace = match args.get_string_arg("-n") { + Some(ns) => ns.to_string(), + None => { + println!("No namespace specified"); + return; + } + }; + + class_name = match args.get_string_arg("-c") { + Some(class) => class.to_string(), + None => { + println!("No class name specified"); + return; + } + }; + + method_name = match args.get_string_arg("-m") { + Some(method) => method.to_string(), + None => { + println!("No method name specified"); + return; + } + }; + + match injector.inject(&assembly, &namespace, &class_name, &method_name) { + Ok(remote_assembly) => { + println!( + "{}: {}", + Path::new(&assembly_path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("unknown"), + format_address(remote_assembly, injector.is_64_bit) + ); + } + Err(e) => println!("Failed to inject assembly: {}", e), + } +} + +fn eject(injector: &mut Injector, args: &CommandLineArguments) { + let assembly: usize; + let namespace: String; + let class_name: String; + let method_name: String; + + assembly = if let Some(int_ptr) = args.get_int_arg("-a") { + int_ptr as usize + } else if let Some(long_ptr) = args.get_long_arg("-a") { + long_ptr as usize + } else { + println!("No assembly pointer specified"); + return; + }; + + namespace = match args.get_string_arg("-n") { + Some(ns) => ns.to_string(), + None => { + println!("No namespace specified"); + return; + } + }; + + class_name = match args.get_string_arg("-c") { + Some(class) => class.to_string(), + None => { + println!("No class name specified"); + return; + } + }; + + method_name = match args.get_string_arg("-m") { + Some(method) => method.to_string(), + None => { + println!("No method name specified"); + return; + } + }; + + match injector.eject(assembly, &namespace, &class_name, &method_name) { + Ok(_) => println!("Ejection successful"), + Err(e) => println!("Ejection failed: {}", e), + } +} + +fn format_address(address: T, is_64_bit: bool) -> String { + if is_64_bit { + format!("0x{:016X}", address) + } else { + format!("0x{:08X}", address) + } +}