emulation

This commit is contained in:
nora 2025-02-01 22:28:37 +01:00
parent e8ba83c443
commit 8c23716f8f
3 changed files with 250 additions and 156 deletions

View file

@ -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")
}
/// <https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-deletecriticalsection>
fn DeleteCriticalSection(_lpCriticalSection: *mut ()) {}
fn DeleteFiber() {
todo!("DeleteFiber")
}
@ -314,8 +325,19 @@ emulate!(
fn EncodePointer() {
todo!("EncodePointer")
}
fn EnterCriticalSection() {
todo!("EnterCriticalSection")
/// <https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-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")
/// <https://learn.microsoft.com/en-us/windows/win32/api/fibersapi/nf-fibersapi-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")
}
/// <https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress>
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::<CRITICAL_SECTION>()
.write(CRITICAL_SECTION {
mutex: Default::default(),
pad: Default::default(),
});
const _: () = assert!(size_of::<CRITICAL_SECTION>() == 40);
true
}
fn InitializeProcThreadAttributeList() {
todo!("InitializeProcThreadAttributeList")
@ -590,11 +635,13 @@ emulate!(
fn InterlockedFlushSList() {
todo!("InterlockedFlushSList")
}
fn IsDebuggerPresent() {
todo!("IsDebuggerPresent")
/// <https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-isdebuggerpresent>
fn IsDebuggerPresent() -> bool {
false
}
fn IsProcessorFeaturePresent() {
todo!("IsProcessorFeaturePresent")
/// <https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-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")
/// <https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-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")
/// <https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-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")
}
/// <https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-rtlcapturecontext>
fn RtlCaptureContext() {
todo!("RtlCaptureContext")
tracing::error!("TODO: RtlCaptureContext - looks like someone feels like crashing...")
}
fn RtlGetLastNtStatus() {
todo!("RtlGetLastNtStatus")
}
fn RtlLookupFunctionEntry() {
todo!("RtlLookupFunctionEntry")
/// <https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-rtllookupfunctionentry>
fn RtlLookupFunctionEntry(
_ControlPc: u64,
_ImageBase: *mut (),
_HistoryTable: *mut (),
) -> *const () {
std::ptr::null()
}
fn RtlNtStatusToDosError() {
todo!("RtlNtStatusToDosError")

View file

@ -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::<usize, unsafe extern "win64" fn() -> 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<CString>),
Emulated {
name: String,
hmodule: u64,
},
Real {
name: String,
img: Image<'static>,
edt: ExportDirectoryTable,
export_names: Vec<CString>,
},
}
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<PathBuf>,
hmodule_to_dll: HashMap<u64, LoadedDll>,
next_emulated_hmodule_idx: AtomicU64,
}
struct GlobalStateWrapper {
state: Mutex<TheGlobalState>,
state: std::sync::LazyLock<Mutex<TheGlobalState>>,
}
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::<u8, u64>(
&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::<u8, u16>(
&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::<usize>(), size_of::<u64>());
@ -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::<u8, u16>(
&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<LoadedDll> {
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) {

View file

@ -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::*;