use std::{any::Any, collections::HashMap, sync::Arc};
use crate::{
analysis::x86_native_body_size,
cilassembly::CleanupRequest,
compiler::{PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
passes::bitmono::UnmanagedStringReversalPass,
techniques::{Detection, Evidence, Technique, TechniqueCategory},
utils::is_guid_name,
},
metadata::token::Token,
utils::decode_utf16le,
CilObject,
};
#[derive(Debug)]
pub struct UnmanagedFindings {
pub native_methods: Vec<Token>,
pub string_map: Vec<(Token, String)>,
}
pub struct BitMonoUnmanaged;
impl Technique for BitMonoUnmanaged {
fn id(&self) -> &'static str {
"bitmono.unmanaged"
}
fn name(&self) -> &'static str {
"BitMono UnmanagedString Reversal"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Value
}
fn detect(&self, assembly: &CilObject) -> Detection {
let types = assembly.types();
let Some(module_type) = types.module_type() else {
return Detection::new_empty();
};
let mut native_methods = Vec::new();
for (_, method_ref) in module_type.methods.iter() {
let Some(method) = method_ref.upgrade() else {
continue;
};
if !method.is_code_native() {
continue;
}
if !method.is_pinvoke() && !method.is_internal_call() {
native_methods.push(method.token);
continue;
}
if is_guid_name(&method.name) {
native_methods.push(method.token);
}
}
if native_methods.is_empty() {
return Detection::new_empty();
}
let is_64bit = assembly.file().is_pe32_plus_format().unwrap_or(false);
let mut string_map = Vec::new();
for native_token in &native_methods {
if let Some(decrypted) = extract_native_string(assembly, *native_token, is_64bit) {
string_map.push((*native_token, decrypted));
}
}
let count = native_methods.len();
let extracted = string_map.len();
let mut evidence = vec![Evidence::Structural(format!(
"{count} fake native methods in <Module> (BitMono UnmanagedString)"
))];
if extracted > 0 {
evidence.push(Evidence::Structural(format!(
"{extracted} embedded strings extracted from native method bodies"
)));
}
Detection::new_detected(
evidence,
Some(Box::new(UnmanagedFindings {
native_methods,
string_map,
}) as Box<dyn Any + Send + Sync>),
)
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Simplify)
}
fn create_pass(
&self,
_ctx: &AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
let Some(findings) = detection.findings::<UnmanagedFindings>() else {
return Vec::new();
};
if findings.string_map.is_empty() {
return Vec::new();
}
let native_string_map: HashMap<Token, String> = findings
.string_map
.iter()
.map(|(token, s)| (*token, s.clone()))
.collect();
vec![Box::new(UnmanagedStringReversalPass { native_string_map })]
}
fn cleanup(&self, detection: &Detection) -> Option<CleanupRequest> {
let findings = detection.findings::<UnmanagedFindings>()?;
if findings.native_methods.is_empty() {
return None;
}
let mut request = CleanupRequest::new();
for token in &findings.native_methods {
request.add_method(*token);
}
Some(request)
}
}
fn extract_native_string(
assembly: &CilObject,
native_token: Token,
is_64bit: bool,
) -> Option<String> {
let method = assembly.method(&native_token)?;
let rva = method.rva.filter(|&r| r > 0)?;
let file = assembly.file();
let offset = file.rva_to_offset(rva as usize).ok()?;
let data = file.data();
if offset >= data.len() {
return None;
}
let native_bytes = &data[offset..];
let prefix_len = x86_native_body_size(native_bytes, is_64bit);
if prefix_len == 0 || prefix_len >= native_bytes.len() {
return None;
}
let string_bytes = &native_bytes[prefix_len..];
let looks_like_utf16 = string_bytes.len() >= 4
&& string_bytes[0] != 0
&& string_bytes[1] == 0
&& string_bytes[2] != 0
&& string_bytes[3] == 0;
if looks_like_utf16 {
if let Some(s) = decode_utf16le(string_bytes) {
return Some(s);
}
}
if let Some(null_pos) = string_bytes.iter().position(|&b| b == 0) {
if null_pos > 0 {
if let Ok(s) = std::str::from_utf8(&string_bytes[..null_pos]) {
return Some(s.to_string());
}
}
}
if !looks_like_utf16 {
if let Some(s) = decode_utf16le(string_bytes) {
return Some(s);
}
}
None
}
#[cfg(test)]
mod tests {
use crate::{
analysis::x86_native_body_size, deobfuscation::techniques::Technique,
test::helpers::load_sample,
};
#[test]
fn test_x86_body_size_x64_pattern() {
let bytes: Vec<u8> = vec![
0x48, 0x8D, 0x05, 0x01, 0x00, 0x00, 0x00, 0xC3, b'H', b'e', b'l', b'l', b'o', 0x00,
];
let body_size = x86_native_body_size(&bytes, true);
assert_eq!(body_size, 8, "x64 stub: LEA + RET = 8 bytes");
let string_bytes = &bytes[body_size..];
let null_pos = string_bytes.iter().position(|&b| b == 0).unwrap();
let extracted = std::str::from_utf8(&string_bytes[..null_pos]).unwrap();
assert_eq!(extracted, "Hello");
}
#[test]
fn test_x86_body_size_simple_pattern() {
let bytes: Vec<u8> = vec![
0x55, 0x89, 0xE5, 0x8D, 0x45, 0x08, 0x5D, 0xC3, b'T', b'e', b's', b't', 0x00,
];
let body_size = x86_native_body_size(&bytes, false);
assert_eq!(body_size, 8);
}
#[test]
fn test_x86_body_size_trampoline_pattern() {
let bytes: Vec<u8> = vec![
0x55, 0x89, 0xE5, 0xE8, 0x05, 0x00, 0x00, 0x00, 0x83, 0xC0, 0x01, 0x5D, 0xC3, 0x58, 0x83, 0xC0, 0x0B, 0xEB, 0xF8, b'H', b'e', b'l', b'l', b'o', 0x00,
];
let body_size = x86_native_body_size(&bytes, false);
assert_eq!(
body_size, 19,
"Traversal should follow call target through the trampoline"
);
let string_bytes = &bytes[body_size..];
let null_pos = string_bytes.iter().position(|&b| b == 0).unwrap();
let extracted = std::str::from_utf8(&string_bytes[..null_pos]).unwrap();
assert_eq!(extracted, "Hello");
}
#[test]
fn test_x86_body_size_empty() {
let bytes = vec![];
assert_eq!(x86_native_body_size(&bytes, false), 0);
}
#[test]
fn test_detect_positive() {
let assembly =
load_sample("tests/samples/packers/bitmono/0.39.0/bitmono_unmanagedstring.exe");
let technique = super::BitMonoUnmanaged;
let detection = technique.detect(&assembly);
assert!(
detection.is_detected(),
"BitMonoUnmanaged should detect fake native methods in bitmono_unmanagedstring.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::BitMonoUnmanaged;
let detection = technique.detect(&assembly);
assert!(
!detection.is_detected(),
"BitMonoUnmanaged should not detect fake native methods in a non-BitMono assembly"
);
}
}