use crate::{
emulation::{
runtime::hook::{Hook, HookContext, HookManager, PreHookResult},
thread::EmulationThread,
EmValue,
},
utils::{base64_decode, base64_encode},
Result,
};
pub fn register(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("System.Convert.ToBase64String")
.match_name("System", "Convert", "ToBase64String")
.pre(to_base64_string_pre),
)?;
manager.register(
Hook::new("System.Convert.FromBase64String")
.match_name("System", "Convert", "FromBase64String")
.pre(from_base64_string_pre),
)?;
manager.register(
Hook::new("System.Convert.ToByte")
.match_name("System", "Convert", "ToByte")
.pre(to_byte_pre),
)?;
manager.register(
Hook::new("System.Convert.ToSByte")
.match_name("System", "Convert", "ToSByte")
.pre(to_sbyte_pre),
)?;
manager.register(
Hook::new("System.Convert.ToInt16")
.match_name("System", "Convert", "ToInt16")
.pre(to_int16_pre),
)?;
manager.register(
Hook::new("System.Convert.ToUInt16")
.match_name("System", "Convert", "ToUInt16")
.pre(to_uint16_pre),
)?;
manager.register(
Hook::new("System.Convert.ToInt32")
.match_name("System", "Convert", "ToInt32")
.pre(to_int32_pre),
)?;
manager.register(
Hook::new("System.Convert.ToUInt32")
.match_name("System", "Convert", "ToUInt32")
.pre(to_uint32_pre),
)?;
manager.register(
Hook::new("System.Convert.ToInt64")
.match_name("System", "Convert", "ToInt64")
.pre(to_int64_pre),
)?;
manager.register(
Hook::new("System.Convert.ToUInt64")
.match_name("System", "Convert", "ToUInt64")
.pre(to_uint64_pre),
)?;
manager.register(
Hook::new("System.Convert.ToSingle")
.match_name("System", "Convert", "ToSingle")
.pre(to_single_pre),
)?;
manager.register(
Hook::new("System.Convert.ToDouble")
.match_name("System", "Convert", "ToDouble")
.pre(to_double_pre),
)?;
manager.register(
Hook::new("System.Convert.ToChar")
.match_name("System", "Convert", "ToChar")
.pre(to_char_pre),
)?;
manager.register(
Hook::new("System.Convert.ToBoolean")
.match_name("System", "Convert", "ToBoolean")
.pre(to_boolean_pre),
)?;
manager.register(
Hook::new("System.Convert.ToString")
.match_name("System", "Convert", "ToString")
.pre(to_string_pre),
)?;
Ok(())
}
fn to_base64_string_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(EmValue::Null));
}
if let EmValue::ObjectRef(handle) = &ctx.args[0] {
if let Some(bytes) = try_hook!(thread.heap().get_byte_array(*handle)) {
let encoded = base64_encode(&bytes);
match thread.heap_mut().alloc_string(&encoded) {
Ok(str_handle) => {
return PreHookResult::Bypass(Some(EmValue::ObjectRef(str_handle)))
}
Err(e) => return PreHookResult::Error(format!("heap allocation failed: {e}")),
}
}
}
PreHookResult::Bypass(Some(EmValue::Null))
}
fn from_base64_string_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(EmValue::Null));
}
if let EmValue::ObjectRef(handle) = &ctx.args[0] {
if let Ok(s) = thread.heap().get_string(*handle) {
if let Some(decoded) = base64_decode(&s) {
match thread.heap_mut().alloc_byte_array(&decoded) {
Ok(arr_handle) => {
return PreHookResult::Bypass(Some(EmValue::ObjectRef(arr_handle)))
}
Err(e) => return PreHookResult::Error(format!("heap allocation failed: {e}")),
}
}
}
}
PreHookResult::Bypass(Some(EmValue::Null))
}
fn to_byte_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0_u8.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_u8_cil().into()))
}
fn to_sbyte_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0_i8.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_i8_cil().into()))
}
fn to_int16_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0_i16.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_i16_cil().into()))
}
fn to_uint16_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0_u16.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_u16_cil().into()))
}
fn to_int32_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0_i32.into()));
}
if let EmValue::ObjectRef(handle) = &ctx.args[0] {
if let Ok(s) = thread.heap_mut().get_string(*handle) {
if let Ok(n) = s.parse::<i32>() {
return PreHookResult::Bypass(Some(n.into()));
}
}
return PreHookResult::Bypass(Some(0_i32.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_i32_cil().into()))
}
fn to_uint32_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0_i32.into()));
}
#[allow(clippy::cast_possible_wrap)]
let value = ctx.args[0].to_u32_cil() as i32;
PreHookResult::Bypass(Some(value.into()))
}
fn to_int64_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0_i64.into()));
}
if let EmValue::ObjectRef(handle) = &ctx.args[0] {
if let Ok(s) = thread.heap_mut().get_string(*handle) {
if let Ok(n) = s.parse::<i64>() {
return PreHookResult::Bypass(Some(n.into()));
}
}
return PreHookResult::Bypass(Some(0_i64.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_i64_cil().into()))
}
fn to_uint64_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0_i64.into()));
}
#[allow(clippy::cast_possible_wrap)]
let value = ctx.args[0].to_u64_cil() as i64;
PreHookResult::Bypass(Some(value.into()))
}
fn to_single_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0.0_f32.into()));
}
if let EmValue::ObjectRef(handle) = &ctx.args[0] {
if let Ok(s) = thread.heap_mut().get_string(*handle) {
if let Ok(f) = s.parse::<f32>() {
return PreHookResult::Bypass(Some(f.into()));
}
}
return PreHookResult::Bypass(Some(0.0_f32.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_f32_cil().into()))
}
fn to_double_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0.0_f64.into()));
}
if let EmValue::ObjectRef(handle) = &ctx.args[0] {
if let Ok(s) = thread.heap_mut().get_string(*handle) {
if let Ok(f) = s.parse::<f64>() {
return PreHookResult::Bypass(Some(f.into()));
}
}
return PreHookResult::Bypass(Some(0.0_f64.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_f64_cil().into()))
}
fn to_char_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(0_u16.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_u16_cil().into()))
}
fn to_boolean_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(false.into()));
}
if let EmValue::ObjectRef(handle) = &ctx.args[0] {
if let Ok(s) = thread.heap().get_string(*handle) {
return PreHookResult::Bypass(Some(
(s.eq_ignore_ascii_case("true") || &*s == "1").into(),
));
}
return PreHookResult::Bypass(Some(false.into()));
}
PreHookResult::Bypass(Some(ctx.args[0].to_bool_cil().into()))
}
fn to_string_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return match thread.heap_mut().alloc_string("") {
Ok(handle) => PreHookResult::Bypass(Some(EmValue::ObjectRef(handle))),
Err(e) => PreHookResult::Error(format!("heap allocation failed: {e}")),
};
}
let s = match &ctx.args[0] {
EmValue::I32(n) => n.to_string(),
EmValue::I64(n) => n.to_string(),
EmValue::F32(f) => f.to_string(),
EmValue::F64(f) => f.to_string(),
EmValue::ObjectRef(handle) => {
if let Ok(existing) = thread.heap_mut().get_string(*handle) {
existing.to_string()
} else {
String::new()
}
}
_ => String::new(),
};
match thread.heap_mut().alloc_string(&s) {
Ok(handle) => PreHookResult::Bypass(Some(EmValue::ObjectRef(handle))),
Err(e) => PreHookResult::Error(format!("heap allocation failed: {e}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
emulation::runtime::hook::HookManager,
metadata::{token::Token, typesystem::PointerSize},
test::emulation::create_test_thread,
};
#[test]
fn test_register_hooks() {
let manager = HookManager::new();
register(&manager).unwrap();
assert_eq!(manager.len(), 15);
}
#[test]
fn test_to_base64_string_hook() {
let mut thread = create_test_thread();
let data = thread.heap_mut().alloc_byte_array(b"Hello").unwrap();
let args = [EmValue::ObjectRef(data)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"Convert",
"ToBase64String",
PointerSize::Bit64,
)
.with_args(&args);
let result = to_base64_string_pre(&ctx, &mut thread);
if let PreHookResult::Bypass(Some(EmValue::ObjectRef(handle))) = result {
assert_eq!(&*thread.heap().get_string(handle).unwrap(), "SGVsbG8=");
} else {
panic!("Expected Bypass with ObjectRef");
}
}
#[test]
fn test_from_base64_string_hook() {
let mut thread = create_test_thread();
let encoded = thread.heap_mut().alloc_string("SGVsbG8=").unwrap();
let args = [EmValue::ObjectRef(encoded)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"Convert",
"FromBase64String",
PointerSize::Bit64,
)
.with_args(&args);
let result = from_base64_string_pre(&ctx, &mut thread);
if let PreHookResult::Bypass(Some(EmValue::ObjectRef(handle))) = result {
assert_eq!(
thread.heap().get_byte_array(handle).unwrap(),
Some(b"Hello".to_vec())
);
} else {
panic!("Expected Bypass with ObjectRef");
}
}
#[test]
fn test_to_int32_hook() {
let mut thread = create_test_thread();
let args = [EmValue::I32(42)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"Convert",
"ToInt32",
PointerSize::Bit64,
)
.with_args(&args);
let result = to_int32_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(42)))
));
let s = thread.heap_mut().alloc_string("123").unwrap();
let args = [EmValue::ObjectRef(s)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"Convert",
"ToInt32",
PointerSize::Bit64,
)
.with_args(&args);
let result = to_int32_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(123)))
));
}
fn ctx<'a>(method: &'a str, args: &'a [EmValue]) -> HookContext<'a> {
HookContext::new(
Token::new(0x0A000001),
"System",
"Convert",
method,
PointerSize::Bit64,
)
.with_args(args)
}
#[test]
fn test_to_byte() {
let mut thread = create_test_thread();
let result = to_byte_pre(&ctx("ToByte", &[EmValue::I32(200)]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(200)))
));
}
#[test]
fn test_to_sbyte() {
let mut thread = create_test_thread();
let result = to_sbyte_pre(&ctx("ToSByte", &[EmValue::I32(-5)]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(-5)))
));
}
#[test]
fn test_to_int16() {
let mut thread = create_test_thread();
let result = to_int16_pre(&ctx("ToInt16", &[EmValue::I32(1000)]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(1000)))
));
}
#[test]
fn test_to_uint16() {
let mut thread = create_test_thread();
let result = to_uint16_pre(&ctx("ToUInt16", &[EmValue::I32(50000)]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(50000)))
));
}
#[test]
fn test_to_uint32() {
let mut thread = create_test_thread();
let result = to_uint32_pre(&ctx("ToUInt32", &[EmValue::I32(42)]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(42)))
));
}
#[test]
fn test_to_int64_from_i32() {
let mut thread = create_test_thread();
let result = to_int64_pre(&ctx("ToInt64", &[EmValue::I32(42)]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(42)))
));
}
#[test]
fn test_to_int64_from_string() {
let mut thread = create_test_thread();
let s = thread.heap_mut().alloc_string("12345678901").unwrap();
let args = [EmValue::ObjectRef(s)];
let result = to_int64_pre(&ctx("ToInt64", &args), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(12345678901)))
));
}
#[test]
fn test_to_uint64() {
let mut thread = create_test_thread();
let result = to_uint64_pre(&ctx("ToUInt64", &[EmValue::I32(42)]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I64(42)))
));
}
#[test]
fn test_to_single() {
let mut thread = create_test_thread();
let result = to_single_pre(&ctx("ToSingle", &[EmValue::I32(42)]), &mut thread);
if let PreHookResult::Bypass(Some(EmValue::F32(v))) = result {
assert!((v - 42.0).abs() < 0.001);
} else {
panic!("Expected Bypass with F32");
}
}
#[test]
fn test_to_double() {
let mut thread = create_test_thread();
let result = to_double_pre(&ctx("ToDouble", &[EmValue::I32(42)]), &mut thread);
if let PreHookResult::Bypass(Some(EmValue::F64(v))) = result {
assert!((v - 42.0).abs() < 0.001);
} else {
panic!("Expected Bypass with F64");
}
}
#[test]
fn test_to_char() {
let mut thread = create_test_thread();
let result = to_char_pre(&ctx("ToChar", &[EmValue::I32(65)]), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(65)))
));
}
#[test]
fn test_to_string_from_i32() {
let mut thread = create_test_thread();
let result = to_string_pre(&ctx("ToString", &[EmValue::I32(42)]), &mut thread);
if let PreHookResult::Bypass(Some(EmValue::ObjectRef(r))) = result {
assert_eq!(&*thread.heap().get_string(r).unwrap(), "42");
} else {
panic!("Expected Bypass with ObjectRef");
}
}
#[test]
fn test_to_int32_invalid_string() {
let mut thread = create_test_thread();
let s = thread.heap_mut().alloc_string("not_a_number").unwrap();
let args = [EmValue::ObjectRef(s)];
let result = to_int32_pre(&ctx("ToInt32", &args), &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(0)))
));
}
#[test]
fn test_from_base64_invalid_string() {
let mut thread = create_test_thread();
let s = thread.heap_mut().alloc_string("!!!invalid!!!").unwrap();
let args = [EmValue::ObjectRef(s)];
let result = from_base64_string_pre(&ctx("FromBase64String", &args), &mut thread);
assert!(matches!(result, PreHookResult::Bypass(Some(EmValue::Null))));
}
#[test]
fn test_to_boolean_hook() {
let mut thread = create_test_thread();
let args = [EmValue::I32(0)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"Convert",
"ToBoolean",
PointerSize::Bit64,
)
.with_args(&args);
let result = to_boolean_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::Bool(false)))
));
let args = [EmValue::I32(1)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"Convert",
"ToBoolean",
PointerSize::Bit64,
)
.with_args(&args);
let result = to_boolean_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::Bool(true)))
));
let s = thread.heap_mut().alloc_string("true").unwrap();
let args = [EmValue::ObjectRef(s)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"Convert",
"ToBoolean",
PointerSize::Bit64,
)
.with_args(&args);
let result = to_boolean_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::Bool(true)))
));
}
}