use std::{any::Any, collections::HashMap};
use crate::{
compiler::{EventKind, EventLog},
deobfuscation::techniques::{
Detection, Detections, Evidence, Technique, TechniqueCategory, WorkingAssembly,
},
metadata::{
signatures::{SignatureMethod, TypeSignature},
tables::{MemberRefRaw, TableId},
token::Token,
typesystem::wellknown,
},
CilObject, Result,
};
#[derive(Debug)]
pub struct HookFindings {
pub infrastructure_type: Option<Token>,
pub redirect_stub: Option<Token>,
pub hook_count: usize,
pub dummy_methods: Vec<Token>,
pub init_methods: Vec<Token>,
}
pub struct BitMonoHooks;
impl Technique for BitMonoHooks {
fn id(&self) -> &'static str {
"bitmono.hooks"
}
fn name(&self) -> &'static str {
"BitMono DotNetHook Reversal"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Protection
}
fn detect(&self, assembly: &CilObject) -> Detection {
let mut infrastructure_type = None;
let mut redirect_stub = None;
let mut hook_count = 0usize;
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
let mut has_jit_hook_setup = false;
let mut has_marshal_write = false;
for (_, method_ref) in cil_type.methods.iter() {
let Some(method) = method_ref.upgrade() else {
continue;
};
for instr in method.instructions() {
if let Some(token) = instr.get_token_operand() {
if let Some(name) = assembly.resolve_method_name(token) {
if name.contains("PrepareMethod")
|| name.contains("GetFunctionPointer")
|| name.contains("VirtualProtect")
|| name.contains("mprotect")
{
has_jit_hook_setup = true;
}
}
if let Some(member) = assembly.member_ref(&token) {
if member.name.contains("Write")
&& member
.declaredby
.fullname()
.is_some_and(|t| t.contains("Marshal"))
{
has_marshal_write = true;
}
}
}
}
}
if has_jit_hook_setup && has_marshal_write {
hook_count += 1;
infrastructure_type = Some(cil_type.token);
for (_, method_ref) in cil_type.methods.iter() {
let Some(method) = method_ref.upgrade() else {
continue;
};
if !method.is_static() {
continue;
}
if !matches!(method.signature.return_type.base, TypeSignature::Void) {
continue;
}
if method.signature.params.len() != 2 {
continue;
}
let both_i4 = method
.signature
.params
.iter()
.all(|p| matches!(p.base, TypeSignature::I4));
if !both_i4 {
continue;
}
let has_hook_api = method.instructions().any(|instr| {
instr
.get_token_operand()
.and_then(|t| assembly.resolve_method_name(t))
.is_some_and(|n| {
n.contains("PrepareMethod") || n.contains("GetFunctionPointer")
})
});
if has_hook_api {
redirect_stub = Some(method.token);
break;
}
}
break;
}
}
if hook_count == 0 {
return Detection::new_empty();
}
let mut dummy_methods = Vec::new();
let mut init_methods = Vec::new();
let module_type_token = assembly.types().module_type().map(|m| m.token);
if let Some(stub_token) = redirect_stub {
for method_entry in assembly.methods() {
let method = method_entry.value();
let in_module = method
.declaring_type_rc()
.map(|dt| Some(dt.token) == module_type_token)
.unwrap_or(false);
if !in_module || !method.is_static() || method.name == wellknown::members::CCTOR {
continue;
}
let instrs: Vec<_> = method.instructions().collect();
if is_dummy_body(&instrs) {
dummy_methods.push(method.token);
}
}
for method_entry in assembly.methods() {
let method = method_entry.value();
for instr in method.instructions() {
if instr.mnemonic == "call" {
if let Some(call_target) = instr.get_token_operand() {
let is_redirect = call_target == stub_token
|| is_redirect_stub_memberref(assembly, call_target, stub_token);
if is_redirect && !init_methods.contains(&method.token) {
init_methods.push(method.token);
}
}
}
}
}
}
let mut evidence = vec![Evidence::Structural(
"BitMono DotNetHook infrastructure (PrepareMethod + Marshal.Write)".to_string(),
)];
if redirect_stub.is_some() {
evidence.push(Evidence::Structural(
"RedirectStub identified: static void(int32, int32)".to_string(),
));
}
let findings = HookFindings {
infrastructure_type,
redirect_stub,
hook_count,
dummy_methods: dummy_methods.clone(),
init_methods: init_methods.clone(),
};
let mut detection = Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
);
if let Some(infra_token) = infrastructure_type {
detection.cleanup_mut().add_type(infra_token);
}
detection.cleanup_mut().add_methods(dummy_methods);
detection.cleanup_mut().add_methods(init_methods);
detection
}
fn byte_transform(
&self,
assembly: &mut WorkingAssembly,
detection: &Detection,
_detections: &Detections,
) -> Option<Result<EventLog>> {
let events = EventLog::new();
let Some(findings) = detection.findings::<HookFindings>() else {
return Some(Ok(events));
};
let co = match assembly.cilobject() {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let redirect_stub_token = match findings.redirect_stub {
Some(token) => token,
None => {
match find_redirect_stub(co) {
Some(token) => token,
None => {
events.record(EventKind::ArtifactRemoved).message(
"DotNetHook: infrastructure detected but RedirectStub not found"
.to_string(),
);
return Some(Ok(events));
}
}
}
};
let view = co;
let (mappings, _init_tokens, stale_correction_map) =
extract_hook_mappings(view, redirect_stub_token);
if mappings.is_empty() {
events
.record(EventKind::ArtifactRemoved)
.message("DotNetHook: no redirect mappings found".to_string());
return Some(Ok(events));
}
let redirect_map: HashMap<u32, u32> = mappings
.iter()
.map(|m| (m.dummy_token, m.target_token))
.collect();
let mut patches: Vec<(u64, u32)> = Vec::new();
for method_entry in view.methods() {
let method = method_entry.value();
for instr in method.instructions() {
if instr.mnemonic == "call"
|| instr.mnemonic == "callvirt"
|| instr.mnemonic == "newobj"
{
if let Some(call_target) = instr.get_token_operand() {
let raw_token = call_target.value();
if let Some(&real_target) = redirect_map.get(&raw_token) {
patches.push((instr.offset + instr.size - 4, real_target));
}
}
continue;
}
if instr.mnemonic == "ldc.i4" {
if let Some(val) = instr.get_i32_operand() {
let u = val as u32;
if (u >> 24) == 0x06 {
if let Some(&corrected) = stale_correction_map.get(&u) {
if corrected != u {
patches.push((instr.offset + 1, corrected));
}
}
}
}
}
}
}
if patches.is_empty() {
events
.record(EventKind::ArtifactRemoved)
.message("DotNetHook: no call sites found to patch".to_string());
return Some(Ok(events));
}
for &(operand_offset, new_token) in &patches {
if let Err(e) = assembly.write_le::<u32>(operand_offset as usize, new_token) {
return Some(Err(e));
}
}
let patch_count = patches.len();
let mapping_count = mappings.len();
events.record(EventKind::ArtifactRemoved).message(format!(
"Reversed {mapping_count} DotNetHook redirections, patched {patch_count} call sites",
));
Some(Ok(events))
}
fn requires_regeneration(&self) -> bool {
true
}
}
fn find_redirect_stub(assembly: &CilObject) -> Option<Token> {
for method_entry in assembly.methods() {
let method = method_entry.value();
if !method.is_static() {
continue;
}
if !matches!(method.signature.return_type.base, TypeSignature::Void) {
continue;
}
if method.signature.params.len() != 2 {
continue;
}
let both_i4 = method
.signature
.params
.iter()
.all(|p| matches!(p.base, TypeSignature::I4));
if !both_i4 {
continue;
}
let has_hook_api = method.instructions().any(|instr| {
instr
.get_token_operand()
.and_then(|t| assembly.resolve_method_name(t))
.is_some_and(|n| n.contains("PrepareMethod") || n.contains("GetFunctionPointer"))
});
if has_hook_api {
return Some(method.token);
}
}
None
}
struct HookMapping {
dummy_token: u32,
target_token: u32,
}
fn extract_hook_mappings(
assembly: &CilObject,
redirect_stub_token: Token,
) -> (Vec<HookMapping>, Vec<Token>, HashMap<u32, u32>) {
let mut stale_pairs: Vec<(u32, u32)> = Vec::new();
let mut init_tokens: Vec<Token> = Vec::new();
let module_type_token = assembly.types().module_type().map(|m| m.token);
let infra_tokens: Vec<Token> = assembly
.types()
.iter()
.filter(|entry| {
let t = entry.value();
let mut has_hook_api = false;
let mut has_marshal = false;
for (_, mr) in t.methods.iter() {
if let Some(m) = mr.upgrade() {
for instr in m.instructions() {
if let Some(tok) = instr.get_token_operand() {
if let Some(name) = assembly.resolve_method_name(tok) {
if name.contains("PrepareMethod")
|| name.contains("GetFunctionPointer")
{
has_hook_api = true;
}
}
if let Some(member) = assembly.member_ref(&tok) {
if member.name.contains("Write")
&& member
.declaredby
.fullname()
.is_some_and(|t| t.contains("Marshal"))
{
has_marshal = true;
}
}
}
}
}
}
has_hook_api && has_marshal
})
.map(|entry| entry.value().token)
.collect();
for method_entry in assembly.methods() {
let method = method_entry.value();
let instructions: Vec<_> = method.instructions().collect();
for (i, instr) in instructions.iter().enumerate() {
if instr.mnemonic != "call" {
continue;
}
let Some(call_target) = instr.get_token_operand() else {
continue;
};
let is_redirect_call = call_target == redirect_stub_token
|| is_redirect_stub_memberref(assembly, call_target, redirect_stub_token);
if !is_redirect_call || i < 2 {
continue;
}
let arg1 = instructions[i - 2]
.mnemonic
.starts_with("ldc.i4")
.then(|| instructions[i - 2].get_i32_operand())
.flatten();
let arg2 = instructions[i - 1]
.mnemonic
.starts_with("ldc.i4")
.then(|| instructions[i - 1].get_i32_operand())
.flatten();
if let (Some(a1), Some(a2)) = (arg1, arg2) {
let d = a1 as u32;
let t = a2 as u32;
if (d >> 24) == 0x06 && (t >> 24) == 0x06 {
stale_pairs.push((d, t));
if !init_tokens.contains(&method.token) {
init_tokens.push(method.token);
}
}
}
}
}
if stale_pairs.is_empty() {
return (Vec::new(), Vec::new(), HashMap::new());
}
let mut dummy_final_tokens: Vec<Token> = Vec::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
let in_module = method
.declaring_type_rc()
.map(|dt| Some(dt.token) == module_type_token)
.unwrap_or(false);
if !in_module || !method.is_static() || method.name == wellknown::members::CCTOR {
continue;
}
let instrs: Vec<_> = method.instructions().collect();
if is_dummy_body(&instrs) {
dummy_final_tokens.push(method.token);
}
}
let mut stale_dummy_sorted: Vec<u32> = stale_pairs.iter().map(|(sd, _)| *sd).collect();
stale_dummy_sorted.sort();
stale_dummy_sorted.dedup();
dummy_final_tokens.sort_by_key(|t| t.row());
if stale_dummy_sorted.len() != dummy_final_tokens.len() {
let mappings = stale_pairs
.iter()
.map(|(d, t)| HookMapping {
dummy_token: *d,
target_token: *t,
})
.collect();
return (mappings, init_tokens, HashMap::new());
}
let stale_to_final_dummy: HashMap<u32, u32> = stale_dummy_sorted
.iter()
.zip(dummy_final_tokens.iter())
.map(|(s, f)| (*s, f.value()))
.collect();
let target_offset = compute_target_offset(
assembly,
&stale_to_final_dummy,
&stale_pairs,
module_type_token,
&infra_tokens,
);
let offset = target_offset.unwrap_or(0);
let total_methods = assembly.methods().iter().count() as u32;
let original_count = if offset > 0 {
(total_methods as i64 - offset) as u32
} else {
total_methods
};
let mut stale_correction_map: HashMap<u32, u32> = (1..=original_count)
.filter_map(|r| {
let stale = 0x0600_0000 | r;
let final_row = (r as i64 + offset) as u32;
if final_row != r && final_row >= 1 && final_row <= total_methods {
Some((stale, 0x0600_0000 | final_row))
} else {
None
}
})
.collect();
let mappings: Vec<HookMapping> = stale_pairs
.iter()
.map(|(stale_dummy, stale_target)| {
let final_dummy = stale_to_final_dummy
.get(stale_dummy)
.copied()
.unwrap_or(*stale_dummy);
let final_target = stale_correction_map
.get(stale_target)
.copied()
.unwrap_or(*stale_target);
HookMapping {
dummy_token: final_dummy,
target_token: final_target,
}
})
.collect();
for mapping in &mappings {
for (stale_dummy, final_dummy_val) in &stale_to_final_dummy {
if *final_dummy_val == mapping.dummy_token {
stale_correction_map.insert(*stale_dummy, mapping.target_token);
}
}
}
(mappings, init_tokens, stale_correction_map)
}
fn is_dummy_body(instructions: &[&crate::assembly::Instruction]) -> bool {
if instructions.is_empty() {
return false;
}
let last = instructions.last().unwrap();
if last.mnemonic != "ret" {
return false;
}
match instructions.len() {
1 => true, 2 => {
let m = instructions[0].mnemonic;
m.starts_with("ldc.") || m == "ldnull"
}
3 => {
let m0 = instructions[0].mnemonic;
let m1 = instructions[1].mnemonic;
(m0.starts_with("ldc.") && m1.starts_with("conv."))
|| (m0.starts_with("ldloca") && m1 == "initobj")
}
_ => false,
}
}
fn compute_target_offset(
assembly: &CilObject,
stale_to_final_dummy: &HashMap<u32, u32>,
stale_pairs: &[(u32, u32)],
module_type_token: Option<Token>,
infra_tokens: &[Token],
) -> Option<i64> {
let stale_dummy_to_target: HashMap<u32, u32> =
stale_pairs.iter().map(|(d, t)| (*d, *t)).collect();
let mut sorted_entries: Vec<(&u32, &u32)> = stale_to_final_dummy.iter().collect();
sorted_entries.sort_by_key(|(k, _)| *k);
let mut offset_votes: HashMap<i64, usize> = HashMap::new();
for (stale_dummy, final_dummy_val) in sorted_entries {
let final_dummy_token = Token::new(*final_dummy_val);
let Some(dummy_method) = assembly.method(&final_dummy_token) else {
continue;
};
let mut candidates: Vec<Token> = Vec::new();
for method_entry in assembly.methods() {
let method = method_entry.value();
if let Some(dt) = method.declaring_type_rc() {
if Some(dt.token) == module_type_token {
continue;
}
if infra_tokens.contains(&dt.token) {
continue;
}
}
if signatures_match(&method.signature, &dummy_method.signature) {
candidates.push(method.token);
}
}
let Some(&stale_target) = stale_dummy_to_target.get(stale_dummy) else {
continue;
};
let stale_row = (stale_target & 0x00FF_FFFF) as i64;
if candidates.len() == 1 {
let final_row = candidates[0].row() as i64;
let offset = final_row - stale_row;
*offset_votes.entry(offset).or_insert(0) += 1;
}
}
offset_votes
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(offset, _)| offset)
}
fn signatures_match(a: &SignatureMethod, b: &SignatureMethod) -> bool {
if a.return_type.base != b.return_type.base {
return false;
}
if a.params.len() != b.params.len() {
return false;
}
a.params
.iter()
.zip(b.params.iter())
.all(|(pa, pb)| pa.base == pb.base)
}
fn is_redirect_stub_memberref(
assembly: &CilObject,
token: Token,
redirect_stub_token: Token,
) -> bool {
if token.table() != 0x0A {
return false;
}
let Some(tables) = assembly.tables() else {
return false;
};
let Some(memberref_table) = tables.table::<MemberRefRaw>() else {
return false;
};
let Some(memberref) = memberref_table.get(token.row()) else {
return false;
};
if memberref.class.tag == TableId::TypeDef {
if let Some(stub_method) = assembly.method(&redirect_stub_token) {
if let Some(stub_type) = stub_method.declaring_type_rc() {
return memberref.class.row == stub_type.token.row();
}
}
}
false
}
#[cfg(test)]
mod tests {
use crate::test::helpers::load_sample;
use crate::{deobfuscation::techniques::Technique, metadata::token::Token};
#[test]
fn test_hook_mapping_token_extraction() {
let dummy_i32: i32 = 0x0600_0010_u32 as i32;
let target_i32: i32 = 0x0600_0020_u32 as i32;
let dummy_u32 = dummy_i32 as u32;
let target_u32 = target_i32 as u32;
assert_eq!(dummy_u32 >> 24, 0x06, "Dummy should be MethodDef");
assert_eq!(target_u32 >> 24, 0x06, "Target should be MethodDef");
let dummy_token = Token::new(dummy_u32);
let target_token = Token::new(target_u32);
assert_eq!(dummy_token.row(), 0x10);
assert_eq!(target_token.row(), 0x20);
}
#[test]
fn test_non_methoddef_tokens_rejected() {
let typeref_val: i32 = 0x0100_0005_u32 as i32;
let memberref_val: i32 = 0x0A00_0003_u32 as i32;
assert_ne!((typeref_val as u32) >> 24, 0x06);
assert_ne!((memberref_val as u32) >> 24, 0x06);
}
#[test]
fn test_detect_positive() {
let assembly = load_sample("tests/samples/packers/bitmono/0.39.0/bitmono_dotnethook.exe");
let technique = super::BitMonoHooks;
let detection = technique.detect(&assembly);
assert!(
detection.is_detected(),
"BitMonoHooks should detect DotNetHook infrastructure in bitmono_dotnethook.exe"
);
assert!(
!detection.evidence().is_empty(),
"Detection should include evidence"
);
}
#[test]
fn test_detect_negative() {
let assembly = load_sample("tests/samples/packers/confuserex/1.6.0/original.exe");
let technique = super::BitMonoHooks;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"BitMonoHooks should not detect DotNetHook in a non-BitMono assembly"
);
}
}