diff --git a/Cargo.lock b/Cargo.lock index 8c40db2..afefac3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,27 @@ dependencies = [ "libc", ] +[[package]] +name = "naked-function" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b8d5fca6ab1e6215b010aefd3b9ac5aae369dae0faea3a7f34f296cc9f719ac" +dependencies = [ + "cfg-if", + "naked-function-macro", +] + +[[package]] +name = "naked-function-macro" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b4123e70df5fe0bb370cff166ae453b9c5324a2cfc932c0f7e55498147a0475" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -122,8 +143,10 @@ version = "0.1.0" dependencies = [ "bitflags", "bytemuck", + "cfg-if", "libc", "memmap2", + "naked-function", "tracing", "tracing-subscriber", "windows", diff --git a/Cargo.toml b/Cargo.toml index 9c02113..181207d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" [dependencies] bitflags = { version = "2.8.0", features = ["bytemuck"] } bytemuck = { version = "1.21.0", features = ["derive"] } +cfg-if = "1.0.0" memmap2 = "0.9.5" +naked-function = "0.1.5" tracing = { version = "0.1.41", features = ["attributes"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/src/emulated.rs b/src/emulated.rs index 7f84f88..319a2e9 100644 --- a/src/emulated.rs +++ b/src/emulated.rs @@ -1,3 +1,28 @@ +mod base_defs { + pub(crate) type HANDLE = usize; + + #[repr(transparent)] + pub(crate) struct LPCWSTR(pub *const u16); + impl std::fmt::Debug for LPCWSTR { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + unsafe { + let mut p = self.0; + let mut v = p.read(); + while v != 0 { + let c = std::char::decode_utf16([v]) + .map(Result::unwrap) + .collect::(); + f.write_str(&c)?; + p = p.add(1); + v = p.read(); + } + } + + Ok(()) + } + } +} + macro_rules! define_emulation_entry { ($($name:ident,)*) => { pub(crate) fn supports_dll(dll_name: &str) -> bool { @@ -32,7 +57,8 @@ define_emulation_entry!( macro_rules! emulate { ($dllname:literal, mod $modname:ident { $( - fn $name:ident($($args:tt)*) $(-> $ret:ty)? { + $(#[$attr:meta])* + fn $name:ident($($argname:ident: $argty:ty),* $(,)?) $(-> $ret:ty)? { $($body:tt)* } )* @@ -40,6 +66,9 @@ macro_rules! emulate { mod $modname { pub(super) const DLL_NAME: &str = $dllname; pub(super) fn emulate(dll_name: &str, function_name: &str) -> Option { + #[allow(unused_imports)] + use crate::emulated::base_defs::*; + if dll_name.to_lowercase() != $dllname { return None; } @@ -47,7 +76,8 @@ macro_rules! emulate { $( if function_name == stringify!($name) { unsafe { - return Some(std::mem::transmute($name as extern "system" fn())); + // NOTE: The ABI string is a lie..... + return Some(std::mem::transmute($name::trampoline as unsafe extern "C" fn($($argty),*))); } } )* @@ -56,10 +86,42 @@ macro_rules! emulate { } $( - // TODO: Windows API adapter... #[allow(non_snake_case)] - extern "system" fn $name($($args)*) $(-> $ret)? { - $($body)* + mod $name { + #[allow(unused_imports)] + use crate::emulated::base_defs::*; + + // https://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions + cfg_if::cfg_if! { + if #[cfg(windows)] { + pub(super) unsafe extern "C" fn trampoline($($argname: $argty)*) { + unsafe { inner($($argname),*) } + } + } else if #[cfg(all(target_os = "linux", target_arch = "x86_64"))] { + #[naked_function::naked] + #[export_name = concat!("portability_callconv_trampoline_", stringify!($modname), "__", stringify!($name))] + pub(super) unsafe extern "C" fn trampoline($($argname: $argty),*) { + // Map Windows arguments to System V arguments. + // For now we only support 4 integer arguments. + asm!( + "mov rdi, rcx", // arg 1 + "mov rsi, rdx", // arg 2 + "mov rdx, r8", // arg 3 + "mov rcx, r9", // arg 4 + "jmp {}", + sym inner, + ); + } + } else { + compile_error!("unsupported architecture or operating system"); + } + } + + $(#[$attr])* + unsafe extern "C" fn inner($($argname: $argty),*) $(-> $ret)? { + #[allow(unused_unsafe)] + unsafe { $($body)* } + } } )* } @@ -331,14 +393,22 @@ emulate!( fn GetCurrentProcess() { todo!("GetCurrentProcess") } - fn GetCurrentProcessId() { - todo!("GetCurrentProcessId") + /// + fn GetCurrentProcessId() -> u32 { + std::process::id() } fn GetCurrentThread() { todo!("GetCurrentThread") } - fn GetCurrentThreadId() { - todo!("GetCurrentThreadId") + // + fn GetCurrentThreadId() -> u32 { + use std::sync::atomic; + static THREAD_ID_COUNTER: atomic::AtomicU32 = atomic::AtomicU32::new(0); + std::thread_local! { + static THREAD_ID: u32 = THREAD_ID_COUNTER.fetch_add(1, atomic::Ordering::Relaxed); + } + + THREAD_ID.with(|id| *id) } fn GetDateFormatW() { todo!("GetDateFormatW") @@ -439,8 +509,17 @@ emulate!( fn GetSystemTime() { todo!("GetSystemTime") } - fn GetSystemTimeAsFileTime() { - todo!("GetSystemTimeAsFileTime") + /// + fn GetSystemTimeAsFileTime(lpSystemTimeAsFileTime: *mut std::ffi::c_void) { + #[repr(C)] + struct _FILETIME { + dwLowDateTime: u32, + dwHighDateTime: u32, + } + lpSystemTimeAsFileTime.cast::<_FILETIME>().write(_FILETIME { + dwLowDateTime: 0, + dwHighDateTime: 0, + }); } fn GetSystemTimePreciseAsFileTime() { todo!("GetSystemTimePreciseAsFileTime") @@ -544,8 +623,8 @@ emulate!( fn LoadLibraryExA() { todo!("LoadLibraryExA") } - fn LoadLibraryExW() { - todo!("LoadLibraryExW") + fn LoadLibraryExW(lpLibFileName: LPCWSTR, hFile: HANDLE, dwFlags: u32) { + todo!("LoadLibraryExW: {:?}", lpLibFileName) } fn LoadLibraryW() { todo!("LoadLibraryW") @@ -568,8 +647,10 @@ emulate!( fn OpenSemaphoreA() { todo!("OpenSemaphoreA") } - fn QueryPerformanceCounter() { - todo!("QueryPerformanceCounter") + /// + fn QueryPerformanceCounter(lpPerformanceCount: *mut u64) -> bool { + lpPerformanceCount.write(0); + true } fn QueryPerformanceFrequency() { todo!("QueryPerformanceFrequency") diff --git a/src/lib.rs b/src/lib.rs index 84809f7..9adece2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use std::{ fmt::Debug, ops::Deref, path::{Path, PathBuf}, + sync::Mutex, }; #[derive(Clone, Copy, Debug, bytemuck::Zeroable, bytemuck::Pod)] @@ -77,7 +78,7 @@ struct OptionalHeader { #[repr(C)] struct DataDirectory { // RVA - va: u32, + rva: u32, size: u32, } @@ -220,7 +221,25 @@ struct ExportDirectoryTable { name_pointer_rva: u32, ordinal_table_rva: u32, } -const _: () = assert!(size_of::() == 40); + +const IMAGE_REL_BASED_ABSOLUTE: u8 = 0; +const IMAGE_REL_BASED_DIR64: u8 = 10; + +struct BaseRelocationType(u8); +impl Debug for BaseRelocationType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self.0 { + IMAGE_REL_BASED_ABSOLUTE => "IMAGE_REL_BASED_ABSOLUTE", + 1 => "IMAGE_REL_BASED_HIGH", + 2 => "IMAGE_REL_BASED_LOW", + 3 => "IMAGE_REL_BASED_HIGHLOW", + 4 => "IMAGE_REL_BASED_HIGHADJ", + IMAGE_REL_BASED_DIR64 => "IMAGE_REL_BASED_DIR64", + _ => "", + }; + f.write_str(s) + } +} const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664; const IMAGE_FILE_MACHINE_ARM64: u16 = 0xaa64; @@ -250,6 +269,14 @@ impl<'pe> Deref for Image<'pe> { } } +struct TheGlobalState { + loaded_libraries: Vec<(String, Image<'static>)>, +} + +static GLOBAL_STATE: Mutex = Mutex::new(TheGlobalState { + loaded_libraries: Vec::new(), +}); + #[tracing::instrument(skip(pe, is_dll))] fn load<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image<'pe> { load_inner(pe, executable_path, is_dll) @@ -354,15 +381,51 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image ); } + tracing::debug!("Applying relocations"); + let mut base_relocations = &image[opt_header.base_relocation_table.rva as usize..] + [..opt_header.base_relocation_table.size as usize]; + while !base_relocations.is_empty() { + let page_rva = u32::from_ne_bytes(base_relocations[..4].try_into().unwrap()); + let block_size = u32::from_ne_bytes(base_relocations[4..][..4].try_into().unwrap()); + + base_relocations = &base_relocations[8..]; + + tracing::trace!(?page_rva, ?block_size, "Base relocation block"); + + let remaining = (block_size - 8) / 2; + for _ in 0..remaining { + let word = u16::from_ne_bytes(base_relocations[..2].try_into().unwrap()); + let relocation_type = word >> 12; + let offset = word & 0xFFF; + + let relocation_type = BaseRelocationType(relocation_type as u8); + + base_relocations = &base_relocations[2..]; + + let diff = image.base.wrapping_sub(opt_header.image_base as usize); + + let va = image.base + page_rva as usize + offset as usize; + let va_ptr = std::ptr::with_exposed_provenance_mut::(va); + match relocation_type.0 as u8 { + IMAGE_REL_BASED_ABSOLUTE => {} // need to ignore + IMAGE_REL_BASED_DIR64 => unsafe { + let old = va_ptr.read_unaligned(); + let new = old as usize + diff; + va_ptr.write_unaligned(new as u64); + }, + _ => panic!("bad relocation type in {executable_path:?}: {relocation_type:?}"), + } + } + } + + tracing::debug!("resolving imports"); let import_directory_table = bytemuck::cast_slice::<_, ImportDirectoryTableEntry>( - &image.loaded[opt_header.import_table.va as usize..] + &image.loaded[opt_header.import_table.rva as usize..] [..opt_header.import_table.size as usize], ) .to_vec(); - - tracing::debug!("checking imports"); for import_directory in import_directory_table { - tracing::debug!(?import_directory, "Resolving next import"); + tracing::debug!(?import_directory, "Resolving next import directory"); let dll_name = CStr::from_bytes_until_nul(&image.loaded[import_directory.name_rva as usize..]) @@ -409,7 +472,7 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image // You may be able to skip the binary search of the "hint" of the import is the correct index already. let export_directory_table = bytemuck::cast_slice::( - &img[img.opt_header.export_table.va as usize..] + &img[img.opt_header.export_table.rva as usize..] [..size_of::()], )[0]; tracing::debug!( @@ -426,7 +489,7 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image let name = u32::from_ne_bytes(img[name_ptr..][..4].try_into().unwrap()); let name = CStr::from_bytes_until_nul(&img[name as usize..]).unwrap(); names.push(name.to_owned()); - tracing::debug!(?name, "DLL {dll_name} has export"); + tracing::trace!(?name, "DLL {dll_name} has export"); } LoadedDll::Real(img, export_directory_table, names) @@ -455,8 +518,8 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image + (unbiased_ordinal * 4); let export_rva = u32::from_ne_bytes(img[eat_addr_rva..][..4].try_into().unwrap()); - if (img.opt_header.export_table.va - ..(img.opt_header.export_table.va + img.opt_header.export_table.size)) + if (img.opt_header.export_table.rva + ..(img.opt_header.export_table.rva + img.opt_header.export_table.size)) .contains(&export_rva) { todo!("symbol forwarding") @@ -468,10 +531,13 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image let ordinal_name_flag = import_lookup >> 63; let resolved_va = if ordinal_name_flag == 1 { let ordinal_number = import_lookup & 0xFFFF; - tracing::debug!(" import by ordinal: {ordinal_number}"); + tracing::debug!("import by ordinal: {ordinal_number}"); match &dll { - LoadedDll::Emulated => panic!("unsupported: emulated import via ordinal"), + LoadedDll::Emulated => { + tracing::error!("unsupported: emulated import via ordinal for {dll_name}. resolving them to 0"); + 0 + } LoadedDll::Real(img, export_directory_table, _) => { // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#export-ordinal-table let unbiased_ordinal = @@ -530,9 +596,9 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image }; assert_eq!(size_of::(), size_of::()); - image.loaded[import_directory.import_address_table_rva as usize..] - [i * size_of::()..][..size_of::()] - .copy_from_slice(&resolved_va.to_ne_bytes()); + let iat = &mut image.loaded[import_directory.import_address_table_rva as usize..] + [i * size_of::()..][..size_of::()]; + iat.copy_from_slice(&resolved_va.to_ne_bytes()); } } diff --git a/src/sys.rs b/src/sys.rs index 7b32c24..0c2a3a4 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -93,13 +93,22 @@ mod imp { } pub(crate) unsafe fn call_entrypoint_via_stdcall(fnptr: usize) -> u32 { - // todo this might be correct or not idk??? is it close enough in this case maybe?? use asm probably. - let fnptr = unsafe { - std::mem::transmute::<*const (), unsafe extern "C" fn() -> u32>( - std::ptr::with_exposed_provenance(fnptr), - ) - }; - unsafe { fnptr() } + let mut out: u32; + + cfg_if::cfg_if! { + if #[cfg(target_arch = "x86_64")] { + std::arch::asm!( + "call {}", + "mov {1:e}, eax", + in(reg) fnptr, + out(reg) out, + ); + } else { + compile_error!("unsupported architecture"); + } + } + + out } } diff --git a/test2/Makefile b/test2/Makefile index d718e22..ac1194c 100644 --- a/test2/Makefile +++ b/test2/Makefile @@ -1,11 +1,14 @@ SHELL = bash RUSTC = rustc --target x86_64-pc-windows-msvc -Copt-level=3 -Cpanic=abort -Clinker=lld-link -Clink-arg=/NODEFAULTLIB -Clink-arg=/debug:none -Cdebuginfo=0 -build: empty_exe.exe one_dll.exe two_dll.exe +build: empty_exe.exe one_dll.exe two_dll.exe constant_data.exe empty_exe.exe: empty_exe.rs $(RUSTC) empty_exe.rs +constant_data.exe: constant_data.rs + $(RUSTC) constant_data.rs + one_dll.exe: one_dll.rs small_dll.dll $(RUSTC) one_dll.rs