use std::{any::Any, collections::HashSet, sync::Arc};
use crate::{
cilassembly::GeneratorConfig,
compiler::{EventKind, EventLog},
deobfuscation::{
techniques::{
confuserex::helpers, Detection, Detections, Evidence, Technique, TechniqueCategory,
WorkingAssembly,
},
utils::find_methods_calling_apis,
},
emulation::{EmulationOutcome, ProcessBuilder},
error::Error,
metadata::{
method::MethodImplCodeType,
tables::{FieldRvaRaw, MethodDefRaw, TableDataOwned, TableId},
token::Token,
validation::ValidationConfig,
},
CilObject, Result,
};
#[derive(Debug)]
pub struct AntiTamperFindings {
pub initializer_token: Option<Token>,
pub encrypted_method_count: usize,
pub encrypted_section_names: Vec<String>,
}
pub struct ConfuserExAntiTamper;
impl Technique for ConfuserExAntiTamper {
fn id(&self) -> &'static str {
"confuserex.tamper"
}
fn name(&self) -> &'static str {
"ConfuserEx Anti-Tamper Decryption"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Protection
}
fn requires(&self) -> &[&'static str] {
&["confuserex.metadata"]
}
fn detect(&self, assembly: &CilObject) -> Detection {
let Some(tables) = assembly.tables() else {
return Detection::new_empty();
};
let mut encrypted_count = 0usize;
const PAT_VIRTUAL_PROTECT: usize = 0;
const PAT_GET_HINSTANCE: usize = 1;
const PAT_GET_MODULE: usize = 2;
const PAT_MARSHAL_COPY: usize = 3;
let api_hits = find_methods_calling_apis(
assembly,
&[
"VirtualProtect",
"GetHINSTANCE",
"get_Module",
"Marshal.Copy",
],
);
let pinvoke_vp_tokens = helpers::resolve_pinvoke_tokens(assembly, "VirtualProtect");
let pinvoke_callers = helpers::find_methods_calling_tokens(assembly, &pinvoke_vp_tokens);
let initializer_token = api_hits
.iter()
.find(|(token, indices)| {
let has_virtualprotect =
indices.contains(&PAT_VIRTUAL_PROTECT) || pinvoke_callers.contains(token);
let has_gethinstance = indices.contains(&PAT_GET_HINSTANCE);
let has_get_module = indices.contains(&PAT_GET_MODULE);
let has_marshal_copy = indices.contains(&PAT_MARSHAL_COPY);
has_virtualprotect && has_gethinstance && has_get_module && !has_marshal_copy
})
.map(|(token, _)| *token);
let mut encrypted_section_names = HashSet::new();
if let Some(method_table) = tables.table::<MethodDefRaw>() {
let file = assembly.file();
let sections = file.sections();
let text_section = sections.iter().find(|s| s.name.starts_with(".text"));
if let Some(text) = text_section {
let text_rva_start = text.virtual_address as usize;
let text_rva_end = text_rva_start + text.virtual_size as usize;
for row in method_table {
if row.rva == 0 {
continue;
}
let code_type = MethodImplCodeType::from_impl_flags(row.impl_flags);
if code_type.contains(MethodImplCodeType::NATIVE)
|| code_type.contains(MethodImplCodeType::RUNTIME)
{
continue;
}
let method_rva = row.rva as usize;
if method_rva < text_rva_start || method_rva >= text_rva_end {
encrypted_count += 1;
for section in sections {
let sec_start = section.virtual_address as usize;
let sec_end = sec_start + section.virtual_size as usize;
if method_rva >= sec_start && method_rva < sec_end {
encrypted_section_names.insert(section.name.clone());
break;
}
}
}
}
}
}
if initializer_token.is_none() && encrypted_count == 0 {
return Detection::new_empty();
}
let mut evidence = Vec::new();
if let Some(token) = initializer_token {
evidence.push(Evidence::BytecodePattern(format!(
"Anti-tamper initializer at 0x{:08X}",
token.value(),
)));
}
if encrypted_count > 0 {
evidence.push(Evidence::BytecodePattern(format!(
"{encrypted_count} method bodies in encrypted section",
)));
}
{
let file = assembly.file();
for section in file.sections() {
if section.name.is_empty() {
encrypted_section_names.insert(section.name.clone());
}
}
}
let encrypted_section_names: Vec<String> = encrypted_section_names.into_iter().collect();
let findings = AntiTamperFindings {
initializer_token,
encrypted_method_count: encrypted_count,
encrypted_section_names: encrypted_section_names.clone(),
};
let mut detection = Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
);
if let Some(token) = initializer_token {
detection.cleanup_mut().add_method(token);
}
for name in &encrypted_section_names {
detection.cleanup_mut().exclude_section(name);
}
detection
}
fn byte_transform(
&self,
assembly: &mut WorkingAssembly,
detection: &Detection,
_detections: &Detections,
) -> Option<Result<EventLog>> {
let events = EventLog::new();
let Some(findings) = detection.findings::<AntiTamperFindings>() else {
return Some(Ok(events));
};
if findings.encrypted_method_count == 0 {
return Some(Ok(events));
}
let co = match assembly.cilobject() {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let bytes = co.file().data().to_vec();
let cilobject =
match CilObject::from_mem_with_validation(bytes, ValidationConfig::analysis()) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let cilobject_arc = Arc::new(cilobject);
let decryptor_method = match findings.initializer_token.ok_or_else(|| {
Error::Deobfuscation("No anti-tamper initialization method found".to_string())
}) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let encrypted_methods = helpers::find_encrypted_methods(&cilobject_arc);
let process = match ProcessBuilder::new()
.assembly_arc(Arc::clone(&cilobject_arc))
.name("anti-tamper-emulation")
.with_max_instructions(10_000_000)
.with_max_call_depth(200)
.with_timeout_ms(120_000) .build()
{
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let loaded_image = match process
.primary_image()
.ok_or_else(|| Error::Deobfuscation("Failed to get loaded PE image info".to_string()))
{
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let pe_base = loaded_image.base_address;
#[allow(clippy::cast_possible_truncation)]
let virtual_size = loaded_image.size_of_image as usize;
let outcome = match process.execute_method(decryptor_method, vec![]) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let instructions_executed = match outcome {
EmulationOutcome::Completed { instructions, .. }
| EmulationOutcome::Breakpoint { instructions, .. } => instructions,
EmulationOutcome::LimitReached { limit, .. } => {
return Some(Err(Error::Deobfuscation(format!(
"Anti-tamper emulation exceeded limit: {limit:?}"
))));
}
EmulationOutcome::Stopped { reason, .. } => {
return Some(Err(Error::Deobfuscation(format!(
"Anti-tamper emulation stopped: {reason}"
))));
}
EmulationOutcome::UnhandledException { exception, .. } => {
return Some(Err(Error::Deobfuscation(format!(
"Anti-tamper emulation threw exception: {exception:?}"
))));
}
EmulationOutcome::RequiresSymbolic { reason, .. } => {
return Some(Err(Error::Deobfuscation(format!(
"Anti-tamper emulation requires symbolic execution: {reason}"
))));
}
};
let virtual_image = match process.read_memory(pe_base, virtual_size) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
drop(process);
log::info!(
"Anti-tamper emulation completed: {} instructions executed via method 0x{:08x}",
instructions_executed,
decryptor_method.value()
);
let all_methods_with_rva = helpers::find_all_methods_with_rva(&cilobject_arc);
let (extracted_bodies, body_failures) =
extract_decrypted_bodies(&cilobject_arc, &virtual_image, &all_methods_with_rva);
if extracted_bodies.is_empty() {
return Some(Err(Error::Deobfuscation(
"No method bodies could be extracted from decrypted image".to_string(),
)));
}
if body_failures > 0 {
log::warn!(
"Failed to extract {} method bodies from decrypted image",
body_failures
);
}
let (extracted_fields, field_failures) =
helpers::extract_decrypted_field_data(&cilobject_arc, &virtual_image);
if field_failures > 0 {
log::warn!(
"Failed to extract {} field data entries from decrypted image",
field_failures
);
}
let encrypted_count = encrypted_methods.len();
for &token in &encrypted_methods {
events
.record(EventKind::MethodBodyDecrypted)
.method(token)
.message(format!("Decrypted method body 0x{:08x}", token.value()));
}
let cilobject = match Arc::try_unwrap(cilobject_arc)
.map_err(|_| Error::Deobfuscation("Assembly still shared after emulation".into()))
{
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let mut cil_assembly = cilobject.into_assembly();
for (method_token, body_bytes) in extracted_bodies {
let placeholder_rva = cil_assembly.store_method_body(body_bytes);
let rid = method_token.row();
#[allow(clippy::redundant_closure_for_method_calls)]
let existing_row = match cil_assembly
.view()
.tables()
.and_then(|t| t.table::<MethodDefRaw>())
.and_then(|table| table.get(rid))
.ok_or_else(|| Error::Deobfuscation(format!("MethodDef row {rid} not found")))
{
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let updated_row = MethodDefRaw {
rid: existing_row.rid,
token: existing_row.token,
offset: existing_row.offset,
rva: placeholder_rva,
impl_flags: existing_row.impl_flags,
flags: existing_row.flags,
name: existing_row.name,
signature: existing_row.signature,
param_list: existing_row.param_list,
};
if let Err(e) = cil_assembly.table_row_update(
TableId::MethodDef,
rid,
TableDataOwned::MethodDef(updated_row),
) {
return Some(Err(e));
}
}
let field_count = extracted_fields.len();
for (rid, _original_rva, data) in extracted_fields {
let placeholder_rva = cil_assembly.store_field_data(data);
#[allow(clippy::redundant_closure_for_method_calls)]
let existing_row = match cil_assembly
.view()
.tables()
.and_then(|t| t.table::<FieldRvaRaw>())
.and_then(|table| table.get(rid))
.ok_or_else(|| Error::Deobfuscation(format!("FieldRVA row {rid} not found")))
{
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let updated_row = FieldRvaRaw {
rid: existing_row.rid,
token: existing_row.token,
offset: existing_row.offset,
rva: placeholder_rva,
field: existing_row.field,
};
if let Err(e) = cil_assembly.table_row_update(
TableId::FieldRVA,
rid,
TableDataOwned::FieldRVA(updated_row),
) {
return Some(Err(e));
}
}
events.record(EventKind::AntiTamperRemoved).message(format!(
"Anti-tamper protection removed: {} method bodies, {} field data entries decrypted",
encrypted_count, field_count,
));
let excluded: HashSet<String> = findings.encrypted_section_names.iter().cloned().collect();
let config = GeneratorConfig::default()
.with_skip_original_method_bodies(true)
.with_excluded_sections(excluded);
let new_assembly =
match cil_assembly.into_cilobject_with(ValidationConfig::analysis(), config) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
assembly.replace_assembly(new_assembly);
Some(Ok(events))
}
fn requires_regeneration(&self) -> bool {
true
}
}
fn extract_decrypted_bodies(
assembly: &CilObject,
virtual_image: &[u8],
methods: &[Token],
) -> (Vec<(Token, Vec<u8>)>, usize) {
let mut bodies = Vec::new();
let mut failed_count = 0;
for &token in methods {
let Some(rva) = helpers::get_method_rva(assembly, token) else {
failed_count += 1;
continue;
};
if rva == 0 || rva as usize >= virtual_image.len() {
failed_count += 1;
continue;
}
match helpers::extract_method_body_at_rva(virtual_image, rva) {
Some(body_bytes) => {
bodies.push((token, body_bytes));
}
None => {
failed_count += 1;
}
}
}
(bodies, failed_count)
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{
confuserex::tamper::{AntiTamperFindings, ConfuserExAntiTamper},
Technique,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_positive() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_antitamper.exe");
let technique = ConfuserExAntiTamper;
let detection = technique.detect(&assembly);
assert!(
detection.is_detected(),
"ConfuserExAntiTamper should detect anti-tamper in mkaring_antitamper.exe"
);
assert!(
!detection.evidence().is_empty(),
"Detection should have evidence"
);
let findings = detection
.findings::<AntiTamperFindings>()
.expect("Should have AntiTamperFindings");
assert!(
findings.encrypted_method_count > 0 || findings.initializer_token.is_some(),
"Should have encrypted methods or an initializer"
);
}
#[test]
fn test_detect_negative() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = ConfuserExAntiTamper;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"ConfuserExAntiTamper should not detect anti-tamper in original.exe"
);
}
}