00734c6553
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).
243 lines
9.3 KiB
Rust
243 lines
9.3 KiB
Rust
//! 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}"),
|
|
}
|
|
}
|