use std::sync::Arc;
use crate::{
assembly::{opcodes, Operand},
cilassembly::{CilAssembly, GeneratorConfig},
compiler::{EventKind, EventLog},
deobfuscation::{
detection::{DetectionEvidence, DetectionScore},
findings::DeobfuscationFindings,
obfuscators::confuserex::{
candidates::{find_candidates, ProtectionType},
utils,
},
},
emulation::{EmulationOutcome, ProcessBuilder, TracingConfig},
error::Error,
metadata::{
tables::{FieldRvaRaw, MethodDefRaw, TableDataOwned, TableId},
token::Token,
validation::ValidationConfig,
},
CilObject, Result,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AntiTamperMode {
Normal,
Anti,
Jit,
Unknown,
}
#[derive(Debug, Clone)]
pub struct AntiTamperMethodInfo {
pub token: Token,
pub mode: AntiTamperMode,
pub calls_virtualprotect: bool,
pub calls_gethinstance: bool,
pub calls_get_module: bool,
pub calls_check_debugger: bool,
pub calls_loadlibrary: bool,
pub calls_getprocaddress: bool,
pub calls_failfast: bool,
}
#[derive(Debug, Default)]
pub struct AntiTamperDetectionResult {
pub methods: Vec<AntiTamperMethodInfo>,
pub encrypted_method_count: usize,
pub detected_mode: Option<AntiTamperMode>,
pub pinvoke_methods: Vec<Token>,
}
impl AntiTamperDetectionResult {
pub fn is_detected(&self) -> bool {
!self.methods.is_empty() || self.encrypted_method_count > 0
}
pub fn best_init_method(&self) -> Option<Token> {
self.methods
.iter()
.filter(|m| {
let indicators = [
m.calls_virtualprotect,
m.calls_gethinstance,
m.calls_get_module,
m.calls_loadlibrary && m.calls_getprocaddress,
]
.iter()
.filter(|&&x| x)
.count();
indicators >= 2
})
.map(|m| m.token)
.next()
.or_else(|| self.methods.first().map(|m| m.token))
}
}
pub fn detect(assembly: &CilObject, score: &DetectionScore, findings: &mut DeobfuscationFindings) {
let result = detect_antitamper(assembly);
for method_info in &result.methods {
findings.anti_tamper_methods.push(method_info.token);
}
findings.encrypted_method_count = result.encrypted_method_count;
add_evidence(&result, score);
}
pub fn detect_antitamper(assembly: &CilObject) -> AntiTamperDetectionResult {
let mut result = AntiTamperDetectionResult::default();
result.encrypted_method_count = utils::find_encrypted_methods(assembly).len();
result.pinvoke_methods = find_antitamper_pinvokes(assembly);
result.methods = find_antitamper_methods(assembly);
result.detected_mode = determine_mode(&result);
result
}
fn add_evidence(result: &AntiTamperDetectionResult, score: &DetectionScore) {
if !result.methods.is_empty() {
let locations: boxcar::Vec<Token> = boxcar::Vec::new();
for m in &result.methods {
locations.push(m.token);
}
let mode_name = match result.detected_mode {
Some(AntiTamperMode::Normal) => "Normal",
Some(AntiTamperMode::Anti) => "Anti",
Some(AntiTamperMode::Jit) => "JIT",
Some(AntiTamperMode::Unknown) | None => "Unknown",
};
let confidence = (result.methods.len() * 25).min(50);
score.add(DetectionEvidence::BytecodePattern {
name: format!(
"ConfuserEx anti-tamper ({} mode, {} methods)",
mode_name,
result.methods.len()
),
locations,
confidence,
});
}
if result.encrypted_method_count > 0 {
let confidence = result.encrypted_method_count.min(50);
score.add(DetectionEvidence::EncryptedMethodBodies {
count: result.encrypted_method_count,
confidence,
});
}
if !result.pinvoke_methods.is_empty() {
let locations: boxcar::Vec<Token> = boxcar::Vec::new();
for t in &result.pinvoke_methods {
locations.push(*t);
}
score.add(DetectionEvidence::BytecodePattern {
name: format!(
"Anti-tamper P/Invoke methods ({} native calls)",
result.pinvoke_methods.len()
),
locations,
confidence: 20,
});
}
}
fn find_antitamper_pinvokes(assembly: &CilObject) -> Vec<Token> {
let mut pinvokes = Vec::new();
let antitamper_apis = [
"VirtualProtect",
"CheckRemoteDebuggerPresent",
"LoadLibrary",
"LoadLibraryA",
"LoadLibraryW",
"GetProcAddress",
];
let import_map = utils::build_pinvoke_import_map(assembly);
for method in &assembly.query_methods().native() {
let import_name = import_map.get(&method.token).map(String::as_str);
if let Some(name) = import_name {
if antitamper_apis.contains(&name) {
pinvokes.push(method.token);
}
}
}
pinvokes
}
fn find_antitamper_methods(assembly: &CilObject) -> Vec<AntiTamperMethodInfo> {
let mut found = Vec::new();
let import_map = utils::build_pinvoke_import_map(assembly);
for method in &assembly.query_methods().has_body() {
let Some(cfg) = method.cfg() else {
continue;
};
let mut calls_virtualprotect = false;
let mut calls_gethinstance = false;
let mut calls_get_module = false;
let mut calls_check_debugger = false;
let mut calls_loadlibrary = false;
let mut calls_getprocaddress = false;
let mut calls_failfast = false;
for node_id in cfg.node_ids() {
let Some(block) = cfg.block(node_id) else {
continue;
};
for instr in &block.instructions {
if instr.opcode == opcodes::CALL || instr.opcode == opcodes::CALLVIRT {
if let Operand::Token(token) = &instr.operand {
if let Some(name) =
utils::resolve_call_target(assembly, *token, &import_map)
{
match name.as_str() {
"VirtualProtect" => calls_virtualprotect = true,
"GetHINSTANCE" => calls_gethinstance = true,
"get_Module" => calls_get_module = true,
"CheckRemoteDebuggerPresent" => calls_check_debugger = true,
"LoadLibrary" | "LoadLibraryA" | "LoadLibraryW" => {
calls_loadlibrary = true;
}
"GetProcAddress" => calls_getprocaddress = true,
"FailFast" => calls_failfast = true,
_ => {}
}
}
}
}
}
}
let is_normal_mode = calls_gethinstance && calls_get_module && calls_virtualprotect;
let is_anti_mode = is_normal_mode && calls_check_debugger;
let is_jit_mode = calls_loadlibrary && calls_getprocaddress && calls_virtualprotect;
if is_normal_mode || is_anti_mode || is_jit_mode {
let mode = if is_jit_mode {
AntiTamperMode::Jit
} else if is_anti_mode {
AntiTamperMode::Anti
} else {
AntiTamperMode::Normal
};
found.push(AntiTamperMethodInfo {
token: method.token,
mode,
calls_virtualprotect,
calls_gethinstance,
calls_get_module,
calls_check_debugger,
calls_loadlibrary,
calls_getprocaddress,
calls_failfast,
});
}
}
found
}
fn determine_mode(result: &AntiTamperDetectionResult) -> Option<AntiTamperMode> {
if result.methods.is_empty() {
return None;
}
let mut normal_votes = 0;
let mut anti_votes = 0;
let mut jit_votes = 0;
for method in &result.methods {
match method.mode {
AntiTamperMode::Normal => normal_votes += 1,
AntiTamperMode::Anti => anti_votes += 1,
AntiTamperMode::Jit => jit_votes += 1,
AntiTamperMode::Unknown => {}
}
}
if jit_votes > 0 {
return Some(AntiTamperMode::Jit);
}
if anti_votes > 0 {
return Some(AntiTamperMode::Anti);
}
if normal_votes > 0 {
return Some(AntiTamperMode::Normal);
}
if result.encrypted_method_count > 0 {
return Some(AntiTamperMode::Unknown);
}
None
}
fn find_all_methods_with_rva(assembly: &CilObject) -> Vec<Token> {
assembly
.methods()
.iter()
.filter_map(|entry| {
let method = entry.value();
if method.rva.is_some_and(|rva| rva > 0) {
Some(method.token)
} else {
None
}
})
.collect()
}
#[derive(Debug)]
struct ExtractedMethodBodies {
bodies: Vec<(Token, Vec<u8>)>,
failed_count: usize,
}
fn extract_decrypted_bodies(
assembly: &CilObject,
virtual_image: &[u8],
encrypted_methods: &[Token],
) -> ExtractedMethodBodies {
let mut bodies = Vec::new();
let mut failed_count = 0;
for &token in encrypted_methods {
let Some(rva) = utils::get_method_rva(assembly, token) else {
failed_count += 1;
continue;
};
if rva == 0 || rva as usize >= virtual_image.len() {
failed_count += 1;
continue;
}
match utils::extract_method_body_at_rva(virtual_image, rva) {
Some(body_bytes) => {
bodies.push((token, body_bytes));
}
None => {
failed_count += 1;
}
}
}
ExtractedMethodBodies {
bodies,
failed_count,
}
}
#[derive(Debug)]
struct ExtractedFieldData {
fields: Vec<(u32, u32, Vec<u8>)>,
failed_count: usize,
}
fn extract_decrypted_field_data(assembly: &CilObject, virtual_image: &[u8]) -> ExtractedFieldData {
let mut fields = Vec::new();
let mut failed_count = 0;
let Some(tables) = assembly.tables() else {
return ExtractedFieldData {
fields,
failed_count,
};
};
let Some(fieldrva_table) = tables.table::<FieldRvaRaw>() else {
return ExtractedFieldData {
fields,
failed_count,
};
};
for row in fieldrva_table {
let rva = row.rva;
if rva == 0 {
continue;
}
let Some(field_size) = utils::get_field_data_size(assembly, row.field) else {
failed_count += 1;
continue;
};
let rva_usize = rva as usize;
if rva_usize + field_size > virtual_image.len() {
failed_count += 1;
continue;
}
let data = virtual_image[rva_usize..rva_usize + field_size].to_vec();
fields.push((row.rid, rva, data));
}
ExtractedFieldData {
fields,
failed_count,
}
}
#[derive(Debug)]
struct EmulationResult {
virtual_image: Vec<u8>,
encrypted_methods: Vec<Token>,
decryptor_method: Token,
instructions_executed: u64,
}
pub fn decrypt_bodies(
assembly: CilObject,
events: &mut EventLog,
tracing: Option<TracingConfig>,
) -> Result<CilObject> {
let assembly_arc = Arc::new(assembly);
let emulation_result = emulate_antitamper(&assembly_arc, tracing)?;
events.info(format!(
"Anti-tamper emulation completed: {} instructions executed via method 0x{:08x}",
emulation_result.instructions_executed,
emulation_result.decryptor_method.value()
));
let all_methods_with_rva = find_all_methods_with_rva(&assembly_arc);
let extracted = extract_decrypted_bodies(
&assembly_arc,
&emulation_result.virtual_image,
&all_methods_with_rva,
);
if extracted.bodies.is_empty() {
return Err(Error::Deobfuscation(
"No method bodies could be extracted from decrypted image".to_string(),
));
}
if extracted.failed_count > 0 {
events.warn(format!(
"Failed to extract {} method bodies from decrypted image",
extracted.failed_count
));
}
let encrypted_count = emulation_result.encrypted_methods.len();
for &token in &emulation_result.encrypted_methods {
events
.record(EventKind::MethodBodyDecrypted)
.method(token)
.message(format!("Decrypted method body 0x{:08x}", token.value()));
}
let mut cil_assembly = CilAssembly::from_bytes_with_validation(
assembly_arc.file().data().to_vec(),
ValidationConfig::analysis(),
)?;
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 = 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")))?;
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,
};
cil_assembly.table_row_update(
TableId::MethodDef,
rid,
TableDataOwned::MethodDef(updated_row),
)?;
}
let extracted_fields =
extract_decrypted_field_data(&assembly_arc, &emulation_result.virtual_image);
let field_count = extracted_fields.fields.len();
if extracted_fields.failed_count > 0 {
events.warn(format!(
"Failed to extract {} field data entries from decrypted image",
extracted_fields.failed_count
));
}
for (rid, _original_rva, data) in extracted_fields.fields {
let placeholder_rva = cil_assembly.store_field_data(data);
#[allow(clippy::redundant_closure_for_method_calls)]
let existing_row = 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")))?;
let updated_row = FieldRvaRaw {
rid: existing_row.rid,
token: existing_row.token,
offset: existing_row.offset,
rva: placeholder_rva,
field: existing_row.field,
};
cil_assembly.table_row_update(
TableId::FieldRVA,
rid,
TableDataOwned::FieldRVA(updated_row),
)?;
}
events.record(EventKind::AntiTamperRemoved).message(
format!("Anti-tamper protection removed: {encrypted_count} method bodies, {field_count} field data entries decrypted")
);
let config = GeneratorConfig::default().with_skip_original_method_bodies(true);
cil_assembly.into_cilobject_with(ValidationConfig::analysis(), config)
}
fn emulate_antitamper(
assembly: &Arc<CilObject>,
tracing: Option<TracingConfig>,
) -> Result<EmulationResult> {
let candidates = find_candidates(assembly, ProtectionType::AntiTamper);
let decryptor_method = candidates.best().map(|c| c.token).ok_or_else(|| {
Error::Deobfuscation("No anti-tamper initialization method found".to_string())
})?;
let encrypted_methods = utils::find_encrypted_methods(assembly);
let mut builder = ProcessBuilder::new()
.assembly_arc(Arc::clone(assembly))
.name("anti-tamper-emulation")
.with_max_instructions(10_000_000)
.with_max_call_depth(200)
.with_timeout_ms(120_000);
if let Some(mut tracing_config) = tracing {
tracing_config.context_prefix = Some("anti-tamper".to_string());
builder = builder.with_tracing(tracing_config);
}
let process = builder.build()?;
let loaded_image = process
.primary_image()
.ok_or_else(|| Error::Deobfuscation("Failed to get loaded PE image info".to_string()))?;
let pe_base = loaded_image.base_address;
#[allow(clippy::cast_possible_truncation)]
let virtual_size = loaded_image.size_of_image as usize;
let outcome = process.execute_method(decryptor_method, vec![])?;
let instructions_executed = match outcome {
EmulationOutcome::Completed { instructions, .. }
| EmulationOutcome::Breakpoint { instructions, .. } => instructions,
EmulationOutcome::LimitReached { limit, .. } => {
return Err(Error::Deobfuscation(format!(
"Anti-tamper emulation exceeded limit: {limit:?}"
)));
}
EmulationOutcome::Stopped { reason, .. } => {
return Err(Error::Deobfuscation(format!(
"Anti-tamper emulation stopped: {reason}"
)));
}
EmulationOutcome::UnhandledException { exception, .. } => {
return Err(Error::Deobfuscation(format!(
"Anti-tamper emulation threw exception: {exception:?}"
)));
}
EmulationOutcome::RequiresSymbolic { reason, .. } => {
return Err(Error::Deobfuscation(format!(
"Anti-tamper emulation requires symbolic execution: {reason}"
)));
}
};
let virtual_image = process.read_memory(pe_base, virtual_size)?;
Ok(EmulationResult {
virtual_image,
encrypted_methods,
decryptor_method,
instructions_executed,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::deobfuscation::obfuscators::confuserex::utils::find_encrypted_methods;
const SAMPLES_DIR: &str = "tests/samples/packers/confuserex";
#[test]
fn test_original_no_antitamper() -> crate::Result<()> {
let path = format!("{}/original.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let result = detect_antitamper(&assembly);
assert!(
result.methods.is_empty(),
"Original should have no anti-tamper methods"
);
assert_eq!(
result.encrypted_method_count, 0,
"Original should have no encrypted methods"
);
assert!(
result.pinvoke_methods.is_empty(),
"Original should have no anti-tamper P/Invoke"
);
assert!(!result.is_detected());
Ok(())
}
#[test]
fn test_normal_no_antitamper() -> crate::Result<()> {
let path = format!("{}/mkaring_normal.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let result = detect_antitamper(&assembly);
assert_eq!(
result.encrypted_method_count, 0,
"Normal preset should have no encrypted methods"
);
Ok(())
}
#[test]
fn test_antitamper_sample_detection() -> crate::Result<()> {
let path = format!("{}/mkaring_antitamper.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let result = detect_antitamper(&assembly);
assert!(
result.encrypted_method_count > 0,
"Antitamper sample should have encrypted methods, found {}",
result.encrypted_method_count
);
assert!(result.is_detected(), "Anti-tamper should be detected");
assert!(
result.best_init_method().is_some(),
"Should identify an anti-tamper initialization method"
);
Ok(())
}
#[test]
fn test_antitamper_sample_decryption() -> crate::Result<()> {
let path = format!("{}/mkaring_antitamper.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let encrypted_before = find_encrypted_methods(&assembly);
assert!(
!encrypted_before.is_empty(),
"Should have encrypted methods before decryption"
);
let mut events = EventLog::new();
let decrypted = decrypt_bodies(assembly, &mut events, None)?;
let encrypted_after = find_encrypted_methods(&decrypted);
assert!(
encrypted_after.is_empty(),
"Should have no encrypted methods after decryption, found {}",
encrypted_after.len()
);
for token in &encrypted_before {
let method = decrypted.method(token);
assert!(
method.is_some(),
"Method 0x{:08X} should exist",
token.value()
);
let method = method.unwrap();
let body = method.body.get();
assert!(
body.is_some(),
"Decrypted method 0x{:08X} should have a body",
token.value()
);
}
Ok(())
}
#[test]
fn test_maximum_detection() -> crate::Result<()> {
let path = format!("{}/mkaring_maximum.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let result = detect_antitamper(&assembly);
assert!(
result.encrypted_method_count > 0,
"Maximum should have encrypted methods, found {}",
result.encrypted_method_count
);
assert!(result.is_detected(), "Anti-tamper should be detected");
assert!(
result.best_init_method().is_some(),
"Should identify an anti-tamper initialization method"
);
Ok(())
}
#[test]
#[cfg(not(feature = "skip-expensive-tests"))]
fn test_maximum_decryption() -> crate::Result<()> {
let path = format!("{}/mkaring_maximum.exe", SAMPLES_DIR);
let assembly = CilObject::from_path_with_validation(&path, ValidationConfig::analysis())?;
let encrypted_before = find_encrypted_methods(&assembly);
assert!(
!encrypted_before.is_empty(),
"Should have encrypted methods before decryption"
);
let mut events = EventLog::new();
let decrypted = decrypt_bodies(assembly, &mut events, None)?;
let encrypted_after = find_encrypted_methods(&decrypted);
assert!(
encrypted_after.is_empty(),
"Should have no encrypted methods after decryption, found {}",
encrypted_after.len()
);
for token in &encrypted_before {
let method = decrypted.method(token);
assert!(
method.is_some(),
"Method 0x{:08X} should exist",
token.value()
);
let method = method.unwrap();
let body = method.body.get();
assert!(
body.is_some(),
"Decrypted method 0x{:08X} should have a body",
token.value()
);
}
Ok(())
}
}