use std::any::Any;
use crate::{
compiler::{EventKind, EventLog},
deobfuscation::{
techniques::{
Detection, Detections, Evidence, Technique, TechniqueCategory, WorkingAssembly,
},
utils::resolve_custom_attr_type,
},
metadata::{
tables::{CustomAttributeRaw, TableId},
token::Token,
},
utils::read_packed_len,
CilObject, Result,
};
#[derive(Debug)]
pub struct MarkerFindings {
pub tokens: Vec<Token>,
pub typedef_tokens: Vec<Token>,
pub version: Option<String>,
}
pub struct ConfuserExMarker;
impl Technique for ConfuserExMarker {
fn id(&self) -> &'static str {
"confuserex.marker"
}
fn name(&self) -> &'static str {
"ConfuserEx Marker Attribute Removal"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Metadata
}
fn detect(&self, assembly: &CilObject) -> Detection {
let Some(tables) = assembly.tables() else {
return Detection::new_empty();
};
let Some(ca_table) = tables.table::<CustomAttributeRaw>() else {
return Detection::new_empty();
};
let mut marker_tokens = Vec::new();
let mut typedef_tokens = Vec::new();
let mut version = None;
for attr in ca_table {
let Some(resolved) = resolve_custom_attr_type(assembly, &attr) else {
continue;
};
let name = resolved.name;
if name.contains("ConfuserVersion") || name.contains("ConfusedByAttribute") {
let attr_token = Token::new((TableId::CustomAttribute as u32) << 24 | attr.rid);
marker_tokens.push(attr_token);
if let Some(token) = resolved.typedef_token {
if !typedef_tokens.contains(&token) {
typedef_tokens.push(token);
}
}
if name.contains("ConfuserVersion") && version.is_none() {
version = extract_version_from_blob(assembly, &attr);
}
}
}
if marker_tokens.is_empty() {
return Detection::new_empty();
}
let count = marker_tokens.len();
let mut evidence = vec![Evidence::Attribute(format!(
"{count} ConfuserEx marker attributes"
))];
if let Some(ref ver) = version {
evidence.push(Evidence::Attribute(format!("ConfuserEx version: {ver}")));
}
let attr_tokens_for_cleanup: Vec<Token> = marker_tokens.clone();
let findings = MarkerFindings {
tokens: marker_tokens,
typedef_tokens: typedef_tokens.clone(),
version,
};
let mut detection = Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
);
detection
.cleanup_mut()
.add_attributes(attr_tokens_for_cleanup);
detection.cleanup_mut().add_types(typedef_tokens);
detection
}
fn byte_transform(
&self,
_assembly: &mut WorkingAssembly,
detection: &Detection,
_detections: &Detections,
) -> Option<Result<EventLog>> {
let events = EventLog::new();
let Some(findings) = detection.findings::<MarkerFindings>() else {
return Some(Ok(events));
};
if !findings.tokens.is_empty() {
let version_info = findings
.version
.as_deref()
.map(|v| format!(" (version: {v})"))
.unwrap_or_default();
events.record(EventKind::ArtifactRemoved).message(format!(
"Marked {} ConfuserEx attributes for removal{}",
findings.tokens.len(),
version_info,
));
}
Some(Ok(events))
}
fn requires_regeneration(&self) -> bool {
true
}
}
fn extract_version_from_blob(assembly: &CilObject, row: &CustomAttributeRaw) -> Option<String> {
let blob = assembly.blob()?;
let data = blob.get(row.value as usize).ok()?;
if data.len() < 4 {
return None;
}
if data[0] != 0x01 || data[1] != 0x00 {
return None;
}
let (str_len, offset) = read_packed_len(&data[2..])?;
let start = 2 + offset;
let end = start + str_len;
if end > data.len() {
return None;
}
let s = std::str::from_utf8(&data[start..end]).ok()?;
if s.is_empty() {
return None;
}
Some(s.to_string())
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{
confuserex::marker::{ConfuserExMarker, MarkerFindings},
Evidence, Technique,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_positive() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_normal.exe");
let technique = ConfuserExMarker;
let detection = technique.detect(&assembly);
assert!(
detection.is_detected(),
"ConfuserExMarker should detect markers in mkaring_normal.exe"
);
assert!(
!detection.evidence().is_empty(),
"Detection should have evidence"
);
}
#[test]
fn test_detect_negative() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = ConfuserExMarker;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"ConfuserExMarker should not detect markers in original.exe"
);
}
#[test]
fn test_detect_findings_have_tokens_and_typedefs() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_normal.exe");
let technique = ConfuserExMarker;
let detection = technique.detect(&assembly);
assert!(detection.is_detected());
let findings = detection
.findings::<MarkerFindings>()
.expect("Should have MarkerFindings");
assert!(
!findings.tokens.is_empty(),
"Should have marker attribute tokens"
);
assert!(
!findings.typedef_tokens.is_empty(),
"Should have typedef tokens for locally-defined marker types"
);
if findings.version.is_some() {
let has_version_evidence = detection.evidence().iter().any(|e| {
if let Evidence::Attribute(s) = e {
s.contains("version")
} else {
false
}
});
assert!(
has_version_evidence,
"Evidence should mention the ConfuserEx version when version is extracted"
);
}
}
}