From 8c23716f8ff7d620152308b602d6ab0987d5a43f Mon Sep 17 00:00:00 2001 From: Noratrieb <48135649+Noratrieb@users.noreply.github.com> Date: Sat, 1 Feb 2025 22:28:37 +0100 Subject: [PATCH] emulation --- src/emulated.rs | 186 +++++++++++++++++++++++++++++---------------- src/lib.rs | 196 +++++++++++++++++++++++++++++++----------------- src/sys.rs | 24 ------ 3 files changed, 250 insertions(+), 156 deletions(-) diff --git a/src/emulated.rs b/src/emulated.rs index 3f68cbc..dcc0627 100644 --- a/src/emulated.rs +++ b/src/emulated.rs @@ -1,4 +1,9 @@ +#![allow(non_snake_case)] +#![allow(non_camel_case_types)] + mod base_defs { + use std::{ffi::CStr, fmt::Debug}; + pub(crate) type HANDLE = usize; #[repr(transparent)] @@ -23,6 +28,25 @@ mod base_defs { f.write_str(&self.to_string()) } } + + #[repr(transparent)] + pub(crate) struct LPCSTR(pub *const std::ffi::c_char); + impl LPCSTR { + pub(crate) fn as_cstr(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0) } + } + } + impl std::fmt::Debug for LPCSTR { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.as_cstr(), f) + } + } + + #[repr(C)] + pub(super) struct CRITICAL_SECTION { + pub(super) mutex: std::sync::atomic::AtomicU64, + pub(super) pad: [u8; 40 - 8], + } } macro_rules! define_emulation_entry { @@ -56,6 +80,16 @@ define_emulation_entry!( ws2_32, ); +macro_rules! make_body { + ($name:ident $($argname:ident),* @ delegate($dll:ident)) => { + crate::emulated::$dll::$name($($argname),*) + }; + ($name:ident $($argname:ident),* @ $($body:tt)*) => { + #[allow(unused_unsafe)] + unsafe { $($body)* } + }; +} + macro_rules! emulate { ($dllname:literal, mod $modname:ident { $( @@ -79,7 +113,7 @@ macro_rules! emulate { if function_name == stringify!($name) { unsafe { // NOTE: The ABI string is a lie..... - return Some(std::mem::transmute($name::trampoline as unsafe extern "C" fn($($argty),*))); + return Some(std::mem::transmute($name as unsafe extern "win64" fn($($argty),*) $(-> $ret)?)); } } )* @@ -87,43 +121,13 @@ macro_rules! emulate { None } + #[allow(unused_imports)] + use crate::emulated::base_defs::*; + $( - #[allow(non_snake_case)] - 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)* } - } + $(#[$attr])* + pub(super) unsafe extern "win64" fn $name($($argname: $argty),*) $(-> $ret)? { + make_body! { $name $($argname),* @ $($body)* } } )* } @@ -165,9 +169,17 @@ emulate!( } } ); + emulate!( "api-ms-win-core-synch-l1-2-0.dll", mod api_ms_win_core_synch_l1_2_0 { + fn InitializeCriticalSectionEx( + lpCriticalSection: *mut (), + dwSpinCount: u32, + flags: u32, + ) -> bool { + delegate(kernel32) + } fn WaitOnAddress() { todo!("WaitOnAddress") } @@ -293,9 +305,8 @@ emulate!( fn DecodePointer() { todo!("DecodePointer") } - fn DeleteCriticalSection() { - todo!("DeleteCriticalSection") - } + /// + fn DeleteCriticalSection(_lpCriticalSection: *mut ()) {} fn DeleteFiber() { todo!("DeleteFiber") } @@ -314,8 +325,19 @@ emulate!( fn EncodePointer() { todo!("EncodePointer") } - fn EnterCriticalSection() { - todo!("EnterCriticalSection") + /// + fn EnterCriticalSection(lpCriticalSection: *mut CRITICAL_SECTION) { + // a shitty spinlock + while (&*lpCriticalSection) + .mutex + .compare_exchange_weak( + 0, + 1, + std::sync::atomic::Ordering::Acquire, + std::sync::atomic::Ordering::Relaxed, + ) + .is_err() + {} } fn EnumSystemLocalesW() { todo!("EnumSystemLocalesW") @@ -335,8 +357,10 @@ emulate!( fn FindNextFileW() { todo!("FindNextFileW") } - fn FlsAlloc() { - todo!("FlsAlloc") + /// + fn FlsAlloc(_callback: extern "win64" fn()) -> u32 { + const FLS_OUT_OF_INDEXES: u32 = -1_i32 as u32; + FLS_OUT_OF_INDEXES } fn FlsFree() { todo!("FlsFree") @@ -448,8 +472,8 @@ emulate!( fn GetFullPathNameW() { todo!("GetFullPathNameW") } - fn GetLastError() { - todo!("GetLastError") + fn GetLastError() -> u32 { + 1 } fn GetLocaleInfoEx() { todo!("GetLocaleInfoEx") @@ -469,8 +493,9 @@ emulate!( fn GetModuleHandleExW() { todo!("GetModuleHandleExW") } - fn GetModuleHandleW() { - todo!("GetModuleHandleW") + fn GetModuleHandleW() -> u64 { + tracing::error!("TODO GetModuleHandleW"); + 0 } fn GetNativeSystemInfo() { todo!("GetNativeSystemInfo") @@ -482,8 +507,17 @@ emulate!( todo!("GetOverlappedResult") } /// - fn GetProcAddress(hModule: u64, lpProcName: LPCWSTR) { - todo!("GetProcAddress: {lpProcName:?}") + fn GetProcAddress(hModule: u64, lpProcName: LPCSTR) -> usize { + let dll = crate::GLOBAL_STATE + .state + .lock() + .unwrap() + .hmodule_to_dll + .get(&hModule) + .cloned() + .unwrap(); + // TODO: error handling... + crate::va_for_dll_export_by_name(&dll, lpProcName.as_cstr(), 0) } fn GetProcessHeap() { todo!("GetProcessHeap") @@ -578,8 +612,19 @@ emulate!( fn InitializeCriticalSectionAndSpinCount() { todo!("InitializeCriticalSectionAndSpinCount") } - fn InitializeCriticalSectionEx() { - todo!("InitializeCriticalSectionEx") + fn InitializeCriticalSectionEx( + lpCriticalSection: *mut (), + _dwSpinCount: u32, + _flags: u32, + ) -> bool { + lpCriticalSection + .cast::() + .write(CRITICAL_SECTION { + mutex: Default::default(), + pad: Default::default(), + }); + const _: () = assert!(size_of::() == 40); + true } fn InitializeProcThreadAttributeList() { todo!("InitializeProcThreadAttributeList") @@ -590,11 +635,13 @@ emulate!( fn InterlockedFlushSList() { todo!("InterlockedFlushSList") } - fn IsDebuggerPresent() { - todo!("IsDebuggerPresent") + /// + fn IsDebuggerPresent() -> bool { + false } - fn IsProcessorFeaturePresent() { - todo!("IsProcessorFeaturePresent") + /// + fn IsProcessorFeaturePresent(_ProcessorFeature: u32) -> bool { + false } fn IsThreadAFiber() { todo!("IsThreadAFiber") @@ -633,8 +680,8 @@ emulate!( &crate::GLOBAL_STATE.executable_path(), ); match result { - crate::LoadedDll::Emulated => 1, - crate::LoadedDll::Real(img, _, _) => img.base as u64, + Some(result) => result.hmodule(), + None => 0, } } fn LoadLibraryW() { @@ -762,8 +809,9 @@ emulate!( fn SetThreadStackGuarantee() { todo!("SetThreadStackGuarantee") } - fn SetUnhandledExceptionFilter() { - todo!("SetUnhandledExceptionFilter") + /// + fn SetUnhandledExceptionFilter(_lpTopLevelExceptionFilter: *mut ()) -> *mut () { + std::ptr::null_mut() } fn SetWaitableTimer() { todo!("SetWaitableTimer") @@ -804,8 +852,10 @@ emulate!( fn TryAcquireSRWLockExclusive() { todo!("TryAcquireSRWLockExclusive") } - fn UnhandledExceptionFilter() { - todo!("UnhandledExceptionFilter") + /// + fn UnhandledExceptionFilter(_ExceptionInfo: *const ()) -> u64 { + const EXCEPTION_CONTINUE_SEARCH: u64 = 0x0; + EXCEPTION_CONTINUE_SEARCH } fn UnlockFile() { todo!("UnlockFile") @@ -866,14 +916,20 @@ emulate!( fn NtWriteFile() { todo!("NtWriteFile") } + /// fn RtlCaptureContext() { - todo!("RtlCaptureContext") + tracing::error!("TODO: RtlCaptureContext - looks like someone feels like crashing...") } fn RtlGetLastNtStatus() { todo!("RtlGetLastNtStatus") } - fn RtlLookupFunctionEntry() { - todo!("RtlLookupFunctionEntry") + /// + fn RtlLookupFunctionEntry( + _ControlPc: u64, + _ImageBase: *mut (), + _HistoryTable: *mut (), + ) -> *const () { + std::ptr::null() } fn RtlNtStatusToDosError() { todo!("RtlNtStatusToDosError") diff --git a/src/lib.rs b/src/lib.rs index 64c74ee..198066e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,11 +2,15 @@ mod emulated; mod sys; use std::{ + collections::HashMap, ffi::{CStr, CString}, fmt::Debug, ops::{Deref, DerefMut}, path::{Path, PathBuf}, - sync::Mutex, + sync::{ + atomic::{AtomicU64, Ordering}, + LazyLock, Mutex, + }, }; #[derive(Clone, Copy, Debug, bytemuck::Zeroable, bytemuck::Pod)] @@ -252,7 +256,9 @@ pub fn execute(pe: &[u8], executable_path: &Path) { tracing::debug!("YOLO to {:#x}", entrypoint); unsafe { - let result = sys::call_entrypoint_via_stdcall(entrypoint); + let entrypoint = + std::mem::transmute:: u32>(entrypoint); + let result = entrypoint(); tracing::info!("result: {result}"); }; } @@ -280,29 +286,59 @@ impl<'pe> DerefMut for Image<'pe> { #[derive(Clone)] enum LoadedDll { - Emulated, - Real(Image<'static>, ExportDirectoryTable, Vec), + Emulated { + name: String, + hmodule: u64, + }, + Real { + name: String, + img: Image<'static>, + edt: ExportDirectoryTable, + export_names: Vec, + }, +} + +impl LoadedDll { + fn hmodule(&self) -> u64 { + match self { + Self::Emulated { hmodule, .. } => *hmodule, + Self::Real { img, .. } => img.base as u64, + } + } } struct TheGlobalState { loaded_libraries: Vec<(String, LoadedDll)>, executable_path: Option, + hmodule_to_dll: HashMap, + next_emulated_hmodule_idx: AtomicU64, } struct GlobalStateWrapper { - state: Mutex, + state: std::sync::LazyLock>, } impl GlobalStateWrapper { fn executable_path(&self) -> PathBuf { self.state.lock().unwrap().executable_path.clone().unwrap() } + fn get_emulated_hmodule_idx(&self) -> u64 { + self.state + .lock() + .unwrap() + .next_emulated_hmodule_idx + .fetch_add(1, Ordering::Relaxed) + } } static GLOBAL_STATE: GlobalStateWrapper = GlobalStateWrapper { - state: Mutex::new(TheGlobalState { - loaded_libraries: Vec::new(), - executable_path: None, + state: LazyLock::new(|| { + Mutex::new(TheGlobalState { + loaded_libraries: Vec::new(), + executable_path: None, + hmodule_to_dll: HashMap::new(), + next_emulated_hmodule_idx: AtomicU64::new(1), + }) }), }; @@ -465,7 +501,8 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image break; } - let dll = load_dll(dll_name, executable_path); + let dll = load_dll(dll_name, executable_path) + .unwrap_or_else(|| panic!("could not find dll {dll_name}")); let import_lookups = bytemuck::cast_slice::( &image[import_directory.import_address_table_rva as usize..], @@ -477,36 +514,21 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image break; } - fn compute_export_rva( - export_directory_table: &ExportDirectoryTable, - unbiased_ordinal: usize, - img: &Image<'_>, - ) -> usize { - let eat_addr_rva = export_directory_table.export_address_table_rva as usize - + (unbiased_ordinal * 4); - let export_rva = u32::from_ne_bytes(img[eat_addr_rva..][..4].try_into().unwrap()); - - 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") - } - - export_rva as usize - } - 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}"); match &dll { - LoadedDll::Emulated => { + LoadedDll::Emulated { .. } => { tracing::error!("unsupported: emulated import via ordinal for {dll_name}. resolving them to 0"); 0 } - LoadedDll::Real(img, export_directory_table, _) => { + LoadedDll::Real { + img, + edt: export_directory_table, + .. + } => { // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#export-ordinal-table let unbiased_ordinal = ordinal_number as usize - export_directory_table.ordinal_base as usize; @@ -528,37 +550,7 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image CStr::from_bytes_until_nul(&image[hint_name_table_rva as usize + 2..]).unwrap(); tracing::debug!("import by name: hint={hint} name={func_name:?}"); - match &dll { - LoadedDll::Emulated => emulated::emulate(dll_name, func_name.to_str().unwrap()) - .unwrap_or_else(|| { - panic!("could not find function {func_name:?} in dll {dll_name:?}"); - }), - LoadedDll::Real(img, export_directory_table, names) => { - let idx = - if names.get(hint as usize) == Some(&func_name.to_owned()) { - hint as usize - } else { - names.binary_search(&func_name.to_owned()).unwrap_or_else(|_| { - panic!("could not find function {func_name:?} in dll {dll_name}") - }) - }; - - // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#export-ordinal-table - let ordinal_table = bytemuck::cast_slice::( - &img[export_directory_table.ordinal_table_rva as usize..] - [..2 * export_directory_table.number_of_name_pointers as usize], - ); - let unbiased_ordinal = ordinal_table[idx]; - - let export_rva = compute_export_rva( - export_directory_table, - unbiased_ordinal as usize, - img, - ); - - img.base + export_rva as usize - } - } + va_for_dll_export_by_name(&dll, func_name, hint as usize) }; assert_eq!(size_of::(), size_of::()); @@ -597,7 +589,63 @@ fn load_inner<'pe>(pe: &'pe [u8], executable_path: &Path, is_dll: bool) -> Image image } -fn load_dll(dll_name: &str, executable_path: &Path) -> LoadedDll { +fn va_for_dll_export_by_name(dll: &LoadedDll, func_name: &CStr, hint: usize) -> usize { + match &dll { + LoadedDll::Emulated { name, .. } => emulated::emulate(name, func_name.to_str().unwrap()) + .unwrap_or_else(|| { + panic!("could not find function {func_name:?} in dll {name:?}"); + }), + LoadedDll::Real { + name, + img, + edt: export_directory_table, + export_names: names, + } => { + let idx = if names.get(hint) == Some(&func_name.to_owned()) { + hint as usize + } else { + names + .binary_search(&func_name.to_owned()) + .unwrap_or_else(|_| { + panic!("could not find function {func_name:?} in dll {name}") + }) + }; + + // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#export-ordinal-table + let ordinal_table = bytemuck::cast_slice::( + &img[export_directory_table.ordinal_table_rva as usize..] + [..2 * export_directory_table.number_of_name_pointers as usize], + ); + let unbiased_ordinal = ordinal_table[idx]; + + let export_rva = + compute_export_rva(export_directory_table, unbiased_ordinal as usize, img); + + img.base + export_rva as usize + } + } +} + +fn compute_export_rva( + export_directory_table: &ExportDirectoryTable, + unbiased_ordinal: usize, + img: &Image<'_>, +) -> usize { + let eat_addr_rva = + export_directory_table.export_address_table_rva as usize + (unbiased_ordinal * 4); + let export_rva = u32::from_ne_bytes(img[eat_addr_rva..][..4].try_into().unwrap()); + + 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") + } + + export_rva as usize +} + +fn load_dll(dll_name: &str, executable_path: &Path) -> Option { tracing::debug!("loading dll {dll_name}"); let already_loaded = GLOBAL_STATE @@ -609,14 +657,17 @@ fn load_dll(dll_name: &str, executable_path: &Path) -> LoadedDll { .find(|(name, _)| name == dll_name) .map(Clone::clone); if let Some((_, already_loaded)) = already_loaded { - return already_loaded; + return Some(already_loaded); } let dll = find_dll(&dll_name, executable_path); let dll = match dll { Some(DllLocation::Emulated) => { tracing::debug!("emulating {dll_name:?}"); - LoadedDll::Emulated + LoadedDll::Emulated { + name: dll_name.to_owned(), + hmodule: GLOBAL_STATE.get_emulated_hmodule_idx(), + } } Some(DllLocation::Found(path)) => { let file = std::fs::File::open(&path).unwrap(); @@ -659,10 +710,15 @@ fn load_dll(dll_name: &str, executable_path: &Path) -> LoadedDll { tracing::trace!(?name, "DLL {dll_name} has export"); } - LoadedDll::Real(img, export_directory_table, names) + LoadedDll::Real { + name: dll_name.to_owned(), + img, + edt: export_directory_table, + export_names: names, + } } None => { - panic!("could not find dll {dll_name:?}"); + return None; } }; @@ -672,8 +728,14 @@ fn load_dll(dll_name: &str, executable_path: &Path) -> LoadedDll { .unwrap() .loaded_libraries .push((dll_name.to_owned(), dll.clone())); + GLOBAL_STATE + .state + .lock() + .unwrap() + .hmodule_to_dll + .insert(dll.hmodule(), dll.clone()); - dll + Some(dll) } fn parse_header(pe: &[u8]) -> (&CoffHeader, usize) { diff --git a/src/sys.rs b/src/sys.rs index 0c2a3a4..529d2bd 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -56,11 +56,6 @@ mod imp { .map_err(Into::into) } } - - pub(crate) unsafe fn call_entrypoint_via_stdcall(fnptr: *const ()) -> u32 { - let fnptr = unsafe { std::mem::transmute::<_, unsafe extern "stdcall" fn() -> u32>(fnptr) }; - unsafe { fnptr() } - } } #[cfg(unix)] @@ -91,25 +86,6 @@ mod imp { Err(io::Error::last_os_error()) } } - - pub(crate) unsafe fn call_entrypoint_via_stdcall(fnptr: usize) -> u32 { - 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 - } } pub(crate) use imp::*;