Repository: joaoviictorti/uwd
Branch: main
Commit: 6b39d40843d9
Files: 22
Total size: 86.4 KB
Directory structure:
gitextract_me8cjxhe/
├── .gitattributes
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .vscode/
│ └── settings.json
├── Cargo.toml
├── Justfile
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── build.rs
├── clippy.toml
├── rust-toolchain.toml
├── rustfmt.toml
├── src/
│ ├── asm/
│ │ ├── gnu/
│ │ │ ├── desync.asm
│ │ │ └── synthetic.asm
│ │ └── msvc/
│ │ ├── desync.asm
│ │ └── synthetic.asm
│ ├── lib.rs
│ ├── types.rs
│ ├── util.rs
│ └── uwd.rs
└── taplo.toml
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .github/workflows/ci.yml
================================================
name: build
on: [push, pull_request]
jobs:
clippy:
# Runs Clippy to check for lints in the Rust code
name: Clippy Lint Check
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust
run: |
rustup default stable
rustup component add clippy
- name: Run Clippy
run: cargo clippy -- -D warnings
doc:
# Builds project documentation, including private items
name: Docs Check
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Check docs
run: cargo doc --no-deps --document-private-items
================================================
FILE: .gitignore
================================================
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
examples/Cargo.lock
tests/Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
================================================
FILE: .vscode/settings.json
================================================
{
"rust-analyzer.linkedProjects": [
"Cargo.toml"
],
}
================================================
FILE: Cargo.toml
================================================
[package]
name = "uwd"
version = "0.3.5"
edition = "2024"
description = "Call Stack Spoofing for Rust"
license = "MIT OR Apache-2.0"
repository = "https://github.com/joaoviictorti/uwd"
homepage = "https://github.com/joaoviictorti/uwd"
readme = "README.md"
build = "build.rs"
keywords = ["spoofing", "stack", "windows", "rust", "redteam"]
categories = ["os", "security"]
include = [
"src/**",
"build.rs",
"Cargo.toml",
"README.md",
"LICENSE",
]
[lib]
doctest = false
[dependencies]
bitfield = "0.19.0"
obfstr = "0.4.4"
dinvk = "0.4.2"
anyhow = { version = "1.0.98", default-features = false }
memchr = { version = "2.7.4", default-features = false }
[build-dependencies]
cc = "1.2.19"
nasm-rs = "0.3.0"
[features]
default = []
desync = []
[package.metadata.docs.rs]
default-target = "x86_64-pc-windows-msvc"
targets = ["x86_64-pc-windows-gnu", "x86_64-pc-windows-msvc"]
================================================
FILE: Justfile
================================================
# Aliases
alias c := clean
alias up := update
# Use PowerShell shell on Windows
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
# Clean target
clean:
cargo clean
# Updates dependencies as per Cargo.toml
update:
cargo update
# Publishes the crate to crates.io
publish:
cargo publish --allow-dirty
# Format all .toml files using Taplo
taplo:
taplo format
================================================
FILE: LICENSE-APACHE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: LICENSE-MIT
================================================
MIT License
Copyright (c) 2025 Victor
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# uwd




[](https://github.com/joaoviictorti/uwd/actions)
Rust library for call stack spoofing on Windows, allowing you to execute arbitrary functions with a forged call stack that evades analysis, logging, or detection during stack unwinding.
Inspired by [SilentMoonwalk](https://github.com/klezVirus/SilentMoonwalk), this crate brings low-level spoofing capabilities into a clean, idiomatic Rust interface with full support for `#[no_std]`, `MSVC` and `GNU` toolchains, and automated gadget resolution.
## Features
- ✅ Call stack spoofing via `Synthetic` and `Desync`.
- ✅ Compatible with both `MSVC` and `GNU` toolchains (**x64**).
- ✅ Inline macros: `spoof!` / `syscall!`.
- ✅ Supports `#[no_std]` environments (with `alloc`).
To enable Desync mode, activate the `desync` feature in your project, the macros will automatically use Desync behavior when the feature is enabled.
## Getting started
Add `uwd` to your project by updating your `Cargo.toml`:
```bash
cargo add uwd
```
## Usage
`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.
You can spoof:
* Normal functions (like `VirtualAlloc`, `WinExec`, etc.)
* Native syscalls with automatic SSN and stub resolution (like `NtAllocateVirtualMemory`)
### Spoofing WinExec
This example shows how to spawn `calc.exe` using a spoofed call stack. We call `WinExec` twice once using the Desync technique, and again using the Synthetic one.
```rust
use dinvk::module::{get_module_address, get_proc_address};
use uwd::spoof;
// Resolves addresses of the WinAPI functions to be used
let kernel32 = get_module_address("kernel32.dll", None);
let win_exec = get_proc_address(kernel32, "WinExec", None);
// Execute command with `WinExec`
let cmd = c"calc.exe";
let mut result = spoof!(win_exec, cmd.as_ptr(), 1)?;
if result.is_null() {
eprintln!("WinExec Failed");
return Ok(());
}
```
### Spoofing an Indirect Syscall
This example performs a indirect system call to `NtAllocateVirtualMemory` with a spoofed call stack.
```rust
use std::{ffi::c_void, ptr::null_mut};
use dinvk::winapis::NT_SUCCESS;
use uwd::{syscall, AsPointer};
// Running indirect syscall with Call Stack Spoofing
let mut addr = null_mut::<c_void>();
let mut size = (1 << 12) as usize;
let mut status = syscall!("NtAllocateVirtualMemory", -1isize, addr.as_ptr_mut(), 0, size.as_ptr_mut(), 0x3000, 0x04)? as i32;
if !NT_SUCCESS(status) {
eprintln!("[-] NtAllocateVirtualMemory Failed With Status: {status:#X}");
return Ok(())
}
println!("[+] Address allocated: {:?}", addr);
```
## References
I want to express my gratitude to these projects that inspired me to create `uwd` and contribute with some features:
- [SilentMoonwalk](https://github.com/klezVirus/SilentMoonwalk)
Special thanks to:
- [Kudaes](https://x.com/_Kudaes_)
- [Klez](https://x.com/KlezVirus)
- [Waldo-IRC](https://x.com/waldoirc)
- [Trickster0](https://x.com/trickster012)
- [namazso](https://x.com/namazso)
## License
uwd is licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](https://github.com/joaoviictorti/uwd/tree/main/LICENSE-APACHE) or
<https://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](https://github.com/joaoviictorti/uwd/tree/main/LICENSE-MIT) or <https://opensource.org/licenses/MIT>)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in uwd
by you, as defined in the Apache-2.0 license, shall be dually licensed as above, without any
additional terms or conditions.
================================================
FILE: build.rs
================================================
use std::env;
fn main() {
if env::var("DOCS_RS").is_ok() {
println!("cargo:warning=Skipping ASM build for docs.rs");
return;
}
let target = env::var("TARGET").expect("Missing TARGET environment variable");
let out_dir = env::var("OUT_DIR").expect("Missing OUT_DIR environment variable");
// Supports x86_64 environments only
if !target.contains("x86_64") {
panic!("This build script only supports x86_64 targets.");
}
if target.contains("msvc") {
// Use MASM with cc
cc::Build::new()
.file("src/asm/msvc/desync.asm")
.file("src/asm/msvc/synthetic.asm")
.compile("spoof");
} else if target.contains("gnu") {
// Use NASM with nasm_rs
let sources = ["src/asm/gnu/desync.asm", "src/asm/gnu/synthetic.asm"];
if let Err(e) = nasm_rs::compile_library("spoof", &sources) {
panic!("Failed to compile with NASM [spoof]: {}", e);
}
for source in &sources {
println!("cargo:rerun-if-changed={}", source);
}
println!("cargo:rustc-link-search=native={}", out_dir);
println!("cargo:rustc-link-lib=static=spoof");
} else {
panic!("Unsupported target: {}", target);
}
}
================================================
FILE: clippy.toml
================================================
msrv = "1.89"
================================================
FILE: rust-toolchain.toml
================================================
[toolchain]
channel = "stable"
targets = ["x86_64-pc-windows-msvc"]
================================================
FILE: rustfmt.toml
================================================
# Uses Rustfmt 2024 edition rules, but not all of them are enforced 100%.
edition = "2024"
style_edition = "2024"
================================================
FILE: src/asm/gnu/desync.asm
================================================
;;
;; Code responsible for Call Stack Spoofing Via Desync (NASM)
;;
[BITS 64]
;;
;; Export
;;
GLOBAL Spoof
[SECTION .data]
;;
;; Configuration structure passed to the spoof ASM routine
;;
STRUC Config
.RtlUserThreadStartAddr RESQ 1
.RtlUserThreadStartFrameSize RESQ 1
.BaseThreadInitThunkAddr RESQ 1
.BaseThreadInitThunkFrameSize RESQ 1
.FirstFrame RESQ 1
.SecondFrame RESQ 1
.JmpRbxGadget RESQ 1
.AddRspXGadget RESQ 1
.FirstFrameSize RESQ 1
.SecondFrameSize RESQ 1
.JmpRbxGadgetFrameSize RESQ 1
.AddRspXGadgetFrameSize RESQ 1
.RbpOffset RESQ 1
.SpooFunction RESQ 1
.ReturnAddress RESQ 1
.IsSyscall RESD 1
.Ssn RESD 1
.NArgs RESQ 1
.Arg01 RESQ 1
.Arg02 RESQ 1
.Arg03 RESQ 1
.Arg04 RESQ 1
.Arg05 RESQ 1
.Arg06 RESQ 1
.Arg07 RESQ 1
.Arg08 RESQ 1
.Arg09 RESQ 1
.Arg10 RESQ 1
.Arg11 RESQ 1
ENDSTRUC
[SECTION .text]
;;
;; Function responsible for Call Stack Spoofing
;;
Spoof:
;;
;; Saving non-vol registers
;;
push rbp
push rbx
;;
;; Return main
;;
mov rbp, rsp
;;
;; Creating stack pointer to Restore PROC
;;
lea rax, [rel Restore]
push rax
lea rbx, [rsp]
;;
;; First Frame (Fake origin)
;;
push QWORD [rcx + Config.FirstFrame]
mov rax, [rcx + Config.ReturnAddress]
sub rax, [rcx + Config.FirstFrameSize]
sub rsp, [rcx + Config.SecondFrameSize]
mov r10, [rcx + Config.RbpOffset]
mov [rsp + r10], rax
;;
;; ROP Frames
;;
push QWORD [rcx + Config.SecondFrame]
;;
;; JMP [RBX] Gadget / Stack Pivot (To restore original Control Flow Stack)
;;
sub rsp, [rcx + Config.JmpRbxGadgetFrameSize]
push QWORD [rcx + Config.JmpRbxGadget]
sub rsp, [rcx + Config.AddRspXGadgetFrameSize]
push QWORD [rcx + Config.AddRspXGadget]
;;
;; Set the pointer to the function to call in R11
;;
mov r11, [rcx + Config.SpooFunction]
jmp Parameters
;;
;; Set the parameters to pass to the target function
;;
Parameters:
mov r12, rcx
mov rax, [r12 + Config.NArgs]
; Arg01 (rcx)
cmp rax, 1
jb skip_1
mov rcx, [r12 + Config.Arg01]
skip_1:
; Arg02 (rdx)
cmp rax, 2
jb skip_2
mov rdx, [r12 + Config.Arg02]
skip_2:
; Arg03 (r8)
cmp rax, 3
jb skip_3
mov r8, [r12 + Config.Arg03]
skip_3:
; Arg04 (r9)
cmp rax, 4
jb skip_4
mov r9, [r12 + Config.Arg04]
skip_4:
; Stack-based args
lea r13, [rsp]
cmp rax, 5
jb skip_5
mov r10, [r12 + Config.Arg05]
mov [r13 + 28h], r10
skip_5:
; Arg06
cmp rax, 6
jb skip_6
mov r10, [r12 + Config.Arg06]
mov [r13 + 30h], r10
skip_6:
; Arg07
cmp rax, 7
jb skip_7
mov r10, [r12 + Config.Arg07]
mov [r13 + 38h], r10
skip_7:
; Arg08
cmp rax, 8
jb skip_8
mov r10, [r12 + Config.Arg08]
mov [r13 + 40h], r10
skip_8:
; Arg09
cmp rax, 9
jb skip_9
mov r10, [r12 + Config.Arg09]
mov [r13 + 48h], r10
skip_9:
; Arg10
cmp rax, 10
jb skip_10
mov r10, [r12 + Config.Arg10]
mov [r13 + 50h], r10
skip_10:
; Arg11
cmp rax, 11
jb skip_11
mov r10, [r12 + Config.Arg11]
mov [r13 + 58h], r10
skip_11:
cmp BYTE [r12 + Config.IsSyscall], 1
je ExecuteSyscall
jmp Execute
;;
;; Restores the original stack frame
;;
Restore:
mov rsp, rbp
pop rbx
pop rbp
ret
;;
;; Executes the target function
;;
Execute:
jmp r11
;;
;; Executes a native Windows system call using the spoofed context
;;
ExecuteSyscall:
mov r10, rcx
mov eax, DWORD [r12 + Config.Ssn]
jmp r11
================================================
FILE: src/asm/gnu/synthetic.asm
================================================
;;
;; Code responsible for Call Stack Spoofing Via Synthetic (NASM)
;;
[BITS 64]
;;
;; Export
;;
GLOBAL SpoofSynthetic
[SECTION .data]
;;
;; Configuration structure passed to the spoof ASM routine
;;
STRUC Config
.RtlUserThreadStartAddr RESQ 1
.RtlUserThreadStartFrameSize RESQ 1
.BaseThreadInitThunkAddr RESQ 1
.BaseThreadInitThunkFrameSize RESQ 1
.FirstFrame RESQ 1
.SecondFrame RESQ 1
.JmpRbxGadget RESQ 1
.AddRspXGadget RESQ 1
.FirstFrameSize RESQ 1
.SecondFrameSize RESQ 1
.JmpRbxGadgetFrameSize RESQ 1
.AddRspXGadgetFrameSize RESQ 1
.RbpOffset RESQ 1
.SpooFunction RESQ 1
.ReturnAddress RESQ 1
.IsSyscall RESD 1
.Ssn RESD 1
.NArgs RESQ 1
.Arg01 RESQ 1
.Arg02 RESQ 1
.Arg03 RESQ 1
.Arg04 RESQ 1
.Arg05 RESQ 1
.Arg06 RESQ 1
.Arg07 RESQ 1
.Arg08 RESQ 1
.Arg09 RESQ 1
.Arg10 RESQ 1
.Arg11 RESQ 1
ENDSTRUC
[SECTION .text]
;;
;; Function responsible for Call Stack Spoofing
;;
SpoofSynthetic:
;;
;; Saving non-vol registers
;;
push QWORD rbp
push QWORD rbx
push QWORD r15
;;
;; Everything between RSP and RBP is our new stack frame for unwinding
;;
sub rsp, 210h
mov rbp, rsp
;;
;; Creating stack pointer to Restore PROC
;;
lea rax, [rel RestoreSynthetic]
push rax
lea rbx, [rsp]
;;
;; Cutting the call stack. The 0 pushed in this position will be the return address
;; of the next frame "RtlUserThreadStart", making it effectively the originating function
;;
xor rax, rax
push rax
;;
;; RtlUserThreadStart
;;
sub rsp, [rcx + Config.RtlUserThreadStartFrameSize]
push QWORD [rcx + Config.RtlUserThreadStartAddr]
add QWORD [rsp], 21h
;;
;; BaseThreadInitThunk
;;
sub rsp, [rcx + Config.BaseThreadInitThunkFrameSize]
push QWORD [rcx + Config.BaseThreadInitThunkAddr]
add QWORD [rsp], 14h
;;
;; Return Adress
;;
mov rax, rsp
;;
;; First Frame (Fake origin)
;;
push QWORD [rcx + Config.FirstFrame]
sub rax, [rcx + Config.FirstFrameSize]
sub rsp, [rcx + Config.SecondFrameSize]
mov r10, [rcx + Config.RbpOffset]
mov [rsp + r10], rax
;;
;; ROP Frames
;;
push QWORD [rcx + Config.SecondFrame]
;;
;; JMP [RBX] Gadget / Stack Pivot (To restore original Control Flow Stack)
;;
sub rsp, [rcx + Config.JmpRbxGadgetFrameSize]
push QWORD [rcx + Config.JmpRbxGadget]
sub rsp, [rcx + Config.AddRspXGadgetFrameSize]
push QWORD [rcx + Config.AddRspXGadget]
;;
;; Set the pointer to the function to call in R11
;;
mov r11, [rcx + Config.SpooFunction]
jmp ParametersSynthetic
;;
;; Set the parameters to pass to the target function
;;
ParametersSynthetic:
mov r12, rcx
mov rax, [r12 + Config.NArgs]
; Arg01 (rcx)
cmp rax, 1
jb skip_1
mov rcx, [r12 + Config.Arg01]
skip_1:
; Arg02 (rdx)
cmp rax, 2
jb skip_2
mov rdx, [r12 + Config.Arg02]
skip_2:
; Arg03 (r8)
cmp rax, 3
jb skip_3
mov r8, [r12 + Config.Arg03]
skip_3:
; Arg04 (r9)
cmp rax, 4
jb skip_4
mov r9, [r12 + Config.Arg04]
skip_4:
; Stack-based args
lea r13, [rsp]
cmp rax, 5
jb skip_5
mov r10, [r12 + Config.Arg05]
mov [r13 + 28h], r10
skip_5:
; Arg06
cmp rax, 6
jb skip_6
mov r10, [r12 + Config.Arg06]
mov [r13 + 30h], r10
skip_6:
; Arg07
cmp rax, 7
jb skip_7
mov r10, [r12 + Config.Arg07]
mov [r13 + 38h], r10
skip_7:
; Arg08
cmp rax, 8
jb skip_8
mov r10, [r12 + Config.Arg08]
mov [r13 + 40h], r10
skip_8:
; Arg09
cmp rax, 9
jb skip_9
mov r10, [r12 + Config.Arg09]
mov [r13 + 48h], r10
skip_9:
; Arg10
cmp rax, 10
jb skip_10
mov r10, [r12 + Config.Arg10]
mov [r13 + 50h], r10
skip_10:
; Arg11
cmp rax, 11
jb skip_11
mov r10, [r12 + Config.Arg11]
mov [r13 + 58h], r10
skip_11:
cmp BYTE [r12 + Config.IsSyscall], 1
je ExecuteSyscallSynthetic
jmp ExecuteSynthetic
;;
;; Restores the original stack frame
;;
RestoreSynthetic:
mov rsp, rbp
add QWORD rsp, 210h
pop QWORD r15
pop QWORD rbx
pop QWORD rbp
ret
;;
;; Executes the target function
;;
ExecuteSynthetic:
jmp r11
;;
;; Executes a native Windows system call using the spoofed context
;;
ExecuteSyscallSynthetic:
mov r10, rcx
mov eax, DWORD [r12 + Config.Ssn]
jmp r11
================================================
FILE: src/asm/msvc/desync.asm
================================================
;;
;; Code responsible for Call Stack Spoofing Via Desync (MASM)
;;
;;
;; Export
;;
Spoof proto
.data
;;
;; Configuration structure passed to the spoof ASM routine
;;
Config STRUCT
RtlUserThreadStartAddr DQ 1
RtlUserThreadStartFrameSize DQ 1
BaseThreadInitThunkAddr DQ 1
BaseThreadInitThunkFrameSize DQ 1
FirstFrame DQ 1
SecondFrame DQ 1
JmpRbxGadget DQ 1
AddRspXGadget DQ 1
FirstFrameSize DQ 1
SecondFrameSize DQ 1
JmpRbxGadgetFrameSize DQ 1
AddRspXGadgetFrameSize DQ 1
RbpOffset DQ 1
SpooFunction DQ 1
ReturnAddress DQ 1
IsSyscall DD 0
Ssn DD 0
NArgs DQ 1
Arg01 DQ 1
Arg02 DQ 1
Arg03 DQ 1
Arg04 DQ 1
Arg05 DQ 1
Arg06 DQ 1
Arg07 DQ 1
Arg08 DQ 1
Arg09 DQ 1
Arg10 DQ 1
Arg11 DQ 1
Config ENDS
.code
;;
;; Function responsible for Call Stack Spoofing
;;
Spoof PROC
;;
;; Saving non-vol registers
;;
push rbp
push rbx
;;
;; Return main
;;
mov rbp, rsp
;;
;; Creating stack pointer to Restore PROC
;;
lea rax, Restore
push rax
lea rbx, [rsp]
;;
;; First Frame (Fake origin)
;;
push [rcx].Config.FirstFrame
mov rax, [rcx].Config.ReturnAddresS
sub rax, [rcx].Config.FirstFrameSize
sub rsp, [rcx].Config.SecondFrameSize
mov r10, [rcx].Config.RbpOffset
mov [rsp + r10], rax
;;
;; ROP Frames
;;
push [rcx].Config.SecondFrame
;;
;; JMP [RBX] Gadget / Stack Pivot (To restore original Control Flow Stack)
;;
sub rsp, [rcx].Config.JmpRbxGadgetFrameSize
push [rcx].Config.JmpRbxGadget
sub rsp, [rcx].Config.AddRspXGadgetFrameSize
push [rcx].Config.AddRspXGadget
;;
;; Set the pointer to the function to call in R11
;;
mov r11, [rcx].Config.SpooFunction
jmp Parameters
Spoof ENDP
;;
;; Set the parameters to pass to the target function
;;
Parameters PROC
mov r12, rcx
mov rax, [r12].Config.NArgs
; Arg01 (rcx)
cmp rax, 1
jb skip_1
mov rcx, [r12].Config.Arg01
skip_1:
; Arg02 (rdx)
cmp rax, 2
jb skip_2
mov rdx, [r12].Config.Arg02
skip_2:
; Arg03 (r8)
cmp rax, 3
jb skip_3
mov r8, [r12].Config.Arg03
skip_3:
; Arg04 (r9)
cmp rax, 4
jb skip_4
mov r9, [r12].Config.Arg04
skip_4:
; Stack-based args
lea r13, [rsp]
cmp rax, 5
jb skip_5
mov r10, [r12].Config.Arg05
mov [r13 + 28h], r10
skip_5:
; Arg06
cmp rax, 6
jb skip_6
mov r10, [r12].Config.Arg06
mov [r13 + 30h], r10
skip_6:
; Arg07
cmp rax, 7
jb skip_7
mov r10, [r12].Config.Arg07
mov [r13 + 38h], r10
skip_7:
; Arg08
cmp rax, 8
jb skip_8
mov r10, [r12].Config.Arg08
mov [r13 + 40h], r10
skip_8:
; Arg09
cmp rax, 9
jb skip_9
mov r10, [r12].Config.Arg09
mov [r13 + 48h], r10
skip_9:
; Arg10
cmp rax, 10
jb skip_10
mov r10, [r12].Config.Arg10
mov [r13 + 50h], r10
skip_10:
; Arg11
cmp rax, 11
jb skip_11
mov r10, [r12].Config.Arg11
mov [r13 + 58h], r10
skip_11:
cmp [r12].Config.IsSyscall, 1
je ExecuteSyscall
jmp Execute
Parameters ENDP
;;
;; Restores the original stack frame
;;
Restore PROC
mov rsp, rbp
pop rbx
pop rbp
ret
Restore ENDP
;;
;; Executes the target function
;;
Execute PROC
jmp QWORD PTR r11
Execute ENDP
;;
;; Executes a native Windows system call using the spoofed context
;;
ExecuteSyscall PROC
mov r10, rcx
mov eax, [r12].Config.Ssn
jmp QWORD PTR r11
ExecuteSyscall ENDP
END
================================================
FILE: src/asm/msvc/synthetic.asm
================================================
;;
;; Code responsible for Call Stack Spoofing Via Synthetic (MASM)
;;
;;
;; Export
;;
SpoofSynthetic proto
.data
;;
;; Configuration structure passed to the spoof ASM routine
;;
Config STRUCT
RtlUserThreadStartAddr DQ 1
RtlUserThreadStartFrameSize DQ 1
BaseThreadInitThunkAddr DQ 1
BaseThreadInitThunkFrameSize DQ 1
FirstFrame DQ 1
SecondFrame DQ 1
JmpRbxGadget DQ 1
AddRspXGadget DQ 1
FirstFrameSize DQ 1
SecondFrameSize DQ 1
JmpRbxGadgetFrameSize DQ 1
AddRspXGadgetFrameSize DQ 1
RbpOffset DQ 1
SpooFunction DQ 1
ReturnAddress DQ 1
IsSyscall DD 0
Ssn DD 0
NArgs DQ 1
Arg01 DQ 1
Arg02 DQ 1
Arg03 DQ 1
Arg04 DQ 1
Arg05 DQ 1
Arg06 DQ 1
Arg07 DQ 1
Arg08 DQ 1
Arg09 DQ 1
Arg10 DQ 1
Arg11 DQ 1
Config ENDS
.code
;;
;; Function responsible for Call Stack Spoofing
;;
SpoofSynthetic PROC
;;
;; Saving non-vol registers
;;
push rbp
push rbx
push r15
;;
;; Everything between RSP and RBP is our new stack frame for unwinding
;;
sub rsp, 210h
mov rbp, rsp
;;
;; Creating stack pointer to Restore PROC
;;
lea rax, RestoreSynthetic
push rax
lea rbx, [rsp]
;;
;; Cutting the call stack. The 0 pushed in this position will be the return address
;; of the next frame "RtlUserThreadStart", making it effectively the originating function
;;
xor rax, rax
push rax
;;
;; RtlUserThreadStart
;;
sub rsp, [rcx].Config.RtlUserThreadStartFrameSize
push [rcx].Config.RtlUserThreadStartAddr
add QWORD PTR [rsp], 21h
;;
;; BaseThreadInitThunk
;;
sub rsp, [rcx].Config.BaseThreadInitThunkFrameSize
push [rcx].Config.BaseThreadInitThunkAddr
add QWORD PTR [rsp], 14h
;;
;; Return address
;;
mov rax, rsp
;;
;; First Frame (Fake origin)
;;
push [rcx].Config.FirstFrame
sub rax, [rcx].Config.FirstFrameSize
sub rsp, [rcx].Config.SecondFrameSize
mov r10, [rcx].Config.RbpOffset
mov [rsp + r10], rax
;;
;; ROP Frames
;;
push [rcx].Config.SecondFrame
;;
;; JMP [RBX] Gadget / Stack Pivot (To restore original Control Flow Stack)
;;
sub rsp, [rcx].Config.JmpRbxGadgetFrameSize
push [rcx].Config.JmpRbxGadget
sub rsp, [rcx].Config.AddRspXGadgetFrameSize
push [rcx].Config.AddRspXGadget
;;
;; Set the pointer to the function to call in R11
;;
mov r11, [rcx].Config.SpooFunction
jmp ParametersSynthetic
SpoofSynthetic ENDP
;;
;; Set the parameters to pass to the target function
;;
ParametersSynthetic PROC
mov r12, rcx
mov rax, [r12].Config.NArgs
; Arg01 (rcx)
cmp rax, 1
jb skip_1
mov rcx, [r12].Config.Arg01
skip_1:
; Arg02 (rdx)
cmp rax, 2
jb skip_2
mov rdx, [r12].Config.Arg02
skip_2:
; Arg03 (r8)
cmp rax, 3
jb skip_3
mov r8, [r12].Config.Arg03
skip_3:
; Arg04 (r9)
cmp rax, 4
jb skip_4
mov r9, [r12].Config.Arg04
skip_4:
; Stack-based args
lea r13, [rsp]
cmp rax, 5
jb skip_5
mov r10, [r12].Config.Arg05
mov [r13 + 28h], r10
skip_5:
; Arg06
cmp rax, 6
jb skip_6
mov r10, [r12].Config.Arg06
mov [r13 + 30h], r10
skip_6:
; Arg07
cmp rax, 7
jb skip_7
mov r10, [r12].Config.Arg07
mov [r13 + 38h], r10
skip_7:
; Arg08
cmp rax, 8
jb skip_8
mov r10, [r12].Config.Arg08
mov [r13 + 40h], r10
skip_8:
; Arg09
cmp rax, 9
jb skip_9
mov r10, [r12].Config.Arg09
mov [r13 + 48h], r10
skip_9:
; Arg10
cmp rax, 10
jb skip_10
mov r10, [r12].Config.Arg10
mov [r13 + 50h], r10
skip_10:
; Arg11
cmp rax, 11
jb skip_11
mov r10, [r12].Config.Arg11
mov [r13 + 58h], r10
skip_11:
cmp [r12].Config.IsSyscall, 1
je ExecuteSyscallSynthetic
jmp ExecuteSynthetic
ParametersSynthetic ENDP
;;
;; Restores the original stack frame
;;
RestoreSynthetic PROC
mov rsp, rbp
add rsp, 210h
pop r15
pop rbx
pop rbp
ret
RestoreSynthetic ENDP
;;
;; Executes the target function
;;
ExecuteSynthetic PROC
jmp QWORD PTR r11
ExecuteSynthetic ENDP
;;
;; Executes a native Windows system call using the spoofed context
;;
ExecuteSyscallSynthetic PROC
mov r10, rcx
mov eax, [r12].Config.Ssn
jmp QWORD PTR r11
ExecuteSyscallSynthetic ENDP
END
================================================
FILE: src/lib.rs
================================================
#![no_std]
#![doc = include_str!("../README.md")]
extern crate alloc;
mod types;
mod uwd;
mod util;
pub use uwd::*;
================================================
FILE: src/types.rs
================================================
#![allow(non_snake_case, non_camel_case_types)]
use core::{ffi::c_void, slice::from_raw_parts};
use dinvk::{types::*, helper::PE};
/// Indicates the presence of an exception handler in the function.
pub const UNW_FLAG_EHANDLER: u8 = 0x1;
/// Indicates chained unwind information is present.
pub const UNW_FLAG_CHAININFO: u8 = 0x4;
/// Provides access to the unwind (exception handling) information of a PE image.
#[derive(Debug)]
pub struct Unwind {
/// Reference to the parsed PE image.
pub pe: PE,
}
impl Unwind {
/// Creates a new [`Unwind`].
pub fn new(pe: PE) -> Self {
Unwind { pe }
}
/// Returns all runtime function entries.
pub fn entries(&self) -> Option<&[IMAGE_RUNTIME_FUNCTION]> {
let nt = self.pe.nt_header()?;
let dir = unsafe {
(*nt).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION]
};
if dir.VirtualAddress == 0 || dir.Size == 0 {
return None;
}
let addr = (self.pe.base as usize + dir.VirtualAddress as usize) as *const IMAGE_RUNTIME_FUNCTION;
let len = dir.Size as usize / size_of::<IMAGE_RUNTIME_FUNCTION>();
Some(unsafe { from_raw_parts(addr, len) })
}
/// Finds a runtime function by its RVA.
pub fn function_by_offset(&self, offset: u32) -> Option<&IMAGE_RUNTIME_FUNCTION> {
self.entries()?.iter().find(|f| f.BeginAddress == offset)
}
/// Gets the size in bytes of a function using the unwind table.
#[cfg(feature = "desync")]
pub fn function_size(&self, func: *mut c_void) -> Option<u64> {
let offset = (func as usize - self.pe.base as usize) as u32;
let entry = self.function_by_offset(offset)?;
let start = self.pe.base as u64 + entry.BeginAddress as u64;
let end = self.pe.base as u64 + entry.EndAddress as u64;
Some(end - start)
}
}
/// Configuration structure passed to the spoof ASM routine.
#[repr(C)]
#[derive(Debug)]
pub struct Config {
/// Address RtlUserThreadStart
pub rtl_user_addr: *const c_void,
/// Stack Size RtlUserThreadStart
pub rtl_user_thread_size: u64,
/// Address BaseThreadInitThunk
pub base_thread_addr: *const c_void,
/// Stack Size BaseThreadInitThunk
pub base_thread_size: u64,
/// First (fake) return address frame
pub first_frame_fp: *const c_void,
/// Second (ROP) return address frame
pub second_frame_fp: *const c_void,
/// Gadget: `jmp [rbx]`
pub jmp_rbx_gadget: *const c_void,
/// Gadget: `add rsp, X; ret`
pub add_rsp_gadget: *const c_void,
/// Stack size of first spoofed frame
pub first_frame_size: u64,
/// Stack size of second spoofed frame
pub second_frame_size: u64,
/// Stack frame size where the `jmp [rbx]` gadget resides
pub jmp_rbx_frame_size: u64,
/// Stack frame size where the `add rsp, X` gadget resides
pub add_rsp_frame_size: u64,
/// Offset on the stack where `rbp` is pushed
pub rbp_stack_offset: u64,
/// The function to be spoofed / called
pub spoof_function: *const c_void,
/// Return address (used as stack-resume point after call)
pub return_address: *const c_void,
/// Checks if the target is a syscall
pub is_syscall: u32,
/// System Service Number (SSN)
pub ssn: u32,
/// Arguments that will be passed to the function that will be spoofed
pub number_args: u64,
pub arg01: *const c_void,
pub arg02: *const c_void,
pub arg03: *const c_void,
pub arg04: *const c_void,
pub arg05: *const c_void,
pub arg06: *const c_void,
pub arg07: *const c_void,
pub arg08: *const c_void,
pub arg09: *const c_void,
pub arg10: *const c_void,
pub arg11: *const c_void,
}
impl Default for Config {
fn default() -> Self {
unsafe { core::mem::zeroed() }
}
}
/// Enumeration of x86_64 general-purpose registers.
///
/// Used in unwind parsing or register mapping logic.
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
#[allow(dead_code)]
pub enum Registers {
Rax = 0,
Rcx,
Rdx,
Rbx,
Rsp,
Rbp,
Rsi,
Rdi,
R8,
R9,
R10,
R11,
R12,
R13,
R14,
R15,
}
impl PartialEq<usize> for Registers {
fn eq(&self, other: &usize) -> bool {
*self as usize == *other
}
}
/// Union representing a single unwind operation code.
#[repr(C)]
pub union UNWIND_CODE {
/// Offset into the stack frame for the operation.
pub FrameOffset: u16,
/// Structured fields of the unwind code.
pub Anonymous: UNWIND_CODE_0,
}
bitfield::bitfield! {
/// Bitfield representation of an unwind code entry.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct UNWIND_CODE_0(u16);
/// Byte offset from the start of the prologue where this operation is applied.
pub u8, CodeOffset, SetCodeOffset: 7, 0;
/// The unwind operation code.
pub u8, UnwindOp, SetUnwindOp: 11, 8;
/// Additional operation-specific information.
pub u8, OpInfo, SetOpInfo: 15, 12;
}
/// Union representing optional exception handler or chained function entry.
#[repr(C)]
pub union UNWIND_INFO_0 {
/// Address of the exception handler (RVA).
pub ExceptionHandler: u32,
/// Address of a chained function entry.
pub FunctionEntry: u32,
}
bitfield::bitfield! {
/// Combines the `Version` and `Flags` fields in a compact format.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct UNWIND_VERSION_FLAGS(u8);
/// Unwind info format version.
pub u8, Version, SetVersion: 2, 0;
/// Unwind flags.
pub u8, Flags, SetFlags: 7, 3;
}
bitfield::bitfield! {
/// Compact representation of frame register and offset fields.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct UNWIND_FRAME_INFO(u8);
/// The register used as the frame pointer.
pub u8, FrameRegister, SetFrameRegister: 3, 0;
/// Offset from the stack pointer to the frame pointer.
pub u8, FrameOffset, SetFrameOffset: 7, 4;
}
/// Structure containing the unwind information of a function.
#[repr(C)]
pub struct UNWIND_INFO {
/// Separate structure containing `Version` and `Flags`.
pub VersionFlags: UNWIND_VERSION_FLAGS,
/// Size of the function prologue in bytes.
pub SizeOfProlog: u8,
/// Number of non-array `UnwindCode` entries.
pub CountOfCodes: u8,
/// Separate structure containing `FrameRegister` and `FrameOffset`.
pub FrameInfo: UNWIND_FRAME_INFO,
/// Array of unwind codes describing specific operations.
pub UnwindCode: UNWIND_CODE,
/// Union containing `ExceptionHandler` or `FunctionEntry`.
pub Anonymous: UNWIND_INFO_0,
/// Optional exception data.
pub ExceptionData: u32,
}
/// Unwind operation codes used by the Windows x64 exception handling model.
///
/// For full details, refer to Microsoft documentation:
/// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64
#[repr(u8)]
#[allow(dead_code)]
pub enum UNWIND_OP_CODES {
UWOP_PUSH_NONVOL = 0,
UWOP_ALLOC_LARGE = 1,
UWOP_ALLOC_SMALL = 2,
UWOP_SET_FPREG = 3,
UWOP_SAVE_NONVOL = 4,
UWOP_SAVE_NONVOL_BIG = 5,
UWOP_EPILOG = 6,
UWOP_SPARE_CODE = 7,
UWOP_SAVE_XMM128 = 8,
UWOP_SAVE_XMM128BIG = 9,
UWOP_PUSH_MACH_FRAME = 10,
}
impl TryFrom<u8> for UNWIND_OP_CODES {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0..=10 => Ok(unsafe { core::mem::transmute::<u8, UNWIND_OP_CODES>(value) }),
_ => Err(()),
}
}
}
================================================
FILE: src/util.rs
================================================
use core::{ffi::c_void, slice::from_raw_parts};
use alloc::vec::Vec;
use obfstr::obfbytes as b;
use dinvk::types::IMAGE_RUNTIME_FUNCTION;
use crate::ignoring_set_fpreg;
/// Searches for a valid instruction offset in a function.
///
/// This scans the function's code region for a `call qword ptr [rip+0]`
/// instruction sequence and returns the offset *after* the instruction.
///
/// # Notes
///
/// The searched gadget pattern is `48 FF 15 00 00 00 00`, and the
/// returned value is `match_offset + 7`.
pub fn find_valid_instruction_offset(
module: *mut c_void,
runtime: &IMAGE_RUNTIME_FUNCTION,
) -> Option<u32> {
let start = module as u64 + runtime.BeginAddress as u64;
let end = module as u64 + runtime.EndAddress as u64;
let size = end - start;
// Find a gadget `call qword ptr [rip+0]`
let pattern = b!(&[0x48, 0xFF, 0x15]);
unsafe {
let bytes = from_raw_parts(start as *const u8, size as usize);
if let Some(pos) = memchr::memmem::find(bytes, pattern) {
// Returns valid RVA: offset of the gadget inside the function
return Some((pos + 7) as u32);
}
}
None
}
/// Scans the code of a module for a given byte pattern, restricted to valid
/// RUNTIME_FUNCTION regions.
pub fn find_gadget(
module: *mut c_void,
pattern: &[u8],
runtime_table: &[IMAGE_RUNTIME_FUNCTION]
) -> Option<(*mut u8, u32)> {
unsafe {
let mut gadgets = runtime_table
.iter()
.filter_map(|runtime| {
let start = module as u64 + runtime.BeginAddress as u64;
let end = module as u64 + runtime.EndAddress as u64;
let size = end.saturating_sub(start);
// Read bytes from the function's code region
let bytes = from_raw_parts(start as *const u8, size as usize);
let pos = memchr::memmem::find(bytes, pattern)?;
let addr = (start as *mut u8).wrapping_add(pos);
let frame_size = ignoring_set_fpreg(module, runtime)?;
if frame_size == 0 {
return None;
}
Some((addr, frame_size))
})
.collect::<Vec<(*mut u8, u32)>>();
if gadgets.is_empty() {
return None;
}
// Shuffle to reduce pattern predictability.
shuffle(&mut gadgets);
gadgets.first().copied()
}
}
/// Scans the current thread's stack to locate the return address that falls within
/// the range of the `BaseThreadInitThunk` function from `kernel32.dll`.
#[cfg(feature = "desync")]
pub fn find_base_thread_return_address() -> Option<usize> {
use dinvk::module::{get_module_address, get_proc_address};
use dinvk::{hash::{jenkins3, murmur3}, helper::PE};
use crate::types::Unwind;
unsafe {
// Get handle for kernel32.dll
let kernel32 = get_module_address(2808682670u32, Some(murmur3));
if kernel32.is_null() {
return None;
}
// Resolves the address of the BaseThreadInitThunk function
let base_thread = get_proc_address(kernel32, 4073232152u32, Some(jenkins3));
if base_thread.is_null() {
return None;
}
// Calculate the size of the BaseThreadInitThunk function
let pe_kernel32 = Unwind::new(PE::parse(kernel32));
let size = pe_kernel32.function_size(base_thread)? as usize;
// Access the TEB and stack limits
let teb = dinvk::winapis::NtCurrentTeb();
let stack_base = (*teb).Reserved1[1] as usize;
let stack_limit = (*teb).Reserved1[2] as usize;
// Stack scanning begins
let base_addr = base_thread as usize;
let mut rsp = stack_base - 8;
while rsp >= stack_limit {
let val = (rsp as *const usize).read();
// Checks if the return is in the BaseThreadInitThunk range
if val >= base_addr && val < base_addr + size {
return Some(rsp);
}
rsp -= 8;
}
None
}
}
/// Randomly shuffles the elements of a list in place.
pub fn shuffle<T>(list: &mut [T]) {
let mut seed = unsafe { core::arch::x86_64::_rdtsc() };
for i in (1..list.len()).rev() {
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
let j = seed as usize % (i + 1);
list.swap(i, j);
}
}
================================================
FILE: src/uwd.rs
================================================
use alloc::{string::String, vec::Vec};
use core::ffi::c_void;
use anyhow::{Context, Result, bail};
use obfstr::obfstring as s;
use dinvk::module::{get_module_address, get_proc_address};
use dinvk::types::IMAGE_RUNTIME_FUNCTION;
use dinvk::hash::murmur3;
use dinvk::helper::PE;
#[cfg(feature = "desync")]
use crate::util::find_base_thread_return_address;
use crate::util::{find_gadget, shuffle, find_valid_instruction_offset};
use crate::types::{Config, Registers, UNWIND_OP_CODES::{self, *}};
use crate::types::{UNW_FLAG_CHAININFO, UNW_FLAG_EHANDLER};
use crate::types::{UNWIND_CODE, UNWIND_INFO};
use crate::types::Unwind;
#[cfg(feature = "desync")]
unsafe extern "C" {
/// Function responsible for Call Stack Spoofing (Desync)
fn Spoof(config: &mut Config) -> *mut c_void;
}
#[cfg(not(feature = "desync"))]
unsafe extern "C" {
/// Function responsible for Call Stack Spoofing (Synthetic)
fn SpoofSynthetic(config: &mut Config) -> *mut c_void;
}
/// Invokes a function using a synthetic spoofed call stack.
///
/// # Examples
///
/// ```
/// use core::ptr;
/// use dinvk::module::{get_module_address, get_proc_address};
/// use uwd::spoof;
///
/// let kernel32 = get_module_address("kernel32.dll", None);
/// let virtual_alloc = get_proc_address(kernel32, "VirtualAlloc", None);
///
/// let addr = spoof!(
/// virtual_alloc,
/// ptr::null_mut::<core::ffi::c_void>(),
/// 1 << 12,
/// 0x3000,
/// 0x04
/// ).unwrap();
///
/// assert!(!addr.is_null());
/// ```
#[macro_export]
macro_rules! spoof {
($addr:expr, $($arg:expr),+ $(,)?) => {
unsafe {
$crate::__private::spoof(
$addr,
&[$(::core::mem::transmute($arg as usize)),*],
$crate::SpoofKind::Function,
)
}
};
}
/// Invokes a Windows native syscall using a spoofed stack.
///
/// # Examples
///
/// ```
/// use core::ptr;
/// use uwd::{AsPointer, syscall};
///
/// let mut addr = ptr::null_mut::<core::ffi::c_void>();
/// let mut size = (1 << 12) as usize;
///
/// let status = syscall!(
/// "NtAllocateVirtualMemory",
/// -1isize,
/// addr.as_ptr_mut(),
/// 0,
/// size.as_ptr_mut(),
/// 0x3000,
/// 0x04
/// ).unwrap() as i32;
///
/// assert_eq!(status, 0);
/// ```
#[macro_export]
macro_rules! syscall {
($name:expr, $($arg:expr),* $(,)?) => {
unsafe {
$crate::__private::spoof(
core::ptr::null_mut(),
&[$(::core::mem::transmute($arg as usize)),*],
$crate::SpoofKind::Syscall($name),
)
}
};
}
#[doc(hidden)]
pub mod __private {
use core::ffi::c_void;
use super::*;
/// Performs call stack spoofing in `synthetic` mode.
#[cfg(not(feature = "desync"))]
pub fn spoof(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind) -> Result<*mut c_void> {
// Max 11 args
if args.len() > 11 {
bail!(s!("too many arguments"));
}
// Function pointer must be valid unless syscall spoof
if let SpoofKind::Function = kind && addr.is_null() {
bail!(s!("null function address"));
}
let mut config = Config::default();
// Resolve kernelbase
let kernelbase = get_module_address(2737729883u32, Some(murmur3));
// Parse unwind table
let pe_kernelbase = Unwind::new(PE::parse(kernelbase));
let tables = pe_kernelbase
.entries()
.context(s!(
"failed to read IMAGE_RUNTIME_FUNCTION entries from .pdata section"
))?;
// Resolve APIs
let ntdll = get_module_address(2788516083u32, Some(murmur3));
if ntdll.is_null() {
bail!(s!("ntdll.dll not found"));
}
let kernel32 = get_module_address(2808682670u32, Some(murmur3));
let rlt_user_addr = get_proc_address(ntdll, 1578834099u32, Some(murmur3));
let base_thread_addr = get_proc_address(kernel32, 4083630997u32, Some(murmur3));
config.rtl_user_addr = rlt_user_addr;
config.base_thread_addr = base_thread_addr;
// Unwind lookup
let pe_ntdll = Unwind::new(PE::parse(ntdll));
let rtl_user_runtime = pe_ntdll
.function_by_offset(rlt_user_addr as u32 - ntdll as u32)
.context(s!("RtlUserThreadStart unwind info not found"))?;
let pe_kernel32 = Unwind::new(PE::parse(kernel32));
let base_thread_runtime = pe_kernel32
.function_by_offset(base_thread_addr as u32 - kernel32 as u32)
.context(s!("BaseThreadInitThunk unwind info not found"))?;
// Stack sizes
let rtl_user_size = ignoring_set_fpreg(ntdll, rtl_user_runtime)
.context(s!("RtlUserThreadStart stack size not found"))?;
let base_thread_size = ignoring_set_fpreg(kernel32, base_thread_runtime)
.context(s!("BaseThreadInitThunk stack size not found"))?;
config.rtl_user_thread_size = rtl_user_size as u64;
config.base_thread_size = base_thread_size as u64;
// First prologue
let first_prolog = Prolog::find_prolog(kernelbase, tables)
.context(s!("first prolog not found"))?;
config.first_frame_fp = (first_prolog.frame + first_prolog.offset as u64) as *const c_void;
config.first_frame_size = first_prolog.stack_size as u64;
// Second prologue
let second_prolog = Prolog::find_push_rbp(kernelbase, tables)
.context(s!("second prolog not found"))?;
config.second_frame_fp = (second_prolog.frame + second_prolog.offset as u64) as *const c_void;
config.second_frame_size = second_prolog.stack_size as u64;
config.rbp_stack_offset = second_prolog.rbp_offset as u64;
// Gadget: `add rsp, 0x58; ret`
let (add_rsp_addr, size) = find_gadget(kernelbase, &[0x48, 0x83, 0xC4, 0x58, 0xC3], tables)
.context(s!("add rsp gadget not found"))?;
config.add_rsp_gadget = add_rsp_addr as *const c_void;
config.add_rsp_frame_size = size as u64;
// Gadget: `jmp rbx`
let (jmp_rbx_addr, size) = find_gadget(kernelbase, &[0xFF, 0x23], tables)
.context(s!("jmp rbx gadget not found"))?;
config.jmp_rbx_gadget = jmp_rbx_addr as *const c_void;
config.jmp_rbx_frame_size = size as u64;
// Prepare arguments
let len = args.len();
config.number_args = len as u64;
for (i, &arg) in args.iter().take(len).enumerate() {
match i {
0 => config.arg01 = arg,
1 => config.arg02 = arg,
2 => config.arg03 = arg,
3 => config.arg04 = arg,
4 => config.arg05 = arg,
5 => config.arg06 = arg,
6 => config.arg07 = arg,
7 => config.arg08 = arg,
8 => config.arg09 = arg,
9 => config.arg10 = arg,
10 => config.arg11 = arg,
_ => break,
}
}
// Handle syscall spoofing
match kind {
SpoofKind::Function => config.spoof_function = addr,
SpoofKind::Syscall(name) => {
let addr = get_proc_address(ntdll, name, None);
if addr.is_null() {
bail!(s!("get_proc_address returned null"));
}
config.is_syscall = true as u32;
config.ssn = dinvk::ssn(name, ntdll).context(s!("ssn not found"))?.into();
config.spoof_function = dinvk::get_syscall_address(addr)
.context(s!("syscall address not found"))? as *const c_void;
}
}
Ok(unsafe { SpoofSynthetic(&mut config) })
}
/// Performs call stack spoofing in `desync` mode.
#[cfg(feature = "desync")]
pub fn spoof(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind) -> Result<*mut c_void> {
// Max 11 args
if args.len() > 11 {
bail!(s!("too many arguments"));
}
// Function pointer must be valid unless syscall spoof
if let SpoofKind::Function = kind && addr.is_null() {
bail!(s!("null function address"));
}
let mut config = Config::default();
// Resolve kernelbase
let kernelbase = get_module_address(2737729883u32, Some(murmur3));
// Parse unwind table
let pe = Unwind::new(PE::parse(kernelbase));
let tables = pe
.entries()
.context(s!(
"failed to read IMAGE_RUNTIME_FUNCTION entries from .pdata section"
))?;
// Locate a return address from BaseThreadInitThunk on the current stack
config.return_address = find_base_thread_return_address()
.context(s!("return address not found"))? as *const c_void;
// First prologue
let first_prolog = Prolog::find_prolog(kernelbase, tables)
.context(s!("first prolog not found"))?;
config.first_frame_fp = (first_prolog.frame + first_prolog.offset as u64) as *const c_void;
config.first_frame_size = first_prolog.stack_size as u64;
// Second prologue
let second_prolog = Prolog::find_push_rbp(kernelbase, tables)
.context(s!("second prolog not found"))?;
config.second_frame_fp = (second_prolog.frame + second_prolog.offset as u64) as *const c_void;
config.second_frame_size = second_prolog.stack_size as u64;
config.rbp_stack_offset = second_prolog.rbp_offset as u64;
// Gadget: `add rsp, 0x58; ret`
let (add_rsp_addr, size) = find_gadget(kernelbase, &[0x48, 0x83, 0xC4, 0x58, 0xC3], tables)
.context(s!("add rsp gadget not found"))?;
config.add_rsp_gadget = add_rsp_addr as *const c_void;
config.add_rsp_frame_size = size as u64;
// Gadget: `jmp rbx`
let (jmp_rbx_addr, size) = find_gadget(kernelbase, &[0xFF, 0x23], tables)
.context(s!("jmp rbx gadget not found"))?;
config.jmp_rbx_gadget = jmp_rbx_addr as *const c_void;
config.jmp_rbx_frame_size = size as u64;
// Prepare arguments
let len = args.len();
config.number_args = len as u64;
for (i, &arg) in args.iter().take(len).enumerate() {
match i {
0 => config.arg01 = arg,
1 => config.arg02 = arg,
2 => config.arg03 = arg,
3 => config.arg04 = arg,
4 => config.arg05 = arg,
5 => config.arg06 = arg,
6 => config.arg07 = arg,
7 => config.arg08 = arg,
8 => config.arg09 = arg,
9 => config.arg10 = arg,
10 => config.arg11 = arg,
_ => break,
}
}
// Handle syscall spoofing
match kind {
SpoofKind::Function => config.spoof_function = addr,
SpoofKind::Syscall(name) => {
let ntdll = get_module_address(2788516083u32, Some(murmur3));
if ntdll.is_null() {
bail!(s!("ntdll.dll not found"));
}
let addr = get_proc_address(ntdll, name, None);
if addr.is_null() {
bail!(s!("get_proc_address returned null"));
}
config.is_syscall = true as u32;
config.ssn = dinvk::ssn(name, ntdll).context(s!("ssn not found"))?.into();
config.spoof_function = dinvk::get_syscall_address(addr)
.context(s!("syscall address not found"))? as *const c_void;
}
}
Ok(unsafe { Spoof(&mut config) })
}
}
/// Metadata extracted from a function prologue that is suitable for spoofing.
#[derive(Copy, Clone, Default)]
struct Prolog {
/// Address of the selected function frame.
frame: u64,
/// Total stack space reserved by the function.
stack_size: u32,
/// Offset inside the function where a valid instruction pattern was found.
offset: u32,
/// Offset in the stack where `rbp` is pushed or saved.
rbp_offset: u32,
}
impl Prolog {
/// Finds the first prologue in the unwind table that looks safe for spoofing.
///
/// This scans the RUNTIME_FUNCTION entries for a function that:
/// - Allocates a stack frame.
/// - Has a predictable prologue layout.
fn find_prolog(module_base: *mut c_void, runtime_table: &[IMAGE_RUNTIME_FUNCTION]) -> Option<Self> {
let mut prologs = runtime_table
.iter()
.filter_map(|runtime| {
let (is_valid, stack_size) = stack_frame(module_base, runtime)?;
if !is_valid {
return None;
}
let offset = find_valid_instruction_offset(module_base, runtime)?;
let frame = module_base as u64 + runtime.BeginAddress as u64;
Some(Self {
frame,
stack_size,
offset,
..Default::default()
})
})
.collect::<Vec<Self>>();
if prologs.is_empty() {
return None;
}
// Shuffle to reduce pattern predictability.
shuffle(&mut prologs);
prologs.first().copied()
}
/// Finds a prologue that uses `push rbp` and an RBP-based frame.
///
/// This is useful when spoofing techniques rely on classic frame-pointer
/// based layouts rather than purely RSP-based stack frames.
fn find_push_rbp(module_base: *mut c_void, runtime_table: &[IMAGE_RUNTIME_FUNCTION]) -> Option<Self> {
let mut prologs = runtime_table
.iter()
.filter_map(|runtime| {
let (rbp_offset, stack_size) = rbp_offset(module_base, runtime)?;
if rbp_offset == 0 || stack_size == 0 || stack_size <= rbp_offset {
return None;
}
let offset = find_valid_instruction_offset(module_base, runtime)?;
let frame = module_base as u64 + runtime.BeginAddress as u64;
Some(
Self {
frame,
stack_size,
offset,
rbp_offset,
}
)
})
.collect::<Vec<Self>>();
if prologs.is_empty() {
return None;
}
// The first frame is often not suitable on many Windows versions.
prologs.remove(0);
// Shuffle to reduce pattern predictability.
shuffle(&mut prologs);
prologs.first().copied()
}
}
/// Determines whether RBP is pushed or saved in a spoof-compatible manner and
/// computes the total stack size for a function.
///
/// This inspects the unwind codes associated with the `IMAGE_RUNTIME_FUNCTION`
/// entry to determine if the function frame uses a layout suitable for
/// call stack spoofing.
pub fn rbp_offset(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option<(u32, u32)> {
unsafe {
let unwind_info = (module as usize + runtime.UnwindData as usize) as *mut UNWIND_INFO;
let unwind_code = (unwind_info as *mut u8).add(4) as *mut UNWIND_CODE;
let flag = (*unwind_info).VersionFlags.Flags();
let mut i = 0usize;
let mut total_stack = 0u32;
let mut rbp_pushed = false;
let mut stack_offset = 0;
while i < (*unwind_info).CountOfCodes as usize {
// Accessing `UNWIND_CODE` based on the index
let unwind_code = unwind_code.add(i);
// Information used in operation codes
let op_info = (*unwind_code).Anonymous.OpInfo() as usize;
let unwind_op = (*unwind_code).Anonymous.UnwindOp();
match UNWIND_OP_CODES::try_from(unwind_op) {
// Saves a non-volatile register on the stack.
//
// Example: push <reg>
Ok(UWOP_PUSH_NONVOL) => {
if Registers::Rsp == op_info {
return None;
}
if Registers::Rbp == op_info {
if rbp_pushed {
return None;
}
rbp_pushed = true;
stack_offset = total_stack;
}
total_stack += 8;
i += 1;
}
// Allocates large space on the stack.
// - OpInfo == 0: The next slot contains the /8 size of the allocation (maximum 512 KB - 8).
// - OpInfo == 1: The next two slots contain the full size of the allocation (up to 4 GB - 8).
//
// Example (OpInfo == 0): sub rsp, 0x100 ; Allocates 256 bytes
// Example (OpInfo == 1): sub rsp, 0x10000 ; Allocates 65536 bytes (two slots used)
Ok(UWOP_ALLOC_LARGE) => {
if (*unwind_code).Anonymous.OpInfo() == 0 {
// Case 1: OpInfo == 0 (Size in 1 slot, divided by 8)
// Multiplies by 8 to the actual value
let frame_offset = ((*unwind_code.add(1)).FrameOffset as i32) * 8;
total_stack += frame_offset as u32;
// Consumes 2 slots (1 for the instruction, 1 for the size divided by 8)
i += 2
} else {
// Case 2: OpInfo == 1 (Size in 2 slots, 32 bits)
let frame_offset = *(unwind_code.add(1) as *mut i32);
total_stack += frame_offset as u32;
// Consumes 3 slots (1 for the instruction, 2 for the full size)
i += 3
}
}
// Allocates small space in the stack.
//
// Example (OpInfo = 3): sub rsp, 0x20 ; Aloca 32 bytes (OpInfo + 1) * 8
Ok(UWOP_ALLOC_SMALL) => {
total_stack += ((op_info + 1) * 8) as u32;
i += 1;
}
// UWOP_SAVE_NONVOL: Saves the contents of a non-volatile register in a specific position on the stack.
// - Reg: Name of the saved register.
// - FrameOffset: Offset indicating where the value of the register is saved.
//
// Example: mov [rsp + 0x40], rsi ; Saves the contents of RSI in RSP + 0x40
Ok(UWOP_SAVE_NONVOL) => {
if Registers::Rsp == op_info {
return None;
}
if Registers::Rbp == op_info {
if rbp_pushed {
return None;
}
let offset = (*unwind_code.add(1)).FrameOffset * 8;
stack_offset = total_stack + offset as u32;
rbp_pushed = true;
}
i += 2;
}
// Saves a non-volatile register to a stack address with a long offset.
// - Reg: Name of the saved register.
// - FrameOffset: Long offset indicating where the value of the register is saved.
//
// Example: mov [rsp + 0x1040], rsi ; Saves the contents of RSI in RSP + 0x1040.
Ok(UWOP_SAVE_NONVOL_BIG) => {
if Registers::Rsp == op_info {
return None;
}
if Registers::Rbp == op_info {
if rbp_pushed {
return None;
}
let offset = *(unwind_code.add(1) as *mut u32);
stack_offset = total_stack + offset;
rbp_pushed = true;
}
i += 3;
}
// Return
Ok(UWOP_SET_FPREG) => return None,
// - Reg: Name of the saved XMM register.
// - FrameOffset: Offset indicating where the value of the register is saved.
Ok(UWOP_SAVE_XMM128) => i += 2,
// UWOP_SAVE_XMM128BIG: Saves the contents of a non-volatile XMM register to a stack address with a long offset.
// - Reg: Name of the saved XMM register.
// - FrameOffset: Long offset indicating where the value of the register is saved.
//
// Example: movaps [rsp + 0x1040], xmm6 ; Saves the contents of XMM6 in RSP + 0x1040.
Ok(UWOP_SAVE_XMM128BIG) => i += 3,
// Reserved code, not currently used.
Ok(UWOP_EPILOG) | Ok(UWOP_SPARE_CODE) => i += 1,
// Push a machine frame. This unwind code is used to record the effect of a hardware interrupt or exception.
Ok(UWOP_PUSH_MACH_FRAME) => {
total_stack += if op_info == 0 { 0x40 } else { 0x48 };
i += 1
}
_ => {}
}
}
// If there is a chain unwind structure, it too must be processed
// recursively and included in the stack size calculation.
if (flag & UNW_FLAG_CHAININFO) != 0 {
let count = (*unwind_info).CountOfCodes as usize;
let index = if count & 1 == 1 { count + 1 } else { count };
let runtime = unwind_code.add(index) as *const IMAGE_RUNTIME_FUNCTION;
if let Some((_, child_total)) = rbp_offset(module, &*runtime) {
total_stack += child_total;
} else {
return None;
}
}
Some((stack_offset, total_stack))
}
}
/// Computes stack frame metadata while rejecting `setfp` frames.
///
/// Used when locating suitable prologues for spoofed call frames.
pub fn stack_frame(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option<(bool, u32)> {
unsafe {
let unwind_info = (module as usize + runtime.UnwindData as usize) as *mut UNWIND_INFO;
let unwind_code = (unwind_info as *mut u8).add(4) as *mut UNWIND_CODE;
let flag = (*unwind_info).VersionFlags.Flags();
let mut i = 0usize;
let mut set_fpreg_hit = false;
let mut total_stack = 0i32;
while i < (*unwind_info).CountOfCodes as usize {
// Accessing `UNWIND_CODE` based on the index
let unwind_code = unwind_code.add(i);
// Information used in operation codes
let op_info = (*unwind_code).Anonymous.OpInfo() as usize;
let unwind_op = (*unwind_code).Anonymous.UnwindOp();
match UNWIND_OP_CODES::try_from(unwind_op) {
// Saves a non-volatile register on the stack.
//
// Example: push <reg>
Ok(UWOP_PUSH_NONVOL) => {
if Registers::Rsp == op_info && !set_fpreg_hit {
return None;
}
total_stack += 8;
i += 1;
}
// Allocates small space in the stack.
//
// Example (OpInfo = 3): sub rsp, 0x20 ; Aloca 32 bytes (OpInfo + 1) * 8
Ok(UWOP_ALLOC_SMALL) => {
total_stack += ((op_info + 1) * 8) as i32;
i += 1;
}
// Allocates large space on the stack.
// - OpInfo == 0: The next slot contains the /8 size of the allocation (maximum 512 KB - 8).
// - OpInfo == 1: The next two slots contain the full size of the allocation (up to 4 GB - 8).
//
// Example (OpInfo == 0): sub rsp, 0x100 ; Allocates 256 bytes
// Example (OpInfo == 1): sub rsp, 0x10000 ; Allocates 65536 bytes (two slots used)
Ok(UWOP_ALLOC_LARGE) => {
if (*unwind_code).Anonymous.OpInfo() == 0 {
// Case 1: OpInfo == 0 (Size in 1 slot, divided by 8)
// Multiplies by 8 to the actual value
let frame_offset = ((*unwind_code.add(1)).FrameOffset as i32) * 8;
total_stack += frame_offset;
// Consumes 2 slots (1 for the instruction, 1 for the size divided by 8)
i += 2
} else {
// Case 2: OpInfo == 1 (Size in 2 slots, 32 bits)
let frame_offset = *(unwind_code.add(1) as *mut i32);
total_stack += frame_offset;
// Consumes 3 slots (1 for the instruction, 2 for the full size)
i += 3
}
}
// UWOP_SAVE_NONVOL: Saves the contents of a non-volatile register in a specific position on the stack.
// - Reg: Name of the saved register.
// - FrameOffset: Offset indicating where the value of the register is saved.
//
// Example: mov [rsp + 0x40], rsi ; Saves the contents of RSI in RSP + 0x40
Ok(UWOP_SAVE_NONVOL) => {
if Registers::Rsp == op_info || Registers::Rbp == op_info {
return None;
}
i += 2;
}
// Saves a non-volatile register to a stack address with a long offset.
// - Reg: Name of the saved register.
// - FrameOffset: Long offset indicating where the value of the register is saved.
//
// Example: mov [rsp + 0x1040], rsi ; Saves the contents of RSI in RSP + 0x1040.
Ok(UWOP_SAVE_NONVOL_BIG) => {
if Registers::Rsp == op_info || Registers::Rbp == op_info {
return None;
}
i += 3;
}
// Saves the contents of a non-volatile XMM register on the stack.
// - Reg: Name of the saved XMM register.
// - FrameOffset: Offset indicating where the value of the register is saved.
//
// Example: movaps [rsp + 0x20], xmm6 ; Saves the contents of XMM6 in RSP + 0x20.
Ok(UWOP_SAVE_XMM128) => i += 2,
// UWOP_SAVE_XMM128BIG: Saves the contents of a non-volatile XMM register to a stack address with a long offset.
// - Reg: Name of the saved XMM register.
// - FrameOffset: Long offset indicating where the value of the register is saved.
//
// Example: movaps [rsp + 0x1040], xmm6 ; Saves the contents of XMM6 in RSP + 0x1040.
Ok(UWOP_SAVE_XMM128BIG) => i += 3,
// UWOP_SET_FPREG: Marks use of register as stack base (e.g. RBP).
// Ignore if not RBP, has EH handler or chained unwind.
// Subtract `FrameOffset << 4` from the stack total.
Ok(UWOP_SET_FPREG) => {
if (flag & UNW_FLAG_EHANDLER) != 0 && (flag & UNW_FLAG_CHAININFO) != 0 {
return None;
}
if (*unwind_info).FrameInfo.FrameRegister() != Registers::Rbp as u8 {
return None;
}
set_fpreg_hit = true;
let offset = ((*unwind_info).FrameInfo.FrameOffset() as i32) << 4;
total_stack -= offset;
i += 1
}
// Reserved code, not currently used.
Ok(UWOP_EPILOG) | Ok(UWOP_SPARE_CODE) => i += 1,
// Push a machine frame. This unwind code is used to record the effect of a hardware interrupt or exception.
Ok(UWOP_PUSH_MACH_FRAME) => {
total_stack += if op_info == 0 { 0x40 } else { 0x48 };
i += 1
}
_ => {}
}
}
// If there is a chain unwind structure, it too must be processed
// recursively and included in the stack size calculation.
if (flag & UNW_FLAG_CHAININFO) != 0 {
let count = (*unwind_info).CountOfCodes as usize;
let index = if count & 1 == 1 { count + 1 } else { count };
let runtime = unwind_code.add(index) as *const IMAGE_RUNTIME_FUNCTION;
if let Some((chained_fpreg_hit, chained_stack)) = stack_frame(module, &*runtime) {
total_stack += chained_stack as i32;
set_fpreg_hit |= chained_fpreg_hit;
} else {
return None;
}
}
Some((set_fpreg_hit, total_stack as u32))
}
}
/// Computes the total stack frame size of a function while ignoring any `setfp` frames.
/// Useful for identifying spoof-compatible RUNTIME_FUNCTION entries.
pub fn ignoring_set_fpreg(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION) -> Option<u32> {
unsafe {
let unwind_info = (module as usize + runtime.UnwindData as usize) as *mut UNWIND_INFO;
let unwind_code = (unwind_info as *mut u8).add(4) as *mut UNWIND_CODE;
let flag = (*unwind_info).VersionFlags.Flags();
let mut i = 0usize;
let mut total_stack = 0u32;
while i < (*unwind_info).CountOfCodes as usize {
// Accessing `UNWIND_CODE` based on the index
let unwind_code = unwind_code.add(i);
// Information used in operation codes
let op_info = (*unwind_code).Anonymous.OpInfo() as usize;
let unwind_op = (*unwind_code).Anonymous.UnwindOp();
match UNWIND_OP_CODES::try_from(unwind_op) {
// Saves a non-volatile register on the stack.
//
// Example: push <reg>
Ok(UWOP_PUSH_NONVOL) => {
if Registers::Rsp == op_info {
return None;
}
total_stack += 8;
i += 1;
}
// Allocates small space in the stack.
//
// Example (OpInfo = 3): sub rsp, 0x20 ; Aloca 32 bytes (OpInfo + 1) * 8
Ok(UWOP_ALLOC_SMALL) => {
total_stack += ((op_info + 1) * 8) as u32;
i += 1;
}
// Allocates large space on the stack.
// - OpInfo == 0: The next slot contains the /8 size of the allocation (maximum 512 KB - 8).
// - OpInfo == 1: The next two slots contain the full size of the allocation (up to 4 GB - 8).
//
// Example (OpInfo == 0): sub rsp, 0x100 ; Allocates 256 bytes
// Example (OpInfo == 1): sub rsp, 0x10000 ; Allocates 65536 bytes (two slots used)
Ok(UWOP_ALLOC_LARGE) => {
if (*unwind_code).Anonymous.OpInfo() == 0 {
// Case 1: OpInfo == 0 (Size in 1 slot, divided by 8)
// Multiplies by 8 to the actual value
let frame_offset = ((*unwind_code.add(1)).FrameOffset as i32) * 8;
total_stack += frame_offset as u32;
// Consumes 2 slots (1 for the instruction, 1 for the size divided by 8)
i += 2
} else {
// Case 2: OpInfo == 1 (Size in 2 slots, 32 bits)
let frame_offset = *(unwind_code.add(1) as *mut i32);
total_stack += frame_offset as u32;
// Consumes 3 slots (1 for the instruction, 2 for the full size)
i += 3
}
}
// UWOP_SAVE_NONVOL: Saves the contents of a non-volatile register in a specific position on the stack.
// - Reg: Name of the saved register.
// - FrameOffset: Offset indicating where the value of the register is saved.
//
// Example: mov [rsp + 0x40], rsi ; Saves the contents of RSI in RSP + 0x40
Ok(UWOP_SAVE_NONVOL) => {
if Registers::Rsp == op_info {
return None;
}
i += 2;
}
// Saves a non-volatile register to a stack address with a long offset.
// - Reg: Name of the saved register.
// - FrameOffset: Long offset indicating where the value of the register is saved.
//
// Example: mov [rsp + 0x1040], rsi ; Saves the contents of RSI in RSP + 0x1040.
Ok(UWOP_SAVE_NONVOL_BIG) => {
if Registers::Rsp == op_info {
return None;
}
i += 3;
}
// Saves the contents of a non-volatile XMM register on the stack.
// - Reg: Name of the saved XMM register.
// - FrameOffset: Offset indicating where the value of the register is saved.
//
// Example: movaps [rsp + 0x20], xmm6 ; Saves the contents of XMM6 in RSP + 0x20.
Ok(UWOP_SAVE_XMM128) => i += 2,
// UWOP_SAVE_XMM128BIG: Saves the contents of a non-volatile XMM register to a stack address with a long offset.
// - Reg: Name of the saved XMM register.
// - FrameOffset: Long offset indicating where the value of the register is saved.
//
// Example: movaps [rsp + 0x1040], xmm6 ; Saves the contents of XMM6 in RSP + 0x1040.
Ok(UWOP_SAVE_XMM128BIG) => i += 3,
// Ignoring.
Ok(UWOP_SET_FPREG) => i += 1,
// Reserved code, not currently used.
Ok(UWOP_EPILOG) | Ok(UWOP_SPARE_CODE) => i += 1,
// Push a machine frame. This unwind code is used to record the effect of a hardware interrupt or exception.
Ok(UWOP_PUSH_MACH_FRAME) => {
total_stack += if op_info == 0 { 0x40 } else { 0x48 };
i += 1
}
_ => {}
}
}
// If there is a chain unwind structure, it too must be processed
// recursively and included in the stack size calculation.
if (flag & UNW_FLAG_CHAININFO) != 0 {
let count = (*unwind_info).CountOfCodes as usize;
let index = if count & 1 == 1 { count + 1 } else { count };
let runtime = unwind_code.add(index) as *const IMAGE_RUNTIME_FUNCTION;
if let Some(chained_stack) = ignoring_set_fpreg(module, &*runtime) {
total_stack += chained_stack;
} else {
return None;
}
}
Some(total_stack)
}
}
/// Trait for safely converting any reference or mutable reference into a raw
/// pointer usable in spoofing routines.
pub trait AsPointer {
/// Returns a raw immutable pointer to `self`.
fn as_ptr_const(&self) -> *const c_void;
/// Returns a raw mutable pointer to `self`.
fn as_ptr_mut(&mut self) -> *mut c_void;
}
impl<T> AsPointer for T {
#[inline(always)]
fn as_ptr_const(&self) -> *const c_void {
self as *const _ as *const c_void
}
#[inline(always)]
fn as_ptr_mut(&mut self) -> *mut c_void {
self as *mut _ as *mut c_void
}
}
/// Specifies the spoofing mode used by the engine.
pub enum SpoofKind<'a> {
/// Spoofs a direct function call.
Function,
/// Spoofs a syscall using its name.
Syscall(&'a str),
}
#[cfg(test)]
mod tests {
use core::ptr;
use alloc::boxed::Box;
use super::*;
#[test]
fn test_spoof() -> Result<(), Box<dyn core::error::Error>> {
let kernel32 = get_module_address("kernel32.dll", None);
let virtual_alloc = get_proc_address(kernel32, "VirtualAlloc", None);
let addr = spoof!(virtual_alloc, ptr::null_mut::<c_void>(), 1 << 12, 0x3000, 0x04)?;
assert_ne!(addr, ptr::null_mut());
Ok(())
}
#[test]
fn test_syscall() -> Result<(), Box<dyn core::error::Error>> {
let mut addr = ptr::null_mut::<c_void>();
let mut size = (1 << 12) as usize;
let status = syscall!("NtAllocateVirtualMemory", -1isize, addr.as_ptr_mut(), 0, size.as_ptr_mut(), 0x3000, 0x04)? as i32;
assert_eq!(status, 0);
Ok(())
}
}
================================================
FILE: taplo.toml
================================================
[formatting]
crlf = true
array_auto_expand = true
gitextract_me8cjxhe/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── Cargo.toml ├── Justfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── clippy.toml ├── rust-toolchain.toml ├── rustfmt.toml ├── src/ │ ├── asm/ │ │ ├── gnu/ │ │ │ ├── desync.asm │ │ │ └── synthetic.asm │ │ └── msvc/ │ │ ├── desync.asm │ │ └── synthetic.asm │ ├── lib.rs │ ├── types.rs │ ├── util.rs │ └── uwd.rs └── taplo.toml
SYMBOL INDEX (38 symbols across 4 files)
FILE: build.rs
function main (line 3) | fn main() {
FILE: src/types.rs
constant UNW_FLAG_EHANDLER (line 7) | pub const UNW_FLAG_EHANDLER: u8 = 0x1;
constant UNW_FLAG_CHAININFO (line 10) | pub const UNW_FLAG_CHAININFO: u8 = 0x4;
type Unwind (line 14) | pub struct Unwind {
method new (line 21) | pub fn new(pe: PE) -> Self {
method entries (line 26) | pub fn entries(&self) -> Option<&[IMAGE_RUNTIME_FUNCTION]> {
method function_by_offset (line 43) | pub fn function_by_offset(&self, offset: u32) -> Option<&IMAGE_RUNTIME...
method function_size (line 49) | pub fn function_size(&self, func: *mut c_void) -> Option<u64> {
type Config (line 62) | pub struct Config {
method default (line 130) | fn default() -> Self {
type Registers (line 141) | pub enum Registers {
method eq (line 161) | fn eq(&self, other: &usize) -> bool {
type UNWIND_INFO (line 230) | pub struct UNWIND_INFO {
type UNWIND_OP_CODES (line 259) | pub enum UNWIND_OP_CODES {
type Error (line 274) | type Error = ();
method try_from (line 276) | fn try_from(value: u8) -> Result<Self, Self::Error> {
FILE: src/util.rs
function find_valid_instruction_offset (line 18) | pub fn find_valid_instruction_offset(
function find_gadget (line 41) | pub fn find_gadget(
function find_base_thread_return_address (line 82) | pub fn find_base_thread_return_address() -> Option<usize> {
function shuffle (line 128) | pub fn shuffle<T>(list: &mut [T]) {
FILE: src/uwd.rs
function Spoof (line 22) | fn Spoof(config: &mut Config) -> *mut c_void;
function SpoofSynthetic (line 28) | fn SpoofSynthetic(config: &mut Config) -> *mut c_void;
function spoof (line 109) | pub fn spoof(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind)...
function spoof (line 238) | pub fn spoof(addr: *mut c_void, args: &[*const c_void], kind: SpoofKind)...
type Prolog (line 343) | struct Prolog {
method find_prolog (line 363) | fn find_prolog(module_base: *mut c_void, runtime_table: &[IMAGE_RUNTIM...
method find_push_rbp (line 397) | fn find_push_rbp(module_base: *mut c_void, runtime_table: &[IMAGE_RUNT...
function rbp_offset (line 439) | pub fn rbp_offset(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION)...
function stack_frame (line 607) | pub fn stack_frame(module: *mut c_void, runtime: &IMAGE_RUNTIME_FUNCTION...
function ignoring_set_fpreg (line 761) | pub fn ignoring_set_fpreg(module: *mut c_void, runtime: &IMAGE_RUNTIME_F...
type AsPointer (line 898) | pub trait AsPointer {
method as_ptr_const (line 900) | fn as_ptr_const(&self) -> *const c_void;
method as_ptr_mut (line 903) | fn as_ptr_mut(&mut self) -> *mut c_void;
method as_ptr_const (line 908) | fn as_ptr_const(&self) -> *const c_void {
method as_ptr_mut (line 913) | fn as_ptr_mut(&mut self) -> *mut c_void {
type SpoofKind (line 919) | pub enum SpoofKind<'a> {
function test_spoof (line 934) | fn test_spoof() -> Result<(), Box<dyn core::error::Error>> {
function test_syscall (line 944) | fn test_syscall() -> Result<(), Box<dyn core::error::Error>> {
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (93K chars).
[
{
"path": ".gitattributes",
"chars": 18,
"preview": "* text=auto eol=lf"
},
{
"path": ".github/workflows/ci.yml",
"chars": 636,
"preview": "name: build\n\non: [push, pull_request]\n\njobs:\n clippy:\n # Runs Clippy to check for lints in the Rust code\n name: C"
},
{
"path": ".gitignore",
"chars": 821,
"preview": "# Generated by Cargo\n# will have compiled files and executables\ndebug/\ntarget/\n\n# Remove Cargo.lock from gitignore if cr"
},
{
"path": ".vscode/settings.json",
"chars": 69,
"preview": "{\n \"rust-analyzer.linkedProjects\": [\n \"Cargo.toml\"\n ],\n}"
},
{
"path": "Cargo.toml",
"chars": 894,
"preview": "[package]\nname = \"uwd\"\nversion = \"0.3.5\"\nedition = \"2024\"\ndescription = \"Call Stack Spoofing for Rust\"\nlicense = \"MIT OR"
},
{
"path": "Justfile",
"chars": 388,
"preview": "# Aliases\nalias c := clean\nalias up := update\n\n# Use PowerShell shell on Windows\nset windows-shell := [\"powershell.exe\","
},
{
"path": "LICENSE-APACHE",
"chars": 11357,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "LICENSE-MIT",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) 2025 Victor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "README.md",
"chars": 4058,
"preview": "# uwd\n\n\n\n![do"
},
{
"path": "build.rs",
"chars": 1275,
"preview": "use std::env;\n\nfn main() {\n if env::var(\"DOCS_RS\").is_ok() {\n println!(\"cargo:warning=Skipping ASM build for d"
},
{
"path": "clippy.toml",
"chars": 13,
"preview": "msrv = \"1.89\""
},
{
"path": "rust-toolchain.toml",
"chars": 68,
"preview": "[toolchain]\nchannel = \"stable\"\ntargets = [\"x86_64-pc-windows-msvc\"]\n"
},
{
"path": "rustfmt.toml",
"chars": 114,
"preview": "# Uses Rustfmt 2024 edition rules, but not all of them are enforced 100%.\nedition = \"2024\"\nstyle_edition = \"2024\"\n"
},
{
"path": "src/asm/gnu/desync.asm",
"chars": 4203,
"preview": ";;\n;; Code responsible for Call Stack Spoofing Via Desync (NASM)\n;;\n[BITS 64]\n\n;;\n;; Export\n;;\nGLOBAL Spoof\n\n[SECTION .d"
},
{
"path": "src/asm/gnu/synthetic.asm",
"chars": 5074,
"preview": ";;\n;; Code responsible for Call Stack Spoofing Via Synthetic (NASM)\n;;\n[BITS 64]\n\n;;\n;; Export\n;;\nGLOBAL SpoofSynthetic\n"
},
{
"path": "src/asm/msvc/desync.asm",
"chars": 4116,
"preview": ";;\n;; Code responsible for Call Stack Spoofing Via Desync (MASM)\n;;\n\n;;\n;; Export\n;;\nSpoof proto\n\n.data\n\n;;\n;; Configura"
},
{
"path": "src/asm/msvc/synthetic.asm",
"chars": 4979,
"preview": ";;\n;; Code responsible for Call Stack Spoofing Via Synthetic (MASM)\n;;\n\n;;\n;; Export\n;;\nSpoofSynthetic proto\n\n.data\n\n;;\n"
},
{
"path": "src/lib.rs",
"chars": 119,
"preview": "#![no_std]\n#![doc = include_str!(\"../README.md\")]\n\nextern crate alloc;\n\nmod types;\nmod uwd;\nmod util;\n\npub use uwd::*;\n"
},
{
"path": "src/types.rs",
"chars": 7598,
"preview": "#![allow(non_snake_case, non_camel_case_types)]\n\nuse core::{ffi::c_void, slice::from_raw_parts};\nuse dinvk::{types::*, h"
},
{
"path": "src/util.rs",
"chars": 4430,
"preview": "use core::{ffi::c_void, slice::from_raw_parts};\nuse alloc::vec::Vec;\n\nuse obfstr::obfbytes as b;\nuse dinvk::types::IMAGE"
},
{
"path": "src/uwd.rs",
"chars": 37126,
"preview": "use alloc::{string::String, vec::Vec};\nuse core::ffi::c_void;\n\nuse anyhow::{Context, Result, bail};\nuse obfstr::obfstrin"
},
{
"path": "taplo.toml",
"chars": 50,
"preview": "[formatting]\ncrlf = true\narray_auto_expand = true\n"
}
]
About this extraction
This page contains the full source code of the joaoviictorti/uwd GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (86.4 KB), approximately 23.1k tokens, and a symbol index with 38 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.