use std::collections::HashSet;
use crate::{
analysis::{ConstValue, SsaFunction, SsaOp},
deobfuscation::{
renamer::context::{ApiCallInfo, OpcodeProfile},
utils::is_obfuscated_name,
},
metadata::token::Token,
CilObject,
};
pub fn collect_call_targets(ssa: &SsaFunction, assembly: &CilObject) -> Vec<String> {
let mut targets = Vec::new();
let mut seen = HashSet::new();
for (_block_idx, _instr_idx, instr) in ssa.iter_instructions() {
let method_token = match instr.op() {
SsaOp::Call { method, .. }
| SsaOp::CallVirt { method, .. }
| SsaOp::NewObj { ctor: method, .. } => method.token(),
_ => continue,
};
if let Some(name) = resolve_qualified_method_name(assembly, method_token) {
if has_obfuscated_component(&name) {
continue;
}
if seen.insert(name.clone()) {
targets.push(name);
}
}
}
targets
}
pub fn collect_string_literals(ssa: &SsaFunction, assembly: &CilObject) -> Vec<String> {
let mut strings = Vec::new();
for (_block_idx, _instr_idx, instr) in ssa.iter_instructions() {
if let SsaOp::Const { value, .. } = instr.op() {
match value {
ConstValue::DecryptedString(s) if !s.is_empty() => {
strings.push(s.clone());
}
ConstValue::String(idx) => {
if let Some(us) = assembly.userstrings() {
if let Ok(s) = us.get(*idx as usize) {
if !s.is_empty() {
if let Ok(decoded) = s.to_string() {
strings.push(decoded);
}
}
}
}
}
_ => {}
}
}
}
strings
}
pub fn collect_field_accesses(ssa: &SsaFunction, assembly: &CilObject) -> Vec<String> {
let mut accesses = Vec::new();
let mut seen = HashSet::new();
for (_block_idx, _instr_idx, instr) in ssa.iter_instructions() {
let field_token = match instr.op() {
SsaOp::LoadField { field, .. }
| SsaOp::StoreField { field, .. }
| SsaOp::LoadStaticField { field, .. }
| SsaOp::StoreStaticField { field, .. }
| SsaOp::LoadFieldAddr { field, .. }
| SsaOp::LoadStaticFieldAddr { field, .. } => field.token(),
_ => continue,
};
if let Some(name) = resolve_qualified_field_name(assembly, field_token) {
if has_obfuscated_component(&name) {
continue;
}
if seen.insert(name.clone()) {
accesses.push(name);
}
}
}
accesses
}
pub fn build_opcode_profile(ssa: &SsaFunction) -> OpcodeProfile {
let mut profile = OpcodeProfile::default();
for (_block_idx, _instr_idx, instr) in ssa.iter_instructions() {
match instr.op() {
SsaOp::Call { .. } | SsaOp::CallVirt { .. } | SsaOp::CallIndirect { .. } => {
profile.calls += 1;
}
SsaOp::Const {
value: ConstValue::String(_) | ConstValue::DecryptedString(_),
..
} => {
profile.strings += 1;
}
SsaOp::LoadField { .. }
| SsaOp::StoreField { .. }
| SsaOp::LoadStaticField { .. }
| SsaOp::StoreStaticField { .. }
| SsaOp::LoadFieldAddr { .. }
| SsaOp::LoadStaticFieldAddr { .. } => {
profile.field_io += 1;
}
SsaOp::And { .. }
| SsaOp::Or { .. }
| SsaOp::Xor { .. }
| SsaOp::Not { .. }
| SsaOp::Shl { .. }
| SsaOp::Shr { .. } => {
profile.bitwise += 1;
}
SsaOp::Add { .. }
| SsaOp::AddOvf { .. }
| SsaOp::Sub { .. }
| SsaOp::SubOvf { .. }
| SsaOp::Mul { .. }
| SsaOp::MulOvf { .. }
| SsaOp::Div { .. }
| SsaOp::Rem { .. }
| SsaOp::Neg { .. } => {
profile.arithmetic += 1;
}
SsaOp::NewArr { .. }
| SsaOp::LoadElement { .. }
| SsaOp::StoreElement { .. }
| SsaOp::LoadElementAddr { .. }
| SsaOp::ArrayLength { .. } => {
profile.array += 1;
}
SsaOp::Ceq { .. }
| SsaOp::Clt { .. }
| SsaOp::Cgt { .. }
| SsaOp::Branch { .. }
| SsaOp::BranchCmp { .. } => {
profile.comparison += 1;
}
SsaOp::Conv { .. } => {
profile.conversion += 1;
}
_ => {}
}
}
profile
}
pub fn extract_anchors(ssa: &SsaFunction, assembly: &CilObject) -> Vec<ApiCallInfo> {
let mut anchors = Vec::new();
for (_block_idx, _instr_idx, instr) in ssa.iter_instructions() {
let (method_token, args) = match instr.op() {
SsaOp::Call { method, args, .. } => (method.token(), args),
SsaOp::CallVirt { method, args, .. } => (method.token(), args),
_ => continue,
};
let Some(method_name) = resolve_qualified_method_name(assembly, method_token) else {
continue;
};
if method_token.table() != 0x0A && method_token.table() != 0x2B {
continue;
}
for (pos, _arg) in args.iter().enumerate() {
anchors.push(ApiCallInfo {
method_name: method_name.clone(),
argument_position: Some(pos),
});
}
}
anchors
}
pub fn collect_call_site_context(
caller_ssa: &SsaFunction,
callee_token: Token,
assembly: &CilObject,
) -> (Vec<String>, Option<String>) {
let mut nearby_strings = Vec::new();
let mut return_usage: Option<String> = None;
let all_instrs: Vec<_> = caller_ssa.iter_instructions().collect();
for (idx, (_block_idx, _instr_idx, instr)) in all_instrs.iter().enumerate() {
let (method_token, dest) = match instr.op() {
SsaOp::Call { method, dest, .. } => (method.token(), dest),
SsaOp::CallVirt { method, dest, .. } => (method.token(), dest),
_ => continue,
};
let resolved = assembly
.resolver()
.resolve_method(method_token)
.unwrap_or(method_token);
if resolved != callee_token && method_token != callee_token {
continue;
}
let window_start = idx.saturating_sub(5);
let window_end = (idx + 6).min(all_instrs.len());
for (_, _, nearby_instr) in &all_instrs[window_start..window_end] {
if let SsaOp::Const { value, .. } = nearby_instr.op() {
match value {
ConstValue::DecryptedString(s)
if !s.is_empty() && !nearby_strings.contains(s) =>
{
nearby_strings.push(s.clone());
}
ConstValue::String(us_idx) => {
if let Some(us) = assembly.userstrings() {
if let Ok(s) = us.get(*us_idx as usize) {
if !s.is_empty() {
if let Ok(decoded) = s.to_string() {
if !nearby_strings.contains(&decoded) {
nearby_strings.push(decoded);
}
}
}
}
}
}
_ => {}
}
}
}
if let Some(dest_var) = dest {
for (_, _, later_instr) in all_instrs.iter().skip(idx + 1).take(5) {
let (usage_token, usage_args) = match later_instr.op() {
SsaOp::Call { method, args, .. } => (method.token(), args),
SsaOp::CallVirt { method, args, .. } => (method.token(), args),
_ => continue,
};
if usage_args.contains(dest_var) {
if let Some(name) = resolve_qualified_method_name(assembly, usage_token) {
if !has_obfuscated_component(&name) {
return_usage = Some(name);
}
}
break;
}
}
}
break;
}
nearby_strings.truncate(3);
(nearby_strings, return_usage)
}
fn resolve_qualified_method_name(assembly: &CilObject, token: Token) -> Option<String> {
match token.table() {
0x0A => {
let member = assembly.member_ref(&token)?;
let type_name = member.declaredby.fullname()?;
Some(format!("{type_name}::{}", member.name))
}
0x06 => {
let method = assembly.method(&token)?;
if let Some(type_name) = method.declaring_type_fullname() {
Some(format!("{type_name}::{}", method.name))
} else {
Some(method.name.clone())
}
}
0x2B => {
let method_name = assembly.resolve_method_name(token)?;
let resolver = assembly.resolver();
if let Some(cil_type) = resolver.declaring_type(token) {
Some(format!("{}::{method_name}", cil_type.fullname()))
} else {
Some(method_name)
}
}
_ => None,
}
}
fn resolve_qualified_field_name(assembly: &CilObject, token: Token) -> Option<String> {
match token.table() {
0x0A => {
let member = assembly.member_ref(&token)?;
let type_name = member.declaredby.fullname()?;
Some(format!("{type_name}.{}", member.name))
}
0x04 => {
let resolver = assembly.resolver();
if let Some(cil_type) = resolver.declaring_type_of_field(token) {
for (_, field_rc) in cil_type.fields.iter() {
if field_rc.token == token {
return Some(format!("{}.{}", cil_type.fullname(), field_rc.name));
}
}
}
None
}
_ => None,
}
}
fn has_obfuscated_component(qualified_name: &str) -> bool {
qualified_name
.split([':', '.', '/'])
.filter(|s| !s.is_empty())
.any(is_obfuscated_name)
}
#[cfg(test)]
mod tests {
use crate::deobfuscation::renamer::context::OpcodeProfile;
#[test]
fn test_opcode_profile_default() {
let profile = OpcodeProfile::default();
assert_eq!(profile.calls, 0);
assert_eq!(profile.strings, 0);
assert_eq!(profile.bitwise, 0);
assert_eq!(profile.arithmetic, 0);
assert_eq!(profile.array, 0);
assert_eq!(profile.comparison, 0);
assert_eq!(profile.conversion, 0);
assert_eq!(profile.field_io, 0);
}
}