use std::collections::HashMap;
use crate::{
analysis::{ConstValue, SsaFunction, SsaOp, SsaVarId},
compiler::{CompilerContext, EventKind, ModificationScope, SsaPass},
metadata::token::Token,
CilObject, Result,
};
pub struct UnmanagedStringReversalPass {
pub(crate) native_string_map: HashMap<Token, String>,
}
impl SsaPass for UnmanagedStringReversalPass {
fn name(&self) -> &'static str {
"BitMonoUnmanagedString"
}
fn description(&self) -> &'static str {
"Replaces calls to fake native string methods with ldstr constants"
}
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;
for block_idx in 0..ssa.blocks().len() {
let sites = find_unmanaged_string_sites(ssa, block_idx, &self.native_string_map);
if sites.is_empty() {
continue;
}
let block = match ssa.block_mut(block_idx) {
Some(b) => b,
None => continue,
};
for site in sites.iter().rev() {
if let Some(instr) = block.instruction_mut(site.newobj_idx) {
instr.set_op(SsaOp::Const {
dest: site.newobj_dest,
value: ConstValue::DecryptedString(site.decrypted.clone()),
});
}
if let Some(instr) = block.instruction_mut(site.call_idx) {
instr.set_op(SsaOp::Nop);
}
changed = true;
}
if !sites.is_empty() {
ctx.events
.record(EventKind::StringDecrypted)
.message(format!(
"BitMonoUnmanagedString: reversed {} call+newobj sites in block {}",
sites.len(),
block_idx,
));
}
}
Ok(changed)
}
}
struct UnmanagedStringSite {
call_idx: usize,
newobj_idx: usize,
newobj_dest: SsaVarId,
decrypted: String,
}
fn find_unmanaged_string_sites(
ssa: &SsaFunction,
block_idx: usize,
native_map: &HashMap<Token, String>,
) -> Vec<UnmanagedStringSite> {
let mut sites = Vec::new();
let Some(block) = ssa.block(block_idx) else {
return sites;
};
let instructions = block.instructions();
for (i, instr) in instructions.iter().enumerate() {
let (call_dest, call_token) = match instr.op() {
SsaOp::Call { dest, method, .. } => {
let Some(d) = dest else { continue };
(*d, method.token())
}
_ => continue,
};
let Some(decrypted) = native_map.get(&call_token) else {
continue;
};
const MAX_SEARCH_DISTANCE: usize = 20;
let search_end = (i + 1 + MAX_SEARCH_DISTANCE).min(instructions.len());
for (j, next) in instructions
.iter()
.enumerate()
.skip(i + 1)
.take(search_end - (i + 1))
{
if let SsaOp::NewObj { dest, args, .. } = next.op() {
if args.len() == 1 && args[0] == call_dest {
sites.push(UnmanagedStringSite {
call_idx: i,
newobj_idx: j,
newobj_dest: *dest,
decrypted: decrypted.clone(),
});
break;
}
}
}
}
sites
}