From fdc0da310fbb301407899cd2419419a272667d27 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Wed, 21 Feb 2024 23:34:34 +0000 Subject: [PATCH 01/13] Set 'nightly' toolchain as default. --- rust-toolchain.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 rust-toolchain.toml diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file From 539b6647af60b642313c98d34ae6366f8f55a75a Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 23 Feb 2024 00:52:56 +0000 Subject: [PATCH 02/13] Improved: Now runs tests on Linux/Wine Cargo: Set default targets as Windows Updated: Script to run `wine` on host, for faster testing. --- .cargo/config.toml | 2 + README.md | 42 +++++++++++++++++--- clean.ps1 | 9 ----- scripts/clean.ps1 | 11 ++++++ scripts/test-wine.ps1 | 17 ++++++++ scripts/test.ps1 | 8 ++++ test.ps1 | 2 - tests/common/mod.rs | 90 +++++++++++++++++++++---------------------- tests/process.rs | 32 ++++++++++++++- 9 files changed, 149 insertions(+), 64 deletions(-) create mode 100644 .cargo/config.toml delete mode 100644 clean.ps1 create mode 100644 scripts/clean.ps1 create mode 100644 scripts/test-wine.ps1 create mode 100644 scripts/test.ps1 delete mode 100644 test.ps1 diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..6f9aacc --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = ['i686-pc-windows-msvc','x86_64-pc-windows-msvc'] \ No newline at end of file diff --git a/README.md b/README.md index b233341..211a8a3 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,11 @@ syringe.eject(injected_payload).unwrap(); ## Remote Procedure Calls (RPC) This crate supports two mechanisms for rpc. Both only work one-way for calling exported functions in the target process and are only intended for one-time initialization usage. For extended communication a dedicated rpc library should be used. -| | `RemotePayloadProcedure` | `RemoteRawProcedure` | -| ---------------- | ------------------------------ | ------------------------------------------ | -| Feature | `rpc-payload` | `rpc-raw` | -| Argument and Return Requirements | `Serialize + DeserializeOwned` | `Copy`, Argument size has to be smaller than `usize` in target process | -| Function Definition | Using macro `payload_procedure!` | Any `extern "system"` or `extern "C"` with `#[no_mangle]` | +| | `RemotePayloadProcedure` | `RemoteRawProcedure` | +| -------------------------------- | -------------------------------- | ---------------------------------------------------------------------- | +| Feature | `rpc-payload` | `rpc-raw` | +| Argument and Return Requirements | `Serialize + DeserializeOwned` | `Copy`, Argument size has to be smaller than `usize` in target process | +| Function Definition | Using macro `payload_procedure!` | Any `extern "system"` or `extern "C"` with `#[no_mangle]` | ### RemotePayloadProcedure A rpc mechanism based on [`bincode`](https://crates.io/crates/bincode). @@ -119,6 +119,36 @@ syringe.eject(injected_payload).unwrap(); ## License Licensed under MIT license ([LICENSE](https://github.com/OpenByteDev/dll-syringe/blob/master/LICENSE) or http://opensource.org/licenses/MIT) +## Instructions for Contributors + +### Prerequisites + +You will need the nightly toolchains of Rust and Cargo to build/test this project. + +``` +rustup target add x86_64-pc-windows-msvc --toolchain nightly +rustup target add i686-pc-windows-msvc --toolchain nightly +``` + +> [!NOTE] +> Also applies to developing on Linux, you'll need it for your IDE (i.e. rust-analyzer or RustRover) to work properly. + +### Run Tests + +Run the `./scripts/test.ps1` script from PowerShell. + +### Running Tests on Linux + +You'll need `cargo xwin` to build the MSVC targets on Linux: + +``` +cargo install cargo-xwin +``` + +After that, you can run the tests with `./scripts/test-wine.ps1` PowerShell script. +(As opposed to `./scripts/test.ps1`) + +Make sure you have Wine installed! + ## Attribution Inspired by [Reloaded.Injector](https://github.com/Reloaded-Project/Reloaded.Injector) from [Sewer](https://github.com/Sewer56). - diff --git a/clean.ps1 b/clean.ps1 deleted file mode 100644 index b5e7270..0000000 --- a/clean.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -cargo clean - -Set-Location .\tests\helpers\test_payload -cargo clean -Set-Location ..\..\.. - -Set-Location .\tests\helpers\test_target -cargo clean -Set-Location ..\..\.. diff --git a/scripts/clean.ps1 b/scripts/clean.ps1 new file mode 100644 index 0000000..4c6df25 --- /dev/null +++ b/scripts/clean.ps1 @@ -0,0 +1,11 @@ +# Navigate up one folder from the current script location +Set-Location "$PSScriptRoot\.." +cargo clean + +Set-Location "./tests/helpers/test_payload" +cargo clean +Set-Location "../../.." + +Set-Location "./tests/helpers/test_target" +cargo clean +Set-Location "../../.." diff --git a/scripts/test-wine.ps1 b/scripts/test-wine.ps1 new file mode 100644 index 0000000..2518492 --- /dev/null +++ b/scripts/test-wine.ps1 @@ -0,0 +1,17 @@ +# Navigate up one folder from the current script location +Set-Location "$PSScriptRoot\.." + +# Testing +$env:CROSS_SYSROOT = "." # pretend we cross + +# Prebuild dummy projects. +cargo +nightly xwin rustc --manifest-path "tests/helpers/test_target/Cargo.toml" --target i686-pc-windows-msvc --xwin-arch x86 --xwin-cache-dir "target/cache/x86" +cargo +nightly xwin rustc --manifest-path "tests/helpers/test_payload/Cargo.toml" --target i686-pc-windows-msvc --xwin-arch x86 --xwin-cache-dir "target/cache/x86" +cargo +nightly xwin rustc --manifest-path "tests/helpers/test_target/Cargo.toml" --target x86_64-pc-windows-msvc --xwin-arch x86_64 --xwin-cache-dir "target/cache/x64" +cargo +nightly xwin rustc --manifest-path "tests/helpers/test_payload/Cargo.toml" --target x86_64-pc-windows-msvc --xwin-arch x86_64 --xwin-cache-dir "target/cache/x64" + +# Windows/MSVC x86 +cargo +nightly xwin test --target i686-pc-windows-msvc --xwin-arch x86 --xwin-cache-dir "target/cache/x86" + +# Windows/MSVC x64 +cargo +nightly xwin test --target x86_64-pc-windows-msvc --xwin-arch x86_64 --xwin-cache-dir "target/cache/x64" \ No newline at end of file diff --git a/scripts/test.ps1 b/scripts/test.ps1 new file mode 100644 index 0000000..a6c1e6c --- /dev/null +++ b/scripts/test.ps1 @@ -0,0 +1,8 @@ +# Navigate up one folder from the current script location +Set-Location "$PSScriptRoot\.." + +# Windows/MSVC x86 +cargo test --target i686-pc-windows-msvc -- --nocapture + +# Windows/MSVC x64 +cargo test --target x86_64-pc-windows-msvc -- --nocapture \ No newline at end of file diff --git a/test.ps1 b/test.ps1 deleted file mode 100644 index bf8ab5c..0000000 --- a/test.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -cargo test --target i686-pc-windows-msvc -- --nocapture -cargo test --target x86_64-pc-windows-msvc -- --nocapture diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d666d51..9fa3aa3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,44 +1,30 @@ use std::{ + env::{current_dir, var}, error::Error, + fs::{canonicalize, remove_file, File}, + io::{copy, ErrorKind}, path::PathBuf, process::{Command, Stdio}, str::FromStr, + sync::Mutex, }; +static mut RUST_INSTALL_LOCK: Mutex = Mutex::new(0); + pub fn build_test_payload_x86() -> Result> { - build_helper_crate( - "test_payload", - Some(&find_x86_variant_of_target()), - false, - "dll", - ) + build_helper_crate("test_payload", &find_x86_variant_of_target(), false, "dll") } pub fn build_test_target_x86() -> Result> { - build_helper_crate( - "test_target", - Some(&find_x86_variant_of_target()), - false, - "exe", - ) + build_helper_crate("test_target", &find_x86_variant_of_target(), false, "exe") } pub fn build_test_payload_x64() -> Result> { - build_helper_crate( - "test_payload", - Some(&find_x64_variant_of_target()), - false, - "dll", - ) + build_helper_crate("test_payload", &find_x64_variant_of_target(), false, "dll") } pub fn build_test_target_x64() -> Result> { - build_helper_crate( - "test_target", - Some(&find_x64_variant_of_target()), - false, - "exe", - ) + build_helper_crate("test_target", &find_x64_variant_of_target(), false, "exe") } fn find_x64_variant_of_target() -> String { @@ -51,7 +37,7 @@ fn find_x86_variant_of_target() -> String { pub fn build_helper_crate( crate_name: &str, - target: Option<&str>, + target: &str, release: bool, ext: &str, ) -> Result> { @@ -59,37 +45,49 @@ pub fn build_helper_crate( .join(crate_name) .canonicalize()?; - let mut command = Command::new("cargo"); - command - .arg("build") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - if let Some(target) = target { - command.arg("--target").arg(target); + // For cross/wine testing, we precompile in external script. + if !is_cross() { + let mut command = Command::new("cargo"); + command + .arg("build") + .arg("--target") + .arg(target) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + let exit_code = command.current_dir(&payload_crate_path).spawn()?.wait()?; + assert!( + exit_code.success(), + "Failed to build helper crate {} for target {}", + crate_name, + target + ); } - let exit_code = command.current_dir(&payload_crate_path).spawn()?.wait()?; - assert!( - exit_code.success(), - "Failed to build helper crate {} for target {}", - crate_name, - target.unwrap_or("default") - ); let mut payload_artifact_path = payload_crate_path; payload_artifact_path.push("target"); - - if let Some(target) = target { - payload_artifact_path.push(target); - } - + payload_artifact_path.push(target); payload_artifact_path.push(if release { "release" } else { "debug" }); payload_artifact_path.push(format!("{crate_name}.{ext}")); - assert!(&payload_artifact_path.exists()); + assert!(&payload_artifact_path.exists(), "Artifact doesn't exist! {:?}", &payload_artifact_path); Ok(payload_artifact_path) } +/// Detects cross-rs. +/// +/// Remarks: +/// +/// I wish I could install Rust itself via `Rustup` here, but the Ubuntu image that ships with +/// `cross` doesn't have the right packages to support encryption, thus we can't download toolchains +/// (I tried). And I also didn't have good luck with pre-build step and downloading extra packages. +/// +/// So as a compromise, we build the test binaries outside for testing from Linux. +fn is_cross() -> bool { + return var("CROSS_SYSROOT").is_ok(); +} + #[macro_export] macro_rules! syringe_test { (fn $test_name:ident ($process:ident : OwnedProcess, $payload_path:ident : &Path $(,)?) $body:block) => { diff --git a/tests/process.rs b/tests/process.rs index a190905..0f44102 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -1,5 +1,6 @@ use dll_syringe::process::{BorrowedProcess, OwnedProcess, Process}; -use std::{fs, mem, time::Duration}; +use winapi::um::{libloaderapi::{GetProcAddress, LoadLibraryA}, sysinfoapi::GetVersion}; +use std::{fs, mem, time::Duration, ffi::CString}; #[allow(unused)] mod common; @@ -91,6 +92,11 @@ process_test! { fn long_process_paths_are_supported( process: OwnedProcess ) { + if is_running_under_wine() || is_older_than_windows_10() { + println!("Test skipped due to running under an environment with unsupported long paths. (Wine or older than Windows 10)."); + return; + } + let process_path = process.path().unwrap(); process.kill().unwrap(); @@ -138,3 +144,27 @@ fn current_pseudo_process_eq_current_process() { assert_eq!(pseudo.try_to_owned().unwrap(), normal); assert_eq!(pseudo, normal.try_clone().unwrap()); } + +fn is_running_under_wine() -> bool { + unsafe { + let ntdll = CString::new("ntdll.dll").unwrap(); + let lib = LoadLibraryA(ntdll.as_ptr()); + if !lib.is_null() { + let func_name = CString::new("wine_get_version").unwrap(); + let func = GetProcAddress(lib, func_name.as_ptr()); + !func.is_null() + } else { + false + } + } +} + +fn is_older_than_windows_10() -> bool { + unsafe { + let version = GetVersion(); + let major = (version & 0xFF) as u8; + + // Windows 10 is version 10.0. Threshold for older versions is any major version less than 10. + major < 10 + } +} \ No newline at end of file From bbfe38afeaee06ae339cc2b2ca9e4aefb8fc1fa2 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Thu, 22 Feb 2024 22:04:20 +0000 Subject: [PATCH 03/13] Added: VSCode Shortcuts --- .gitignore | 1 - .vscode/settings.json | 11 +++++++++++ .vscode/tasks.json | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json diff --git a/.gitignore b/.gitignore index c640ca5..96ef6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target Cargo.lock -.vscode diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..28151fb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "rust-analyzer.checkOnSave.command": "clippy", + "[rust]": { + "editor.formatOnSave": true + }, + "rust-analyzer.linkedProjects": [ + "Cargo.toml", + "tests/helpers/test_payload/Cargo.toml", + "tests/helpers/test_target/Cargo.toml", + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0b90685 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Test on Windows", + "type": "shell", + "command": "pwsh ./scripts/test.ps1", + }, + { + "label": "Clean", + "type": "shell", + "command": "pwsh ./scripts/clean.ps1", + }, + { + "label": "Test on Wine (See Readme)", + "type": "shell", + "command": "pwsh ./scripts/test-wine.ps1", + }, + ] +} From c2e8f6f243569bc90410e7582ac8c0192e7d1286 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 23 Feb 2024 01:37:12 +0000 Subject: [PATCH 04/13] Added: Workaround for zero sized write issue on Wine --- src/process/memory/buffer.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/process/memory/buffer.rs b/src/process/memory/buffer.rs index 5ebb334..be746f8 100644 --- a/src/process/memory/buffer.rs +++ b/src/process/memory/buffer.rs @@ -338,6 +338,12 @@ impl<'a> ProcessMemorySlice<'a> { } let mut bytes_written = 0; + if buf.is_empty() { + // This works around a discrepancy between Wine and actual Windows. + // On Wine, a 0 sized write fails, on Windows this suceeds. Will file as bug soon. + return Ok(()); + } + let result = unsafe { WriteProcessMemory( self.process.as_raw_handle(), From 801e0cca4a52da6dbd1f67e9e1302bbf98424c12 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 23 Feb 2024 15:31:13 +0000 Subject: [PATCH 05/13] Removed: Unused lock from earlier code revision --- tests/common/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9fa3aa3..cb1cdd2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -9,8 +9,6 @@ use std::{ sync::Mutex, }; -static mut RUST_INSTALL_LOCK: Mutex = Mutex::new(0); - pub fn build_test_payload_x86() -> Result> { build_helper_crate("test_payload", &find_x86_variant_of_target(), false, "dll") } @@ -55,7 +53,7 @@ pub fn build_helper_crate( .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()); - + let exit_code = command.current_dir(&payload_crate_path).spawn()?.wait()?; assert!( exit_code.success(), @@ -70,7 +68,11 @@ pub fn build_helper_crate( payload_artifact_path.push(target); payload_artifact_path.push(if release { "release" } else { "debug" }); payload_artifact_path.push(format!("{crate_name}.{ext}")); - assert!(&payload_artifact_path.exists(), "Artifact doesn't exist! {:?}", &payload_artifact_path); + assert!( + &payload_artifact_path.exists(), + "Artifact doesn't exist! {:?}", + &payload_artifact_path + ); Ok(payload_artifact_path) } From 1e2d1ac9f6f4da71504105a157d70d5134ea1849 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 23 Feb 2024 15:32:08 +0000 Subject: [PATCH 06/13] Fix: Redundant Return in Code Style --- tests/common/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index cb1cdd2..98f691c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -87,7 +87,7 @@ pub fn build_helper_crate( /// /// So as a compromise, we build the test binaries outside for testing from Linux. fn is_cross() -> bool { - return var("CROSS_SYSROOT").is_ok(); + var("CROSS_SYSROOT").is_ok() } #[macro_export] From 4b7afe93379b0830de2d71ed335f60d82c5cd59f Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 23 Feb 2024 16:01:44 +0000 Subject: [PATCH 07/13] VSCode: Don't prompt for which errors/warnings to show when running tasks. --- .vscode/tasks.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0b90685..5b576a4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,16 +5,19 @@ "label": "Test on Windows", "type": "shell", "command": "pwsh ./scripts/test.ps1", + "problemMatcher": [] }, { "label": "Clean", "type": "shell", "command": "pwsh ./scripts/clean.ps1", + "problemMatcher": [] }, { "label": "Test on Wine (See Readme)", "type": "shell", "command": "pwsh ./scripts/test-wine.ps1", + "problemMatcher": [] }, ] -} +} \ No newline at end of file From fa8f7e9b6de4acab2edbc219f8483653eef640cb Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Fri, 23 Feb 2024 18:12:31 +0000 Subject: [PATCH 08/13] Added: Support for suspended processes --- src/syringe.rs | 22 +++++++++ tests/common/mod.rs | 106 +++++++++++++++++++++++++++++++++++++++----- tests/eject.rs | 4 +- tests/inject.rs | 8 ++-- tests/module.rs | 6 +-- tests/process.rs | 20 +++++---- tests/rpc.rs | 38 ++++++++-------- 7 files changed, 156 insertions(+), 48 deletions(-) diff --git a/src/syringe.rs b/src/syringe.rs index 4351c01..010719f 100644 --- a/src/syringe.rs +++ b/src/syringe.rs @@ -116,6 +116,28 @@ impl Syringe { } } + /// Creates a new syringe for the given suspended target process. + pub fn for_suspended_process(process: OwnedProcess) -> Result { + let syringe = Self::for_process(process); + + // If we are injecting into a 'suspended' process, then said process is said to not be fully + // initialized. This means: + // - We can't use `EnumProcessModulesEx` and friends. + // - So we can't locate Kernel32.dll in 32-bit process (from 64-bit process) + // - And therefore calling LoadLibrary is not possible. + + // Thankfully we can 'initialize' this suspended process without running any end user logic + // (e.g. a game's entry point) by creating a dummy method and invoking it. + let ret = 0xC3; + let bx = syringe.remote_allocator.alloc_and_copy(&ret)?; + let mut dummy_param = 0; + syringe + .process() + .run_remote_thread(unsafe { mem::transmute(bx.as_raw_ptr()) }, &mut dummy_param)?; + + Ok(syringe) + } + /// Returns the target process for this syringe. pub fn process(&self) -> BorrowedProcess<'_> { self.remote_allocator.process() diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 98f691c..657172c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,3 +1,14 @@ +use core::iter::once; +use dll_syringe::process::OwnedProcess; +use dll_syringe::process::Process; +use std::ffi::CString; +use std::ffi::OsStr; +use std::mem::zeroed; +use std::os::windows::io::FromRawHandle; +use std::os::windows::io::OwnedHandle; +use std::os::windows::prelude::OsStrExt; +use std::path::Path; +use std::ptr::null_mut; use std::{ env::{current_dir, var}, error::Error, @@ -8,6 +19,8 @@ use std::{ str::FromStr, sync::Mutex, }; +use winapi::um::processthreadsapi::{CreateProcessW, PROCESS_INFORMATION, STARTUPINFOW}; +use winapi::um::winbase::CREATE_SUSPENDED; pub fn build_test_payload_x86() -> Result> { build_helper_crate("test_payload", &find_x86_variant_of_target(), false, "dll") @@ -90,6 +103,37 @@ fn is_cross() -> bool { var("CROSS_SYSROOT").is_ok() } +pub(crate) fn start_suspended_process(path: &Path) -> OwnedProcess { + unsafe { + let mut startup_info: STARTUPINFOW = zeroed(); + let mut process_info: PROCESS_INFORMATION = zeroed(); + startup_info.cb = std::mem::size_of::() as u32; + + let target_path_wide: Vec = OsStr::new(path.to_str().unwrap()) + .encode_wide() + .chain(once(0)) // null terminator + .collect(); + + if CreateProcessW( + target_path_wide.as_ptr(), + null_mut(), // Command line + null_mut(), // Process security attributes + null_mut(), // Thread security attributes + 0, // Inherit handles + CREATE_SUSPENDED, // Creation flags + null_mut(), // Environment + null_mut(), // Current directory + &mut startup_info, + &mut process_info, + ) == 0 + { + panic!("Failed to create suspended process"); + } else { + OwnedProcess::from_handle_unchecked(OwnedHandle::from_raw_handle(process_info.hProcess)) + } + } +} + #[macro_export] macro_rules! syringe_test { (fn $test_name:ident ($process:ident : OwnedProcess, $payload_path:ident : &Path $(,)?) $body:block) => { @@ -98,8 +142,8 @@ macro_rules! syringe_test { use dll_syringe::process::OwnedProcess; use std::{ path::Path, - process::{Command, Stdio}, }; + use $crate::common::start_suspended_process; #[test] #[cfg(any( @@ -126,21 +170,59 @@ macro_rules! syringe_test { payload_path: impl AsRef, target_path: impl AsRef, ) { - let dummy_process: OwnedProcess = Command::new(target_path.as_ref()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn().unwrap() - .into(); + let proc = start_suspended_process(target_path.as_ref()); + let _guard = proc.try_clone().unwrap().kill_on_drop(); + test(proc, payload_path.as_ref()); + } - let _guard = dummy_process.try_clone().unwrap().kill_on_drop(); + fn test( + $process : OwnedProcess, + $payload_path : &Path, + ) $body + } + }; +} + +#[macro_export] +macro_rules! suspended_process_test { + (fn $test_name:ident ($process:ident : OwnedProcess $(,)?) $body:block) => { + mod $test_name { + use super::*; + use dll_syringe::process::OwnedProcess; + use std::{ + path::Path + }; + use $crate::common::start_suspended_process; + + #[test] + #[cfg(any( + target_arch = "x86", + all(target_arch = "x86_64", feature = "into-x86-from-x64") + ))] + fn x86() { + test_with_setup( + common::build_test_target_x86().unwrap(), + ) + } - test(dummy_process, payload_path.as_ref()) + #[test] + #[cfg(target_arch = "x86_64")] + fn x86_64() { + test_with_setup( + common::build_test_target_x64().unwrap(), + ) + } + + fn test_with_setup( + target_path: impl AsRef, + ) { + let proc = start_suspended_process(target_path.as_ref()); + let _guard = proc.try_clone().unwrap().kill_on_drop(); + test(proc) } fn test( $process : OwnedProcess, - $payload_path : &Path, ) $body } }; @@ -154,7 +236,8 @@ macro_rules! process_test { use dll_syringe::process::OwnedProcess; use std::{ path::Path, - process::{Command, Stdio}, + process::Command, + process::Stdio, }; #[test] @@ -188,7 +271,6 @@ macro_rules! process_test { .into(); let _guard = dummy_process.try_clone().unwrap().kill_on_drop(); - test(dummy_process) } diff --git a/tests/eject.rs b/tests/eject.rs index e3f8ffd..5e7f3b5 100644 --- a/tests/eject.rs +++ b/tests/eject.rs @@ -10,7 +10,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); syringe.eject(module).unwrap(); } @@ -21,7 +21,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); syringe.process().kill().unwrap(); diff --git a/tests/inject.rs b/tests/inject.rs index 3ad6b69..86db93b 100644 --- a/tests/inject.rs +++ b/tests/inject.rs @@ -10,16 +10,16 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); syringe.inject(payload_path).unwrap(); } } -process_test! { +suspended_process_test! { fn inject_with_invalid_path_fails_with_remote_io( process: OwnedProcess, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let result = syringe.inject("invalid path"); assert!(result.is_err()); let err = result.unwrap_err(); @@ -37,7 +37,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); syringe.process().kill().unwrap(); let result = syringe.inject(payload_path); diff --git a/tests/module.rs b/tests/module.rs index a424263..190a0e1 100644 --- a/tests/module.rs +++ b/tests/module.rs @@ -33,7 +33,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); assert!(module.guess_is_loaded()); } @@ -45,7 +45,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); syringe.eject(module).unwrap(); assert!(!module.try_guess_is_loaded().unwrap()); @@ -58,7 +58,7 @@ syringe_test! { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); syringe.process().kill().unwrap(); assert!(!module.try_guess_is_loaded().unwrap()); diff --git a/tests/process.rs b/tests/process.rs index 0f44102..0f782e8 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -1,6 +1,10 @@ +use core::mem::{size_of, zeroed}; use dll_syringe::process::{BorrowedProcess, OwnedProcess, Process}; -use winapi::um::{libloaderapi::{GetProcAddress, LoadLibraryA}, sysinfoapi::GetVersion}; -use std::{fs, mem, time::Duration, ffi::CString}; +use std::{ffi::CString, fs, mem, process::Command, time::Duration}; +use winapi::um::{ + libloaderapi::{GetProcAddress, LoadLibraryA}, + winnt::OSVERSIONINFOW, +}; #[allow(unused)] mod common; @@ -29,7 +33,7 @@ process_test! { } } -process_test! { +suspended_process_test! { fn list_module_handles_on_crashed_does_not_hang( process: OwnedProcess ) { @@ -39,7 +43,7 @@ process_test! { } } -process_test! { +suspended_process_test! { fn is_alive_is_true_for_running( process: OwnedProcess ) { @@ -49,7 +53,7 @@ process_test! { } } -process_test! { +suspended_process_test! { fn is_alive_is_false_for_killed( process: OwnedProcess ) { @@ -77,7 +81,7 @@ process_test! { } } -process_test! { +suspended_process_test! { fn kill_guard_kills_process_on_drop( process: OwnedProcess ) { @@ -88,7 +92,7 @@ process_test! { } } -process_test! { +suspended_process_test! { fn long_process_paths_are_supported( process: OwnedProcess ) { @@ -167,4 +171,4 @@ fn is_older_than_windows_10() -> bool { // Windows 10 is version 10.0. Threshold for older versions is any major version less than 10. major < 10 } -} \ No newline at end of file +} diff --git a/tests/rpc.rs b/tests/rpc.rs index d96b026..f7dd569 100644 --- a/tests/rpc.rs +++ b/tests/rpc.rs @@ -9,11 +9,11 @@ mod core { pub use super::*; use std::time::Duration; - process_test! { + suspended_process_test! { fn get_procedure_address_of_win32_fn( process: OwnedProcess, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.process().wait_for_module_by_name("kernel32.dll", Duration::from_secs(1)).unwrap().unwrap(); let open_process = syringe.get_procedure_address( @@ -29,7 +29,7 @@ mod core { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let dll_main = syringe.get_procedure_address(module, "DllMain").unwrap(); @@ -37,11 +37,11 @@ mod core { } } - process_test! { + suspended_process_test! { fn get_procedure_address_of_invalid( process: OwnedProcess, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.process().wait_for_module_by_name("kernel32.dll", Duration::from_secs(1)).unwrap().unwrap(); let invalid = syringe.get_procedure_address(module, "ProcedureThatDoesNotExist").unwrap(); assert!(invalid.is_none()); @@ -59,7 +59,7 @@ mod payload { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_payload_procedure:: u32>(module, "add") }.unwrap().unwrap(); @@ -73,7 +73,7 @@ mod payload { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sum = unsafe { syringe.get_payload_procedure::) -> u64>(module, "sum") }.unwrap().unwrap(); @@ -87,7 +87,7 @@ mod payload { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_does_panic = unsafe { syringe.get_payload_procedure::(module, "does_panic") }.unwrap().unwrap(); @@ -114,7 +114,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u32>(module, "add_raw") }.unwrap().unwrap(); @@ -128,7 +128,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sub = unsafe { syringe.get_raw_procedure:: u32>(module, "sub_raw") }.unwrap().unwrap(); @@ -142,7 +142,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u16>(module, "add_smol_raw") }.unwrap().unwrap(); @@ -156,7 +156,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sum = unsafe { syringe.get_raw_procedure:: u32>(module, "sum_5_raw") }.unwrap().unwrap(); @@ -170,7 +170,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sum = unsafe { syringe.get_raw_procedure:: u32>(module, "sum_10_raw") }.unwrap().unwrap(); @@ -184,7 +184,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sub = unsafe { syringe.get_raw_procedure:: f32>(module, "sub_float_raw") }.unwrap().unwrap(); @@ -198,7 +198,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u32>(module, "add_raw_c") }.unwrap().unwrap(); @@ -212,7 +212,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_sum = unsafe { syringe.get_raw_procedure:: u32>(module, "sum_10_raw_c") }.unwrap().unwrap(); @@ -226,7 +226,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u32>(module, "add_raw_c") }.unwrap().unwrap(); syringe.eject(module).unwrap(); @@ -240,7 +240,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure:: u32>(module, "add_raw_c") }.unwrap().unwrap(); syringe.process().kill().unwrap(); @@ -254,7 +254,7 @@ mod raw { process: OwnedProcess, payload_path: &Path, ) { - let syringe = Syringe::for_process(process); + let syringe = Syringe::for_suspended_process(process).unwrap(); let module = syringe.inject(payload_path).unwrap(); let remote_add = unsafe { syringe.get_raw_procedure::(module, "crash") }.unwrap().unwrap(); let add_err = remote_add.call().unwrap_err(); From b5195c62f44c17091dc92c426c2ef53eff9286d7 Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Fri, 23 Feb 2024 18:12:21 +0000 Subject: [PATCH 09/13] Fix: Windows 10+ Detection --- tests/process.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/process.rs b/tests/process.rs index 0f782e8..abf7858 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -163,12 +163,17 @@ fn is_running_under_wine() -> bool { } } +// winapi crate doesn't have this. +// This is in ntdll, so already loaded for every Windows process. +extern "system" { + fn RtlGetVersion(lpVersionInformation: &mut OSVERSIONINFOW) -> u32; +} + fn is_older_than_windows_10() -> bool { unsafe { - let version = GetVersion(); - let major = (version & 0xFF) as u8; - - // Windows 10 is version 10.0. Threshold for older versions is any major version less than 10. - major < 10 + let mut os_info: OSVERSIONINFOW = zeroed(); + os_info.dwOSVersionInfoSize = size_of::() as u32; + RtlGetVersion(&mut os_info); + os_info.dwMajorVersion < 10 } } From acea5a0832ac1cea24dcea157a2f4470e4b9130d Mon Sep 17 00:00:00 2001 From: sewer56lol Date: Fri, 23 Feb 2024 18:14:12 +0000 Subject: [PATCH 10/13] Removed: Unused 'IcedError' Import --- src/rpc/rpc_core.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rpc/rpc_core.rs b/src/rpc/rpc_core.rs index 3d33e67..d58ac8e 100644 --- a/src/rpc/rpc_core.rs +++ b/src/rpc/rpc_core.rs @@ -1,5 +1,4 @@ -use iced_x86::{code_asm::*, IcedError}; - +use iced_x86::code_asm::*; use std::{ ffi::CString, mem, From c13f00535ae3834c50453590b093dc45787e94cd Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 23 Feb 2024 22:19:01 +0000 Subject: [PATCH 11/13] Fix: Pass 1 byte param correctly, and null parameter. This is 'technically correct', and also fixes Wine. --- src/syringe.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/syringe.rs b/src/syringe.rs index 010719f..6a4d5b5 100644 --- a/src/syringe.rs +++ b/src/syringe.rs @@ -128,12 +128,12 @@ impl Syringe { // Thankfully we can 'initialize' this suspended process without running any end user logic // (e.g. a game's entry point) by creating a dummy method and invoking it. - let ret = 0xC3; + let ret: u8 = 0xC3; let bx = syringe.remote_allocator.alloc_and_copy(&ret)?; - let mut dummy_param = 0; - syringe - .process() - .run_remote_thread(unsafe { mem::transmute(bx.as_raw_ptr()) }, &mut dummy_param)?; + syringe.process().run_remote_thread( + unsafe { mem::transmute(bx.as_raw_ptr()) }, + std::ptr::null::() as *mut u8, + )?; Ok(syringe) } From f82423dc90de250bc0d6bb24b12e142a299c099d Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Fri, 23 Feb 2024 23:24:05 +0000 Subject: [PATCH 12/13] Added: Disclaimer on Wine Bug #56362 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 211a8a3..725ea70 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ A windows dll injection library written in Rust. ## Supported scenarios + +> [!WARNING] +> Although tests currently pass, some uses of 64-bit into 32-bit may potentially fail on +> Linux (WINE) due to [Wine Bug #56362](https://bugs.winehq.org/show_bug.cgi?id=56362). + | Injector Process | Target Process | Supported? | | ---------------- | -------------- | ------------------------------------------ | | 32-bit | 32-bit | Yes | From 02cbdfea85ccbb862ffdb66fced68dfa369c12ca Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Sat, 24 Feb 2024 00:03:19 +0000 Subject: [PATCH 13/13] x86: Skip call to flush_instruction_cache * This is needed for architectures with a separate instruction and data cache. * In x86 and x64 these are unified, so this is not necessary. * If it turns out this is needed for e.g. x64 emulation on ARM64, it may be re-enabled one day. --- src/rpc/raw.rs | 1 + src/rpc/rpc_core.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/rpc/raw.rs b/src/rpc/raw.rs index b6abdff..6ac23ca 100644 --- a/src/rpc/raw.rs +++ b/src/rpc/raw.rs @@ -165,6 +165,7 @@ where Self::build_call_stub_x64(self.ptr, result.as_ptr().as_ptr(), float_mask).unwrap() }; let code = self.remote_allocator.alloc_and_copy_buf(code.as_slice())?; + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] code.memory().flush_instruction_cache()?; Ok(RemoteRawProcedureStub { diff --git a/src/rpc/rpc_core.rs b/src/rpc/rpc_core.rs index d58ac8e..f1cf76f 100644 --- a/src/rpc/rpc_core.rs +++ b/src/rpc/rpc_core.rs @@ -82,6 +82,7 @@ impl Syringe { .unwrap() }; let function_stub = self.remote_allocator.alloc_and_copy_buf(code.as_slice())?; + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] function_stub.memory().flush_instruction_cache()?; Ok(RemoteProcedureStub {