use std::collections::{HashMap, HashSet};
use crate::{
analysis::{SsaFunction, SsaOp, SsaVarId},
metadata::{
signatures::{parse_field_signature, TypeSignature},
streams::Strings,
tables::{
ClassLayoutRaw, FieldRaw, MemberRefRaw, MetadataTable, MethodDefRaw, TableId,
TypeDefRaw, TypeRefRaw,
},
token::Token,
typesystem::{wellknown, PointerSize},
},
CilObject,
};
pub(crate) fn get_field_data_size(assembly: &CilObject, field_rid: u32) -> Option<usize> {
let tables = assembly.tables()?;
let blobs = assembly.blob()?;
let field_table = tables.table::<FieldRaw>()?;
let field_row = field_table.get(field_rid)?;
let sig_data = blobs.get(field_row.signature as usize).ok()?;
let field_sig = parse_field_signature(sig_data).ok()?;
if let Some(size) = field_sig.base.byte_size(PointerSize::Bit32) {
return Some(size);
}
match &field_sig.base {
TypeSignature::ValueType(token) => {
if token.table() != 0x02 {
return None;
}
let type_rid = token.row();
let class_layout_table = tables.table::<ClassLayoutRaw>()?;
for layout in class_layout_table {
if layout.parent == type_rid {
return Some(layout.class_size as usize);
}
}
None
}
_ => None,
}
}
pub(crate) fn build_def_map(ssa: &SsaFunction) -> HashMap<SsaVarId, &SsaOp> {
let mut defs = HashMap::new();
for block in ssa.blocks() {
for instr in block.instructions() {
if let Some(dest) = instr.op().dest() {
defs.insert(dest, instr.op());
}
}
}
defs
}
pub(crate) fn is_obfuscated_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
if name.contains(' ') {
return true;
}
for c in name.chars() {
match c {
'\u{200B}'..='\u{200F}'
| '\u{202A}'..='\u{202E}'
| '\u{2060}'..='\u{206F}'
| '\u{FEFF}'
| '\u{E000}'..='\u{F8FF}' => return true,
c if !c.is_ascii() && !c.is_alphabetic() => return true,
_ => {}
}
}
false
}
pub(crate) fn is_special_name(name: &str) -> bool {
if name == wellknown::members::CTOR || name == wellknown::members::CCTOR {
return true;
}
if name == wellknown::members::MODULE_TYPE || name == wellknown::members::PRIVATE_IMPL {
return true;
}
if name.starts_with('<') && name.ends_with('>') {
return true;
}
if name.contains(' ') {
return false;
}
if name.starts_with("get_")
|| name.starts_with("set_")
|| name.starts_with("add_")
|| name.starts_with("remove_")
{
return true;
}
false
}
pub(crate) fn build_call_site_counts(
assembly: &CilObject,
target_tokens: impl IntoIterator<Item = Token>,
) -> HashMap<Token, usize> {
let targets: HashSet<Token> = target_tokens.into_iter().collect();
if targets.is_empty() {
return HashMap::new();
}
let mut counts: HashMap<Token, usize> = targets.iter().map(|&t| (t, 0)).collect();
let mut memberref_cache: HashMap<Token, Option<Token>> = HashMap::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
for instr in method.instructions() {
if instr.mnemonic == "call" || instr.mnemonic == "callvirt" {
if let Some(token) = instr.get_token_operand() {
if let Some(count) = counts.get_mut(&token) {
*count += 1;
} else if token.is_table(TableId::MemberRef) {
let resolved = memberref_cache
.entry(token)
.or_insert_with(|| assembly.resolver().resolve_memberref_method(token));
if let Some(resolved_token) = resolved {
if let Some(count) = counts.get_mut(resolved_token) {
*count += 1;
}
}
}
}
}
}
}
counts
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedType<'a> {
pub name: &'a str,
pub namespace: Option<&'a str>,
pub typedef_token: Option<Token>,
pub has_module_scope: bool,
}
pub(crate) fn resolve_constructor_type<'a>(
tag: TableId,
row: u32,
methoddef_table: Option<&'a MetadataTable<'a, MethodDefRaw>>,
typedef_table: Option<&'a MetadataTable<'a, TypeDefRaw>>,
typeref_table: Option<&'a MetadataTable<'a, TypeRefRaw>>,
memberref_table: Option<&'a MetadataTable<'a, MemberRefRaw>>,
strings: &'a Strings<'a>,
) -> Option<ResolvedType<'a>> {
match tag {
TableId::MethodDef => {
resolve_methoddef_declaring_type(row, methoddef_table, typedef_table, strings)
}
TableId::MemberRef => resolve_memberref_declaring_type(
row,
memberref_table,
typedef_table,
typeref_table,
strings,
),
_ => None,
}
}
pub(crate) fn resolve_methoddef_declaring_type<'a>(
method_row: u32,
methoddef_table: Option<&'a MetadataTable<'a, MethodDefRaw>>,
typedef_table: Option<&'a MetadataTable<'a, TypeDefRaw>>,
strings: &'a Strings<'a>,
) -> Option<ResolvedType<'a>> {
let methoddef_table = methoddef_table?;
let typedef_table = typedef_table?;
let method = methoddef_table.get(method_row)?;
let typedef = typedef_table
.iter()
.filter(|t| t.method_list <= method.rid)
.last()?;
let name = strings.get(typedef.type_name as usize).ok()?;
let namespace = strings.get(typedef.type_namespace as usize).ok();
Some(ResolvedType {
name,
namespace,
typedef_token: Some(typedef.token),
has_module_scope: false,
})
}
pub(crate) fn resolve_memberref_declaring_type<'a>(
memberref_row: u32,
memberref_table: Option<&'a MetadataTable<'a, MemberRefRaw>>,
typedef_table: Option<&'a MetadataTable<'a, TypeDefRaw>>,
typeref_table: Option<&'a MetadataTable<'a, TypeRefRaw>>,
strings: &'a Strings<'a>,
) -> Option<ResolvedType<'a>> {
let memberref_table = memberref_table?;
let memberref = memberref_table.get(memberref_row)?;
match memberref.class.tag {
TableId::TypeDef => {
let typedef_table = typedef_table?;
let typedef = typedef_table.get(memberref.class.row)?;
let name = strings.get(typedef.type_name as usize).ok()?;
let namespace = strings.get(typedef.type_namespace as usize).ok();
Some(ResolvedType {
name,
namespace,
typedef_token: Some(typedef.token),
has_module_scope: false,
})
}
TableId::TypeRef => {
let typeref_table = typeref_table?;
let typeref = typeref_table.get(memberref.class.row)?;
let name = strings.get(typeref.type_name as usize).ok()?;
let namespace = strings.get(typeref.type_namespace as usize).ok();
Some(ResolvedType {
name,
namespace,
typedef_token: None,
has_module_scope: typeref.resolution_scope.tag == TableId::Module,
})
}
_ => None,
}
}
pub(crate) fn resolve_qualified_method_name(assembly: &CilObject, token: Token) -> Option<String> {
if token.table() == 0x0A {
if let Some(member) = assembly.member_ref(&token) {
if let Some(type_name) = member.declaredby.fullname() {
return Some(format!("{}.{}", type_name, member.name));
}
return Some(member.name.clone());
}
}
assembly.resolve_method_name(token)
}
pub(crate) fn find_methods_calling_apis(
assembly: &CilObject,
patterns: &[&str],
) -> HashMap<Token, Vec<usize>> {
let mut results: HashMap<Token, Vec<usize>> = HashMap::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
let mut matched = Vec::new();
for instr in method.instructions() {
if let Some(token) = instr.get_token_operand() {
if let Some(name) = resolve_qualified_method_name(assembly, token) {
for (i, pattern) in patterns.iter().enumerate() {
if name.contains(pattern) && !matched.contains(&i) {
matched.push(i);
}
}
}
}
}
if !matched.is_empty() {
results.insert(method.token, matched);
}
}
results
}
pub(crate) fn filter_by_call_threshold(
candidates: Vec<Token>,
counts: &HashMap<Token, usize>,
min_calls: usize,
) -> HashSet<Token> {
candidates
.into_iter()
.filter(|t| *counts.get(t).unwrap_or(&0) >= min_calls)
.collect()
}
pub(crate) fn exclude_cross_calling_candidates(
candidates: HashSet<Token>,
assembly: &CilObject,
) -> HashSet<Token> {
if candidates.len() <= 1 {
return candidates;
}
candidates
.iter()
.filter(|token| {
let Some(method) = assembly.method(token) else {
return true;
};
let calls_other = method.instructions().any(|instr| {
instr
.get_token_operand()
.is_some_and(|t| t != **token && candidates.contains(&t))
});
if calls_other {
log::trace!(
"exclude_cross_calling: dropping {} ({}) — calls another candidate",
token,
method.name
);
}
!calls_other
})
.copied()
.collect()
}
pub(crate) fn build_init_array_map(assembly: &CilObject) -> HashMap<Token, Token> {
let mut map = HashMap::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
if method.name != wellknown::members::CCTOR {
continue;
}
let instructions: Vec<_> = method.instructions().collect();
for (i, instr) in instructions.iter().enumerate() {
if instr.mnemonic != "call" {
continue;
}
let Some(call_token) = instr.get_token_operand() else {
continue;
};
let is_init_array = assembly
.refs_members()
.get(&call_token)
.is_some_and(|r| r.value().name == "InitializeArray");
if !is_init_array {
continue;
}
if i < 1 || i + 1 >= instructions.len() {
continue;
}
let mut backing_field_token = None;
for j in (0..i).rev() {
if instructions[j].mnemonic == "ldtoken" {
backing_field_token = instructions[j].get_token_operand();
break;
}
if i - j > 3 {
break;
}
}
let stsfld_instr = &instructions[i + 1];
if stsfld_instr.mnemonic != "stsfld" {
continue;
}
if let (Some(backing), Some(byte_array)) =
(backing_field_token, stsfld_instr.get_token_operand())
{
map.insert(byte_array, backing);
}
}
}
map
}
pub(crate) fn is_guid_name(name: &str) -> bool {
if name.len() != 36 {
return false;
}
let bytes = name.as_bytes();
if bytes[8] != b'-' || bytes[13] != b'-' || bytes[18] != b'-' || bytes[23] != b'-' {
return false;
}
bytes.iter().enumerate().all(|(i, &b)| {
if i == 8 || i == 13 || i == 18 || i == 23 {
true
} else {
b.is_ascii_hexdigit()
}
})
}
pub(crate) fn resolve_custom_attr_type<'a>(
assembly: &'a CilObject,
attr: &crate::metadata::tables::CustomAttributeRaw,
) -> Option<ResolvedType<'a>> {
let tables = assembly.tables()?;
let strings = assembly.strings()?;
resolve_constructor_type(
attr.constructor.tag,
attr.constructor.row,
tables.table::<MethodDefRaw>(),
tables.table::<TypeDefRaw>(),
tables.table::<TypeRefRaw>(),
tables.table::<MemberRefRaw>(),
strings,
)
}
pub(crate) fn is_method_named(assembly: &CilObject, token: Token, name: &str) -> bool {
assembly
.resolve_method_name(token)
.is_some_and(|n| n.contains(name))
}
pub(crate) fn is_method_on_type(assembly: &CilObject, token: Token, type_name: &str) -> bool {
match token.table() {
0x06 => assembly
.method(&token)
.and_then(|m| m.declaring_type_rc())
.is_some_and(|ty| ty.name.contains(type_name)),
0x0A => assembly
.refs_members()
.get(&token)
.and_then(|entry| entry.value().declaredby.fullname())
.is_some_and(|name| name.contains(type_name)),
_ => false,
}
}
pub(crate) fn is_typed_method_named(
assembly: &CilObject,
token: Token,
type_name: &str,
method_name: &str,
) -> bool {
is_method_on_type(assembly, token, type_name) && is_method_named(assembly, token, method_name)
}
#[cfg(test)]
mod tests {
use crate::test::helpers::load_sample;
use crate::{
deobfuscation::utils::{
build_call_site_counts, is_method_named, is_obfuscated_name, is_special_name,
resolve_constructor_type,
},
metadata::{
tables::{MemberRefRaw, MethodDefRaw, TableId, TypeDefRaw, TypeRefRaw},
token::Token,
},
};
#[test]
fn test_is_obfuscated_name_normal_names() {
assert!(!is_obfuscated_name("Main"));
assert!(!is_obfuscated_name("Program"));
assert!(!is_obfuscated_name("get_Count"));
assert!(!is_obfuscated_name(".ctor"));
assert!(!is_obfuscated_name("<Module>"));
}
#[test]
fn test_is_obfuscated_name_empty() {
assert!(!is_obfuscated_name(""));
}
#[test]
fn test_is_obfuscated_name_spaces() {
assert!(is_obfuscated_name("Hello World"));
assert!(is_obfuscated_name(" "));
}
#[test]
fn test_is_obfuscated_name_zero_width() {
assert!(is_obfuscated_name("\u{200B}"));
assert!(is_obfuscated_name("a\u{FEFF}b"));
assert!(is_obfuscated_name("\u{202A}test"));
}
#[test]
fn test_is_obfuscated_name_pua() {
assert!(is_obfuscated_name("\u{E000}"));
assert!(is_obfuscated_name("abc\u{F800}"));
}
#[test]
fn test_is_special_name_constructors() {
assert!(is_special_name(".ctor"));
assert!(is_special_name(".cctor"));
}
#[test]
fn test_is_special_name_module_types() {
assert!(is_special_name("<Module>"));
assert!(is_special_name("<PrivateImplementationDetails>"));
}
#[test]
fn test_is_special_name_clr_internal() {
assert!(is_special_name("<Generic Parameter>"));
assert!(!is_special_name("<>c__DisplayClass0_0"));
}
#[test]
fn test_is_special_name_accessors() {
assert!(is_special_name("get_Count"));
assert!(is_special_name("set_Value"));
assert!(is_special_name("add_Click"));
assert!(is_special_name("remove_Changed"));
}
#[test]
fn test_is_special_name_regular() {
assert!(!is_special_name("Main"));
assert!(!is_special_name("DoWork"));
assert!(!is_special_name("ToString"));
}
#[test]
fn test_is_special_name_spaces_not_special() {
assert!(!is_special_name("get_ Count"));
assert!(!is_special_name("Hello World"));
}
#[test]
fn test_build_call_site_counts_empty_targets() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let counts = build_call_site_counts(&asm, std::iter::empty());
assert!(counts.is_empty());
}
#[test]
fn test_build_call_site_counts_nonexistent_token() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let bogus = Token::new(0x06FFFFFF);
let counts = build_call_site_counts(&asm, std::iter::once(bogus));
assert_eq!(counts.get(&bogus), Some(&0));
}
#[test]
fn test_resolve_constructor_type_confuserex_marker() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/mkaring_normal.exe");
let tables = asm.tables().expect("tables should be present");
let strings = asm.strings().expect("strings should be present");
let ca_table = tables
.table::<crate::metadata::tables::CustomAttributeRaw>()
.expect("CustomAttribute table should be present");
let methoddef_table = tables.table::<MethodDefRaw>();
let typedef_table = tables.table::<TypeDefRaw>();
let typeref_table = tables.table::<TypeRefRaw>();
let memberref_table = tables.table::<MemberRefRaw>();
let mut found_marker = false;
for attr in ca_table {
if let Some(resolved) = resolve_constructor_type(
attr.constructor.tag,
attr.constructor.row,
methoddef_table,
typedef_table,
typeref_table,
memberref_table,
strings,
) {
if resolved.name.contains("ConfuserVersion")
|| resolved.name.contains("ConfusedByAttribute")
{
found_marker = true;
assert!(resolved.typedef_token.is_some());
break;
}
}
}
assert!(found_marker, "Expected to find ConfuserEx marker attribute");
}
#[test]
fn test_resolve_constructor_type_unsupported_tag() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let tables = asm.tables().expect("tables should be present");
let strings = asm.strings().expect("strings should be present");
let result = resolve_constructor_type(
TableId::Field,
1,
tables.table::<MethodDefRaw>(),
tables.table::<TypeDefRaw>(),
tables.table::<TypeRefRaw>(),
tables.table::<MemberRefRaw>(),
strings,
);
assert!(result.is_none());
}
#[test]
fn test_is_method_named_with_real_assembly() {
let asm = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
assert!(!is_method_named(&asm, Token::new(0x06FFFFFF), "Main"));
}
}