use std::{
collections::{HashMap, HashSet},
sync::{Mutex, OnceLock},
};
use crate::{
analysis::{ConstValue, SsaFunction, SsaOp, SsaVarId},
compiler::{CompilerContext, EventKind, ModificationScope, SsaPass},
deobfuscation::{
techniques::BitMonoStringFindings,
utils::{self, build_init_array_map},
},
metadata::{tables::FieldRvaRaw, token::Token},
utils::{apply_crypto_transform, derive_key_iv, CryptoParameters},
CilObject, Error, Result,
};
type KeyCache = HashMap<(Token, Token), (Vec<u8>, Vec<u8>)>;
type StringReplacement = (usize, SsaVarId, [(usize, usize); 3], String);
type FieldRvaEntry = (u32, usize);
pub struct StringDecryptionPass {
decryptor_tokens: HashSet<Token>,
field_rva_map: OnceLock<HashMap<u32, FieldRvaEntry>>,
key_cache: Mutex<KeyCache>,
string_cache: Mutex<HashMap<Token, String>>,
crypto_params: CryptoParameters,
}
impl StringDecryptionPass {
#[must_use]
pub fn from_findings(
findings: &BitMonoStringFindings,
crypto_params: CryptoParameters,
) -> Self {
let decryptor_tokens = findings.decryptor_tokens.iter().copied().collect();
Self {
decryptor_tokens,
field_rva_map: OnceLock::new(),
key_cache: Mutex::new(HashMap::new()),
string_cache: Mutex::new(HashMap::new()),
crypto_params,
}
}
fn get_or_derive_key(
&self,
assembly: &CilObject,
salt_token: Token,
key_token: Token,
field_rva_map: &HashMap<u32, FieldRvaEntry>,
) -> Result<(Vec<u8>, Vec<u8>)> {
let cache_key = (salt_token, key_token);
{
let cache = self
.key_cache
.lock()
.map_err(|e| Error::LockError(format!("key_cache lock failed: {e}")))?;
if let Some(cached) = cache.get(&cache_key) {
return Ok(cached.clone());
}
}
let salt_bytes = get_field_rva_data(assembly, salt_token.row(), field_rva_map)?;
let key_bytes = get_field_rva_data(assembly, key_token.row(), field_rva_map)?;
let derived = derive_key_iv(&key_bytes, &salt_bytes, &self.crypto_params);
self.key_cache
.lock()
.map_err(|e| Error::LockError(format!("key_cache lock failed: {e}")))?
.insert(cache_key, derived.clone());
Ok(derived)
}
fn decrypt_field(
&self,
assembly: &CilObject,
site: &DecryptionSite,
field_rva_map: &HashMap<u32, FieldRvaEntry>,
) -> Result<String> {
{
let cache = self
.string_cache
.lock()
.map_err(|e| Error::LockError(format!("string_cache lock failed: {e}")))?;
if let Some(cached) = cache.get(&site.encrypted_field_token) {
return Ok(cached.clone());
}
}
let (aes_key, aes_iv) = self.get_or_derive_key(
assembly,
site.salt_field_token,
site.key_field_token,
field_rva_map,
)?;
let encrypted =
get_field_rva_data(assembly, site.encrypted_field_token.row(), field_rva_map)?;
let decrypted = decrypt_string(&encrypted, &aes_key, &aes_iv)?;
self.string_cache
.lock()
.map_err(|e| Error::LockError(format!("string_cache lock failed: {e}")))?
.insert(site.encrypted_field_token, decrypted.clone());
Ok(decrypted)
}
}
impl SsaPass for StringDecryptionPass {
fn name(&self) -> &'static str {
"BitMonoStringDecryption"
}
fn description(&self) -> &'static str {
"Decrypts BitMono AES-encrypted strings via SSA pattern matching"
}
fn modification_scope(&self) -> ModificationScope {
ModificationScope::InstructionsOnly
}
fn run_on_method(
&self,
ssa: &mut SsaFunction,
method_token: Token,
ctx: &CompilerContext,
assembly: &CilObject,
) -> Result<bool> {
let mut changed = false;
let ldsfld_index = build_ldsfld_index(ssa);
for block_idx in 0..ssa.blocks().len() {
let sites =
find_decryption_sites(ssa, block_idx, &self.decryptor_tokens, &ldsfld_index);
if sites.is_empty() {
continue;
}
let field_rva_map = self
.field_rva_map
.get_or_init(|| build_field_rva_map(assembly));
let mut replacements: Vec<StringReplacement> = Vec::new();
for site in &sites {
match self.decrypt_field(assembly, site, field_rva_map) {
Ok(decrypted) => {
replacements.push((
site.call_idx,
site.call_dest,
site.ldsfld_locations,
decrypted,
));
}
Err(_) => continue,
}
}
if replacements.is_empty() {
continue;
}
for (call_idx, call_dest, ldsfld_locations, decrypted) in replacements.iter().rev() {
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instruction_mut(*call_idx) {
instr.set_op(SsaOp::Const {
dest: *call_dest,
value: ConstValue::DecryptedString(decrypted.clone()),
});
}
}
for &(ldsfld_block, ldsfld_idx) in ldsfld_locations {
if let Some(block) = ssa.block_mut(ldsfld_block) {
if let Some(instr) = block.instruction_mut(ldsfld_idx) {
instr.set_op(SsaOp::Nop);
}
}
}
changed = true;
}
ctx.events
.record(EventKind::StringDecrypted)
.method(method_token)
.message(format!(
"BitMonoStringDecryption: decrypted {} strings in block {}",
replacements.len(),
block_idx,
));
}
Ok(changed)
}
}
struct DecryptionSite {
call_idx: usize,
call_dest: SsaVarId,
ldsfld_locations: [(usize, usize); 3],
encrypted_field_token: Token,
salt_field_token: Token,
key_field_token: Token,
}
type LdsfldIndex = HashMap<SsaVarId, (usize, usize, Token)>;
fn build_ldsfld_index(ssa: &SsaFunction) -> LdsfldIndex {
let mut index = HashMap::new();
for (block_idx, block) in ssa.blocks().iter().enumerate() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let SsaOp::LoadStaticField { dest, field } = instr.op() {
index.insert(*dest, (block_idx, instr_idx, field.token()));
}
}
}
index
}
fn find_decryption_sites(
ssa: &SsaFunction,
block_idx: usize,
decryptor_tokens: &HashSet<Token>,
ldsfld_index: &LdsfldIndex,
) -> Vec<DecryptionSite> {
let mut sites = Vec::new();
let Some(block) = ssa.block(block_idx) else {
return sites;
};
for (i, instr) in block.instructions().iter().enumerate() {
let (call_dest, call_token, args) = match instr.op() {
SsaOp::Call { dest, method, args } => {
let Some(d) = dest else { continue };
(*d, method.token(), args.clone())
}
_ => continue,
};
if !decryptor_tokens.contains(&call_token) {
continue;
}
if args.len() != 3 {
continue;
}
let mut ldsfld_locations = [(0usize, 0usize); 3];
let mut field_tokens = [Token::new(0); 3];
let mut all_found = true;
for (arg_idx, arg_var) in args.iter().enumerate() {
if let Some(&(blk, idx, token)) = ldsfld_index.get(arg_var) {
ldsfld_locations[arg_idx] = (blk, idx);
field_tokens[arg_idx] = token;
} else {
all_found = false;
break;
}
}
if !all_found {
continue;
}
sites.push(DecryptionSite {
call_idx: i,
call_dest,
ldsfld_locations,
encrypted_field_token: field_tokens[0],
salt_field_token: field_tokens[1],
key_field_token: field_tokens[2],
});
}
sites
}
fn build_field_rva_map(assembly: &CilObject) -> HashMap<u32, FieldRvaEntry> {
let mut map = HashMap::new();
let Some(tables) = assembly.tables() else {
return map;
};
let Some(fieldrva_table) = tables.table::<FieldRvaRaw>() else {
return map;
};
for row in fieldrva_table {
if row.rva == 0 {
continue;
}
let size = utils::get_field_data_size(assembly, row.field).unwrap_or(0);
if size > 0 {
map.insert(row.field, (row.rva, size));
}
}
let init_map = build_init_array_map(assembly);
for (byte_array_token, backing_token) in &init_map {
if let Some(&entry) = map.get(&backing_token.row()) {
map.insert(byte_array_token.row(), entry);
}
}
map
}
fn get_field_rva_data(
assembly: &CilObject,
field_rid: u32,
field_rva_map: &HashMap<u32, FieldRvaEntry>,
) -> Result<Vec<u8>> {
let (rva, size) = field_rva_map.get(&field_rid).ok_or_else(|| {
Error::Deobfuscation(format!("No FieldRVA entry for field RID 0x{:X}", field_rid))
})?;
let file = assembly.file();
let offset = file.rva_to_offset(*rva as usize)?;
let data = file.data_slice(offset, *size)?;
Ok(data.to_vec())
}
fn decrypt_string(encrypted: &[u8], key: &[u8], iv: &[u8]) -> Result<String> {
if encrypted.is_empty() {
return Ok(String::new());
}
let decrypted =
apply_crypto_transform("AES", key, iv, false, encrypted, 1, 2).ok_or_else(|| {
Error::Deobfuscation(format!(
"AES-256-CBC decryption failed for {} bytes",
encrypted.len()
))
})?;
let text = String::from_utf8(decrypted)
.map_err(|e| Error::Deobfuscation(format!("UTF-8 decode failed: {e}")))?;
if !text.is_empty() {
let control_count = text
.chars()
.filter(|c| c.is_control() && !matches!(c, '\n' | '\r' | '\t'))
.count();
if control_count > text.len() / 2 {
return Err(Error::Deobfuscation(format!(
"Decrypted output appears corrupted ({control_count}/{} control chars)",
text.len()
)));
}
}
Ok(text)
}
#[cfg(all(test, feature = "legacy-crypto"))]
mod tests {
use crate::utils::{apply_crypto_transform, derive_key_iv, CryptoParameters};
use super::decrypt_string;
#[test]
fn test_derive_key_iv_zeros() {
let salt = [0u8; 8];
let key = [0u8; 8];
let params = CryptoParameters::default();
let (aes_key, aes_iv) = derive_key_iv(&key, &salt, ¶ms);
assert_eq!(aes_key.len(), 32, "AES-256 key should be 32 bytes");
assert_eq!(aes_iv.len(), 16, "AES IV should be 16 bytes");
let (aes_key2, aes_iv2) = derive_key_iv(&key, &salt, ¶ms);
assert_eq!(aes_key, aes_key2, "Key derivation should be deterministic");
assert_eq!(aes_iv, aes_iv2, "IV derivation should be deterministic");
}
#[test]
fn test_decrypt_string_roundtrip() {
let salt = [0u8; 8];
let key = [0u8; 8];
let params = CryptoParameters::default();
let (aes_key, aes_iv) = derive_key_iv(&key, &salt, ¶ms);
let original = "Hello, BitMono!";
let encrypted =
apply_crypto_transform("AES", &aes_key, &aes_iv, true, original.as_bytes(), 1, 2)
.expect("AES encryption should succeed");
let decrypted = decrypt_string(&encrypted, &aes_key, &aes_iv).unwrap();
assert_eq!(decrypted, original);
}
#[test]
fn test_decrypt_empty_string() {
let salt = [0u8; 8];
let key = [0u8; 8];
let params = CryptoParameters::default();
let (aes_key, aes_iv) = derive_key_iv(&key, &salt, ¶ms);
let result = decrypt_string(&[], &aes_key, &aes_iv).unwrap();
assert_eq!(result, "");
}
#[test]
fn test_decrypt_string_invalid_data() {
let salt = [0u8; 8];
let key = [0u8; 8];
let params = CryptoParameters::default();
let (aes_key, aes_iv) = derive_key_iv(&key, &salt, ¶ms);
let result = decrypt_string(&[1, 2, 3], &aes_key, &aes_iv);
assert!(result.is_err());
}
}