use crate::{
emulation::{
runtime::hook::{Hook, HookContext, HookManager, PreHookResult},
thread::EmulationThread,
EmValue,
},
metadata::{tables::FieldRvaRaw, typesystem::CilFlavor},
};
pub fn register(manager: &mut HookManager) {
manager.register(
Hook::new("System.Runtime.CompilerServices.RuntimeHelpers.InitializeArray")
.match_name(
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"InitializeArray",
)
.pre(runtime_helpers_initialize_array_pre),
);
manager.register(
Hook::new("System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode")
.match_name(
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"GetHashCode",
)
.pre(runtime_helpers_get_hash_code_pre),
);
manager.register(
Hook::new("System.Runtime.CompilerServices.RuntimeHelpers.Equals")
.match_name(
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"Equals",
)
.pre(runtime_helpers_equals_pre),
);
manager.register(
Hook::new("System.Runtime.CompilerServices.RuntimeHelpers.GetObjectValue")
.match_name(
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"GetObjectValue",
)
.pre(runtime_helpers_get_object_value_pre),
);
manager.register(
Hook::new("System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor")
.match_name(
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"RunClassConstructor",
)
.pre(runtime_helpers_run_class_constructor_pre),
);
manager.register(
Hook::new("System.Runtime.CompilerServices.RuntimeHelpers.RunModuleConstructor")
.match_name(
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"RunModuleConstructor",
)
.pre(runtime_helpers_run_module_constructor_pre),
);
manager.register(
Hook::new("System.Object.Equals")
.match_name("System", "Object", "Equals")
.pre(object_equals_pre),
);
}
fn runtime_helpers_initialize_array_pre(
ctx: &HookContext<'_>,
thread: &mut EmulationThread,
) -> PreHookResult {
if ctx.args.len() < 2 {
return PreHookResult::Bypass(None);
}
let array_ref = match &ctx.args[0] {
EmValue::ObjectRef(r) => *r,
_ => return PreHookResult::Bypass(None),
};
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let field_token = match &ctx.args[1] {
EmValue::I32(v) => (*v).cast_unsigned(),
EmValue::NativeInt(v) | EmValue::I64(v) => *v as u32,
_ => return PreHookResult::Bypass(None),
};
let Some(assembly) = thread.assembly() else {
return PreHookResult::Bypass(None);
};
let Some(tables) = assembly.tables() else {
return PreHookResult::Bypass(None);
};
let Some(fieldrva_table) = tables.table::<FieldRvaRaw>() else {
return PreHookResult::Bypass(None);
};
let mut rva: Option<u32> = None;
for row in fieldrva_table {
let row_token = row.field | 0x0400_0000;
if row_token == field_token && row.rva > 0 {
rva = Some(row.rva);
break;
}
}
let Some(rva) = rva else {
return PreHookResult::Bypass(None);
};
let file = assembly.file();
let Ok(file_offset) = file.rva_to_offset(rva as usize) else {
return PreHookResult::Bypass(None);
};
let pe_data = file.data();
if file_offset >= pe_data.len() {
return PreHookResult::Bypass(None);
}
let array_len = thread.heap().get_array_length(array_ref).unwrap_or(0);
if array_len == 0 {
return PreHookResult::Bypass(None);
}
let element_type = thread
.heap()
.get_array_element_type(array_ref)
.unwrap_or(CilFlavor::U1);
let Some(element_size) = element_type.element_size(ctx.pointer_size) else {
return PreHookResult::Bypass(None);
};
let total_bytes = array_len * element_size;
let bytes_to_read = total_bytes.min(pe_data.len() - file_offset);
let data = &pe_data[file_offset..file_offset + bytes_to_read];
for i in 0..array_len {
let byte_offset = i * element_size;
if byte_offset + element_size > data.len() {
break;
}
let value = match element_type {
CilFlavor::Boolean | CilFlavor::I1 | CilFlavor::U1 => {
EmValue::I32(i32::from(data[byte_offset]))
}
CilFlavor::Char | CilFlavor::I2 | CilFlavor::U2 => {
let bytes = [data[byte_offset], data[byte_offset + 1]];
EmValue::I32(i32::from(i16::from_le_bytes(bytes)))
}
CilFlavor::I4 | CilFlavor::U4 => {
let bytes = [
data[byte_offset],
data[byte_offset + 1],
data[byte_offset + 2],
data[byte_offset + 3],
];
EmValue::I32(i32::from_le_bytes(bytes))
}
CilFlavor::R4 => {
let bytes = [
data[byte_offset],
data[byte_offset + 1],
data[byte_offset + 2],
data[byte_offset + 3],
];
EmValue::F32(f32::from_le_bytes(bytes))
}
CilFlavor::I8 | CilFlavor::U8 => {
let bytes = [
data[byte_offset],
data[byte_offset + 1],
data[byte_offset + 2],
data[byte_offset + 3],
data[byte_offset + 4],
data[byte_offset + 5],
data[byte_offset + 6],
data[byte_offset + 7],
];
EmValue::I64(i64::from_le_bytes(bytes))
}
CilFlavor::R8 => {
let bytes = [
data[byte_offset],
data[byte_offset + 1],
data[byte_offset + 2],
data[byte_offset + 3],
data[byte_offset + 4],
data[byte_offset + 5],
data[byte_offset + 6],
data[byte_offset + 7],
];
EmValue::F64(f64::from_le_bytes(bytes))
}
_ => EmValue::I32(i32::from(data[byte_offset])),
};
let _ = thread.heap_mut().set_array_element(array_ref, i, value);
}
PreHookResult::Bypass(None)
}
fn runtime_helpers_get_hash_code_pre(
ctx: &HookContext<'_>,
_thread: &mut EmulationThread,
) -> PreHookResult {
let result = if let Some(EmValue::ObjectRef(r)) = ctx.args.first() {
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let hash = r.id() as i32;
EmValue::I32(hash)
} else {
EmValue::I32(0)
};
PreHookResult::Bypass(Some(result))
}
fn runtime_helpers_equals_pre(
ctx: &HookContext<'_>,
_thread: &mut EmulationThread,
) -> PreHookResult {
if ctx.args.len() < 2 {
return PreHookResult::Bypass(Some(EmValue::I32(0))); }
let equal = match (&ctx.args[0], &ctx.args[1]) {
(EmValue::ObjectRef(a), EmValue::ObjectRef(b)) => a.id() == b.id(),
(EmValue::Null, EmValue::Null) => true,
_ => false,
};
PreHookResult::Bypass(Some(EmValue::I32(i32::from(equal))))
}
fn runtime_helpers_get_object_value_pre(
ctx: &HookContext<'_>,
_thread: &mut EmulationThread,
) -> PreHookResult {
let result = if let Some(arg) = ctx.args.first() {
arg.clone()
} else {
EmValue::Null
};
PreHookResult::Bypass(Some(result))
}
fn runtime_helpers_run_class_constructor_pre(
_ctx: &HookContext<'_>,
_thread: &mut EmulationThread,
) -> PreHookResult {
PreHookResult::Bypass(None)
}
fn runtime_helpers_run_module_constructor_pre(
_ctx: &HookContext<'_>,
_thread: &mut EmulationThread,
) -> PreHookResult {
PreHookResult::Bypass(None)
}
fn object_equals_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if let Some(this) = ctx.this {
let other = if ctx.args.is_empty() {
&EmValue::Null
} else {
&ctx.args[0]
};
let equal = this.clr_equals(other);
return PreHookResult::Bypass(Some(EmValue::I32(i32::from(equal))));
}
if ctx.args.len() >= 2 {
let equal = ctx.args[0].clr_equals(&ctx.args[1]);
return PreHookResult::Bypass(Some(EmValue::I32(i32::from(equal))));
}
PreHookResult::Bypass(Some(EmValue::I32(0)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
emulation::{runtime::hook::HookManager, HeapRef},
metadata::{token::Token, typesystem::PointerSize},
};
#[test]
fn test_register_hooks() {
let mut manager = HookManager::new();
register(&mut manager);
assert_eq!(manager.len(), 7);
}
#[test]
fn test_get_hash_code_hook() {
let obj_ref = HeapRef::new(42);
let args = [EmValue::ObjectRef(obj_ref)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"GetHashCode",
PointerSize::Bit64,
)
.with_args(&args);
let mut thread = crate::test::emulation::create_test_thread();
let result = runtime_helpers_get_hash_code_pre(&ctx, &mut thread);
match result {
PreHookResult::Bypass(Some(EmValue::I32(v))) => assert_eq!(v, 42),
_ => panic!("Expected Bypass with I32(42)"),
}
}
#[test]
fn test_equals_hook_same_reference() {
let obj1 = HeapRef::new(1);
let obj2 = HeapRef::new(1);
let args = [EmValue::ObjectRef(obj1), EmValue::ObjectRef(obj2)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"Equals",
PointerSize::Bit64,
)
.with_args(&args);
let mut thread = crate::test::emulation::create_test_thread();
let result = runtime_helpers_equals_pre(&ctx, &mut thread);
match result {
PreHookResult::Bypass(Some(EmValue::I32(v))) => assert_eq!(v, 1), _ => panic!("Expected Bypass with I32(1)"),
}
}
#[test]
fn test_equals_hook_different_references() {
let obj1 = HeapRef::new(1);
let obj2 = HeapRef::new(2);
let args = [EmValue::ObjectRef(obj1), EmValue::ObjectRef(obj2)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"Equals",
PointerSize::Bit64,
)
.with_args(&args);
let mut thread = crate::test::emulation::create_test_thread();
let result = runtime_helpers_equals_pre(&ctx, &mut thread);
match result {
PreHookResult::Bypass(Some(EmValue::I32(v))) => assert_eq!(v, 0), _ => panic!("Expected Bypass with I32(0)"),
}
}
#[test]
fn test_get_object_value_hook() {
let args = [EmValue::I32(42)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.Runtime.CompilerServices",
"RuntimeHelpers",
"GetObjectValue",
PointerSize::Bit64,
)
.with_args(&args);
let mut thread = crate::test::emulation::create_test_thread();
let result = runtime_helpers_get_object_value_pre(&ctx, &mut thread);
match result {
PreHookResult::Bypass(Some(EmValue::I32(v))) => assert_eq!(v, 42),
_ => panic!("Expected Bypass with I32(42)"),
}
}
}