mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
ASM Obfuscation
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::{Rng, RngCore, SeedableRng};
|
||||
use syn::{LitFloat, parse_macro_input};
|
||||
|
||||
const MAX_INSTRUCTIONS: u32 = 20; // Maximum instructions per recursive block
|
||||
const MIN_LENGTH: f64 = 10.; // Min length per 1/weight
|
||||
|
||||
// The full list of 64-bit registers in AT&T syntax (used by default in asm!)
|
||||
const REGISTERS: &[&str] = &[
|
||||
"%rax", "%rbx", "%rcx", "%rdx", "%rsi", "%rdi", "%r8", "%r9", "%r10", "%r11", "%r12", "%r13",
|
||||
"%r14", "%r15",
|
||||
];
|
||||
|
||||
// Conditional Jumps in AT&T syntax.
|
||||
const COND_JUMPS: &[&str] = &[
|
||||
"je", "jne", "jg", "jge", "jl", "jle", "ja", "jnb", "jc", "jnc", "jz", "jnz",
|
||||
];
|
||||
|
||||
// Arithmetic/Logic operations with the 'q' (quad-word) suffix.
|
||||
const ARITHITHMETIC_OPS: &[&str] = &["addq", "subq", "xorq", "andq", "orq"];
|
||||
|
||||
// --- Helper Functions for Modular Generation ---
|
||||
|
||||
/// Generates a unique label name for the given depth and ID.
|
||||
fn generate_label(prefix: &str, depth: u32, block_id: u32, id: u32) -> String {
|
||||
format!(".L_{}_{}_{}_{}", prefix, depth, block_id, id)
|
||||
}
|
||||
/// Generates a highly randomized, complex instruction using different addressing modes.
|
||||
fn generate_complex_mutation(rng: &mut SmallRng) -> String {
|
||||
let op = ARITHITHMETIC_OPS[rng.random_range(0..ARITHITHMETIC_OPS.len())];
|
||||
|
||||
match rng.random_range(0..3) {
|
||||
// Pattern 0: Register-Immediate
|
||||
// Example: "addq $0x1234, %rax"
|
||||
0 => {
|
||||
let reg = REGISTERS[rng.random_range(0..REGISTERS.len())];
|
||||
let immediate = rng.random_range(1..=0xFFFF);
|
||||
format!("\t{} ${}, {}", op, immediate, reg)
|
||||
}
|
||||
// Pattern 1: Register-Register
|
||||
// Example: "xorq %rbx, %rcx"
|
||||
1 => {
|
||||
let reg_src = REGISTERS[rng.random_range(0..REGISTERS.len())];
|
||||
let reg_dst = REGISTERS[rng.random_range(0..REGISTERS.len())];
|
||||
format!("\t{} {}, {}", op, reg_src, reg_dst)
|
||||
}
|
||||
// Pattern 2: LEA (Complex Address Calculation)
|
||||
// Example: "leaq (%rax, %rbx, 4), %rcx"
|
||||
2 => {
|
||||
let reg_base = REGISTERS[rng.random_range(0..REGISTERS.len())];
|
||||
let reg_index = REGISTERS[rng.random_range(0..REGISTERS.len())];
|
||||
let reg_dst = REGISTERS[rng.random_range(0..REGISTERS.len())];
|
||||
let scale = 1 << rng.random_range(0..4); // Scale is 1, 2, 4, or 8
|
||||
format!(
|
||||
"\tleaq ({}, {}, {}), {}",
|
||||
reg_base, reg_index, scale, reg_dst
|
||||
)
|
||||
}
|
||||
_ => String::new(), // Should not happen
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a comparison followed by a conditional jump to a specific label.
|
||||
fn generate_conditional_jump(rng: &mut SmallRng, label: &str) -> String {
|
||||
let reg1 = REGISTERS[rng.random_range(0..REGISTERS.len())];
|
||||
let reg2 = REGISTERS[rng.random_range(0..REGISTERS.len())];
|
||||
let jump = COND_JUMPS[rng.random_range(0..COND_JUMPS.len())];
|
||||
|
||||
// Example: "cmpq %rdx, %rsi; jg .L_target_"
|
||||
format!("\tcmpq {}, {}; {} {}\n", reg1, reg2, jump, label)
|
||||
}
|
||||
|
||||
// --- The Core DAG Recursive Algorithm ---
|
||||
|
||||
fn generate_dag_block(weight: f64, rng: &mut SmallRng, depth: u32, id_counter: &mut u32) -> String {
|
||||
// 1. Termination Check
|
||||
|
||||
if rng.random_bool(weight) {
|
||||
return String::new(); // Stop recursion
|
||||
}
|
||||
|
||||
let block_id = *id_counter;
|
||||
*id_counter += 1;
|
||||
|
||||
// 2. Randomize Block Length: The length is now based on WEIGHT.
|
||||
// If rng < WEIGHT, stop growing the block. Otherwise, continue.
|
||||
let mut num_labels: u32 = 0;
|
||||
while !rng.random_bool(weight) && num_labels < MAX_INSTRUCTIONS {
|
||||
num_labels += 1;
|
||||
}
|
||||
|
||||
// Ensure at least one instruction/label exists if we entered the block
|
||||
if num_labels == 0 {
|
||||
num_labels = 1;
|
||||
}
|
||||
|
||||
// Generate all labels for this block (L0 to Ln-1)
|
||||
let labels: Vec<String> = (0..num_labels)
|
||||
.map(|i| generate_label("dag", depth, block_id, i))
|
||||
.collect();
|
||||
|
||||
let mut assembly_block = String::new();
|
||||
|
||||
// 3. Instruction Loop and DAG construction
|
||||
for i in 0..num_labels {
|
||||
let current_label = &labels[i as usize];
|
||||
assembly_block.push_str(&format!("{}:\n", current_label));
|
||||
|
||||
let mut instruction_count = 0;
|
||||
|
||||
// Generate a random number of mutations based on WEIGHT
|
||||
while !rng.random_bool(weight.powi(2)) && instruction_count < MAX_INSTRUCTIONS * 2 {
|
||||
assembly_block.push_str(&format!("{}\n", generate_complex_mutation(rng)));
|
||||
instruction_count += 1;
|
||||
}
|
||||
|
||||
// Conditional Forward Jump (Creates DAG edges)
|
||||
if i < num_labels - 1 && !rng.random_bool(weight * 0.5) {
|
||||
// Jump to a random label strictly ahead of the current one
|
||||
let target_index = rng.random_range(i as usize + 1..num_labels as usize);
|
||||
let target_label = &labels[target_index];
|
||||
assembly_block.push_str(&generate_conditional_jump(rng, target_label));
|
||||
}
|
||||
|
||||
// Recursive Call (Nesting)
|
||||
if depth < 2 {
|
||||
// Lower probability for deep nesting
|
||||
assembly_block.push_str(&generate_dag_block(weight, rng, depth + 1, id_counter));
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Backward Conditional Jump (Adds controlled cycles)
|
||||
// Only at the end of the block, allowing a chance to loop back to an earlier instruction.
|
||||
if num_labels > 1 && rng.random_bool(weight) {
|
||||
let target_index = rng.random_range(0..num_labels as usize - 1);
|
||||
let target_label = &labels[target_index];
|
||||
assembly_block.push_str(&format!("{}\n", generate_complex_mutation(rng)));
|
||||
assembly_block.push_str(&generate_conditional_jump(rng, target_label));
|
||||
assembly_block.push_str("// Backward Conditional Jump to maintain short execution\n");
|
||||
}
|
||||
|
||||
assembly_block
|
||||
}
|
||||
|
||||
pub fn junk_asm(input: TokenStream) -> TokenStream {
|
||||
// 1. Parse the input (expecting an optional f32 weight)
|
||||
let weight: f64 = if input.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match parse_macro_input!(input as LitFloat).base10_parse::<f64>() {
|
||||
Ok(w) => Some(w), // Clamp to a sensible range
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
.expect("Expected F64");
|
||||
// let final_weight = input_weight.unwrap_or(WEIGHT);
|
||||
|
||||
// 2. Setup
|
||||
let mut rng = SmallRng::from_os_rng();
|
||||
let mut id_counter = 0;
|
||||
// let random_u64_addr: u64 = rng.next_u64(); // The simulated external address
|
||||
|
||||
// 3. Generate Assembly
|
||||
let main_assembly = {
|
||||
loop {
|
||||
let res = generate_dag_block(weight, &mut rng, 0, &mut id_counter);
|
||||
if res.len() as f64 > weight * MIN_LENGTH {
|
||||
break res;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
println!("{}", main_assembly);
|
||||
|
||||
// 4. Wrap in `asm!`
|
||||
let expanded = quote! {
|
||||
// Output will replace the junk_asm!(...) call
|
||||
{
|
||||
// Execute the code using the standard `asm!` macro.
|
||||
unsafe {
|
||||
#[allow(named_asm_labels)]
|
||||
core::arch::asm!(
|
||||
// The generated junk code
|
||||
// Note: We MUST use AT&T syntax (e.g., %rax, $100) due to options(att_syntax)
|
||||
// The code is generated in AT&T syntax.
|
||||
#main_assembly,
|
||||
|
||||
// Pass the simulated external address into a temporary register (%r15)
|
||||
// This allows instructions to reference an "external" scope using memory reads/writes.
|
||||
// in(reg) external_addr_ref,
|
||||
|
||||
// Clobber all general-purpose registers to force saving/restoring
|
||||
clobber_abi("sysv64"),
|
||||
|
||||
// Correct options for non-volatile junk code
|
||||
options(att_syntax, nomem, nostack, preserves_flags)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expanded.into()
|
||||
}
|
||||
|
||||
// NOTE: To make the example runnable, the `src/main.rs` file would now call
|
||||
// junk_asm!(0.2) or junk_asm!(). The instruction sizes and jump structure
|
||||
// are now compliant with your requirements.
|
||||
+19
-143
@@ -3,171 +3,47 @@
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{ItemFn, parse_macro_input};
|
||||
use syn::parse_macro_input;
|
||||
|
||||
mod format_helper;
|
||||
use format_helper::*;
|
||||
|
||||
use syn::LitStr;
|
||||
mod junk_asm;
|
||||
|
||||
// Put all encrypt-related dependencies in a module, so they are easier to use with the feature flag
|
||||
#[allow(dead_code)]
|
||||
mod no_obfuscate;
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod obfuscate;
|
||||
|
||||
#[cfg(not(feature = "obfuscate"))]
|
||||
use no_obfuscate as obs;
|
||||
#[cfg(feature = "obfuscate")]
|
||||
mod obs_deps {
|
||||
pub use unshell_crypt::BACKUP_ENV_KEY;
|
||||
pub use unshell_crypt::ENV_KEY_NAME;
|
||||
pub use unshell_crypt::STATIC_IV;
|
||||
pub use unshell_crypt::aes::encrypt_aes_lines;
|
||||
pub use unshell_crypt::fill;
|
||||
}
|
||||
#[cfg(feature = "obfuscate")]
|
||||
use obs_deps::*;
|
||||
use obfuscate as obs;
|
||||
|
||||
// String obfuscation
|
||||
|
||||
#[proc_macro]
|
||||
#[cfg(not(feature = "obfuscate"))]
|
||||
pub fn obs(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as LitStr);
|
||||
|
||||
(quote::quote! {
|
||||
String::from(#input)
|
||||
})
|
||||
.into()
|
||||
obs::obs(input)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
#[cfg(not(feature = "obfuscate"))]
|
||||
pub fn obfuscated_symbol(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let func = parse_macro_input!(item as ItemFn);
|
||||
TokenStream::from(quote! {
|
||||
#[unsafe(no_mangle)]
|
||||
#func
|
||||
})
|
||||
obs::obfuscated_symbol(_attr, item)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
#[cfg(not(feature = "obfuscate"))]
|
||||
pub fn symbol(input: TokenStream) -> TokenStream {
|
||||
input
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
#[cfg(feature = "obfuscate")]
|
||||
pub fn obfuscated_symbol(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Parse the input function
|
||||
|
||||
let func = parse_macro_input!(item as ItemFn);
|
||||
|
||||
// Get the original function name
|
||||
let fn_name = func.sig.ident.to_string();
|
||||
|
||||
// get the encryption key
|
||||
let key_str = std::env::var(ENV_KEY_NAME).unwrap_or(BACKUP_ENV_KEY.to_owned());
|
||||
|
||||
// Generate the new, obfuscated name
|
||||
let obfuscated_name = encrypt_aes_lines(&fn_name, &key_str, STATIC_IV);
|
||||
|
||||
// Create a new string literal for the name
|
||||
let new_name_lit = LitStr::new(&obfuscated_name, func.sig.ident.span());
|
||||
|
||||
// Re-build the function, but add #[no_mangle]
|
||||
// and rename the *exported* symbol via #[export_name]
|
||||
TokenStream::from(quote! {
|
||||
#[unsafe(export_name = #new_name_lit)]
|
||||
#func
|
||||
})
|
||||
}
|
||||
|
||||
// --- NEW MACRO 2: The macro for the loader ---
|
||||
|
||||
#[proc_macro]
|
||||
#[cfg(feature = "obfuscate")]
|
||||
pub fn symbol(input: TokenStream) -> TokenStream {
|
||||
// Parse the input as a string literal
|
||||
let lit_str = parse_macro_input!(input as LitStr);
|
||||
let original_name = lit_str.value();
|
||||
|
||||
// get the encryption key
|
||||
let key_str = std::env::var(ENV_KEY_NAME).unwrap_or(BACKUP_ENV_KEY.to_owned());
|
||||
|
||||
// Generate the exact same obfuscated name
|
||||
let obfuscated_name = encrypt_aes_lines(&original_name, &key_str, STATIC_IV);
|
||||
|
||||
// Expand to a static string literal
|
||||
TokenStream::from(quote! {
|
||||
#obfuscated_name
|
||||
})
|
||||
obs::symbol(input)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
#[cfg(feature = "obfuscate")]
|
||||
pub fn obs(input: TokenStream) -> TokenStream {
|
||||
// Parse the input as a string literal
|
||||
let lit_str = parse_macro_input!(input as LitStr);
|
||||
let original_str = lit_str.value();
|
||||
|
||||
// Handle empty strings explicitly
|
||||
if original_str.is_empty() {
|
||||
return TokenStream::from(quote! { String::new() });
|
||||
}
|
||||
|
||||
// --- Obfuscated Branch Logic ---
|
||||
// This code runs at compile-time
|
||||
|
||||
let str_bytes = original_str.as_bytes();
|
||||
let len = str_bytes.len();
|
||||
|
||||
// 1. Generate a unique, random key for this string
|
||||
let mut key = vec![0u8; len];
|
||||
fill(&mut key).expect("Failed to get random bytes for XOR key");
|
||||
|
||||
// 2. XOR the string with the key
|
||||
let mut obfuscated = Vec::with_capacity(len);
|
||||
for i in 0..len {
|
||||
obfuscated.push(str_bytes[i] ^ key[i]);
|
||||
}
|
||||
|
||||
// 3. This is the code that will be injected into the user's binary
|
||||
// It runs at *runtime* to decrypt the string.
|
||||
let obfuscated_expansion = quote! {
|
||||
{
|
||||
// These static arrays are stored directly in your binary
|
||||
static OBFUSCATED_DATA: [u8; #len] = [ #( #obfuscated ),* ];
|
||||
static KEY_DATA: [u8; #len] = [ #( #key ),* ];
|
||||
|
||||
let mut decrypted = Vec::with_capacity(#len);
|
||||
for i in 0..#len {
|
||||
decrypted.push(OBFUSCATED_DATA[i] ^ KEY_DATA[i]);
|
||||
}
|
||||
|
||||
// We can trust this since the source was a valid String literal
|
||||
String::from_utf8(decrypted).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(obfuscated_expansion)
|
||||
pub fn junk_asm(input: TokenStream) -> TokenStream {
|
||||
junk_asm::junk_asm(input)
|
||||
}
|
||||
|
||||
// #[proc_macro]
|
||||
// pub fn file_literal(_input: TokenStream) -> TokenStream {
|
||||
// // let input = input.into_iter().collect::<Vec<_>>();
|
||||
// // if input.len() != 1 {
|
||||
// // let msg = format!("expected exactly one input token, got {}", input.len());
|
||||
// // return quote! { compile_error!(#msg) }.into();
|
||||
// // }
|
||||
|
||||
// let string = file!();
|
||||
// let lit_str = LitStr::new(string, proc_macro2::Span::call_site());
|
||||
|
||||
// // let string_lit = match LitStr::try_from(&input) {
|
||||
// // // Error if the token is not a string literal
|
||||
// // Err(e) => return e.to_compile_error(),
|
||||
// // Ok(lit) => lit,
|
||||
// // };
|
||||
|
||||
// (quote! {
|
||||
// #lit_str
|
||||
// })
|
||||
// .into()
|
||||
// }
|
||||
//
|
||||
|
||||
#[proc_macro]
|
||||
pub fn file_symbol(_input: TokenStream) -> TokenStream {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{ItemFn, LitStr, parse_macro_input};
|
||||
|
||||
pub fn obs(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as LitStr);
|
||||
|
||||
(quote::quote! {
|
||||
String::from(#input)
|
||||
})
|
||||
.into()
|
||||
}
|
||||
pub fn obfuscated_symbol(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let func = parse_macro_input!(item as ItemFn);
|
||||
TokenStream::from(quote! {
|
||||
#[unsafe(no_mangle)]
|
||||
#func
|
||||
})
|
||||
}
|
||||
|
||||
pub fn symbol(input: TokenStream) -> TokenStream {
|
||||
input
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{ItemFn, LitStr, parse_macro_input};
|
||||
use unshell_crypt::{BACKUP_ENV_KEY, ENV_KEY_NAME, STATIC_IV, aes::encrypt_aes_lines, fill};
|
||||
|
||||
#[static_init::dynamic]
|
||||
static KEY: String = {
|
||||
std::env::var(ENV_KEY_NAME).unwrap_or({
|
||||
// Diagnostic::new(proc_macro::Level::Warning, "Using default encryption key!").emit();
|
||||
|
||||
println!("Using default encryption key!");
|
||||
|
||||
BACKUP_ENV_KEY.to_owned()
|
||||
})
|
||||
};
|
||||
|
||||
pub fn obfuscated_symbol(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Parse the input function
|
||||
|
||||
let func = parse_macro_input!(item as ItemFn);
|
||||
|
||||
// Get the original function name
|
||||
let fn_name = func.sig.ident.to_string();
|
||||
|
||||
// Generate the new, obfuscated name
|
||||
let obfuscated_name = encrypt_aes_lines(&fn_name, &KEY, STATIC_IV);
|
||||
|
||||
// Create a new string literal for the name
|
||||
let new_name_lit = LitStr::new(&obfuscated_name, func.sig.ident.span());
|
||||
|
||||
// Re-build the function, but add #[no_mangle]
|
||||
// and rename the *exported* symbol via #[export_name]
|
||||
TokenStream::from(quote! {
|
||||
#[unsafe(export_name = #new_name_lit)]
|
||||
#func
|
||||
})
|
||||
}
|
||||
|
||||
pub fn symbol(input: TokenStream) -> TokenStream {
|
||||
// Parse the input as a string literal
|
||||
let lit_str = parse_macro_input!(input as LitStr);
|
||||
let original_name = lit_str.value();
|
||||
|
||||
// Generate the exact same obfuscated name
|
||||
let obfuscated_name = encrypt_aes_lines(&original_name, &KEY, STATIC_IV);
|
||||
|
||||
// Expand to a static string literal
|
||||
TokenStream::from(quote! {
|
||||
#obfuscated_name
|
||||
})
|
||||
}
|
||||
|
||||
pub fn obs(input: TokenStream) -> TokenStream {
|
||||
// Parse the input as a string literal
|
||||
let lit_str = parse_macro_input!(input as LitStr);
|
||||
let original_str = lit_str.value();
|
||||
|
||||
// Handle empty strings explicitly
|
||||
if original_str.is_empty() {
|
||||
return TokenStream::from(quote! { String::new() });
|
||||
}
|
||||
|
||||
// --- Obfuscated Branch Logic ---
|
||||
// This code runs at compile-time
|
||||
|
||||
let str_bytes = original_str.as_bytes();
|
||||
let len = str_bytes.len();
|
||||
|
||||
// 1. Generate a unique, random key for this string
|
||||
let mut key = vec![0u8; len];
|
||||
fill(&mut key).expect("Failed to get random bytes for XOR key");
|
||||
|
||||
// 2. XOR the string with the key
|
||||
let mut obfuscated = Vec::with_capacity(len);
|
||||
for i in 0..len {
|
||||
obfuscated.push(str_bytes[i] ^ key[i]);
|
||||
}
|
||||
|
||||
// 3. This is the code that will be injected into the user's binary
|
||||
// It runs at *runtime* to decrypt the string.
|
||||
let obfuscated_expansion = quote! {
|
||||
{
|
||||
// These static arrays are stored directly in your binary
|
||||
static OBFUSCATED_DATA: [u8; #len] = [ #( #obfuscated ),* ];
|
||||
static KEY_DATA: [u8; #len] = [ #( #key ),* ];
|
||||
|
||||
let mut decrypted = Vec::with_capacity(#len);
|
||||
for i in 0..#len {
|
||||
decrypted.push(OBFUSCATED_DATA[i] ^ KEY_DATA[i]);
|
||||
}
|
||||
|
||||
// We can trust this since the source was a valid String literal
|
||||
String::from_utf8(decrypted).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(obfuscated_expansion)
|
||||
}
|
||||
Reference in New Issue
Block a user