diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..869df07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..eb5b733 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "hk4e-patch" +version = "0.1.0" +edition = "2021" + +[lib] +name = "version" +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1.0.81" +ilhook = "2.1.0" +lazy_static = "1.4.0" +libloading = "0.8.3" +windows = { version = "0.54.0", features = [ + "Win32_Foundation", + "Win32_System_SystemServices", + "Win32_System_LibraryLoader", + "Win32_System_Console", + "Win32_System_Threading", + "Win32_System_Memory", +] } + +[build-dependencies] +cc = "1.0.90" + +[profile.release] +strip = true # Automatically strip symbols from the binary. +lto = true # Link-time optimization. +opt-level = 3 # Optimization level 3. +codegen-units = 1 # Maximum size reduction optimizations. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..e58dc97 --- /dev/null +++ b/build.rs @@ -0,0 +1,11 @@ +fn main() { + cc::Build::new().file("version.asm").compile("version"); + + println!("cargo:rerun-if-changed=version.asm"); + println!( + "cargo:rustc-link-search=native={}", + std::env::var("OUT_DIR").unwrap() + ); + println!("cargo:rustc-link-lib=static=version"); + println!("cargo:rustc-link-arg=/DEF:version.def"); +} diff --git a/server_public_key.bin b/server_public_key.bin new file mode 100644 index 0000000..9b51993 Binary files /dev/null and b/server_public_key.bin differ diff --git a/src/interceptor.rs b/src/interceptor.rs new file mode 100644 index 0000000..88fd821 --- /dev/null +++ b/src/interceptor.rs @@ -0,0 +1,48 @@ +use anyhow::{bail, Result}; +use ilhook::x64::{ + CallbackOption, HookFlags, HookPoint, HookType, Hooker, JmpBackRoutine, RetnRoutine, +}; + +pub struct Interceptor { + pub hooks: Vec, +} + +impl Interceptor { + pub const fn new() -> Self { + Self { hooks: Vec::new() } + } + + pub unsafe fn attach(&mut self, addr: usize, routine: JmpBackRoutine) -> Result<()> { + let hooker = Hooker::new( + addr, + HookType::JmpBack(routine), + CallbackOption::None, + 0, + HookFlags::empty(), + ); + + let Ok(hook_point) = hooker.hook() else { + bail!("Failed to attach 0x{addr:X}") + }; + + self.hooks.push(hook_point); + Ok(()) + } + + pub unsafe fn replace(&mut self, addr: usize, routine: RetnRoutine) -> Result<()> { + let hooker = Hooker::new( + addr, + HookType::Retn(routine), + CallbackOption::None, + 0, + HookFlags::empty(), + ); + + let Ok(hook_point) = hooker.hook() else { + bail!("Failed to replace 0x{addr:X}") + }; + + self.hooks.push(hook_point); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a24a2de --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,61 @@ +#![feature(str_from_utf16_endian)] + +use std::{sync::RwLock, time::Duration}; + +use lazy_static::lazy_static; +use util::try_get_base_address; +use windows::Win32::Foundation::HINSTANCE; +use windows::Win32::System::Console; +use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH; + +mod interceptor; +mod marshal; +mod modules; +mod util; +mod version; +use version::VersionDllProxy; + +use crate::modules::{Http, MhyContext, ModuleManager, Security}; + +unsafe fn thread_func() { + let base = loop { + if let Some(base) = try_get_base_address("UserAssembly.dll") { + break base; + } + + std::thread::sleep(Duration::from_millis(500)); + }; + + std::thread::sleep(Duration::from_secs(7)); + + util::disable_memprotect_guard(); + Console::AllocConsole().unwrap(); + + println!("Genshin Impact encryption patch\nMade by xeondev\nTo work with NaviaImpact: git.xeondev.com/reversedrooms/NaviaImpact"); + println!("UserAssembly: {:X}", base); + + let mut module_manager = MODULE_MANAGER.write().unwrap(); + module_manager.enable(MhyContext::::new(base)); + module_manager.enable(MhyContext::::new(base)); + + println!("Successfully initialized!"); +} + +lazy_static! { + static ref VERSION_DLL_PROXY: version::VersionDllProxy = + VersionDllProxy::new().expect("Failed to load version.dll"); + static ref MODULE_MANAGER: RwLock = RwLock::new(ModuleManager::default()); +} + +#[no_mangle] +unsafe extern "system" fn DllMain(_: HINSTANCE, call_reason: u32, _: *mut ()) -> bool { + if call_reason == DLL_PROCESS_ATTACH { + VERSION_DLL_PROXY + .load_functions() + .expect("Failed to load functions from version.dll"); + + std::thread::spawn(|| thread_func()); + } + + true +} diff --git a/src/marshal.rs b/src/marshal.rs new file mode 100644 index 0000000..12fb4fa --- /dev/null +++ b/src/marshal.rs @@ -0,0 +1,15 @@ +use std::ffi::CStr; + +use crate::util; + +const PTR_TO_STRING_ANSI: usize = 0xC61DB80; +type MarshalPtrToStringAnsi = unsafe extern "fastcall" fn(*const u8) -> *const u8; + +pub unsafe fn ptr_to_string_ansi(content: &CStr) -> *const u8 { + let func = std::mem::transmute::(base() + PTR_TO_STRING_ANSI); + func(content.to_bytes_with_nul().as_ptr()) +} + +unsafe fn base() -> usize { + util::try_get_base_address("UserAssembly.dll").unwrap() +} diff --git a/src/modules/http.rs b/src/modules/http.rs new file mode 100644 index 0000000..f7d9905 --- /dev/null +++ b/src/modules/http.rs @@ -0,0 +1,45 @@ +use std::ffi::CString; + +use super::{MhyContext, MhyModule, ModuleType}; +use crate::marshal; +use anyhow::Result; +use ilhook::x64::Registers; + +const UNITY_WEB_REQUEST_SET_URL: usize = 0xCFC7FE0; + +pub struct Http; + +impl MhyModule for MhyContext { + unsafe fn init(&mut self) -> Result<()> { + self.interceptor.attach( + self.assembly_base + UNITY_WEB_REQUEST_SET_URL, + on_uwr_set_url, + ) + } + + unsafe fn de_init(&mut self) -> Result<()> { + Ok(()) + } + + fn get_module_type(&self) -> super::ModuleType { + ModuleType::Http + } +} + +unsafe extern "win64" fn on_uwr_set_url(reg: *mut Registers, _: usize) { + let str_length = *((*reg).rdx.wrapping_add(16) as *const u32); + let str_ptr = (*reg).rdx.wrapping_add(20) as *const u8; + + let slice = std::slice::from_raw_parts(str_ptr, (str_length * 2) as usize); + let url = String::from_utf16le(slice).unwrap(); + + let mut new_url = String::from("http://127.0.0.1:21000"); + url.split('/').skip(3).for_each(|s| { + new_url.push_str("/"); + new_url.push_str(s); + }); + + println!("Redirect: {url} -> {new_url}"); + (*reg).rdx = + marshal::ptr_to_string_ansi(CString::new(new_url.as_str()).unwrap().as_c_str()) as u64; +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs new file mode 100644 index 0000000..ed792fa --- /dev/null +++ b/src/modules/mod.rs @@ -0,0 +1,63 @@ +use std::collections::HashMap; + +use anyhow::Result; + +use crate::interceptor::Interceptor; + +mod http; +mod security; + +pub use http::Http; +pub use security::Security; + +#[derive(Default)] +pub struct ModuleManager { + modules: HashMap>, +} +unsafe impl Sync for ModuleManager {} +unsafe impl Send for ModuleManager {} + +impl ModuleManager { + pub unsafe fn enable(&mut self, module: impl MhyModule + 'static) { + let mut boxed_module = Box::new(module); + boxed_module.init().unwrap(); + self.modules + .insert(boxed_module.get_module_type(), boxed_module); + } + + #[allow(dead_code)] + pub unsafe fn disable(&mut self, module_type: ModuleType) { + let module = self.modules.remove(&module_type); + if let Some(mut module) = module { + module.as_mut().de_init().unwrap(); + } + } +} + +#[derive(Copy, Clone, Hash, PartialEq, Eq)] +pub enum ModuleType { + Http, + Security, +} + +pub trait MhyModule { + unsafe fn init(&mut self) -> Result<()>; + unsafe fn de_init(&mut self) -> Result<()>; + fn get_module_type(&self) -> ModuleType; +} + +pub struct MhyContext { + pub assembly_base: usize, + pub interceptor: Interceptor, + _phantom: std::marker::PhantomData, +} + +impl MhyContext { + pub const fn new(assembly_base: usize) -> Self { + Self { + assembly_base, + interceptor: Interceptor::new(), + _phantom: std::marker::PhantomData, + } + } +} diff --git a/src/modules/security.rs b/src/modules/security.rs new file mode 100644 index 0000000..da475ce --- /dev/null +++ b/src/modules/security.rs @@ -0,0 +1,66 @@ +use super::{MhyContext, MhyModule, ModuleType}; +use anyhow::Result; +use ilhook::x64::Registers; + +const IL2CPP_ARRAY_NEW: usize = 0x5432A0; +const KEY_SIGN_CHECK: usize = 0x4235; + +const KEY_SIZE: u64 = 272; +const KEY_PREFIX: u64 = 0x0D700010182020A01; +static SERVER_PUBLIC_KEY: &[u8] = include_bytes!("../../server_public_key.bin"); +type Il2cppArrayNew = unsafe extern "fastcall" fn(u64, u64) -> *const u8; + +pub struct Security; + +impl MhyModule for MhyContext { + unsafe fn init(&mut self) -> Result<()> { + self.interceptor.replace( + self.assembly_base + IL2CPP_ARRAY_NEW, + il2cpp_array_new_replacement, + )?; + self.interceptor + .attach(self.assembly_base + KEY_SIGN_CHECK, after_key_sign_check) + } + + unsafe fn de_init(&mut self) -> Result<()> { + Ok(()) + } + + fn get_module_type(&self) -> super::ModuleType { + ModuleType::Security + } +} + +// Sign check of rsa key that we just replaced. +unsafe extern "win64" fn after_key_sign_check(reg: *mut Registers, _: usize) { + (*reg).rax = 1 +} + +static mut KEY_PTR: Option<*mut u8> = None; +unsafe extern "win64" fn il2cpp_array_new_replacement( + reg: *mut Registers, + actual_func: usize, + _: usize, +) -> usize { + let il2cpp_array_new = std::mem::transmute::(actual_func); + let ret_val = il2cpp_array_new((*reg).rcx, (*reg).rdx) as usize; + + let rdx = (*reg).rdx; + if rdx == KEY_SIZE { + KEY_PTR = Some(ret_val as *mut u8); + } else { + if let Some(key_ptr) = KEY_PTR { + if *(key_ptr.wrapping_add(32) as *const u64) == KEY_PREFIX { + std::ptr::copy_nonoverlapping( + SERVER_PUBLIC_KEY.as_ptr(), + key_ptr.wrapping_add(32), + SERVER_PUBLIC_KEY.len(), + ); + } + } + + KEY_PTR = None; + } + + ret_val +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..6319997 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,63 @@ +use core::iter::once; +use std::ffi::{c_void, OsStr}; + +use std::os::windows::ffi::OsStrExt; +use windows::Win32::System::LibraryLoader::{GetProcAddress, GetModuleHandleW}; +use windows::Win32::System::Memory::{PAGE_EXECUTE_READWRITE, PAGE_PROTECTION_FLAGS, VirtualProtect}; +use windows::core::{PCSTR, PCWSTR}; + +pub fn wide_str(value: &str) -> Vec { + OsStr::new(value).encode_wide().chain(once(0)).collect() +} + +pub unsafe fn try_get_base_address(module_name: &str) -> Option { + let w_module_name = wide_str(module_name); + + match GetModuleHandleW(PCWSTR::from_raw(w_module_name.as_ptr())) { + Ok(module) => Some(module.0 as usize), + Err(_) => None + } +} + +// VMProtect hooks NtProtectVirtualMemory to prevent changing protection of executable segments +// We use this trick to remove hook +pub unsafe fn disable_memprotect_guard() { + let ntdll = wide_str("ntdll.dll"); + let ntdll = GetModuleHandleW(PCWSTR::from_raw(ntdll.as_ptr())).unwrap(); + let proc_addr = GetProcAddress( + ntdll, + PCSTR::from_raw(c"NtProtectVirtualMemory".to_bytes_with_nul().as_ptr()), + ) + .unwrap(); + let nt_query = GetProcAddress(ntdll, PCSTR::from_raw(c"NtQuerySection".to_bytes_with_nul().as_ptr())).unwrap(); + + let mut old_prot = PAGE_PROTECTION_FLAGS(0); + VirtualProtect( + proc_addr as *const usize as *mut c_void, + 1, + PAGE_EXECUTE_READWRITE, + &mut old_prot, + ) + .unwrap(); + + let routine = nt_query as *mut u32; + let routine_val = *(routine as *const usize); + + let lower_bits_mask = !(0xFFu64 << 32); + let lower_bits = routine_val & lower_bits_mask as usize; + + let offset_val = *((routine as usize + 4) as *const u32); + let upper_bits = ((offset_val as usize).wrapping_sub(1) as usize) << 32; + + let result = lower_bits | upper_bits; + + *(proc_addr as *mut usize) = result; + + VirtualProtect( + proc_addr as *const usize as *mut c_void, + 1, + old_prot, + &mut old_prot, + ) + .unwrap(); +} diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..7b651dc --- /dev/null +++ b/src/version.rs @@ -0,0 +1,54 @@ +use libloading::{Error, Library, Symbol}; +use std::env; +use std::ffi::CStr; + +static FUNCTION_NAMES: &[&CStr] = &[ + c"GetFileVersionInfoA", + c"GetFileVersionInfoByHandle", + c"GetFileVersionInfoExA", + c"GetFileVersionInfoExW", + c"GetFileVersionInfoSizeA", + c"GetFileVersionInfoSizeExA", + c"GetFileVersionInfoSizeExW", + c"GetFileVersionInfoSizeW", + c"GetFileVersionInfoW", + c"VerFindFileA", + c"VerFindFileW", + c"VerInstallFileA", + c"VerInstallFileW", + c"VerLanguageNameA", + c"VerLanguageNameW", + c"VerQueryValueA", + c"VerQueryValueW", +]; + +#[no_mangle] +static mut ORIGINAL_FUNCTIONS: [*const usize; 17] = [0 as *const usize; 17]; + +pub struct VersionDllProxy { + library: Library, +} + +impl VersionDllProxy { + pub fn new() -> Result> { + let system_directory = env::var("windir")? + ("\\System32\\"); + let dll_path = system_directory + "version.dll"; + let library = unsafe { Library::new(dll_path) }?; + Ok(Self { library }) + } + + fn get_function(&self, func_name: &CStr) -> Result, Error> { + unsafe { self.library.get(func_name.to_bytes_with_nul()) } + } + + pub fn load_functions(&self) -> Result<(), Error> { + for (i, &name) in FUNCTION_NAMES.iter().enumerate() { + let fn_ptr = self.get_function::>(name)?; + unsafe { ORIGINAL_FUNCTIONS[i] = **fn_ptr }; + println!("Loaded function {}@{:p}", name.to_str().unwrap(), unsafe { + ORIGINAL_FUNCTIONS[i] + }); + } + Ok(()) + } +} diff --git a/version.asm b/version.asm new file mode 100644 index 0000000..44cbfc5 --- /dev/null +++ b/version.asm @@ -0,0 +1,112 @@ +ifdef RAX + .code + extern ORIGINAL_FUNCTIONS:QWORD + GetFileVersionInfoA proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[0 * 8] + GetFileVersionInfoA endp + GetFileVersionInfoByHandle proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[1 * 8] + GetFileVersionInfoByHandle endp + GetFileVersionInfoExA proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[2 * 8] + GetFileVersionInfoExA endp + GetFileVersionInfoExW proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[3 * 8] + GetFileVersionInfoExW endp + GetFileVersionInfoSizeA proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[4 * 8] + GetFileVersionInfoSizeA endp + GetFileVersionInfoSizeExA proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[5 * 8] + GetFileVersionInfoSizeExA endp + GetFileVersionInfoSizeExW proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[6 * 8] + GetFileVersionInfoSizeExW endp + GetFileVersionInfoSizeW proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[7 * 8] + GetFileVersionInfoSizeW endp + GetFileVersionInfoW proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[8 * 8] + GetFileVersionInfoW endp + VerFindFileA proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[9 * 8] + VerFindFileA endp + VerFindFileW proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[10 * 8] + VerFindFileW endp + VerInstallFileA proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[11 * 8] + VerInstallFileA endp + VerInstallFileW proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[12 * 8] + VerInstallFileW endp + VerLanguageNameA proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[13 * 8] + VerLanguageNameA endp + VerLanguageNameW proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[14 * 8] + VerLanguageNameW endp + VerQueryValueA proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[15 * 8] + VerQueryValueA endp + VerQueryValueW proc + jmp QWORD ptr ORIGINAL_FUNCTIONS[16 * 8] + VerQueryValueW endp +else + .model flat, C + .stack 4096 + .code + extern ORIGINAL_FUNCTIONS:DWORD + GetFileVersionInfoA proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[0 * 4] + GetFileVersionInfoA endp + GetFileVersionInfoByHandle proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[1 * 4] + GetFileVersionInfoByHandle endp + GetFileVersionInfoExA proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[2 * 4] + GetFileVersionInfoExA endp + GetFileVersionInfoExW proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[3 * 4] + GetFileVersionInfoExW endp + GetFileVersionInfoSizeA proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[4 * 4] + GetFileVersionInfoSizeA endp + GetFileVersionInfoSizeExA proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[5 * 4] + GetFileVersionInfoSizeExA endp + GetFileVersionInfoSizeExW proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[6 * 4] + GetFileVersionInfoSizeExW endp + GetFileVersionInfoSizeW proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[7 * 4] + GetFileVersionInfoSizeW endp + GetFileVersionInfoW proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[8 * 4] + GetFileVersionInfoW endp + VerFindFileA proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[9 * 4] + VerFindFileA endp + VerFindFileW proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[10 * 4] + VerFindFileW endp + VerInstallFileA proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[11 * 4] + VerInstallFileA endp + VerInstallFileW proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[12 * 4] + VerInstallFileW endp + VerLanguageNameA proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[13 * 4] + VerLanguageNameA endp + VerLanguageNameW proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[14 * 4] + VerLanguageNameW endp + VerQueryValueA proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[15 * 4] + VerQueryValueA endp + VerQueryValueW proc + jmp DWORD ptr ORIGINAL_FUNCTIONS[16 * 4] + VerQueryValueW endp +endif +end diff --git a/version.def b/version.def new file mode 100644 index 0000000..b2f9ce3 --- /dev/null +++ b/version.def @@ -0,0 +1,19 @@ + +EXPORTS + GetFileVersionInfoA + GetFileVersionInfoByHandle + GetFileVersionInfoExA + GetFileVersionInfoExW + GetFileVersionInfoSizeA + GetFileVersionInfoSizeExA + GetFileVersionInfoSizeExW + GetFileVersionInfoSizeW + GetFileVersionInfoW + VerFindFileA + VerFindFileW + VerInstallFileA + VerInstallFileW + VerLanguageNameA + VerLanguageNameW + VerQueryValueA + VerQueryValueW