use crate::{
emulation::{
runtime::hook::{Hook, HookContext, HookManager, PreHookResult},
thread::EmulationThread,
tokens, EmValue,
},
metadata::typesystem::CilFlavor,
Result,
};
const RNG_XORSHIFT64_SEED: u64 = 0xDEAD_BEEF_CAFE_BABE;
pub fn register(manager: &HookManager) -> Result<()> {
manager.register(
Hook::new("System.Security.Cryptography.RNGCryptoServiceProvider..ctor")
.match_name(
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
".ctor",
)
.pre(rng_crypto_ctor_pre),
)?;
manager.register(
Hook::new("System.Security.Cryptography.RNGCryptoServiceProvider.GetBytes")
.match_name(
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
"GetBytes",
)
.pre(rng_crypto_get_bytes_pre),
)?;
manager.register(
Hook::new("System.Security.Cryptography.RandomNumberGenerator.Create")
.match_name(
"System.Security.Cryptography",
"RandomNumberGenerator",
"Create",
)
.pre(rng_crypto_ctor_pre),
)?;
manager.register(
Hook::new("System.Security.Cryptography.RandomNumberGenerator.GetBytes")
.match_name(
"System.Security.Cryptography",
"RandomNumberGenerator",
"GetBytes",
)
.pre(rng_crypto_get_bytes_pre),
)?;
Ok(())
}
fn rng_crypto_ctor_pre(_ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let state_field = tokens::rng_fields::STATE;
let type_token = tokens::helpers::RNG;
let fields = vec![(state_field, CilFlavor::I8)];
match thread
.heap_mut()
.alloc_object_with_fields(type_token, &fields)
{
Ok(obj_ref) => {
#[allow(clippy::cast_possible_wrap)]
let seed_val = EmValue::I64(RNG_XORSHIFT64_SEED as i64);
try_hook!(thread.heap().set_field(obj_ref, state_field, seed_val));
PreHookResult::Bypass(Some(EmValue::ObjectRef(obj_ref)))
}
Err(e) => PreHookResult::Error(format!("heap allocation failed: {e}")),
}
}
fn rng_crypto_get_bytes_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let rng_ref = match ctx.this {
Some(EmValue::ObjectRef(r)) => *r,
_ => return PreHookResult::Bypass(None),
};
let arr_ref = match ctx.args.first() {
Some(EmValue::ObjectRef(h)) => *h,
_ => return PreHookResult::Bypass(None),
};
let arr_len = try_hook!(thread.heap().get_array_length(arr_ref));
let state_field = tokens::rng_fields::STATE;
#[allow(clippy::cast_sign_loss)]
let mut state: u64 = match try_hook!(thread.heap().get_field(rng_ref, state_field)) {
EmValue::I64(v) => v as u64,
_ => RNG_XORSHIFT64_SEED,
};
for i in 0..arr_len {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
#[allow(clippy::cast_possible_truncation)]
let byte_val = (state & 0xFF) as u8;
try_hook!(thread
.heap()
.set_array_element(arr_ref, i, EmValue::I32(i32::from(byte_val))));
}
#[allow(clippy::cast_possible_wrap)]
let state_val = EmValue::I64(state as i64);
try_hook!(thread.heap().set_field(rng_ref, state_field, state_val));
PreHookResult::Bypass(None)
}
#[cfg(test)]
mod tests {
use crate::{
emulation::{
runtime::hook::{HookContext, PreHookResult},
thread::EmulationThread,
EmValue,
},
metadata::{token::Token, typesystem::PointerSize},
test::emulation::create_test_thread,
};
#[test]
fn test_rng_crypto_ctor_hook() {
let mut thread = create_test_thread();
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
".ctor",
PointerSize::Bit64,
);
let result = super::rng_crypto_ctor_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::ObjectRef(_)))
));
}
#[test]
fn test_rng_crypto_get_bytes_fills_array() {
let mut thread = create_test_thread();
let ctor_ctx = HookContext::new(
Token::new(0x0A000001),
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
".ctor",
PointerSize::Bit64,
);
let rng_ref = match super::rng_crypto_ctor_pre(&ctor_ctx, &mut thread) {
PreHookResult::Bypass(Some(EmValue::ObjectRef(h))) => h,
_ => panic!("Expected ObjectRef"),
};
let output = thread.heap().alloc_byte_array(&[0u8; 16]).unwrap();
let args = [EmValue::ObjectRef(output)];
let this = EmValue::ObjectRef(rng_ref);
let get_bytes_ctx = HookContext::new(
Token::new(0x0A000001),
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
"GetBytes",
PointerSize::Bit64,
)
.with_this(Some(&this))
.with_args(&args);
let result = super::rng_crypto_get_bytes_pre(&get_bytes_ctx, &mut thread);
assert!(matches!(result, PreHookResult::Bypass(None)));
let bytes = thread.heap().get_byte_array(output).unwrap().unwrap();
assert_eq!(bytes.len(), 16);
assert!(
bytes.iter().any(|&b| b != 0),
"RNG should produce non-zero bytes"
);
}
#[test]
fn test_rng_crypto_is_deterministic() {
let mut thread1 = create_test_thread();
let mut thread2 = create_test_thread();
fn get_rng_bytes(thread: &mut EmulationThread, len: usize) -> Vec<u8> {
let ctor_ctx = HookContext::new(
Token::new(0x0A000001),
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
".ctor",
PointerSize::Bit64,
);
let rng_ref = match super::rng_crypto_ctor_pre(&ctor_ctx, thread) {
PreHookResult::Bypass(Some(EmValue::ObjectRef(h))) => h,
_ => panic!("Expected ObjectRef"),
};
let output = thread.heap().alloc_byte_array(&vec![0u8; len]).unwrap();
let args = [EmValue::ObjectRef(output)];
let this = EmValue::ObjectRef(rng_ref);
let ctx = HookContext::new(
Token::new(0x0A000001),
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
"GetBytes",
PointerSize::Bit64,
)
.with_this(Some(&this))
.with_args(&args);
super::rng_crypto_get_bytes_pre(&ctx, thread);
thread.heap().get_byte_array(output).unwrap().unwrap()
}
let bytes1 = get_rng_bytes(&mut thread1, 32);
let bytes2 = get_rng_bytes(&mut thread2, 32);
assert_eq!(bytes1, bytes2, "RNG should be deterministic across runs");
}
#[test]
fn test_rng_crypto_successive_calls_differ() {
let mut thread = create_test_thread();
let ctor_ctx = HookContext::new(
Token::new(0x0A000001),
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
".ctor",
PointerSize::Bit64,
);
let rng_ref = match super::rng_crypto_ctor_pre(&ctor_ctx, &mut thread) {
PreHookResult::Bypass(Some(EmValue::ObjectRef(h))) => h,
_ => panic!("Expected ObjectRef"),
};
let output1 = thread.heap().alloc_byte_array(&[0u8; 8]).unwrap();
let args1 = [EmValue::ObjectRef(output1)];
let this = EmValue::ObjectRef(rng_ref);
let ctx1 = HookContext::new(
Token::new(0x0A000001),
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
"GetBytes",
PointerSize::Bit64,
)
.with_this(Some(&this))
.with_args(&args1);
super::rng_crypto_get_bytes_pre(&ctx1, &mut thread);
let output2 = thread.heap().alloc_byte_array(&[0u8; 8]).unwrap();
let args2 = [EmValue::ObjectRef(output2)];
let ctx2 = HookContext::new(
Token::new(0x0A000001),
"System.Security.Cryptography",
"RNGCryptoServiceProvider",
"GetBytes",
PointerSize::Bit64,
)
.with_this(Some(&this))
.with_args(&args2);
super::rng_crypto_get_bytes_pre(&ctx2, &mut thread);
let bytes1 = thread.heap().get_byte_array(output1).unwrap().unwrap();
let bytes2 = thread.heap().get_byte_array(output2).unwrap().unwrap();
assert_ne!(
bytes1, bytes2,
"Successive RNG calls should produce different bytes"
);
}
}