use std::{
collections::HashSet,
sync::{Arc, RwLock},
};
use cowfile::CowFile;
use log::debug;
use crate::{
deobfuscation::{
config::EngineConfig, context::HookFactory, statemachine::StateMachineProvider,
},
emulation::{
EmValue, EmulationError, EmulationOutcome, EmulationProcess, Hook, HookPriority,
PreHookResult, ProcessBuilder,
},
metadata::{tables::ModuleRaw, token::Token},
CilObject, Error, Result,
};
pub struct EmulationTemplatePool {
template: RwLock<Option<EmulationProcess>>,
assembly: RwLock<Option<Arc<CilObject>>>,
original_pe_cow: CowFile,
hooks: Arc<boxcar::Vec<HookFactory>>,
warmup_methods: Arc<boxcar::Vec<(Token, Vec<EmValue>)>>,
statemachine_providers: Arc<boxcar::Vec<Arc<dyn StateMachineProvider>>>,
config: EngineConfig,
}
impl EmulationTemplatePool {
#[must_use]
pub fn new(
assembly: Arc<CilObject>,
original_pe_cow: CowFile,
hooks: Arc<boxcar::Vec<HookFactory>>,
warmup_methods: Arc<boxcar::Vec<(Token, Vec<EmValue>)>>,
statemachine_providers: Arc<boxcar::Vec<Arc<dyn StateMachineProvider>>>,
config: EngineConfig,
) -> Self {
Self {
template: RwLock::new(None),
assembly: RwLock::new(Some(assembly)),
original_pe_cow,
hooks,
warmup_methods,
statemachine_providers,
config,
}
}
pub fn warmup(&self) -> Result<()> {
let assembly = self
.assembly
.read()
.map_err(|e| Error::LockError(format!("template pool assembly read lock: {e}")))?
.as_ref()
.ok_or_else(|| Error::Deobfuscation("template pool assembly already released".into()))?
.clone();
let warmup_instruction_limit = self.config.emulation.max_instructions;
let mut builder = ProcessBuilder::new()
.assembly_arc(assembly.clone())
.with_max_instructions(warmup_instruction_limit)
.with_max_call_depth(100)
.with_timeout_ms(self.config.emulation.warmup_timeout.as_millis() as u64)
.with_max_heap_bytes(512 * 1024 * 1024)
.name("template_pool");
if let Some(ref tracing) = self.config.emulation.tracing {
let pool_tracing = tracing.clone().with_context("template_warmup");
builder = builder.with_tracing(pool_tracing);
}
for (_, hook_factory) in self.hooks.iter() {
builder = builder.hook((hook_factory.factory)());
}
builder = builder.hook(
Hook::new("bypass-tamper-verify-hash")
.match_name(
"System.Security.Cryptography",
"RSACryptoServiceProvider",
"VerifyHash",
)
.with_priority(HookPriority::HIGH)
.pre(|_ctx, _thread| PreHookResult::Bypass(Some(EmValue::I32(1)))),
);
if let Ok(pe_cow) = self.original_pe_cow.fork() {
builder = builder.with_virtual_file_cow("assembly.exe", pe_cow);
}
if let Some(path) = self.original_pe_cow.source_path() {
if let Some(filename) = path.file_name() {
if let Ok(pe_cow2) = self.original_pe_cow.fork() {
builder = builder.with_virtual_file_cow(&filename.to_string_lossy(), pe_cow2);
}
}
}
let module_name = assembly.module().map(|m| m.name.clone()).or_else(|| {
let tables = assembly.tables()?;
let strings = assembly.strings()?;
let module_table = tables.table::<ModuleRaw>()?;
let module_row = module_table.iter().next()?;
strings.get(module_row.name as usize).ok().map(String::from)
});
if let Some(ref name) = module_name {
if let Ok(pe_cow) = self.original_pe_cow.fork() {
builder = builder.with_virtual_file_cow(name, pe_cow);
}
}
let mut process = builder.build()?;
if let Some(module_cctor) = assembly.types().module_cctor() {
let fork = process.fork()?;
match fork.execute_method(module_cctor, vec![]) {
Ok(EmulationOutcome::Completed { instructions, .. }) => {
debug!(
"Template warmup: <Module>.cctor completed ({} instructions)",
instructions
);
process = fork;
}
Ok(outcome) => {
debug!(
"Template warmup: <Module>.cctor did not complete: {} — adopting partial state",
outcome
);
process = fork;
}
Err(e) => {
debug!(
"Template warmup: <Module>.cctor error: {} — adopting partial state",
e
);
process = fork;
}
}
}
self.run_warmup_methods(&mut process);
let mut guard = self
.template
.write()
.map_err(|e| Error::LockError(format!("template pool write lock: {e}")))?;
*guard = Some(process);
Ok(())
}
pub fn fork(&self) -> Result<EmulationProcess> {
let guard = self
.template
.read()
.map_err(|e| Error::LockError(format!("template pool read lock: {e}")))?;
match *guard {
Some(ref template) => template.fork(),
None => Err(Error::Emulation(Box::new(EmulationError::InternalError {
description: "template pool not warmed up".to_string(),
}))),
}
}
pub fn fork_for_targeted_warmup(&self, cctors: &[Token]) -> Option<EmulationProcess> {
let guard = self.template.read().ok()?;
let template = guard.as_ref()?;
let mut process = match template.fork() {
Ok(p) => p,
Err(e) => {
debug!("Targeted warmup: failed to fork template: {e}");
return None;
}
};
if cctors.is_empty() {
return Some(process);
}
let mut completed: HashSet<Token> = HashSet::new();
let mut permanently_failed: HashSet<Token> = HashSet::new();
let mut pass = 0;
loop {
pass += 1;
let mut new_completions = 0;
for cctor in cctors {
if completed.contains(cctor) || permanently_failed.contains(cctor) {
continue;
}
let Ok(fork) = process.fork() else {
continue;
};
match fork.execute_method(*cctor, vec![]) {
Ok(EmulationOutcome::Completed { .. }) => {
debug!(
"Targeted warmup: .cctor 0x{:08X} completed (pass {})",
cctor.value(),
pass
);
process = fork;
completed.insert(*cctor);
new_completions += 1;
}
Ok(EmulationOutcome::UnhandledException { instructions, .. }) => {
debug!(
"Targeted warmup: .cctor 0x{:08X} threw after {} instructions — adopting partial state (pass {})",
cctor.value(), instructions, pass
);
process = fork;
permanently_failed.insert(*cctor);
new_completions += 1;
}
Ok(EmulationOutcome::LimitReached { ref limit, .. }) => {
debug!(
"Targeted warmup: .cctor 0x{:08X} hit limit: {} — adopting partial state (pass {})",
cctor.value(), limit, pass
);
process = fork;
permanently_failed.insert(*cctor);
new_completions += 1;
}
Ok(outcome) => {
debug!(
"Targeted warmup: .cctor 0x{:08X} did not complete: {} (pass {})",
cctor.value(),
outcome,
pass
);
permanently_failed.insert(*cctor);
}
Err(ref e) => {
let is_resource_limit = matches!(
e,
Error::Emulation(em) if matches!(
**em,
EmulationError::Timeout { .. }
| EmulationError::InstructionLimitExceeded { .. }
| EmulationError::HeapMemoryLimitExceeded { .. }
)
);
if is_resource_limit {
process = fork;
permanently_failed.insert(*cctor);
new_completions += 1;
} else if pass == 1 {
debug!(
"Targeted warmup: .cctor 0x{:08X} failed: {} (pass {})",
cctor.value(),
e,
pass
);
}
}
}
}
if new_completions == 0 || pass >= 5 {
break;
}
}
Some(process)
}
#[must_use]
pub fn assembly(&self) -> Option<Arc<CilObject>> {
self.assembly.read().ok()?.clone()
}
#[must_use]
pub fn statemachine_providers(&self) -> &Arc<boxcar::Vec<Arc<dyn StateMachineProvider>>> {
&self.statemachine_providers
}
pub fn release(&self) {
if let Ok(mut guard) = self.template.write() {
*guard = None;
}
if let Ok(mut guard) = self.assembly.write() {
*guard = None;
}
}
fn run_warmup_methods(&self, process: &mut EmulationProcess) {
let warmup_methods: Vec<(Token, Vec<EmValue>)> = self
.warmup_methods
.iter()
.map(|(_, entry)| entry.clone())
.collect();
if warmup_methods.is_empty() {
return;
}
let mut completed = HashSet::new();
let mut permanently_failed = HashSet::new();
for pass in 1..=self.config.emulation.warmup_retry_passes {
let mut new_completions = 0;
for (warmup_token, warmup_args) in &warmup_methods {
if completed.contains(warmup_token) || permanently_failed.contains(warmup_token) {
continue;
}
let Ok(fork) = process.fork() else {
continue;
};
match fork.execute_method(*warmup_token, warmup_args.clone()) {
Ok(EmulationOutcome::Completed { instructions, .. }) => {
debug!(
"Template warmup: 0x{:08X} completed (pass {}, {} instructions)",
warmup_token.value(),
pass,
instructions
);
*process = fork;
completed.insert(*warmup_token);
new_completions += 1;
}
Ok(EmulationOutcome::UnhandledException { instructions, .. }) => {
debug!(
"Template warmup: 0x{:08X} threw after {} instructions — adopting partial state (pass {})",
warmup_token.value(), instructions, pass
);
*process = fork;
permanently_failed.insert(*warmup_token);
new_completions += 1;
}
Ok(EmulationOutcome::LimitReached { ref limit, .. }) => {
debug!(
"Template warmup: 0x{:08X} hit limit: {} — adopting partial state (pass {})",
warmup_token.value(), limit, pass
);
*process = fork;
permanently_failed.insert(*warmup_token);
new_completions += 1;
}
Ok(outcome) => {
debug!(
"Template warmup: 0x{:08X} did not complete: {} (pass {})",
warmup_token.value(),
outcome,
pass
);
permanently_failed.insert(*warmup_token);
}
Err(ref e) => {
let is_resource_limit = matches!(
e,
Error::Emulation(em) if matches!(
**em,
EmulationError::Timeout { .. }
| EmulationError::InstructionLimitExceeded { .. }
| EmulationError::HeapMemoryLimitExceeded { .. }
)
);
if is_resource_limit {
debug!(
"Template warmup: 0x{:08X} hit resource limit: {} — adopting partial state (pass {})",
warmup_token.value(), e, pass
);
*process = fork;
permanently_failed.insert(*warmup_token);
new_completions += 1;
} else if pass == 1 {
debug!(
"Template warmup: 0x{:08X} failed: {} (pass {})",
warmup_token.value(),
e,
pass
);
}
}
}
}
if new_completions == 0 {
break;
}
}
debug!(
"Template warmup: {}/{} methods completed",
completed.len(),
warmup_methods.len(),
);
}
}