use std::sync::Arc;
use crate::{
assembly::Operand,
cilassembly::{CilAssembly, GeneratorConfig},
compiler::EventLog,
deobfuscation::{
detection::{DetectionEvidence, DetectionScore},
findings::DeobfuscationFindings,
obfuscators::confuserex::{
candidates::{find_candidates, ProtectionType},
hooks::create_lzma_hook,
},
},
emulation::{EmulationOutcome, ProcessBuilder},
error::Error,
metadata::{
tables::{
ManifestResourceAttributes, ManifestResourceBuilder, ManifestResourceRaw, TableId,
},
token::Token,
},
prelude::FlowType,
CilObject, Result, ValidationConfig,
};
#[derive(Debug, Clone)]
pub struct ExtractedAssembly {
pub data: Vec<u8>,
pub suggested_name: Option<String>,
pub is_valid_pe: bool,
}
impl ExtractedAssembly {
#[must_use]
pub fn new(data: Vec<u8>) -> Self {
let is_valid_pe = data.len() >= 2 && data[0] == b'M' && data[1] == b'Z';
let suggested_name = if is_valid_pe {
Self::try_extract_name(&data)
} else {
None
};
Self {
data,
suggested_name,
is_valid_pe,
}
}
fn try_extract_name(data: &[u8]) -> Option<String> {
let assembly =
CilObject::from_mem_with_validation(data.to_vec(), ValidationConfig::disabled())
.ok()?;
assembly.assembly().map(|a| format!("{}.dll", a.name))
}
#[must_use]
pub fn size(&self) -> usize {
self.data.len()
}
}
#[derive(Debug, Clone)]
pub struct ExtractedResource {
pub name: String,
pub flags: u32,
pub data: Vec<u8>,
}
impl ExtractedResource {
#[must_use]
pub fn size(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn is_public(&self) -> bool {
(self.flags & ManifestResourceAttributes::PUBLIC.bits()) != 0
}
}
#[derive(Debug)]
pub struct ResourceDecryptionResult {
pub assemblies: Vec<ExtractedAssembly>,
pub resources: Vec<ExtractedResource>,
pub decryptor_method: Option<Token>,
pub instructions_executed: u64,
pub protection_stubs: Vec<Token>,
}
impl ResourceDecryptionResult {
#[must_use]
pub fn has_assemblies(&self) -> bool {
!self.assemblies.is_empty()
}
#[must_use]
pub fn has_resources(&self) -> bool {
!self.resources.is_empty()
}
#[must_use]
pub fn assembly_count(&self) -> usize {
self.assemblies.len()
}
#[must_use]
pub fn resource_count(&self) -> usize {
self.resources.len()
}
#[must_use]
pub fn total_size(&self) -> usize {
self.assemblies.iter().map(ExtractedAssembly::size).sum()
}
#[must_use]
pub fn total_resource_size(&self) -> usize {
self.resources.iter().map(ExtractedResource::size).sum()
}
}
pub fn detect(assembly: &CilObject, score: &DetectionScore, findings: &mut DeobfuscationFindings) {
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_decompress = false;
let mut has_current_domain = false;
for instr in method.instructions() {
if instr.flow_type != FlowType::Call {
continue;
}
let Operand::Token(token) = instr.operand else {
continue;
};
let Some(type_name) = get_call_target_name(assembly, token) else {
continue;
};
if type_name.contains("AppDomain") && type_name.contains("CurrentDomain") {
has_current_domain = true;
}
if type_name.contains("add_AssemblyResolve") || type_name.contains("AssemblyResolve") {
has_assembly_resolve = true;
}
if type_name.contains("add_ResourceResolve") || type_name.contains("ResourceResolve") {
has_resource_resolve = true;
}
if type_name.contains("Assembly") && type_name.contains("Load") {
has_assembly_load = true;
}
if type_name.contains("Decompress")
|| type_name.contains("Lzma")
|| type_name.contains("Inflate")
|| type_name.contains("GZipStream")
|| type_name.contains("DeflateStream")
{
has_decompress = true;
}
}
let is_resource_handler = (has_assembly_resolve || has_resource_resolve)
&& has_current_domain
&& (has_assembly_load || has_decompress);
if is_resource_handler {
findings.resource_handler_methods.push(method.token);
let locations = boxcar::Vec::new();
locations.push(method.token);
score.add(DetectionEvidence::BytecodePattern {
name: format!(
"Resource handler (resolve={} load={} decompress={})",
has_assembly_resolve || has_resource_resolve,
has_assembly_load,
has_decompress
),
locations,
confidence: 5,
});
}
}
}
fn get_call_target_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 => {
if let Some(member_ref) = assembly.member_ref(&token) {
let type_name = member_ref
.declaredby
.fullname()
.unwrap_or_else(|| "Unknown".to_string());
return Some(format!("{}::{}", type_name, member_ref.name));
}
None
}
0x2B => {
None
}
_ => None,
}
}
fn extract_resources_from_assembly(data: &[u8]) -> Vec<ExtractedResource> {
match CilObject::from_mem_with_validation(data.to_vec(), ValidationConfig::disabled()) {
Ok(hidden_assembly) => {
let resources = hidden_assembly.resources();
let mut extracted = Vec::new();
for entry in resources {
let manifest = entry.value();
if manifest.source.is_some() {
continue;
}
if let Some(resource_data) = resources.get_data(manifest) {
extracted.push(ExtractedResource {
name: manifest.name.clone(),
flags: manifest.flags.bits(),
data: resource_data.to_vec(),
});
}
}
extracted
}
Err(_) => {
Vec::new()
}
}
}
pub fn decrypt_resources(
assembly: CilObject,
events: &mut EventLog,
) -> Result<(CilObject, ResourceDecryptionResult)> {
let pe_bytes = assembly.file().data().to_vec();
let assembly_arc = Arc::new(assembly);
let candidates = find_candidates(&assembly_arc, ProtectionType::Resources);
let result = if candidates.is_empty() {
ResourceDecryptionResult {
assemblies: Vec::new(),
resources: Vec::new(),
decryptor_method: None,
instructions_executed: 0,
protection_stubs: Vec::new(),
}
} else {
let mut final_result = None;
for candidate in candidates.iter() {
match try_emulate_resource_decryptor(&assembly_arc, candidate.token, events) {
Ok(result) if result.has_assemblies() || result.has_resources() => {
events.info(format!(
"Extracted {} hidden assemblies ({} bytes) and {} resources ({} bytes) via method 0x{:08x}",
result.assembly_count(),
result.total_size(),
result.resource_count(),
result.total_resource_size(),
candidate.token.value()
));
for (i, asm) in result.assemblies.iter().enumerate() {
let name = asm.suggested_name.as_deref().unwrap_or("<unknown>");
events.info(format!(
" Hidden assembly {}: {} ({} bytes, valid PE: {})",
i + 1,
name,
asm.size(),
asm.is_valid_pe
));
}
for (i, res) in result.resources.iter().enumerate() {
let visibility = if res.is_public() { "public" } else { "private" };
events.info(format!(
" Resource {}: {} ({} bytes, {})",
i + 1,
res.name,
res.size(),
visibility
));
}
final_result = Some(result);
break;
}
Ok(_) => {}
Err(e) => {
events.warn(format!(
"Resource emulation failed for 0x{:08x}: {}",
candidate.token.value(),
e
));
}
}
}
final_result.unwrap_or(ResourceDecryptionResult {
assemblies: Vec::new(),
resources: Vec::new(),
decryptor_method: None,
instructions_executed: 0,
protection_stubs: Vec::new(),
})
};
drop(assembly_arc);
if !result.has_resources() {
let new_assembly =
CilObject::from_mem_with_validation(pe_bytes, ValidationConfig::analysis())?;
return Ok((new_assembly, result));
}
let mut cil_assembly =
CilAssembly::from_bytes_with_validation(pe_bytes.clone(), ValidationConfig::analysis())?;
let names_to_insert: std::collections::HashSet<_> =
result.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) {
events.warn(format!(
"Failed to remove existing ManifestResource row {rid}: {e}"
));
}
}
for resource in &result.resources {
let builder = ManifestResourceBuilder::new()
.name(&resource.name)
.flags(resource.flags)
.resource_data(&resource.data);
match builder.build(&mut cil_assembly) {
Ok(_) => events.info(format!(
"Inserted resource: {} ({} bytes)",
resource.name,
resource.size()
)),
Err(e) => events.warn(format!(
"Failed to insert resource '{}': {}",
resource.name, e
)),
}
}
let new_assembly = cil_assembly
.into_cilobject_with(ValidationConfig::analysis(), GeneratorConfig::default())?;
Ok((new_assembly, result))
}
fn find_manifest_resources_by_name(
cil_assembly: &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 try_emulate_resource_decryptor(
assembly: &Arc<CilObject>,
method_token: Token,
_events: &mut EventLog,
) -> Result<ResourceDecryptionResult> {
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![])?;
let instructions_executed = match outcome {
EmulationOutcome::Completed { instructions, .. }
| EmulationOutcome::Breakpoint { instructions, .. } => instructions,
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 assemblies: Vec<ExtractedAssembly> = process
.capture()
.assemblies()
.iter()
.map(|a| ExtractedAssembly::new(a.data.clone()))
.collect();
let mut resources = Vec::new();
for asm in &assemblies {
if asm.is_valid_pe {
let extracted = extract_resources_from_assembly(&asm.data);
resources.extend(extracted);
}
}
let protection_stubs = vec![method_token];
Ok(ResourceDecryptionResult {
assemblies,
resources,
decryptor_method: Some(method_token),
instructions_executed,
protection_stubs,
})
}
#[cfg(test)]
mod tests {
use crate::{
compiler::EventLog,
deobfuscation::obfuscators::confuserex::{
candidates::{find_candidates, ProtectionType},
resources::decrypt_resources,
},
CilObject, ValidationConfig,
};
const MAXIMUM_PATH: &str = "tests/samples/packers/confuserex/mkaring_maximum.exe";
const RESOURCES_PATH: &str = "tests/samples/packers/confuserex/mkaring_resources.exe";
#[test]
fn test_find_resource_candidates() {
let assembly =
CilObject::from_path_with_validation(MAXIMUM_PATH, ValidationConfig::analysis())
.expect("Failed to load assembly");
let candidates = find_candidates(&assembly, ProtectionType::Resources);
println!("Resource candidates found: {}", candidates.candidates.len());
for (i, c) in candidates.iter().enumerate() {
println!(
" {}. 0x{:08x} score={} reasons={:?}",
i + 1,
c.token.value(),
c.score,
c.reasons
);
}
}
#[test]
fn test_find_resource_candidates_in_resources_sample() {
let assembly =
CilObject::from_path_with_validation(RESOURCES_PATH, ValidationConfig::analysis())
.expect("Failed to load assembly");
let candidates = find_candidates(&assembly, ProtectionType::Resources);
println!(
"Resource candidates in resources sample: {}",
candidates.candidates.len()
);
for (i, c) in candidates.iter().enumerate() {
println!(
" {}. 0x{:08x} score={} reasons={:?}",
i + 1,
c.token.value(),
c.score,
c.reasons
);
}
assert!(
!candidates.is_empty(),
"Resources sample should have resource protection candidates"
);
}
#[test]
fn test_decrypt_resources() {
let assembly =
CilObject::from_path_with_validation(RESOURCES_PATH, ValidationConfig::analysis())
.expect("Failed to load assembly");
let candidates = find_candidates(&assembly, ProtectionType::Resources);
println!("Resource candidates: {}", candidates.candidates.len());
for (i, c) in candidates.iter().enumerate() {
println!(
" {}. 0x{:08x} score={} reasons={:?}",
i + 1,
c.token.value(),
c.score,
c.reasons
);
}
let mut events = EventLog::new();
let result = decrypt_resources(assembly, &mut events);
println!("Events logged: {}", events.len());
for event in events.iter() {
println!(" Event: {:?}", event);
}
match result {
Ok((deobfuscated_assembly, decryption_result)) => {
println!(
"Resource decryption result: {} assemblies extracted",
decryption_result.assembly_count()
);
if let Some(method) = decryption_result.decryptor_method {
println!("Decryptor method: 0x{:08x}", method.value());
}
println!(
"Instructions executed: {}",
decryption_result.instructions_executed
);
for (i, asm) in decryption_result.assemblies.iter().enumerate() {
println!(" Assembly {}: {} bytes", i + 1, asm.size());
}
for (i, res) in decryption_result.resources.iter().enumerate() {
println!(" Resource {}: {} ({} bytes)", i + 1, res.name, res.size());
}
assert!(
decryption_result.has_resources(),
"Should have extracted at least one resource"
);
assert_eq!(
decryption_result.resource_count(),
1,
"Should have extracted exactly one resource (duplicates are filtered)"
);
assert_eq!(
decryption_result.resources[0].name, "ResourceTestApp.testimage.bmp",
"Resource name should match expected"
);
println!("\nVerifying resources in deobfuscated assembly:");
let final_resources = deobfuscated_assembly.resources();
println!(
" Resource count in final assembly: {}",
final_resources.len()
);
let mut found_resource = false;
for entry in final_resources.iter() {
let manifest = entry.value();
println!(
" - {} ({} bytes, embedded: {})",
manifest.name,
manifest.data_size,
manifest.source.is_none()
);
if manifest.name == "ResourceTestApp.testimage.bmp" {
found_resource = true;
let data = final_resources
.get_data(manifest)
.expect("Resource data should be accessible");
assert_eq!(
data.len(),
246,
"Resource data size should match original (246 bytes)"
);
}
}
assert!(
found_resource,
"Inserted resource should be present in final assembly"
);
}
Err(e) => {
panic!("Resource decryption failed: {}", e);
}
}
}
}