use std::{collections::HashSet, hash::BuildHasher, sync::Arc};
use crate::{
emulation::{EmValue, EmulationThread, Hook, HookContext, HookPriority, PreHookResult},
metadata::{tables::TableId, token::Token, typesystem::CilFlavor},
utils::{decompress_confuserex_lzma, is_confuserex_lzma},
};
#[must_use]
pub fn create_lzma_hook() -> Hook {
Hook::new("confuserex-lzma-decompressor")
.with_priority(HookPriority::HIGH) .match_internal_method() .match_runtime("lzma-candidate-check", is_lzma_decompressor_candidate)
.pre(lzma_decompression_handler)
}
#[must_use]
pub fn create_anti_tamper_stub_hook<S: BuildHasher + Send + Sync + 'static>(
tokens: HashSet<Token, S>,
) -> Hook {
let tokens = Arc::new(tokens);
Hook::new("confuserex-protection-stub")
.with_priority(HookPriority::HIGHEST) .match_runtime("protection-token-check", {
let tokens = Arc::clone(&tokens);
move |ctx: &HookContext<'_>, _thread: &EmulationThread| {
let is_methoddef = ctx.method_token.is_table(TableId::MethodDef);
is_methoddef && tokens.contains(&ctx.method_token)
}
})
.pre(|ctx, _thread| {
let return_value = match ctx.return_type {
None | Some(CilFlavor::Void) => None, Some(
CilFlavor::Boolean | CilFlavor::I1 | CilFlavor::U1
| CilFlavor::I2 | CilFlavor::U2 | CilFlavor::Char
| CilFlavor::I4 | CilFlavor::U4
| CilFlavor::TypedRef { .. }
| CilFlavor::Unknown, ) => Some(EmValue::I32(0)),
Some(CilFlavor::I8 | CilFlavor::U8) => Some(EmValue::I64(0)),
Some(CilFlavor::R4) => Some(EmValue::F32(0.0)),
Some(CilFlavor::R8) => Some(EmValue::F64(0.0)),
Some(
CilFlavor::I | CilFlavor::U
| CilFlavor::Pointer | CilFlavor::ByRef
| CilFlavor::FnPtr { .. }
| CilFlavor::Pinned,
) => Some(EmValue::NativeInt(0)),
Some(
CilFlavor::String
| CilFlavor::Object
| CilFlavor::Class
| CilFlavor::Interface
| CilFlavor::Array { .. }
| CilFlavor::GenericInstance
| CilFlavor::GenericParameter { .. },
) => Some(EmValue::Null),
Some(CilFlavor::ValueType) => {
Some(EmValue::I32(0))
}
};
PreHookResult::Bypass(return_value)
})
}
fn is_lzma_decompressor_candidate(ctx: &HookContext<'_>, thread: &EmulationThread) -> bool {
let has_byte_array_param = match ctx.param_types {
Some(params) if !params.is_empty() => params.iter().any(|p| {
matches!(
p,
CilFlavor::Array { element_type, rank, .. }
if *rank == 1 && matches!(element_type.as_ref(), CilFlavor::U1 | CilFlavor::I1)
)
}),
_ => false,
};
if !has_byte_array_param {
return false;
}
let returns_byte_array = match &ctx.return_type {
Some(CilFlavor::Array {
element_type, rank, ..
}) => *rank == 1 && matches!(element_type.as_ref(), CilFlavor::U1 | CilFlavor::I1),
_ => false,
};
if !returns_byte_array && !has_byte_array_param {
return false;
}
is_lzma_input(ctx, thread)
}
fn is_lzma_input(ctx: &HookContext<'_>, thread: &EmulationThread) -> bool {
for (idx, arg) in ctx.args.iter().enumerate() {
if idx == 0 && ctx.this.is_some() {
continue;
}
let EmValue::ObjectRef(heap_ref) = arg else {
continue;
};
let Some(byte_data) = thread
.heap()
.get_array_as_bytes(*heap_ref, ctx.pointer_size)
.or_else(|| thread.heap().get_byte_array(*heap_ref))
else {
continue;
};
if is_confuserex_lzma(&byte_data) {
return true;
}
}
false
}
fn lzma_decompression_handler(
ctx: &HookContext<'_>,
thread: &mut EmulationThread,
) -> PreHookResult {
let mut lzma_data: Option<Vec<u8>> = None;
for (idx, arg) in ctx.args.iter().enumerate() {
if idx == 0 && ctx.this.is_some() {
continue;
}
let EmValue::ObjectRef(heap_ref) = arg else {
continue;
};
let Some(byte_data) = thread
.heap()
.get_array_as_bytes(*heap_ref, ctx.pointer_size)
.or_else(|| thread.heap().get_byte_array(*heap_ref))
else {
continue;
};
if is_confuserex_lzma(&byte_data) {
lzma_data = Some(byte_data);
break;
}
}
let Some(byte_data) = lzma_data else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
let Ok(decompressed) = decompress_confuserex_lzma(&byte_data) else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
let elements: Vec<EmValue> = decompressed
.into_iter()
.map(|b| EmValue::I32(i32::from(b)))
.collect();
match thread
.heap_mut()
.alloc_array_with_values(CilFlavor::U1, elements)
{
Ok(result_ref) => PreHookResult::Bypass(Some(EmValue::ObjectRef(result_ref))),
Err(_) => {
PreHookResult::Bypass(Some(EmValue::Null))
}
}
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::obfuscators::confuserex::hooks::create_lzma_hook, emulation::HookPriority,
};
#[test]
fn test_create_lzma_hook() {
let hook = create_lzma_hook();
assert_eq!(hook.name(), "confuserex-lzma-decompressor");
assert_eq!(hook.priority(), HookPriority::HIGH);
}
}