diff --git a/Cargo.lock b/Cargo.lock index f7b69d5..6b86cd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,9 +57,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "dinvk" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91eaaa6645d9f59464500fe0c3b3f65308456d38f18de09fa0547f8eaea3643e" +checksum = "0ddcae11e0e69466a318b2ca7e9e8c0ddf3ccdb7d282ca2dbdf263ba4e0a4e19" dependencies = [ "bitflags", "cfg-if", @@ -154,7 +154,7 @@ checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "uwd" -version = "0.3.4" +version = "0.3.5" dependencies = [ "anyhow", "bitfield", diff --git a/Cargo.toml b/Cargo.toml index dd91b12..5b3615b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "uwd" -version = "0.3.4" +version = "0.3.5" edition = "2024" description = "Call Stack Spoofing for Rust" -license = "MIT" +license = "MIT OR Apache-2.0" repository = "https://github.com/joaoviictorti/uwd" homepage = "https://github.com/joaoviictorti/uwd" readme = "README.md" @@ -18,10 +18,13 @@ include = [ "LICENSE", ] +[lib] +doctest = false + [dependencies] bitfield = "0.19.0" obfstr = "0.4.4" -dinvk = "0.3.1" +dinvk = "0.4.2" anyhow = { version = "1.0.98", default-features = false } memchr = { version = "2.7.4", default-features = false } diff --git a/Justfile b/Justfile index a6b1753..c5511f0 100644 --- a/Justfile +++ b/Justfile @@ -17,26 +17,6 @@ update: publish: cargo publish --allow-dirty -# Formats all Rust source files -fmt: - cargo +nightly fmt --check - -# Builds local documentation -docs: - cargo doc --no-deps --open - -# Run only integration tests in /tests directory -test: - cargo test --test '*' -- --nocapture - -# Run a specific example -example name: - cargo run --example {{name}} - -# Test this workflow on your machine with `act` -act: - act -P windows-latest=-self-hosted - # Format all .toml files using Taplo taplo: taplo format \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/LICENSE b/LICENSE-MIT similarity index 100% rename from LICENSE rename to LICENSE-MIT diff --git a/README.md b/README.md index 86a2ac2..4595534 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ -# uwd 🦀 +# uwd ![Rust](https://img.shields.io/badge/made%20with-Rust-red) ![crate](https://img.shields.io/crates/v/uwd.svg) ![docs](https://docs.rs/uwd/badge.svg) -[![build](https://github.com/joaoviictorti/uwd/actions/workflows/ci.yml/badge.svg)](https://github.com/joaoviictorti/uwd/actions/workflows/ci.yml) -![Forks](https://img.shields.io/github/forks/joaoviictorti/uwd) -![Stars](https://img.shields.io/github/stars/joaoviictorti/uwd) -![License](https://img.shields.io/github/license/joaoviictorti/uwd) +![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-brightgreen) +[![Actions status](https://github.com/joaoviictorti/uwd/actions/workflows/ci.yml/badge.svg)](https://github.com/joaoviictorti/uwd/actions) -`uwd` is a Rust library for call stack spoofing on Windows, allowing you to execute arbitrary functions with a forged call stack that evades analysis, logging, or detection during stack unwinding. +Rust library for call stack spoofing on Windows, allowing you to execute arbitrary functions with a forged call stack that evades analysis, logging, or detection during stack unwinding. Inspired by [SilentMoonwalk](https://github.com/klezVirus/SilentMoonwalk), this crate brings low-level spoofing capabilities into a clean, idiomatic Rust interface with full support for `#[no_std]`, `MSVC` and `GNU` toolchains, and automated gadget resolution. @@ -42,12 +40,12 @@ You can spoof: This example shows how to spawn `calc.exe` using a spoofed call stack. We call `WinExec` twice once using the Desync technique, and again using the Synthetic one. ```rust -use dinvk::{GetModuleHandle, GetProcAddress}; +use dinvk::module::{get_module_address, get_proc_address}; use uwd::spoof; // Resolves addresses of the WinAPI functions to be used -let kernel32 = GetModuleHandle("kernel32.dll", None); -let win_exec = GetProcAddress(kernel32, "WinExec", None); +let kernel32 = get_module_address("kernel32.dll", None); +let win_exec = get_proc_address(kernel32, "WinExec", None); // Execute command with `WinExec` let cmd = c"calc.exe"; @@ -64,7 +62,7 @@ This example performs a indirect system call to `NtAllocateVirtualMemory` with a ```rust use std::{ffi::c_void, ptr::null_mut}; -use dinvk::NT_SUCCESS; +use dinvk::winapis::NT_SUCCESS; use uwd::{syscall, AsPointer}; // Running indirect syscall with Call Stack Spoofing @@ -79,10 +77,6 @@ if !NT_SUCCESS(status) { println!("[+] Address allocated: {:?}", addr); ``` -## Additional Resources - -For more examples, check the [examples](https://github.com/joaoviictorti/uwd/tree/main/examples) folder in the repository. - ## References I want to express my gratitude to these projects that inspired me to create `uwd` and contribute with some features: @@ -99,4 +93,14 @@ Special thanks to: ## License -This project is licensed under the MIT License. See the [LICENSE](https://github.com/joaoviictorti/uwd/tree/main/LICENSE) file for details. +uwd is licensed under either of + +- Apache License, Version 2.0, ([LICENSE-APACHE](https://github.com/joaoviictorti/uwd/tree/main/LICENSE-APACHE) or + ) +- MIT license ([LICENSE-MIT](https://github.com/joaoviictorti/uwd/tree/main/LICENSE-MIT) or ) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in uwd +by you, as defined in the Apache-2.0 license, shall be dually licensed as above, without any +additional terms or conditions. \ No newline at end of file diff --git a/examples/shellcode/main.rs b/examples/shellcode/main.rs deleted file mode 100644 index 160500a..0000000 --- a/examples/shellcode/main.rs +++ /dev/null @@ -1,43 +0,0 @@ -use dinvk::{GetModuleHandle, GetProcAddress}; -use std::ptr::{copy_nonoverlapping, null_mut}; -use uwd::{AsPointer, spoof}; - -// msfvenom -p windows/x64/exec CMD=calc.exe -f rust -const SHELLCODE: [u8; 276] = [ - 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, - 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, - 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, - 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, - 0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, - 0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44, - 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, - 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, - 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, - 0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff, - 0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89, - 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00, -]; - -fn main() -> Result<(), Box> { - // Resolves addresses of the WinAPI functions to be used - let kernel32 = GetModuleHandle("kernel32.dll", None); - let virtual_alloc = GetProcAddress(kernel32, "VirtualAlloc", None); - let virtual_protect = GetProcAddress(kernel32, "VirtualProtect", None); - let create_thread = GetProcAddress(kernel32, "CreateThread", None); - - // Allocate memory with RW using spoofed VirtualAlloc - let addr = spoof!(virtual_alloc, null_mut::(), SHELLCODE.len(), 0x3000, 0x04)?; - - // Copies the shellcode to the allocated memory - unsafe { copy_nonoverlapping(SHELLCODE.as_ptr().cast(), addr, SHELLCODE.len()) }; - - // Changes protection to PAGE_EXECUTE_READ - let mut old_protection = 0usize; - spoof!(virtual_protect, addr, SHELLCODE.len(), 0x20, old_protection.as_ptr_mut())?; - - // Execute shellcode in new thread - spoof!(create_thread, null_mut::(), 0, addr, null_mut::(), 0, 0)?; - - std::thread::sleep(std::time::Duration::from_secs(2000)); - Ok(()) -} diff --git a/examples/syscall/main.rs b/examples/syscall/main.rs deleted file mode 100644 index 6129ea6..0000000 --- a/examples/syscall/main.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::{ffi::c_void, ptr::null_mut}; -use uwd::{AsPointer, syscall}; - -fn main() -> Result<(), Box> { - // Running indirect syscall with Call Stack Spoofing - let mut addr = null_mut::(); - let mut size = (1 << 12) as usize; - let status = syscall!("NtAllocateVirtualMemory", -1isize, addr.as_ptr_mut(), 0, size.as_ptr_mut(), 0x3000, 0x04)? as i32; - if status < 0 { - eprintln!("NtAllocateVirtualMemory Failed With Status: {status:#X}"); - return Ok(()); - } - - println!("[+] Address allocated: {:?}", addr); - - Ok(()) -} diff --git a/examples/winexec/main.rs b/examples/winexec/main.rs deleted file mode 100644 index d8326ce..0000000 --- a/examples/winexec/main.rs +++ /dev/null @@ -1,18 +0,0 @@ -use dinvk::{GetModuleHandle, GetProcAddress}; -use uwd::spoof; - -fn main() -> Result<(), Box> { - // Resolves addresses of the WinAPI functions to be used - let kernel32 = GetModuleHandle("kernel32.dll", None); - let win_exec = GetProcAddress(kernel32, "WinExec", None); - - // Execute command with `WinExec` - let cmd = c"calc.exe"; - let result = spoof!(win_exec, cmd.as_ptr(), 1)?; - if result.is_null() { - eprintln!("WinExec Failed"); - return Ok(()); - } - - Ok(()) -} diff --git a/src/asm/gnu/desync.asm b/src/asm/gnu/desync.asm index a9162c1..9a65400 100644 --- a/src/asm/gnu/desync.asm +++ b/src/asm/gnu/desync.asm @@ -1,6 +1,3 @@ -;; Copyright (c) 2025 joaoviictorti -;; Licensed under the MIT License. See LICENSE file in the project root for details. - ;; ;; Code responsible for Call Stack Spoofing Via Desync (NASM) ;; diff --git a/src/asm/gnu/synthetic.asm b/src/asm/gnu/synthetic.asm index 55ef929..58b7b35 100644 --- a/src/asm/gnu/synthetic.asm +++ b/src/asm/gnu/synthetic.asm @@ -1,6 +1,3 @@ -;; Copyright (c) 2025 joaoviictorti -;; Licensed under the MIT License. See LICENSE file in the project root for details. - ;; ;; Code responsible for Call Stack Spoofing Via Synthetic (NASM) ;; diff --git a/src/asm/msvc/desync.asm b/src/asm/msvc/desync.asm index dc58696..45efcc1 100644 --- a/src/asm/msvc/desync.asm +++ b/src/asm/msvc/desync.asm @@ -1,6 +1,3 @@ -;; Copyright (c) 2025 joaoviictorti -;; Licensed under the MIT License. See LICENSE file in the project root for details. - ;; ;; Code responsible for Call Stack Spoofing Via Desync (MASM) ;; diff --git a/src/asm/msvc/synthetic.asm b/src/asm/msvc/synthetic.asm index d672b79..136e10c 100644 --- a/src/asm/msvc/synthetic.asm +++ b/src/asm/msvc/synthetic.asm @@ -1,6 +1,3 @@ -;; Copyright (c) 2025 joaoviictorti -;; Licensed under the MIT License. See LICENSE file in the project root for details. - ;; ;; Code responsible for Call Stack Spoofing Via Synthetic (MASM) ;; diff --git a/src/lib.rs b/src/lib.rs index 5761a49..4052397 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,9 @@ -// Copyright (c) 2025 joaoviictorti -// Licensed under the MIT License. See LICENSE file in the project root for details. - #![no_std] #![doc = include_str!("../README.md")] extern crate alloc; -mod data; +mod types; mod uwd; mod util; diff --git a/src/data.rs b/src/types.rs similarity index 78% rename from src/data.rs rename to src/types.rs index 7e30d03..21b4c16 100644 --- a/src/data.rs +++ b/src/types.rs @@ -1,9 +1,7 @@ -// Copyright (c) 2025 joaoviictorti -// Licensed under the MIT License. See LICENSE file in the project root for details. - #![allow(non_snake_case, non_camel_case_types)] -use core::ffi::c_void; +use core::{ffi::c_void, slice::from_raw_parts}; +use dinvk::{types::*, helper::PE}; /// Indicates the presence of an exception handler in the function. pub const UNW_FLAG_EHANDLER: u8 = 0x1; @@ -11,6 +9,53 @@ pub const UNW_FLAG_EHANDLER: u8 = 0x1; /// Indicates chained unwind information is present. pub const UNW_FLAG_CHAININFO: u8 = 0x4; +/// Provides access to the unwind (exception handling) information of a PE image. +#[derive(Debug)] +pub struct Unwind { + /// Reference to the parsed PE image. + pub pe: PE, +} + +impl Unwind { + /// Creates a new [`Unwind`]. + pub fn new(pe: PE) -> Self { + Unwind { pe: pe } + } + + /// Returns all runtime function entries. + pub fn entries(&self) -> Option<&[IMAGE_RUNTIME_FUNCTION]> { + let nt = self.pe.nt_header()?; + let dir = unsafe { + (*nt).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION] + }; + + if dir.VirtualAddress == 0 || dir.Size == 0 { + return None; + } + + let addr = (self.pe.base as usize + dir.VirtualAddress as usize) as *const IMAGE_RUNTIME_FUNCTION; + let len = dir.Size as usize / size_of::(); + + Some(unsafe { from_raw_parts(addr, len) }) + } + + /// Finds a runtime function by its RVA. + pub fn function_by_offset(&self, offset: u32) -> Option<&IMAGE_RUNTIME_FUNCTION> { + self.entries()?.iter().find(|f| f.BeginAddress == offset) + } + + /// Gets the size in bytes of a function using the unwind table. + #[cfg(feature = "desync")] + pub fn function_size(&self, func: *mut c_void) -> Option { + let offset = (func as usize - self.pe.base as usize) as u32; + let entry = self.function_by_offset(offset)?; + + let start = self.pe.base as u64 + entry.BeginAddress as u64; + let end = self.pe.base as u64 + entry.EndAddress as u64; + Some(end - start) + } +} + /// Configuration structure passed to the spoof ASM routine. #[repr(C)] #[derive(Debug)] diff --git a/src/util.rs b/src/util.rs index ef72220..7a71950 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,31 +1,24 @@ -// Copyright (c) 2025 joaoviictorti -// Licensed under the MIT License. See LICENSE file in the project root for details. - use core::{ffi::c_void, slice::from_raw_parts}; use alloc::vec::Vec; use obfstr::obfbytes as b; -use dinvk::data::IMAGE_RUNTIME_FUNCTION; +use dinvk::types::IMAGE_RUNTIME_FUNCTION; -use super::ignoring_set_fpreg; +use crate::ignoring_set_fpreg; -/// Searches for a specific instruction pattern inside a function's code region, -/// returning the relative offset from the function's start if found. +/// Searches for a valid instruction offset in a function. /// -/// # Arguments -/// -/// * `module` - Base address of the module containing the target function. -/// * `runtime` - A reference to the IMAGE_RUNTIME_FUNCTION describing the function. -/// -/// # Returns -/// -/// The relative offset inside the function where the gadget was found. +/// This scans the function's code region for a `call qword ptr [rip+0]` +/// instruction sequence and returns the offset *after* the instruction. /// /// # Notes /// -/// The pattern being searched is a `call qword ptr [rip+0]`, encoded as `48 FF 15 00 00 00 00`, -/// and the function returns the offset *after* the full instruction (+7). -pub fn find_valid_instruction_offset(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option { +/// The searched gadget pattern is `48 FF 15 00 00 00 00`, and the +/// returned value is `match_offset + 7`. +pub fn find_valid_instruction_offset( + module: *mut c_void, + runtime: &IMAGE_RUNTIME_FUNCTION, +) -> Option { let start = module as u64 + runtime.BeginAddress as u64; let end = module as u64 + runtime.EndAddress as u64; let size = end - start; @@ -43,18 +36,8 @@ pub fn find_valid_instruction_offset(module: *mut c_void, runtime: &IMAGE_RUNTIM None } -/// Scans the memory of a module for a specific byte pattern, constrained to -/// valid runtime functions with corresponding unwind info. -/// -/// # Arguments -/// -/// * `module` - Base address of the loaded module to scan. -/// * `pattern` - Byte sequence representing the target gadget. -/// * `runtime_table` - Slice of `IMAGE_RUNTIME_FUNCTION` entries describing the module's valid code ranges. -/// -/// # Returns -/// -/// Pointer to the start of the matching gadget and the associated stack frame size. +/// Scans the code of a module for a given byte pattern, restricted to valid +/// RUNTIME_FUNCTION regions. pub fn find_gadget( module: *mut c_void, pattern: &[u8], @@ -82,50 +65,44 @@ pub fn find_gadget( }) .collect::>(); - // No gadget found? return None if gadgets.is_empty() { return None; } - // Randomizes the order of possible frames found (if there is more than one), - // helps to shuffle patterns and reduce repetition-based heuristics + // Shuffle to reduce pattern predictability. shuffle(&mut gadgets); - // Take the first occurrence gadgets.first().copied() } } /// Scans the current thread's stack to locate the return address that falls within /// the range of the `BaseThreadInitThunk` function from `kernel32.dll`. -/// -/// # Returns -/// -/// The stack address (`RSP`) where a return to `BaseThreadInitThunk` was found. #[cfg(feature = "desync")] pub fn find_base_thread_return_address() -> Option { - use dinvk::{GetModuleHandle, GetProcAddress}; - use dinvk::{hash::{jenkins3, murmur3}, pe::PE}; + use dinvk::module::{get_module_address, get_proc_address}; + use dinvk::{hash::{jenkins3, murmur3}, helper::PE}; + use crate::types::Unwind; unsafe { // Get handle for kernel32.dll - let kernel32 = GetModuleHandle(2808682670u32, Some(murmur3)); + let kernel32 = get_module_address(2808682670u32, Some(murmur3)); if kernel32.is_null() { return None; } // Resolves the address of the BaseThreadInitThunk function - let base_thread = GetProcAddress(kernel32, 4073232152u32, Some(jenkins3)); + let base_thread = get_proc_address(kernel32, 4073232152u32, Some(jenkins3)); if base_thread.is_null() { return None; } // Calculate the size of the BaseThreadInitThunk function - let pe_kernel32 = PE::parse(kernel32); - let size = pe_kernel32.unwind().function_size(base_thread)? as usize; + let pe_kernel32 = Unwind::new(PE::parse(kernel32)); + let size = pe_kernel32.function_size(base_thread)? as usize; // Access the TEB and stack limits - let teb = dinvk::NtCurrentTeb(); + let teb = dinvk::winapis::NtCurrentTeb(); let stack_base = (*teb).Reserved1[1] as usize; let stack_limit = (*teb).Reserved1[2] as usize; @@ -147,14 +124,7 @@ pub fn find_base_thread_return_address() -> Option { } } -/// Randomly shuffles the elements of a mutable slice in-place using a pseudo-random -/// number generator seeded by the CPU's timestamp counter (`rdtsc`). -/// -/// The shuffling algorithm is a variant of the Fisher-Yates shuffle. -/// -/// # Arguments -/// -/// * `list` - A mutable slice of elements to be shuffled. +/// Randomly shuffles the elements of a list in place. pub fn shuffle(list: &mut [T]) { let mut seed = unsafe { core::arch::x86_64::_rdtsc() }; for i in (1..list.len()).rev() { diff --git a/src/uwd.rs b/src/uwd.rs index 971ecb3..b1b0def 100644 --- a/src/uwd.rs +++ b/src/uwd.rs @@ -1,29 +1,20 @@ -// Copyright (c) 2025 joaoviictorti -// Licensed under the MIT License. See LICENSE file in the project root for details. - use alloc::{string::String, vec::Vec}; use core::ffi::c_void; use anyhow::{Context, Result, bail}; use obfstr::obfstring as s; -use dinvk::{ - GetModuleHandle, GetProcAddress, - data::IMAGE_RUNTIME_FUNCTION, - hash::murmur3, - pe::PE -}; +use dinvk::module::{get_module_address, get_proc_address}; +use dinvk::types::IMAGE_RUNTIME_FUNCTION; +use dinvk::hash::murmur3; +use dinvk::helper::PE; -use super::util::*; -use super::data::{ - Config, Registers, - UNWIND_OP_CODES::{self, *}, -}; -use super::data::{ - UNW_FLAG_CHAININFO, - UNW_FLAG_EHANDLER, - UNWIND_CODE, - UNWIND_INFO, -}; +#[cfg(feature = "desync")] +use crate::util::find_base_thread_return_address; +use crate::util::{find_gadget, shuffle, find_valid_instruction_offset}; +use crate::types::{Config, Registers, UNWIND_OP_CODES::{self, *}}; +use crate::types::{UNW_FLAG_CHAININFO, UNW_FLAG_EHANDLER}; +use crate::types::{UNWIND_CODE, UNWIND_INFO}; +use crate::types::Unwind; #[cfg(feature = "desync")] unsafe extern "C" { @@ -37,17 +28,33 @@ unsafe extern "C" { fn SpoofSynthetic(config: &mut Config) -> *mut c_void; } -/// Invokes the function using a synthetic stack layout. +/// Invokes a function using a synthetic spoofed call stack. /// -/// # Arguments +/// # Examples /// -/// * `$addr` - A pointer to the function to spoof-call. -/// * `$arg` - A list of arguments to be passed to the spoofed function (up to 11 maximum). +/// ``` +/// use core::ptr; +/// use dinvk::module::{get_module_address, get_proc_address}; +/// use uwd::spoof; +/// +/// let kernel32 = get_module_address("kernel32.dll", None); +/// let virtual_alloc = get_proc_address(kernel32, "VirtualAlloc", None); +/// +/// let addr = spoof!( +/// virtual_alloc, +/// ptr::null_mut::(), +/// 1 << 12, +/// 0x3000, +/// 0x04 +/// ).unwrap(); +/// +/// assert!(!addr.is_null()); +/// ``` #[macro_export] macro_rules! spoof { ($addr:expr, $($arg:expr),+ $(,)?) => { unsafe { - $crate::internal::uwd_entry( + $crate::__private::spoof( $addr, &[$(::core::mem::transmute($arg as usize)),*], $crate::SpoofKind::Function, @@ -56,17 +63,34 @@ macro_rules! spoof { }; } -/// Wraps a native Windows syscall with a simulated stack layout. +/// Invokes a Windows native syscall using a spoofed stack. /// -/// # Arguments +/// # Examples +/// +/// ``` +/// use core::ptr; +/// use uwd::{AsPointer, syscall}; /// -/// * `$name` - The name of the syscall as a string literal. -/// * `$arg` - A list of arguments to be passed to the spoofed function (up to 11 maximum) +/// let mut addr = ptr::null_mut::(); +/// let mut size = (1 << 12) as usize; +/// +/// let status = syscall!( +/// "NtAllocateVirtualMemory", +/// -1isize, +/// addr.as_ptr_mut(), +/// 0, +/// size.as_ptr_mut(), +/// 0x3000, +/// 0x04 +/// ).unwrap() as i32; +/// +/// assert_eq!(status, 0); +/// ``` #[macro_export] macro_rules! syscall { ($name:expr, $($arg:expr),* $(,)?) => { unsafe { - $crate::internal::uwd_entry( + $crate::__private::spoof( core::ptr::null_mut(), &[$(::core::mem::transmute($arg as usize)),*], $crate::SpoofKind::Syscall($name), @@ -75,62 +99,62 @@ macro_rules! syscall { }; } -/// Internal module responsible for executing call stack spoofing. -pub mod internal { - use core::{ffi::c_void, ptr::null_mut}; +#[doc(hidden)] +pub mod __private { + use core::ffi::c_void; use super::*; - /// Performs call stack spoofing in `synthetic` mode, simulating a fake stack from scratch. + /// Performs call stack spoofing in `synthetic` mode. #[cfg(not(feature = "desync"))] - fn spoof(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind) -> Result<*mut c_void> { - // Max 11 arguments allowed + pub fn spoof(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind) -> Result<*mut c_void> { + // Max 11 args if args.len() > 11 { bail!(s!("too many arguments")); } - // Prevent calling a null function unless it's a syscall + // Function pointer must be valid unless syscall spoof if let SpoofKind::Function = kind && addr.is_null() { bail!(s!("null function address")); } - // Preparing the `Config` structure for spoofing let mut config = Config::default(); - // Get the base address of kernelbase.dll - let kernelbase = GetModuleHandle(2737729883u32, Some(murmur3)); + // Resolve kernelbase + let kernelbase = get_module_address(2737729883u32, Some(murmur3)); - // Parse the IMAGE_RUNTIME_FUNCTION table into usable Rust slices - let pe_kernelbase = PE::parse(kernelbase); - let tables = pe_kernelbase.unwind().entries().context(s!( - "failed to read IMAGE_RUNTIME_FUNCTION entries from .pdata section" - ))?; + // Parse unwind table + let pe_kernelbase = Unwind::new(PE::parse(kernelbase)); + let tables = pe_kernelbase + .entries() + .context(s!( + "failed to read IMAGE_RUNTIME_FUNCTION entries from .pdata section" + ))?; - // Preparing addresses to use as artificial frames to emulate thread stack initialization - let ntdll = GetModuleHandle(2788516083u32, Some(murmur3)); + // Resolve APIs + let ntdll = get_module_address(2788516083u32, Some(murmur3)); if ntdll.is_null() { bail!(s!("ntdll.dll not found")); } - let kernel32 = GetModuleHandle(2808682670u32, Some(murmur3)); - let rlt_user_addr = GetProcAddress(ntdll, 1578834099u32, Some(murmur3)); - let base_thread_addr = GetProcAddress(kernel32, 4083630997u32, Some(murmur3)); + let kernel32 = get_module_address(2808682670u32, Some(murmur3)); + let rlt_user_addr = get_proc_address(ntdll, 1578834099u32, Some(murmur3)); + let base_thread_addr = get_proc_address(kernel32, 4083630997u32, Some(murmur3)); + config.rtl_user_addr = rlt_user_addr; config.base_thread_addr = base_thread_addr; - // Recovering the IMAGE_RUNTIME_FUNCTION structure of target apis - let pe_ntdll = PE::parse(ntdll); + // Unwind lookup + let pe_ntdll = Unwind::new(PE::parse(ntdll)); let rtl_user_runtime = pe_ntdll - .unwind() .function_by_offset(rlt_user_addr as u32 - ntdll as u32) .context(s!("RtlUserThreadStart unwind info not found"))?; - let pe_kernel32 = PE::parse(kernel32); + let pe_kernel32 = Unwind::new(PE::parse(kernel32)); let base_thread_runtime = pe_kernel32 - .unwind() .function_by_offset(base_thread_addr as u32 - kernel32 as u32) .context(s!("BaseThreadInitThunk unwind info not found"))?; - // Recovering the stack size of target apis + // Stack sizes let rtl_user_size = ignoring_set_fpreg(ntdll, rtl_user_runtime) .context(s!("RtlUserThreadStart stack size not found"))?; @@ -140,14 +164,14 @@ pub mod internal { config.rtl_user_thread_size = rtl_user_size as u64; config.base_thread_size = base_thread_size as u64; - // First frame: a normal function with a clean prologue + // First prologue let first_prolog = Prolog::find_prolog(kernelbase, tables) .context(s!("first prolog not found"))?; config.first_frame_fp = (first_prolog.frame + first_prolog.offset as u64) as *const c_void; config.first_frame_size = first_prolog.stack_size as u64; - // Second frame: looks specifically for a prologue with `push rbp` + // Second prologue let second_prolog = Prolog::find_push_rbp(kernelbase, tables) .context(s!("second prolog not found"))?; @@ -155,23 +179,24 @@ pub mod internal { config.second_frame_size = second_prolog.stack_size as u64; config.rbp_stack_offset = second_prolog.rbp_offset as u64; - // Find a gadget `add rsp, 0x58; ret` + // Gadget: `add rsp, 0x58; ret` let (add_rsp_addr, size) = find_gadget(kernelbase, &[0x48, 0x83, 0xC4, 0x58, 0xC3], tables) .context(s!("add rsp gadget not found"))?; config.add_rsp_gadget = add_rsp_addr as *const c_void; config.add_rsp_frame_size = size as u64; - // Find a gadget that performs `jmp rbx` - to restore the original call + // Gadget: `jmp rbx` let (jmp_rbx_addr, size) = find_gadget(kernelbase, &[0xFF, 0x23], tables) .context(s!("jmp rbx gadget not found"))?; config.jmp_rbx_gadget = jmp_rbx_addr as *const c_void; config.jmp_rbx_frame_size = size as u64; - // Preparing arguments + // Prepare arguments let len = args.len(); config.number_args = len as u32; + for (i, &arg) in args.iter().take(len).enumerate() { match i { 0 => config.arg01 = arg, @@ -189,22 +214,15 @@ pub mod internal { } } - // Spoof kind handling + // Handle syscall spoofing match kind { - // Executes a function that is not syscall - SpoofKind::Function => { - config.spoof_function = addr; - } - - // Executes a syscall indirectly + SpoofKind::Function => config.spoof_function = addr, SpoofKind::Syscall(name) => { - // Retrieves the address of the function - let addr = GetProcAddress(ntdll, name, None); + let addr = get_proc_address(ntdll, name, None); if addr.is_null() { - bail!(s!("GetProcAddress returned null")); + bail!(s!("get_proc_address returned null")); } - // Configures the parameters to be sent to execute the syscall indirectly config.is_syscall = true as u32; config.ssn = dinvk::ssn(name, ntdll).context(s!("ssn not found"))?; config.spoof_function = dinvk::get_syscall_address(addr) @@ -212,47 +230,47 @@ pub mod internal { } } - // Call the external spoofing routine Ok(unsafe { SpoofSynthetic(&mut config) }) } - /// Performs call stack spoofing in `desync` mode, reusing the thread's real stack. + /// Performs call stack spoofing in `desync` mode. #[cfg(feature = "desync")] - fn spoof(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind) -> Result<*mut c_void> { - // Max 11 arguments allowed + pub fn spoof(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind) -> Result<*mut c_void> { + // Max 11 args if args.len() > 11 { bail!(s!("too many arguments")); } - // Prevent calling a null function unless it's a syscall + // Function pointer must be valid unless syscall spoof if let SpoofKind::Function = kind && addr.is_null() { bail!(s!("null function address")); } - // Preparing the `Config` structure for spoofing let mut config = Config::default(); - // Get the base address of kernelbase.dll - let kernelbase = GetModuleHandle(2737729883u32, Some(murmur3)); + // Resolve kernelbase + let kernelbase = get_module_address(2737729883u32, Some(murmur3)); - // Parse the IMAGE_RUNTIME_FUNCTION table into usable Rust slices - let pe = PE::parse(kernelbase); - let tables = pe.unwind().entries().context(s!( - "failed to read IMAGE_RUNTIME_FUNCTION entries from .pdata section" - ))?; + // Parse unwind table + let pe = Unwind::new(PE::parse(kernelbase)); + let tables = pe + .entries() + .context(s!( + "failed to read IMAGE_RUNTIME_FUNCTION entries from .pdata section" + ))?; // Locate a return address from BaseThreadInitThunk on the current stack config.return_address = find_base_thread_return_address() .context(s!("return address not found"))? as *const c_void; - // First frame: a normal function with a clean prologue + // First prologue let first_prolog = Prolog::find_prolog(kernelbase, tables) .context(s!("first prolog not found"))?; config.first_frame_fp = (first_prolog.frame + first_prolog.offset as u64) as *const c_void; config.first_frame_size = first_prolog.stack_size as u64; - // Second frame: looks specifically for a prologue with `push rbp` + // Second prologue let second_prolog = Prolog::find_push_rbp(kernelbase, tables) .context(s!("second prolog not found"))?; @@ -260,23 +278,24 @@ pub mod internal { config.second_frame_size = second_prolog.stack_size as u64; config.rbp_stack_offset = second_prolog.rbp_offset as u64; - // Find a gadget `add rsp, 0x58; ret` + // Gadget: `add rsp, 0x58; ret` let (add_rsp_addr, size) = find_gadget(kernelbase, &[0x48, 0x83, 0xC4, 0x58, 0xC3], tables) .context(s!("add rsp gadget not found"))?; config.add_rsp_gadget = add_rsp_addr as *const c_void; config.add_rsp_frame_size = size as u64; - // Find a gadget that performs `jmp rbx` - to restore the original call + // Gadget: `jmp rbx` let (jmp_rbx_addr, size) = find_gadget(kernelbase, &[0xFF, 0x23], tables) .context(s!("jmp rbx gadget not found"))?; config.jmp_rbx_gadget = jmp_rbx_addr as *const c_void; config.jmp_rbx_frame_size = size as u64; - // Preparing arguments + // Prepare arguments let len = args.len(); config.number_args = len as u32; + for (i, &arg) in args.iter().take(len).enumerate() { match i { 0 => config.arg01 = arg, @@ -294,28 +313,20 @@ pub mod internal { } } - // Spoof kind handling + // Handle syscall spoofing match kind { - // Executes a function that is not syscall - SpoofKind::Function => { - config.spoof_function = addr; - } - - // Executes a syscall indirectly + SpoofKind::Function => config.spoof_function = addr, SpoofKind::Syscall(name) => { - // Retrieves the ntdll address - let ntdll = GetModuleHandle(2788516083u32, Some(murmur3)); + let ntdll = get_module_address(2788516083u32, Some(murmur3)); if ntdll.is_null() { bail!(s!("ntdll.dll not found")); } - // Retrieves the address of the function - let addr = GetProcAddress(ntdll, name, None); + let addr = get_proc_address(ntdll, name, None); if addr.is_null() { - bail!(s!("GetProcAddress returned null")); + bail!(s!("get_proc_address returned null")); } - // Configures the parameters to be sent to execute the syscall indirectly config.is_syscall = true as u32; config.ssn = dinvk::ssn(name, ntdll).context(s!("ssn not found"))?; config.spoof_function = dinvk::get_syscall_address(addr) @@ -323,68 +334,32 @@ pub mod internal { } } - // Call the external spoofing routine Ok(unsafe { Spoof(&mut config) }) } - - /// Launches a spoofed execution using either desynchronized or synthetic stack spoofing. - /// - /// # Arguments - /// - /// * `addr` - Target function pointer. Can be `null` if `kind` is `SpoofKind::Syscall`. - /// * `args` - Up to 11 arguments that will be passed to the target, cast as `*const c_void`. - /// * `kind` - The spoofing mode: - /// - [`SpoofKind::Function`]: Directly call a function using the spoofed call stack. - /// - [`SpoofKind::Syscall`]: Resolve and invoke a Windows syscall via its shadow stub. - /// - /// # Returns - /// - /// The raw return value of the spoofed call, or an error if spoofing fails. - #[inline(always)] - pub fn uwd_entry( - addr: *mut c_void, - args: &[*const c_void], - kind: SpoofKind<'_>, - ) -> Result<*mut c_void> { - match kind { - SpoofKind::Function => { - spoof(addr, args, SpoofKind::Function) - } - SpoofKind::Syscall(name) => { - spoof(null_mut(), args, SpoofKind::Syscall(name)) - } - } - } } -/// Represents metadata extracted from a function's prologue used for call stack spoofing. -#[derive(Copy, Clone)] +/// Metadata extracted from a function prologue that is suitable for spoofing. +#[derive(Copy, Clone, Default)] struct Prolog { - /// Address of the function's entry point or relevant instruction. + /// Address of the selected function frame. frame: u64, /// Total stack space reserved by the function. stack_size: u32, - /// Offset inside the function where a specific instruction pattern is found. + /// Offset inside the function where a valid instruction pattern was found. offset: u32, - /// Offset in the stack where `rbp` is pushed. + /// Offset in the stack where `rbp` is pushed or saved. rbp_offset: u32, } impl Prolog { - /// Scans the `RUNTIME_FUNCTION` table to locate the first function with a prologue - /// considered safe and predictable for call stack spoofing. + /// Finds the first prologue in the unwind table that looks safe for spoofing. /// - /// # Arguments - /// - /// * `module_base` - Base address of the module being analyzed. - /// * `runtime_table` - Slice containing the exception directory entries. - /// - /// # Returns - /// - /// Metadata of the first suitable prologue, or `None` if no suitable prologue is found. + /// This scans the RUNTIME_FUNCTION entries for a function that: + /// - Allocates a stack frame. + /// - Has a predictable prologue layout. fn find_prolog(module_base: *mut c_void, runtime_table: &[IMAGE_RUNTIME_FUNCTION]) -> Option { let mut prologs = runtime_table .iter() @@ -400,35 +375,25 @@ impl Prolog { frame, stack_size, offset, - rbp_offset: 0, + ..Default::default() }) }) .collect::>(); - // No prologue found? return None if prologs.is_empty() { return None; } - // Randomizes the order of possible frames found (if there is more than one), - // helps to shuffle patterns and reduce repetition-based heuristics + // Shuffle to reduce pattern predictability. shuffle(&mut prologs); - // Take the first occurrence prologs.first().copied() } - /// Searches for the first function in the exception directory that contains a classic - /// `push rbp` prologue, which is typically associated with frame pointer-based stack frames. + /// Finds a prologue that uses `push rbp` and an RBP-based frame. /// - /// # Arguments - /// - /// * `module_base` - Base address of the loaded module. - /// * `runtime_table` - Slice of entries from the exception directory. - /// - /// # Returns - /// - /// The prologue metadata if a valid `push rbp` function is found, or `None` if no suitable match exists. + /// This is useful when spoofing techniques rely on classic frame-pointer + /// based layouts rather than purely RSP-based stack frames. fn find_push_rbp(module_base: *mut c_void, runtime_table: &[IMAGE_RUNTIME_FUNCTION]) -> Option { let mut prologs = runtime_table .iter() @@ -451,33 +416,26 @@ impl Prolog { }) .collect::>(); - // No prologue found? return None if prologs.is_empty() { return None; } - // The first pop rbp frame is not suitable on most windows versions + // The first frame is often not suitable on many Windows versions. prologs.remove(0); - // Randomizes the order of possible frames found (if there is more than one), - // helps to shuffle patterns and reduce repetition-based heuristics + // Shuffle to reduce pattern predictability. shuffle(&mut prologs); - // Take the first occurrence prologs.first().copied() } } -/// Checks if the `RBP` register is pushed or saved on the stack in a spoofable manner. +/// Determines whether RBP is pushed or saved in a spoof-compatible manner and +/// computes the total stack size for a function. /// -/// # Arguments -/// -/// * `module` - Base address of the loaded module. -/// * `runtime` - A reference to the function's `IMAGE_RUNTIME_FUNCTION`. -/// -/// # Returns -/// -/// Tuple with the RBP offset and the total stack size. +/// This inspects the unwind codes associated with the `IMAGE_RUNTIME_FUNCTION` +/// entry to determine if the function frame uses a layout suitable for +/// call stack spoofing. pub fn rbp_offset(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option<(u32, u32)> { unsafe { let unwind_info = (module as usize + runtime.UnwindData as usize) as *mut UNWIND_INFO; @@ -643,16 +601,9 @@ pub fn rbp_offset(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Opti } } -/// Calculates the stack size of a function and checks if it uses `RBP` as frame pointer. +/// Computes stack frame metadata while rejecting `setfp` frames. /// -/// # Arguments -/// -/// * `module` - Base address of the loaded module. -/// * `runtime` - A reference to the function's `IMAGE_RUNTIME_FUNCTION`. -/// -/// # Returns -/// -/// A flag indicating RBP usage and the total stack size. +/// Used when locating suitable prologues for spoofed call frames. pub fn stack_frame(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option<(bool, u32)> { unsafe { let unwind_info = (module as usize + runtime.UnwindData as usize) as *mut UNWIND_INFO; @@ -805,18 +756,8 @@ pub fn stack_frame(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Opt } } -/// Calculates the total stack frame size of a function, ignoring `setfp` frames. -/// -/// Rejects any function that uses `UWOP_SET_FPREG` or manipulates `RSP` directly. -/// -/// # Arguments -/// -/// * `module` - Base address of the loaded module. -/// * `runtime` - A reference to the function's `IMAGE_RUNTIME_FUNCTION`. -/// -/// # Returns -/// -/// Total stack size in bytes for a spoof‑safe frame. +/// Computes the total stack frame size of a function while ignoring any `setfp` frames. +/// Useful for identifying spoof-compatible RUNTIME_FUNCTION entries. pub fn ignoring_set_fpreg(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option { unsafe { let unwind_info = (module as usize + runtime.UnwindData as usize) as *mut UNWIND_INFO; @@ -952,12 +893,13 @@ pub fn ignoring_set_fpreg(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) } } -/// Trait for casting references to raw `c_void` pointers. +/// Trait for safely converting any reference or mutable reference into a raw +/// pointer usable in spoofing routines. pub trait AsPointer { - /// Returns a raw immutable pointer to `self` as `*const c_void`. + /// Returns a raw immutable pointer to `self`. fn as_ptr_const(&self) -> *const c_void; - /// Returns a raw mutable pointer to `self` as `*mut c_void`. + /// Returns a raw mutable pointer to `self`. fn as_ptr_mut(&mut self) -> *mut c_void; } @@ -973,11 +915,38 @@ impl AsPointer for T { } } -/// Specifies the type of spoof being performed +/// Specifies the spoofing mode used by the engine. pub enum SpoofKind<'a> { - /// Spoofs a call to a regular function pointer. + /// Spoofs a direct function call. Function, - /// Spoofs a native system call using its name. + /// Spoofs a syscall using its name. Syscall(&'a str), } + +#[cfg(test)] +mod tests { + use core::ptr; + use alloc::boxed::Box; + use super::*; + + #[test] + fn test_spoof() -> Result<(), Box> { + let kernel32 = get_module_address("kernel32.dll", None); + let virtual_alloc = get_proc_address(kernel32, "VirtualAlloc", None); + let addr = spoof!(virtual_alloc, ptr::null_mut::(), 1 << 12, 0x3000, 0x04)?; + assert_ne!(addr, ptr::null_mut()); + + Ok(()) + } + + #[test] + fn test_syscall() -> Result<(), Box> { + let mut addr = ptr::null_mut::(); + let mut size = (1 << 12) as usize; + let status = syscall!("NtAllocateVirtualMemory", -1isize, addr.as_ptr_mut(), 0, size.as_ptr_mut(), 0x3000, 0x04)? as i32; + assert_eq!(status, 0); + + Ok(()) + } +} \ No newline at end of file diff --git a/tests/Cargo.toml b/tests/Cargo.toml deleted file mode 100644 index 81fb5a1..0000000 --- a/tests/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "tests" -version = "0.1.0" -edition = "2024" - -[dependencies] -dinvk = "0.3.1" -uwd = { path = "../" } diff --git a/tests/src/main.rs b/tests/src/main.rs deleted file mode 100644 index 05f4010..0000000 --- a/tests/src/main.rs +++ /dev/null @@ -1,33 +0,0 @@ -#[test] -fn test_win_exec() -> Result<(), Box> { - // Resolves addresses of the WinAPI functions to be used - let kernel32 = dinvk::GetModuleHandle("kernel32.dll", None); - let win_exec = dinvk::GetProcAddress(kernel32, "WinExec", None); - - // Execute command with `WinExec` - let cmd = c"calc.exe"; - - // 0:000> bp kernel32!WinExec "k ; g" - for _ in 0..50 { - uwd::spoof!(win_exec, cmd.as_ptr(), 1)?; - } - - Ok(()) -} - -#[test] -fn test_win_exec_synthetic() -> Result<(), Box> { - // Resolves addresses of the WinAPI functions to be used - let kernel32 = dinvk::GetModuleHandle("kernel32.dll", None); - let win_exec = dinvk::GetProcAddress(kernel32, "WinExec", None); - - // Execute command with `WinExec` - let cmd = c"calc.exe"; - - // 0:000> bp kernel32!WinExec "k ; g" - for _ in 0..50 { - uwd::spoof!(win_exec, cmd.as_ptr(), 1)?; - } - - Ok(()) -}