use std::{any::Any, sync::Arc};
use crate::{
cilassembly::GeneratorConfig,
compiler::{EventKind, EventLog},
deobfuscation::techniques::{
confuserex::hooks::create_lzma_hook, Detection, Detections, Evidence, Technique,
TechniqueCategory, WorkingAssembly,
},
emulation::{EmulationOutcome, ProcessBuilder},
error::Error,
metadata::{
tables::{
ManifestResourceAttributes, ManifestResourceBuilder, ManifestResourceRaw, TableId,
},
token::Token,
validation::ValidationConfig,
},
prelude::FlowType,
CilObject, Result,
};
#[derive(Debug)]
pub struct ResourceFindings {
pub handler_tokens: Vec<Token>,
}
pub struct ConfuserExResources;
impl Technique for ConfuserExResources {
fn id(&self) -> &'static str {
"confuserex.resources"
}
fn name(&self) -> &'static str {
"ConfuserEx Resource Protection"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Protection
}
fn detect(&self, assembly: &CilObject) -> Detection {
let mut handler_tokens = Vec::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
let mut has_assembly_resolve = false;
let mut has_resource_resolve = false;
let mut has_assembly_load = false;
let mut has_current_domain = false;
let mut has_decompress = false;
for instr in method.instructions() {
if instr.flow_type != FlowType::Call {
continue;
}
let Some(token) = instr.get_token_operand() else {
continue;
};
let Some(name) = resolve_call_name(assembly, token) else {
continue;
};
if name.contains("AppDomain") && name.contains("CurrentDomain") {
has_current_domain = true;
}
if name.contains("add_AssemblyResolve") || name.contains("AssemblyResolve") {
has_assembly_resolve = true;
}
if name.contains("add_ResourceResolve") || name.contains("ResourceResolve") {
has_resource_resolve = true;
}
if name.contains("Assembly") && name.contains("Load") {
has_assembly_load = true;
}
if name.contains("Decompress")
|| name.contains("Lzma")
|| name.contains("Inflate")
|| name.contains("GZipStream")
|| name.contains("DeflateStream")
{
has_decompress = true;
}
}
let is_handler = (has_assembly_resolve || has_resource_resolve)
&& has_current_domain
&& (has_assembly_load || has_decompress);
if is_handler {
handler_tokens.push(method.token);
}
}
if handler_tokens.is_empty() {
return Detection::new_empty();
}
let count = handler_tokens.len();
let mut detection = Detection::new_detected(
vec![Evidence::BytecodePattern(format!(
"{count} resource handler method(s) with AssemblyResolve + Assembly.Load",
))],
Some(Box::new(ResourceFindings {
handler_tokens: handler_tokens.clone(),
}) as Box<dyn Any + Send + Sync>),
);
for token in &handler_tokens {
detection.cleanup_mut().add_method(*token);
}
detection
}
fn byte_transform(
&self,
assembly: &mut WorkingAssembly,
detection: &Detection,
_detections: &Detections,
) -> Option<Result<EventLog>> {
let events = EventLog::new();
let Some(findings) = detection.findings::<ResourceFindings>() else {
return Some(Ok(events));
};
if findings.handler_tokens.is_empty() {
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 mut extracted_resources = Vec::new();
for &handler_token in &findings.handler_tokens {
match try_emulate_resource_handler(&cilobject_arc, handler_token) {
Ok(resources) if !resources.is_empty() => {
log::info!(
"Extracted {} resources via method 0x{:08x}",
resources.len(),
handler_token.value()
);
for res in &resources {
log::info!(" Resource: {} ({} bytes)", res.name, res.data.len());
}
extracted_resources = resources;
break;
}
Ok(_) => {}
Err(e) => {
log::warn!(
"Resource emulation failed for 0x{:08x}: {}",
handler_token.value(),
e
);
}
}
}
if extracted_resources.is_empty() {
events.record(EventKind::ArtifactRemoved).message(
"Resource handler detected but no resources could be extracted".to_string(),
);
return Some(Ok(events));
}
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();
let names_to_insert: std::collections::HashSet<_> = extracted_resources
.iter()
.map(|r| r.name.as_str())
.collect();
let rids_to_remove = find_manifest_resources_by_name(&cil_assembly, &names_to_insert);
for rid in rids_to_remove {
if let Err(e) = cil_assembly.table_row_remove(TableId::ManifestResource, rid) {
log::warn!("Failed to remove existing ManifestResource row {rid}: {e}");
}
}
let mut inserted_count = 0;
for resource in &extracted_resources {
let builder = ManifestResourceBuilder::new()
.name(&resource.name)
.flags(resource.flags)
.resource_data(&resource.data);
match builder.build(&mut cil_assembly) {
Ok(_) => {
inserted_count += 1;
log::info!(
"Inserted resource: {} ({} bytes)",
resource.name,
resource.data.len()
);
}
Err(e) => {
log::warn!("Failed to insert resource '{}': {}", resource.name, e);
}
}
}
if inserted_count > 0 {
events
.record(EventKind::ArtifactRemoved)
.message(format!("Restored {} encrypted resource(s)", inserted_count,));
}
let new_assembly = match cil_assembly
.into_cilobject_with(ValidationConfig::analysis(), GeneratorConfig::default())
{
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
assembly.replace_assembly(new_assembly);
Some(Ok(events))
}
fn requires_regeneration(&self) -> bool {
true
}
}
struct ExtractedResource {
name: String,
flags: ManifestResourceAttributes,
data: Vec<u8>,
}
fn try_emulate_resource_handler(
assembly: &Arc<CilObject>,
method_token: Token,
) -> Result<Vec<ExtractedResource>> {
let process = ProcessBuilder::new()
.assembly_arc(Arc::clone(assembly))
.with_max_instructions(2_000_000)
.with_max_call_depth(100)
.capture_assemblies()
.hook(create_lzma_hook())
.build()?;
let outcome = process.execute_method(method_token, vec![])?;
match outcome {
EmulationOutcome::Completed { .. } | EmulationOutcome::Breakpoint { .. } => {}
EmulationOutcome::LimitReached { limit, .. } => {
return Err(Error::Deobfuscation(format!(
"Resource emulation exceeded limit: {limit:?}"
)));
}
EmulationOutcome::Stopped { reason, .. } => {
return Err(Error::Deobfuscation(format!(
"Resource emulation stopped: {reason}"
)));
}
EmulationOutcome::UnhandledException { exception, .. } => {
return Err(Error::Deobfuscation(format!(
"Resource emulation threw exception: {exception:?}"
)));
}
EmulationOutcome::RequiresSymbolic { reason, .. } => {
return Err(Error::Deobfuscation(format!(
"Resource emulation requires symbolic execution: {reason}"
)));
}
}
let mut resources = Vec::new();
for captured_asm in process.capture().assemblies().iter() {
let data = &captured_asm.data;
if data.len() < 2 || data[0] != b'M' || data[1] != b'Z' {
continue;
}
let Ok(hidden) =
CilObject::from_mem_with_validation(data.clone(), ValidationConfig::disabled())
else {
continue;
};
let hidden_resources = hidden.resources();
for entry in hidden_resources {
let manifest = entry.value();
if manifest.source.is_some() {
continue;
}
if let Some(resource_data) = hidden_resources.get_data(manifest) {
resources.push(ExtractedResource {
name: manifest.name.clone(),
flags: manifest.flags,
data: resource_data.to_vec(),
});
}
}
}
Ok(resources)
}
fn find_manifest_resources_by_name(
cil_assembly: &crate::cilassembly::CilAssembly,
names: &std::collections::HashSet<&str>,
) -> Vec<u32> {
let view = cil_assembly.view();
let Some(strings) = view.strings() else {
return Vec::new();
};
let Some(tables) = view.tables() else {
return Vec::new();
};
let Some(manifest_table) = tables.table::<ManifestResourceRaw>() else {
return Vec::new();
};
manifest_table
.iter()
.filter_map(|row| {
strings
.get(row.name as usize)
.ok()
.filter(|name| names.contains(*name))
.map(|_| row.rid)
})
.collect()
}
fn resolve_call_name(assembly: &CilObject, token: Token) -> Option<String> {
let table_id = token.table();
match table_id {
0x06 => {
let method = assembly.method(&token)?;
Some(method.name.clone())
}
0x0A => {
let member_ref = assembly.member_ref(&token)?;
let type_name = member_ref
.declaredby
.fullname()
.unwrap_or_else(|| "Unknown".to_string());
Some(format!("{}::{}", type_name, member_ref.name))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{
confuserex::resources::{ConfuserExResources, ResourceFindings},
Technique,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_positive() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_resources.exe");
let technique = ConfuserExResources;
let detection = technique.detect(&assembly);
assert!(
detection.is_detected(),
"ConfuserExResources should detect resource protection in mkaring_resources.exe"
);
assert!(
!detection.evidence().is_empty(),
"Detection should have evidence"
);
let findings = detection
.findings::<ResourceFindings>()
.expect("Should have ResourceFindings");
assert!(
!findings.handler_tokens.is_empty(),
"Should have resource handler tokens"
);
}
#[test]
fn test_detect_negative() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = ConfuserExResources;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"ConfuserExResources should not detect resource protection in original.exe"
);
}
}