use std::{
any::Any,
collections::{HashMap, HashSet},
sync::Arc,
};
#[cfg(feature = "legacy-crypto")]
use crate::deobfuscation::passes::bitmono::StringDecryptionPass;
use crate::{
analysis::{ConstValue, SsaFunction, SsaOp, SsaVarId},
cilassembly::CleanupRequest,
compiler::{PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
techniques::{Detection, Evidence, Technique, TechniqueCategory},
},
metadata::{
tables::{MemberRefRaw, TableId, TypeDefRaw, TypeRefRaw},
token::Token,
},
utils::CryptoParameters,
CilObject,
};
#[derive(Debug)]
pub struct StringFindings {
pub decryptor_type: Option<Token>,
pub decryptor_tokens: Vec<Token>,
pub call_sites: usize,
pub infrastructure_fields: Vec<Token>,
pub constant_data_fields: Vec<Token>,
pub constant_data_types: Vec<Token>,
pub crypto_params: CryptoParameters,
}
pub struct BitMonoStrings;
impl Technique for BitMonoStrings {
fn id(&self) -> &'static str {
"bitmono.strings"
}
fn name(&self) -> &'static str {
"BitMono String Decryption"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Value
}
fn requires(&self) -> &[&'static str] {
&["bitmono.calli"]
}
#[cfg(feature = "legacy-crypto")]
fn enabled(&self, _config: &crate::deobfuscation::config::EngineConfig) -> bool {
true
}
#[cfg(not(feature = "legacy-crypto"))]
fn enabled(&self, _config: &crate::deobfuscation::config::EngineConfig) -> bool {
false
}
fn detect(&self, _assembly: &CilObject) -> Detection {
Detection::new_empty()
}
fn detect_ssa(&self, ctx: &AnalysisContext, assembly: &CilObject) -> Detection {
let mut decryptor_tokens: Vec<Token> = Vec::new();
let mut decryptor_type: Option<Token> = None;
for entry in ctx.ssa_functions.iter() {
let method_token = *entry.key();
let ssa = entry.value();
if has_crypto_ops(ssa, assembly) {
decryptor_tokens.push(method_token);
if decryptor_type.is_none() {
if let Some(method) = assembly.method(&method_token) {
if let Some(decl_type) = method.declaring_type_rc() {
decryptor_type = Some(decl_type.token);
}
}
}
}
}
if decryptor_tokens.is_empty() {
return Detection::new_empty();
}
let crypto_params = decryptor_tokens
.first()
.and_then(|token| ctx.ssa_functions.get(token))
.map(|ssa| extract_crypto_parameters(&ssa, assembly))
.unwrap_or_default();
log::info!(
"BitMono strings: extracted crypto params — {} iterations, {}/{} key/iv, {}",
crypto_params.iterations,
crypto_params.key_size,
crypto_params.iv_size,
crypto_params.hash_algorithm,
);
let decryptor_set: HashSet<Token> = decryptor_tokens.iter().copied().collect();
let mut call_site_count = 0usize;
let mut infrastructure_fields: Vec<Token> = Vec::new();
for entry in ctx.ssa_functions.iter() {
let ssa = entry.value();
for block in ssa.blocks() {
for instr in block.instructions() {
let (call_token, args) = match instr.op() {
SsaOp::Call { method, args, .. } => (method.token(), args),
_ => continue,
};
if !decryptor_set.contains(&call_token) {
continue;
}
call_site_count += 1;
for arg in args {
if let Some(field_token) = resolve_ldsfld_field(ssa, *arg) {
if !infrastructure_fields.contains(&field_token) {
infrastructure_fields.push(field_token);
}
}
}
}
}
}
let (constant_data_fields, constant_data_types) =
collect_constant_data_tokens(assembly, &infrastructure_fields);
let mut evidence = vec![Evidence::BytecodePattern(
"BitMono string decryptor (AES + Rfc2898DeriveBytes)".to_string(),
)];
if call_site_count > 0 {
evidence.push(Evidence::BytecodePattern(format!(
"{call_site_count} call sites reference the string decryptor"
)));
}
let findings = StringFindings {
decryptor_type,
decryptor_tokens,
call_sites: call_site_count,
infrastructure_fields,
constant_data_fields,
constant_data_types,
crypto_params,
};
Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
)
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Simplify)
}
#[cfg(feature = "legacy-crypto")]
fn create_pass(
&self,
_ctx: &AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
let Some(findings) = detection.findings::<StringFindings>() else {
return Vec::new();
};
vec![Box::new(StringDecryptionPass::from_findings(
findings,
findings.crypto_params.clone(),
))]
}
#[cfg(not(feature = "legacy-crypto"))]
fn create_pass(
&self,
_ctx: &AnalysisContext,
_detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
Vec::new()
}
fn cleanup(&self, detection: &Detection) -> Option<CleanupRequest> {
let findings = detection.findings::<StringFindings>()?;
let mut request = CleanupRequest::new();
if let Some(decryptor_type) = findings.decryptor_type {
request.add_type(decryptor_type);
}
for token in &findings.infrastructure_fields {
request.add_field(*token);
}
for token in &findings.constant_data_fields {
request.add_field(*token);
}
for token in &findings.constant_data_types {
request.add_type(*token);
}
if request.has_deletions() {
Some(request)
} else {
None
}
}
}
fn collect_constant_data_tokens(
assembly: &CilObject,
infrastructure_fields: &[Token],
) -> (Vec<Token>, Vec<Token>) {
let mut constant_data_fields = Vec::new();
let mut constant_data_types = Vec::new();
if infrastructure_fields.is_empty() {
return (constant_data_fields, constant_data_types);
}
let init_map = crate::deobfuscation::utils::build_init_array_map(assembly);
if init_map.is_empty() {
return (constant_data_fields, constant_data_types);
}
for infra_token in infrastructure_fields {
if let Some(&backing_token) = init_map.get(infra_token) {
if !constant_data_fields.contains(&backing_token) {
constant_data_fields.push(backing_token);
}
}
}
let removed_set: HashSet<Token> = constant_data_fields.iter().copied().collect();
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if cil_type.token.row() == 1 && cil_type.token.table() == 0x02 {
continue;
}
let rva_fields: Vec<Token> = cil_type
.fields
.iter()
.filter(|(_, field)| field.flags.has_field_rva())
.map(|(_, field)| field.token)
.collect();
if rva_fields.is_empty() {
continue;
}
if rva_fields.iter().all(|t| removed_set.contains(t)) {
constant_data_types.push(cil_type.token);
}
}
(constant_data_fields, constant_data_types)
}
fn has_crypto_ops(ssa: &SsaFunction, assembly: &CilObject) -> bool {
let mut has_aes = false;
let mut has_pbkdf2 = false;
for block in ssa.blocks() {
for instr in block.instructions() {
let token = match instr.op() {
SsaOp::NewObj { ctor, .. } => ctor.token(),
SsaOp::Call { method, .. } | SsaOp::CallVirt { method, .. } => method.token(),
_ => continue,
};
if let Some(name) = resolve_type_name(assembly, token) {
if name.contains("RijndaelManaged") || name.contains("Aes") {
has_aes = true;
}
if name.contains("Rfc2898DeriveBytes") {
has_pbkdf2 = true;
}
}
if has_aes && has_pbkdf2 {
return true;
}
}
}
false
}
fn extract_crypto_parameters(ssa: &SsaFunction, assembly: &CilObject) -> CryptoParameters {
let mut params = CryptoParameters::default();
let const_map = build_const_map(ssa);
let mut get_bytes_sizes: Vec<u32> = Vec::new();
for block in ssa.blocks() {
for instr in block.instructions() {
match instr.op() {
SsaOp::NewObj { ctor, args, .. } => {
if let Some(name) = resolve_type_name(assembly, ctor.token()) {
if name.contains("Rfc2898DeriveBytes") && args.len() >= 3 {
if let Some(ConstValue::I32(iters)) = const_map.get(&args[2]) {
if *iters > 0 {
params.iterations = *iters as u32;
}
}
}
}
}
SsaOp::CallVirt { method, args, .. } => {
if let Some(name) = assembly.resolve_method_name(method.token()) {
if name == "GetBytes" && args.len() == 2 {
if let Some(ConstValue::I32(size)) = const_map.get(&args[1]) {
if *size > 0 {
get_bytes_sizes.push(*size as u32);
}
}
}
}
}
_ => {}
}
}
}
if let Some(&key_size) = get_bytes_sizes.first() {
params.key_size = key_size as usize;
}
if let Some(&iv_size) = get_bytes_sizes.get(1) {
params.iv_size = iv_size as usize;
}
params
}
fn build_const_map(ssa: &SsaFunction) -> HashMap<SsaVarId, ConstValue> {
let mut map = HashMap::new();
for block in ssa.blocks() {
for instr in block.instructions() {
if let SsaOp::Const { dest, value } = instr.op() {
map.insert(*dest, value.clone());
}
}
}
map
}
fn resolve_ldsfld_field(ssa: &SsaFunction, var_id: SsaVarId) -> Option<Token> {
for block in ssa.blocks() {
for instr in block.instructions() {
if let SsaOp::LoadStaticField { dest, field } = instr.op() {
if *dest == var_id {
return Some(field.token());
}
}
}
}
None
}
fn resolve_type_name(assembly: &CilObject, token: Token) -> Option<String> {
let tables = assembly.tables()?;
let strings = assembly.strings()?;
match token.table() {
0x0A => {
let memberref_table = tables.table::<MemberRefRaw>()?;
let memberref = memberref_table.get(token.row())?;
if memberref.class.tag == TableId::TypeRef {
let typeref_table = tables.table::<TypeRefRaw>()?;
let typeref = typeref_table.get(memberref.class.row)?;
let name = strings.get(typeref.type_name as usize).ok()?;
let ns = strings
.get(typeref.type_namespace as usize)
.ok()
.unwrap_or("");
Some(format!("{ns}.{name}"))
} else if memberref.class.tag == TableId::TypeDef {
let typedef_table = tables.table::<TypeDefRaw>()?;
let typedef = typedef_table.get(memberref.class.row)?;
let name = strings.get(typedef.type_name as usize).ok()?;
let ns = strings
.get(typedef.type_namespace as usize)
.ok()
.unwrap_or("");
Some(format!("{ns}.{name}"))
} else {
None
}
}
0x01 => {
let typeref_table = tables.table::<TypeRefRaw>()?;
let typeref = typeref_table.get(token.row())?;
let name = strings.get(typeref.type_name as usize).ok()?;
let ns = strings
.get(typeref.type_namespace as usize)
.ok()
.unwrap_or("");
Some(format!("{ns}.{name}"))
}
0x02 => {
let typedef_table = tables.table::<TypeDefRaw>()?;
let typedef = typedef_table.get(token.row())?;
let name = strings.get(typedef.type_name as usize).ok()?;
let ns = strings
.get(typedef.type_namespace as usize)
.ok()
.unwrap_or("");
Some(format!("{ns}.{name}"))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{bitmono::BitMonoStrings, Technique},
test::helpers::load_sample,
};
#[test]
fn test_detect_is_noop() {
let assembly = load_sample("tests/samples/packers/bitmono/0.39.0/bitmono_strings.exe");
let technique = BitMonoStrings;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"IL-level detect() should be a no-op — detection happens in detect_ssa()"
);
}
#[test]
fn test_detect_negative() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = BitMonoStrings;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"BitMonoStrings should not detect string encryption in a non-BitMono assembly"
);
}
}