commit 2b7cb77400cf53ebf7af3b46033ef3419100b629 Author: Luke Street Date: Thu Sep 7 00:27:47 2023 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a8cabc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cb50b66 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,129 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "memexec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc62ccb14881da5d1862cda3a9648fb4a4897b2aff0b2557b89da44a5e550b7c" + +[[package]] +name = "sjiswrap" +version = "0.1.0" +dependencies = [ + "anyhow", + "encoding_rs", + "itertools", + "lazy_static", + "memexec", + "windows", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ceeabab --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sjiswrap" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.72" +encoding_rs = "0.8.32" +itertools = "0.11.0" +lazy_static = "1.4.0" +memexec = { version = "0.2.0", features = ["hook"] } + +[dependencies.windows] +version = "0.48.0" +features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_Environment", + "Win32_System_IO", + "Win32_System_LibraryLoader", + "Win32_System_Memory", +] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..0a9eda5 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +fn_single_line = true +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +overflow_delimited_expr = true +reorder_impl_items = true +use_field_init_shorthand = true +use_small_heuristics = "Max" +where_single_line = true diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..129d22e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,366 @@ +#![allow(non_snake_case)] + +use std::{ + borrow::Cow, + cmp::min, + collections::HashMap, + ffi::{c_char, c_void, CStr, CString, OsString}, + fs::File, + io::Read, + iter::{Cloned, Peekable}, + path::PathBuf, + process::exit, + sync::Mutex, +}; + +use anyhow::Result; +use encoding_rs::SHIFT_JIS; +use lazy_static::lazy_static; +use windows::{ + core::PCSTR, + Win32::{ + Foundation::{ + CloseHandle, GetLastError, SetLastError, BOOL, GENERIC_ACCESS_RIGHTS, GENERIC_READ, + HANDLE, + }, + Security::SECURITY_ATTRIBUTES, + Storage::FileSystem::{ + CreateFileA, GetFileSize, GetFullPathNameA, ReadFile, SetFilePointer, FILE_BEGIN, + FILE_CREATION_DISPOSITION, FILE_CURRENT, FILE_END, FILE_FLAGS_AND_ATTRIBUTES, + FILE_SHARE_MODE, SET_FILE_POINTER_MOVE_METHOD, + }, + System::{Environment::GetCommandLineA, LibraryLoader::SetDllDirectoryA, IO::OVERLAPPED}, + }, +}; + +struct FileHandle { + data: Vec, + pos: u64, +} +#[derive(Default)] +struct GlobalState { + cmdline: Option, + file_handles: HashMap, +} +lazy_static! { + static ref GLOBAL_STATE: Mutex = Default::default(); +} + +fn get_full_path(path: &CStr) -> Result { + let mut buf = [0u8; 4096]; + let len = + unsafe { GetFullPathNameA(PCSTR(path.as_ptr() as *const u8), Some(buf.as_mut()), None) }; + if len == 0 { + return Err(std::io::Error::last_os_error().into()); + } + Ok(unsafe { CString::from_vec_with_nul_unchecked(buf[..len as usize + 1].to_vec()) }) +} + +extern "stdcall" fn hook_GetCommandLineA() -> PCSTR { + let mut guard = GLOBAL_STATE.lock().expect("Failed to lock global state"); + if let Some(str) = &guard.cmdline { + return PCSTR(str.as_ptr() as *const u8); + } + + fn next_arg<'a>( + iter: &mut Peekable>>, + mut cb: impl FnMut(u8), + ) { + while iter.peek().cloned() == Some(b' ') { + iter.next(); + } + let mut quoted = false; + if iter.peek().cloned() == Some(b'"') { + quoted = true; + iter.next(); + } + loop { + let Some(c) = iter.next() else { + if quoted { + panic!("GetCommandLineA(): Unterminated quoted string"); + } + break; + }; + if quoted { + if c == b'"' { + let next = iter.next(); + if next != Some(b' ') && next.is_some() { + panic!( + "GetCommandLineA(): Expected space after quote, got '{}'", + char::from(next.unwrap()) + ); + } + break; + } + } else if c == b' ' { + break; + } + cb(c); + } + } + + let cmdline = unsafe { GetCommandLineA() }; + let cmdline = unsafe { cmdline.as_bytes() }; + let mut iter = cmdline.iter().cloned().peekable(); + next_arg(&mut iter, |_| {}); // Skip executable name + let mut exe = Vec::new(); + next_arg(&mut iter, |c| exe.push(c)); + exe.push(0); + let absolute_path = + get_full_path(unsafe { CStr::from_bytes_with_nul_unchecked(exe.as_slice()) }) + .expect("Failed to get absolute path"); + + let mut cmdline = vec![b'"']; + cmdline.extend_from_slice(absolute_path.as_bytes()); + cmdline.push(b'"'); + if iter.peek().is_some() { + cmdline.push(b' '); + cmdline.extend(iter); + } + cmdline.push(0); + guard.cmdline = Some(unsafe { CString::from_vec_with_nul_unchecked(cmdline) }); + PCSTR(guard.cmdline.as_ref().unwrap().as_ptr() as *const u8) +} + +fn is_text_file(path: &str) -> bool { + path.ends_with(".c") + || path.ends_with(".cc") + || path.ends_with(".cp") + || path.ends_with(".cpp") + || path.ends_with(".h") + || path.ends_with(".hpp") +} + +extern "stdcall" fn hook_CreateFileA( + lpFileName: PCSTR, + dwDesiredAccess: GENERIC_ACCESS_RIGHTS, + dwShareMode: FILE_SHARE_MODE, + lpSecurityAttributes: *const SECURITY_ATTRIBUTES, + dwCreationDisposition: FILE_CREATION_DISPOSITION, + dwFlagsAndAttributes: FILE_FLAGS_AND_ATTRIBUTES, + hTemplateFile: HANDLE, +) -> HANDLE { + let ret = unsafe { + CreateFileA( + lpFileName, + dwDesiredAccess.0, + dwShareMode, + Some(lpSecurityAttributes), + dwCreationDisposition, + dwFlagsAndAttributes, + hTemplateFile, + ) + } + .unwrap_or(HANDLE(0)); + let err = unsafe { GetLastError() }; + + let path = unsafe { CStr::from_ptr(lpFileName.as_ptr() as *const c_char) }.to_string_lossy(); + if !ret.is_invalid() && dwDesiredAccess == GENERIC_READ && is_text_file(&path) { + let mut filesize_high = 0u32; + let mut filesize = unsafe { GetFileSize(ret, Some(&mut filesize_high)) } as u64; + filesize |= (filesize_high as u64) << 32; + + if filesize < u32::MAX as u64 { + let mut data = vec![0u8; filesize as usize]; + let mut bytes_read = 0u32; + if unsafe { + ReadFile( + ret, + Some(data.as_mut_ptr() as *mut c_void), + filesize as u32, + Some(&mut bytes_read), + None, + ) + } + .as_bool() + && bytes_read == filesize as u32 + { + if let Ok(str) = std::str::from_utf8(&data) { + let (encoded, _, _) = SHIFT_JIS.encode(str); + let mut guard = GLOBAL_STATE.lock().expect("Failed to lock global state"); + match encoded { + Cow::Borrowed(_) => { + // No modifications were made, use the original data + // println!("READ FILE {:#X}, size {:#X} (UNCHANGED)", ret.0, filesize); + guard.file_handles.insert(ret.0, FileHandle { data, pos: 0 }); + } + Cow::Owned(data) => { + println!( + "READ FILE {:#X}, size {:#X} (CHANGED: {:#X})", + ret.0, + filesize, + data.len() + ); + guard.file_handles.insert(ret.0, FileHandle { data, pos: 0 }); + } + } + } + } + } + } + // println!("CreateFileA({}, {:#X}) = {:#X}", path, dwDesiredAccess.0, ret.0 as u32); + unsafe { SetLastError(err) }; + ret +} + +extern "stdcall" fn hook_GetFileSize(hFile: HANDLE, lpFileSizeHigh: *mut u32) -> u32 { + if !hFile.is_invalid() { + let guard = GLOBAL_STATE.lock().expect("Failed to lock global state"); + if let Some(file) = guard.file_handles.get(&hFile.0) { + // println!("OVERRIDE GetFileSize({:#X}) = {:#X}", hFile.0, file.data.len() as u32); + return file.data.len() as u32; + } + } + + let ret = unsafe { GetFileSize(hFile, Some(lpFileSizeHigh)) }; + // let err = unsafe { GetLastError() }; + // println!("GetFileSize({:#X}, {:?}) = {:#X}", hFile.0, lpFileSizeHigh, ret); + // unsafe { SetLastError(err) }; + ret +} + +extern "stdcall" fn hook_CloseHandle(hObject: HANDLE) -> BOOL { + if !hObject.is_invalid() { + let mut guard = GLOBAL_STATE.lock().expect("Failed to lock global state"); + if guard.file_handles.remove(&hObject.0).is_some() { + // println!("REMOVED HANDLE {:#X}", hObject.0); + } + } + + let ret = unsafe { CloseHandle(hObject) }; + // let err = unsafe { GetLastError() }; + // println!("CloseHandle({:#X}) = {:#X}", hObject.0, ret.0); + // unsafe { SetLastError(err) }; + ret +} + +extern "stdcall" fn hook_ReadFile( + hFile: HANDLE, + lpBuffer: *mut c_void, + nNumberOfBytesToRead: u32, + lpNumberOfBytesRead: *mut u32, + lpOverlapped: *mut OVERLAPPED, +) -> BOOL { + if !hFile.is_invalid() { + let mut guard = GLOBAL_STATE.lock().expect("Failed to lock global state"); + if let Some(file) = guard.file_handles.get_mut(&hFile.0) { + let count = min( + nNumberOfBytesToRead, + u32::try_from(file.data.len() as u64 - file.pos).unwrap_or(u32::MAX), + ); + unsafe { + std::ptr::copy_nonoverlapping( + file.data.as_ptr().offset(file.pos as isize), + lpBuffer as *mut u8, + count as usize, + ); + } + file.pos += count as u64; + if !lpNumberOfBytesRead.is_null() { + unsafe { *lpNumberOfBytesRead = count }; + } + // println!( + // "OVERRIDE ReadFile({:#X}, {:?}, {:#X}, {:?}) = {:#X}", + // hFile.0, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, count + // ); + return true.into(); + } + } + + let ret = unsafe { + ReadFile( + hFile, + Some(lpBuffer), + nNumberOfBytesToRead, + Some(lpNumberOfBytesRead), + Some(lpOverlapped), + ) + }; + let err = unsafe { GetLastError() }; + // println!( + // "ReadFile({:#X}, {:?}, {:#X}, {:?}) = {:#X}", + // hFile.0, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, ret.0 + // ); + unsafe { SetLastError(err) }; + ret +} + +extern "stdcall" fn hook_SetFilePointer( + hFile: HANDLE, + lDistanceToMove: i32, + lpDistanceToMoveHigh: *mut i32, + dwMoveMethod: SET_FILE_POINTER_MOVE_METHOD, +) -> u32 { + if !hFile.is_invalid() { + let mut guard = GLOBAL_STATE.lock().expect("Failed to lock global state"); + if let Some(file) = guard.file_handles.get_mut(&hFile.0) { + let distance_to_move_high = + if lpDistanceToMoveHigh.is_null() { 0 } else { unsafe { *lpDistanceToMoveHigh } }; + let distance_to_move = lDistanceToMove as i64 | (distance_to_move_high as i64) << 32; + let file_size = file.data.len() as u64; + let pos = min( + match dwMoveMethod { + FILE_BEGIN => distance_to_move as u64, + FILE_CURRENT => file.pos.saturating_add_signed(distance_to_move), + FILE_END => file_size.saturating_add_signed(distance_to_move), + _ => panic!("SetFilePointer(): Unsupported move method {:#X}", dwMoveMethod.0), + }, + file_size, + ); + file.pos = pos; + println!( + "OVERRIDE SetFilePointer({:#X}, {:#X}, {:?}, {}) = {:#X}", + hFile.0, distance_to_move, lpDistanceToMoveHigh, dwMoveMethod.0, pos + ); + if !lpDistanceToMoveHigh.is_null() { + unsafe { *lpDistanceToMoveHigh = (pos >> 32) as i32 }; + } + return pos as u32; + } + } + + let ret = + unsafe { SetFilePointer(hFile, lDistanceToMove, Some(lpDistanceToMoveHigh), dwMoveMethod) }; + let err = unsafe { GetLastError() }; + // println!( + // "SetFilePointer({:#X}, {:#X}, {:?}, {}) = {:#X}", + // hFile.0, lDistanceToMove, lpDistanceToMoveHigh, dwMoveMethod.0, ret + // ); + unsafe { SetLastError(err) }; + ret +} + +fn main() -> Result<()> { + let args: Vec = std::env::args_os().collect(); + if args.len() < 2 { + println!("Usage: {} ", args[0].to_string_lossy()); + exit(1); + } + + let path = PathBuf::from(&args[1]); + let parent = CString::new( + path.parent() + .expect("Failed to get executable parent directory") + .to_string_lossy() + .as_ref(), + ) + .unwrap(); + let parent = + get_full_path(&parent).expect("Failed to get absolute executable parent directory"); + unsafe { SetDllDirectoryA(PCSTR(parent.as_ptr() as *const u8)) } + .expect("SetDllDirectoryA() failed"); + // println!("SetDllDirectoryA({:?})", parent.to_string_lossy()); + + let mut buf = Vec::new(); + File::open(&path).unwrap().read_to_end(&mut buf).unwrap(); + + let mut hooks = HashMap::new(); + hooks.insert("kernel32.dll!GetCommandLineA".into(), hook_GetCommandLineA as *const c_void); + hooks.insert("kernel32.dll!CreateFileA".into(), hook_CreateFileA as *const c_void); + hooks.insert("kernel32.dll!GetFileSize".into(), hook_GetFileSize as *const c_void); + hooks.insert("kernel32.dll!CloseHandle".into(), hook_CloseHandle as *const c_void); + hooks.insert("kernel32.dll!ReadFile".into(), hook_ReadFile as *const c_void); + hooks.insert("kernel32.dll!SetFilePointer".into(), hook_SetFilePointer as *const c_void); + unsafe { memexec::memexec_exe_with_hooks(&buf, &hooks) }.unwrap(); + Ok(()) +}