use std::collections::HashSet;
use crate::{
analysis::{MethodRef, SsaFunction, SsaOp},
compiler::{CompilerContext, EventKind, ModificationScope, SsaPass},
metadata::token::Token,
CilObject, Result,
};
pub struct ResourceShimRewritePass {
shim_method_tokens: HashSet<Token>,
lazy_init_token: Token,
bcl_get_manifest_resource_names: Token,
}
impl ResourceShimRewritePass {
#[must_use]
pub fn new(
shim_method_tokens: impl IntoIterator<Item = Token>,
lazy_init_token: Token,
bcl_get_manifest_resource_names: Token,
) -> Self {
Self {
shim_method_tokens: shim_method_tokens.into_iter().collect(),
lazy_init_token,
bcl_get_manifest_resource_names,
}
}
}
impl SsaPass for ResourceShimRewritePass {
fn name(&self) -> &'static str {
"netreactor-resource-shim-rewrite"
}
fn description(&self) -> &'static str {
"Rewrites NR resource-resolver shim calls (eBxqprrF8 → \
GetManifestResourceNames; lazy_init → Nop)"
}
fn modification_scope(&self) -> ModificationScope {
ModificationScope::InstructionsOnly
}
fn run_on_method(
&self,
ssa: &mut SsaFunction,
method_token: Token,
ctx: &CompilerContext,
_assembly: &CilObject,
) -> Result<bool> {
if self.shim_method_tokens.contains(&method_token) || self.lazy_init_token == method_token {
return Ok(false);
}
let bcl_target = MethodRef::new(self.bcl_get_manifest_resource_names);
let mut shim_rewrites = 0usize;
let mut init_nops = 0usize;
let block_count = ssa.blocks().len();
for block_idx in 0..block_count {
let Some(block) = ssa.block_mut(block_idx) else {
continue;
};
let instr_count = block.instructions().len();
for instr_idx in 0..instr_count {
let Some(instr) = block.instruction_mut(instr_idx) else {
continue;
};
let new_op = match instr.op() {
SsaOp::Call { dest, method, args }
if self.shim_method_tokens.contains(&method.token())
&& self.bcl_get_manifest_resource_names.value() != 0 =>
{
let new = SsaOp::CallVirt {
dest: *dest,
method: bcl_target,
args: args.clone(),
};
shim_rewrites += 1;
Some(new)
}
SsaOp::Call { dest, method, args }
if method.token() == self.lazy_init_token
&& dest.is_none()
&& args.is_empty() =>
{
init_nops += 1;
Some(SsaOp::Nop)
}
_ => None,
};
if let Some(op) = new_op {
instr.set_op(op);
}
}
}
if shim_rewrites > 0 {
ctx.events
.record(EventKind::ConstantFolded)
.at(method_token, 0)
.message(format!(
"NR resources: rewrote {shim_rewrites} GetManifestResourceNames shim call(s)"
));
}
if init_nops > 0 {
ctx.events
.record(EventKind::InstructionRemoved)
.at(method_token, 0)
.message(format!(
"NR resources: nopped {init_nops} lazy-init call(s)"
));
}
Ok(shim_rewrites > 0 || init_nops > 0)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::{
analysis::{CallGraph, MethodRef, SsaFunctionBuilder, SsaType},
compiler::SsaPass,
deobfuscation::context::AnalysisContext,
test::helpers::test_assembly_arc,
};
fn make_ctx() -> AnalysisContext {
AnalysisContext::new(Arc::new(CallGraph::new()))
}
#[test]
fn rewrites_shim_call_to_callvirt() {
let shim = Token::new(0x0600009b);
let bcl = Token::new(0x0a000099);
let lazy_init = Token::new(0x0600009e);
let mut ssa = SsaFunctionBuilder::new(1, 0)
.build_with(|f| {
let arg0 = f.arg(0, SsaType::Object);
f.block(0, |b| {
let _ = b.call(MethodRef::new(shim), &[arg0], SsaType::Object);
b.ret();
});
})
.unwrap();
let pass = ResourceShimRewritePass::new(vec![shim], lazy_init, bcl);
let ctx = make_ctx();
let changed = pass
.run_on_method(&mut ssa, Token::new(0x06000003), &ctx, &test_assembly_arc())
.unwrap();
assert!(changed);
let block = ssa.block(0).unwrap();
match block.instructions()[0].op() {
SsaOp::CallVirt { method, args, .. } => {
assert_eq!(method.token(), bcl);
assert_eq!(args.len(), 1);
}
other => panic!("expected CallVirt, got {other:?}"),
}
}
#[test]
fn nops_lazy_init_call() {
let shim = Token::new(0x0600009b);
let bcl = Token::new(0x0a000099);
let lazy_init = Token::new(0x0600009e);
let mut ssa = SsaFunctionBuilder::new(0, 0)
.build_with(|f| {
f.block(0, |b| {
b.call_void(MethodRef::new(lazy_init), &[]);
b.ret();
});
})
.unwrap();
let pass = ResourceShimRewritePass::new(vec![shim], lazy_init, bcl);
let ctx = make_ctx();
let changed = pass
.run_on_method(&mut ssa, Token::new(0x06000003), &ctx, &test_assembly_arc())
.unwrap();
assert!(changed);
let block = ssa.block(0).unwrap();
assert!(matches!(block.instructions()[0].op(), SsaOp::Nop));
}
#[test]
fn skips_resolver_own_methods() {
let shim = Token::new(0x0600009b);
let bcl = Token::new(0x0a000099);
let lazy_init = Token::new(0x0600009e);
let mut ssa = SsaFunctionBuilder::new(0, 0)
.build_with(|f| {
f.block(0, |b| {
b.call_void(MethodRef::new(lazy_init), &[]);
b.ret();
});
})
.unwrap();
let pass = ResourceShimRewritePass::new(vec![shim], lazy_init, bcl);
let ctx = make_ctx();
let changed = pass
.run_on_method(&mut ssa, lazy_init, &ctx, &test_assembly_arc())
.unwrap();
assert!(!changed);
}
#[test]
fn skips_shim_rewrite_when_bcl_token_zero() {
let shim = Token::new(0x0600009b);
let lazy_init = Token::new(0x0600009e);
let bcl_unset = Token::new(0);
let mut ssa = SsaFunctionBuilder::new(1, 0)
.build_with(|f| {
let arg0 = f.arg(0, SsaType::Object);
f.block(0, |b| {
let _ = b.call(MethodRef::new(shim), &[arg0], SsaType::Object);
b.ret();
});
})
.unwrap();
let pass = ResourceShimRewritePass::new(vec![shim], lazy_init, bcl_unset);
let ctx = make_ctx();
let changed = pass
.run_on_method(&mut ssa, Token::new(0x06000003), &ctx, &test_assembly_arc())
.unwrap();
assert!(!changed);
}
}