use std::collections::{HashMap, HashSet};
use crate::{
assembly::Operand, deobfuscation::utils::find_methods_calling_apis, metadata::token::Token,
CilObject,
};
const MIN_CCTOR_FAN_IN: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StubKind {
Void,
Value,
Reference,
}
#[derive(Debug)]
pub struct StubScanResult {
pub stub_methods: Vec<(Token, StubKind)>,
pub total_il_methods: usize,
pub entry_point_token: Option<Token>,
}
pub fn scan_stub_methods(assembly: &CilObject) -> StubScanResult {
let entry_point_raw = assembly.cor20header().entry_point_token;
let entry_point_token = if entry_point_raw != 0 && Token::new(entry_point_raw).table() == 0x06 {
Some(Token::new(entry_point_raw))
} else {
None
};
let mut stub_methods = Vec::new();
let mut total_il_methods = 0usize;
for method_entry in assembly.methods() {
let method = method_entry.value();
if !method.has_body() {
continue;
}
if method.rva.is_none_or(|rva| rva == 0) {
continue;
}
total_il_methods += 1;
if entry_point_token.is_some_and(|ep| ep == method.token) {
continue;
}
let instrs: Vec<_> = method.instructions().collect();
if instrs.len() != 4 {
continue;
}
if instrs[0].mnemonic != "nop" || instrs[1].mnemonic != "nop" || instrs[3].mnemonic != "ret"
{
continue;
}
let kind = match instrs[2].mnemonic {
"nop" => StubKind::Void,
"ldc.i4.0" => StubKind::Value,
"ldnull" => StubKind::Reference,
_ => continue,
};
stub_methods.push((method.token, kind));
}
StubScanResult {
stub_methods,
total_il_methods,
entry_point_token,
}
}
#[derive(Debug)]
pub struct CctorFanInResult {
pub target_token: Token,
pub calling_cctors: Vec<Token>,
pub target_local_count: usize,
pub target_instruction_count: usize,
}
pub fn find_cctor_fan_in_target(assembly: &CilObject) -> Option<CctorFanInResult> {
let mut call_target_to_cctors: HashMap<Token, Vec<Token>> = HashMap::new();
let types = assembly.types();
for type_entry in types.iter() {
let ty = type_entry.value();
let Some(cctor_token) = ty.cctor() else {
continue;
};
let Some(cctor_method) = assembly.method(&cctor_token) else {
continue;
};
for instr in cctor_method.instructions() {
if instr.mnemonic != "call" {
continue;
}
let Some(target) = instr.get_token_operand() else {
continue;
};
if target.table() != 0x06 {
continue;
}
call_target_to_cctors
.entry(target)
.or_default()
.push(cctor_token);
}
}
let (target_token, calling_cctors) = call_target_to_cctors
.into_iter()
.max_by_key(|(_, cctors)| cctors.len())?;
if calling_cctors.len() < MIN_CCTOR_FAN_IN {
return None;
}
let (target_local_count, target_instruction_count) = assembly
.method(&target_token)
.map(|m| (m.local_vars.count(), m.instruction_count()))
.unwrap_or((0, 0));
Some(CctorFanInResult {
target_token,
calling_cctors,
target_local_count,
target_instruction_count,
})
}
#[derive(Debug)]
pub struct TrialCheckResult {
pub method_token: Token,
pub is_on_module_type: bool,
}
pub fn find_trial_checks(assembly: &CilObject) -> Vec<TrialCheckResult> {
let api_hits = find_methods_calling_apis(assembly, &["DateTime", "get_Days"]);
let mut results = Vec::new();
for (method_token, indices) in &api_hits {
if !indices.contains(&0) || !indices.contains(&1) {
continue;
}
let Some(method) = assembly.method(method_token) else {
continue;
};
let mut has_datetime_ctor = false;
let mut has_get_days = false;
let mut has_throw = false;
for instr in method.instructions() {
match instr.mnemonic {
"newobj" => {
if let Some(ctor_token) = instr.get_token_operand() {
if let Some(name) =
crate::deobfuscation::utils::resolve_qualified_method_name(
assembly, ctor_token,
)
{
if name.contains("DateTime") {
has_datetime_ctor = true;
}
}
}
}
"call" | "callvirt" => {
if let Some(call_token) = instr.get_token_operand() {
if let Some(name) =
crate::deobfuscation::utils::resolve_qualified_method_name(
assembly, call_token,
)
{
if name.contains("get_Days") {
has_get_days = true;
}
}
}
}
"throw" => {
has_throw = true;
}
_ => {}
}
}
if !has_datetime_ctor || !has_get_days || !has_throw {
continue;
}
let is_on_module_type = method
.declaring_type_rc()
.is_some_and(|t| t.is_module_type());
results.push(TrialCheckResult {
method_token: *method_token,
is_on_module_type,
});
}
results
}
pub fn find_body_patcher(assembly: &CilObject) -> Option<Token> {
const PAT_MARSHAL_COPY: usize = 0;
const PAT_READ_INT32: usize = 1;
const PAT_READ_INT64: usize = 2;
const PAT_INTPTR_SIZE: usize = 3;
let api_hits = find_methods_calling_apis(
assembly,
&["Marshal.Copy", "ReadInt32", "ReadInt64", "IntPtr.get_Size"],
);
for (method_token, indices) in &api_hits {
let has_marshal_copy = indices.contains(&PAT_MARSHAL_COPY);
let has_read_int = indices.contains(&PAT_READ_INT32) || indices.contains(&PAT_READ_INT64);
let has_intptr_size = indices.contains(&PAT_INTPTR_SIZE);
if has_marshal_copy && has_read_int && has_intptr_size {
return Some(*method_token);
}
}
None
}
#[derive(Debug)]
pub struct PrivateImplContainer {
pub container_token: Token,
}
fn is_nr_private_impl_name(name: &str) -> bool {
const PREFIX: &str = "<PrivateImplementationDetails>{";
if !name.starts_with(PREFIX) || !name.ends_with('}') {
return false;
}
let body = &name[PREFIX.len()..name.len() - 1];
let segments: Vec<&str> = body.split('-').collect();
if segments.len() != 5 {
return false;
}
let expected_lens = [8usize, 4, 4, 4, 12];
for (seg, expected) in segments.iter().zip(expected_lens.iter()) {
if seg.len() != *expected {
return false;
}
if !seg.bytes().all(|b| b.is_ascii_hexdigit()) {
return false;
}
}
true
}
fn is_nr_guid_module_name(name: &str) -> bool {
const PREFIX: &str = "<Module>{";
if !name.starts_with(PREFIX) || !name.ends_with('}') {
return false;
}
let body = &name[PREFIX.len()..name.len() - 1];
let segments: Vec<&str> = body.split('-').collect();
if segments.len() != 5 {
return false;
}
let expected_lens = [8usize, 4, 4, 4, 12];
for (seg, expected) in segments.iter().zip(expected_lens.iter()) {
if seg.len() != *expected {
return false;
}
if !seg.bytes().all(|b| b.is_ascii_hexdigit()) {
return false;
}
}
true
}
pub fn find_nr_guid_module_containers(assembly: &CilObject) -> Vec<Token> {
let mut results = Vec::new();
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if cil_type.token.table() != 0x02 {
continue;
}
if !is_nr_guid_module_name(&cil_type.name) {
continue;
}
results.push(cil_type.token);
}
results
}
pub fn find_nr_private_impl_containers(assembly: &CilObject) -> Vec<PrivateImplContainer> {
let mut results = Vec::new();
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if cil_type.token.table() != 0x02 {
continue;
}
if !is_nr_private_impl_name(&cil_type.name) {
continue;
}
results.push(PrivateImplContainer {
container_token: cil_type.token,
});
}
results
}
pub fn has_single_shot_bool_guard(assembly: &CilObject, method_token: Token) -> bool {
let Some(method) = assembly.method(&method_token) else {
return false;
};
let mut loaded_static_fields: HashSet<Token> = HashSet::new();
let mut stored_static_fields: HashSet<Token> = HashSet::new();
let mut has_conditional_branch = false;
for instr in method.instructions() {
match instr.mnemonic {
"ldsfld" => {
if let Some(t) = instr.get_token_operand() {
loaded_static_fields.insert(t);
}
}
"stsfld" => {
if let Some(t) = instr.get_token_operand() {
stored_static_fields.insert(t);
}
}
"brtrue" | "brtrue.s" | "brfalse" | "brfalse.s" => {
has_conditional_branch = true;
}
_ => {}
}
}
if !has_conditional_branch {
return false;
}
loaded_static_fields
.intersection(&stored_static_fields)
.next()
.is_some()
}
#[derive(Debug)]
pub struct TokenResolverInfo {
pub type_token: Token,
pub type_handle_accessors: Vec<Token>,
pub field_handle_accessors: Vec<Token>,
pub method_handle_accessors: Vec<Token>,
}
pub fn find_nr_token_resolver(assembly: &CilObject) -> Option<TokenResolverInfo> {
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if cil_type.token.table() != 0x02 {
continue;
}
let mut type_acc = Vec::new();
let mut field_acc = Vec::new();
let mut method_acc = Vec::new();
for (_, method_ref) in cil_type.methods.iter() {
let Some(method) = method_ref.upgrade() else {
continue;
};
if !method.is_static() {
continue;
}
if method.signature.params.len() != 1 {
continue;
}
if !matches!(
method.signature.params[0].base,
crate::metadata::signatures::TypeSignature::I4
) {
continue;
}
let Some(kind) = classify_token_accessor_body(assembly, method.token) else {
continue;
};
match kind {
AccessorKind::Type => type_acc.push(method.token),
AccessorKind::Field => field_acc.push(method.token),
AccessorKind::Method => method_acc.push(method.token),
}
}
if type_acc.is_empty() {
continue;
}
return Some(TokenResolverInfo {
type_token: cil_type.token,
type_handle_accessors: type_acc,
field_handle_accessors: field_acc,
method_handle_accessors: method_acc,
});
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AccessorKind {
Type,
Field,
Method,
}
fn classify_token_accessor_body(assembly: &CilObject, method_token: Token) -> Option<AccessorKind> {
let method = assembly.method(&method_token)?;
let instrs: Vec<_> = method.instructions().collect();
if instrs.len() != 4 {
return None;
}
if instrs[0].mnemonic != "ldsflda"
|| instrs[1].mnemonic != "ldarg.0"
|| instrs[2].mnemonic != "call"
|| instrs[3].mnemonic != "ret"
{
return None;
}
let call_token = instrs[2].get_token_operand()?;
let name = crate::deobfuscation::utils::resolve_qualified_method_name(assembly, call_token)?;
if !name.contains("ModuleHandle") {
return None;
}
if name.contains("GetRuntimeTypeHandleFromMetadataToken") {
Some(AccessorKind::Type)
} else if name.contains("GetRuntimeFieldHandleFromMetadataToken") {
Some(AccessorKind::Field)
} else if name.contains("GetRuntimeMethodHandleFromMetadataToken") {
Some(AccessorKind::Method)
} else {
None
}
}
pub fn find_resources_referenced_by_methods(
assembly: &CilObject,
method_tokens: &[Token],
) -> Vec<Token> {
let Some(userstrings) = assembly.userstrings() else {
return Vec::new();
};
let mut seen: HashSet<Token> = HashSet::new();
let mut results = Vec::new();
for &method_token in method_tokens {
let Some(method) = assembly.method(&method_token) else {
continue;
};
for instr in method.instructions() {
if instr.mnemonic != "ldstr" {
continue;
}
let Operand::Token(token) = &instr.operand else {
continue;
};
if token.table() != 0x70 {
continue;
}
let Ok(s) = userstrings.get(token.row() as usize) else {
continue;
};
let name = s.to_string_lossy();
let Some(resource) = assembly.resources().get(&name) else {
continue;
};
if seen.insert(resource.token) {
results.push(resource.token);
}
}
}
results
}
#[derive(Debug)]
pub struct InjectedCctorClassification {
pub purely_injected: Vec<Token>,
pub modified: Vec<Token>,
}
pub fn classify_injected_cctors(
assembly: &CilObject,
init_token: Token,
calling_cctors: &[Token],
) -> InjectedCctorClassification {
let mut purely_injected = Vec::new();
let mut modified = Vec::new();
for &cctor_token in calling_cctors {
let Some(method) = assembly.method(&cctor_token) else {
continue;
};
let instr_count = method.instruction_count();
let instrs: Vec<_> = method.instructions().collect();
let call_targets: Vec<Token> = instrs
.iter()
.filter(|i| i.mnemonic == "call")
.filter_map(|i| i.get_token_operand())
.collect();
let only_calls_init = call_targets.iter().all(|&t| t == init_token);
if instr_count <= 5 && only_calls_init {
purely_injected.push(cctor_token);
} else {
modified.push(cctor_token);
}
}
InjectedCctorClassification {
purely_injected,
modified,
}
}