use std::any::Any;
use crate::{
compiler::{EventKind, EventLog},
deobfuscation::techniques::{
Detection, Detections, Evidence, Technique, TechniqueCategory, WorkingAssembly,
},
metadata::tables::{
AssemblyRaw, DeclSecurityRaw, EncLogRaw, EncMapRaw, ModuleRaw, TableId, TypeRefRaw,
},
CilObject, Result,
};
const CONFUSEREX_MARKER: u32 = 0x7fff_7fff;
const CONFUSEREX_MARKER_16: u16 = 0x7fff;
#[derive(Debug)]
pub struct CxMetadataFindings {
pub invalid_entries: usize,
pub enc_tables: Vec<u8>,
pub has_duplicate_streams: bool,
pub patches: Vec<CxMetadataPatch>,
}
#[derive(Debug, Clone)]
pub struct CxMetadataPatch {
pub offset: usize,
pub size: u8,
pub corrected: u32,
}
pub struct ConfuserExMetadata;
impl Technique for ConfuserExMetadata {
fn id(&self) -> &'static str {
"confuserex.metadata"
}
fn name(&self) -> &'static str {
"ConfuserEx Invalid Metadata Repair"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Metadata
}
fn supersedes(&self) -> &[&'static str] {
&["generic.metadata"]
}
fn detect(&self, assembly: &CilObject) -> Detection {
let Some(tables) = assembly.tables() else {
return Detection::new_empty();
};
let strings = assembly.strings();
let strings_size = strings.as_ref().map(|s| s.data().len()).unwrap_or(0);
let mut findings = CxMetadataFindings {
invalid_entries: 0,
enc_tables: Vec::new(),
has_duplicate_streams: false,
patches: Vec::new(),
};
let mut evidence = Vec::new();
if let Some(module_table) = tables.table::<ModuleRaw>() {
for row in module_table {
if row.name == CONFUSEREX_MARKER || row.name as usize >= strings_size {
findings.invalid_entries += 1;
findings.patches.push(CxMetadataPatch {
offset: row.offset + 2, size: if strings_size > 0xFFFF { 4 } else { 2 },
corrected: 0,
});
}
}
}
if let Some(assembly_table) = tables.table::<AssemblyRaw>() {
for row in assembly_table {
if row.name == CONFUSEREX_MARKER || row.name as usize >= strings_size {
findings.invalid_entries += 1;
}
}
}
if let Some(declsec_table) = tables.table::<DeclSecurityRaw>() {
for row in declsec_table {
if row.action == CONFUSEREX_MARKER_16 || row.action > 0x000E {
findings.invalid_entries += 1;
}
}
}
if let Some(typeref_table) = tables.table::<TypeRefRaw>() {
for row in typeref_table {
if row.resolution_scope.tag == TableId::Module && row.resolution_scope.row == 0 {
findings.invalid_entries += 1;
}
}
}
if let Some(enclog) = tables.table::<EncLogRaw>() {
if enclog.row_count > 0 {
findings.enc_tables.push(0x1E);
}
}
if let Some(encmap) = tables.table::<EncMapRaw>() {
if encmap.row_count > 0 {
findings.enc_tables.push(0x1F);
}
}
findings.has_duplicate_streams = check_duplicate_streams(assembly);
let has_marker_pattern = findings.invalid_entries > 0;
let has_enc = !findings.enc_tables.is_empty();
let has_dups = findings.has_duplicate_streams;
if !has_marker_pattern && !has_enc && !has_dups {
return Detection::new_empty();
}
if has_marker_pattern {
evidence.push(Evidence::MetadataPattern(format!(
"{} invalid metadata entries with 0x7fff marker",
findings.invalid_entries,
)));
}
if has_enc {
evidence.push(Evidence::MetadataPattern(format!(
"Non-empty ENC tables: {:?}",
findings.enc_tables,
)));
}
if has_dups {
evidence.push(Evidence::MetadataPattern(
"Duplicate metadata stream headers".to_string(),
));
}
Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
)
}
fn byte_transform(
&self,
assembly: &mut WorkingAssembly,
detection: &Detection,
_detections: &Detections,
) -> Option<Result<EventLog>> {
let events = EventLog::new();
let Some(findings) = detection.findings::<CxMetadataFindings>() else {
return Some(Ok(events));
};
let mut patched = 0usize;
for patch in &findings.patches {
match patch.size {
2 => match assembly.write_le::<u16>(patch.offset, patch.corrected as u16) {
Ok(_) => patched += 1,
Err(e) => return Some(Err(e)),
},
4 => match assembly.write_le::<u32>(patch.offset, patch.corrected) {
Ok(_) => patched += 1,
Err(e) => return Some(Err(e)),
},
_ => {}
}
}
if patched > 0 {
events.record(EventKind::ArtifactRemoved).message(format!(
"Patched {} ConfuserEx invalid metadata entries (0x7fff marker)",
patched,
));
}
if !findings.enc_tables.is_empty() {
events.record(EventKind::ArtifactRemoved).message(format!(
"ENC tables {:?} will be cleared on regeneration",
findings.enc_tables,
));
}
if findings.has_duplicate_streams {
events
.record(EventKind::ArtifactRemoved)
.message("Duplicate stream headers will be removed on regeneration");
}
Some(Ok(events))
}
fn requires_regeneration(&self) -> bool {
true
}
}
fn check_duplicate_streams(assembly: &CilObject) -> bool {
let file = assembly.file();
let data = file.data();
let metadata_rva = assembly.cor20header().meta_data_rva as usize;
let Ok(metadata_offset) = file.rva_to_offset(metadata_rva) else {
return false;
};
let header_base = metadata_offset;
if header_base + 16 > data.len() {
return false;
}
let version_len = u32::from_le_bytes(
data[header_base + 12..header_base + 16]
.try_into()
.unwrap_or_default(),
) as usize;
let aligned_len = (version_len + 3) & !3;
let flags_offset = header_base + 16 + aligned_len;
if flags_offset + 4 > data.len() {
return false;
}
let stream_count = u16::from_le_bytes(
data[flags_offset + 2..flags_offset + 4]
.try_into()
.unwrap_or_default(),
) as usize;
let mut seen_names = std::collections::HashSet::new();
let mut pos = flags_offset + 4;
for _ in 0..stream_count {
if pos + 8 > data.len() {
break;
}
pos += 8;
let name_start = pos;
while pos < data.len() && data[pos] != 0 {
pos += 1;
}
if pos >= data.len() {
break;
}
let name = std::str::from_utf8(&data[name_start..pos]).unwrap_or("");
pos += 1; pos = (pos + 3) & !3;
if !name.is_empty() && !seen_names.insert(name.to_string()) {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{confuserex::metadata::ConfuserExMetadata, Technique},
test::helpers::load_sample,
};
#[test]
fn test_detect_negative_original() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = ConfuserExMetadata;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"ConfuserExMetadata should not detect invalid metadata in original.exe"
);
}
#[test]
fn test_detect_negative_normal() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_normal.exe");
let technique = ConfuserExMetadata;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"ConfuserExMetadata should not detect in normal preset (no InvalidMetadata protection)"
);
}
#[test]
fn test_technique_properties() {
let technique = ConfuserExMetadata;
assert_eq!(technique.id(), "confuserex.metadata");
assert_eq!(technique.supersedes(), &["generic.metadata"]);
assert!(
technique.requires_regeneration(),
"Metadata repair requires PE regeneration"
);
}
#[test]
fn test_detect_on_maximum() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_maximum.exe");
let technique = ConfuserExMetadata;
let detection = technique.detect(&assembly);
if detection.is_detected() {
assert!(!detection.evidence().is_empty());
let findings = detection
.findings::<super::CxMetadataFindings>()
.expect("Should have CxMetadataFindings");
assert!(
findings.invalid_entries > 0
|| !findings.enc_tables.is_empty()
|| findings.has_duplicate_streams,
"At least one indicator should be present"
);
}
}
}