use std::arch::asm;
use std::ops::Add;
use std::os::windows::ffi::OsStringExt;
use std::slice::from_raw_parts;
use std::ffi::{c_void, OsString};
use windows::Win32::System::SystemServices::{IMAGE_DOS_HEADER, IMAGE_DOS_SIGNATURE, IMAGE_EXPORT_DIRECTORY, IMAGE_NT_SIGNATURE};
use windows::Win32::System::Diagnostics::Debug::IMAGE_NT_HEADERS64;
use std::fmt;
/// A structure containing the module name, function name, and export address for each function loaded
/// into the portable executable on x64 systems only.
struct ExportResolver<'a> {
module: &'a str,
base_address: usize, // to prevent repeat reads of the peb
function: &'a str,
address: usize,
}
#[derive(Debug)]
/// A list of module names, function names, and function export addresses of the respective function
/// within the memory space of the process this is called from.
///
///
/// **IMPORTANT:** This tool may only be used for ethical, research or legal purposes. There is a massive learning benefit
/// to exploring this tool, or using it as part of a debugging process when reverse engineering malware or other tools.
///
/// This tool may also be used for red teamer's and penetration testers where you have the LAWFUL AUTHORITY to use this tool,
/// such as on a red team client engagement. In no way may this be used for illegal activity.
///
/// Tool only works on x64 by design.
///
///
/// # Example
///
/// ```rust
/// fn main() {
///
/// // Create a new instance of the ExportList
/// let mut exports = ExportList::new();
///
/// // Add the desired functions to the ExportList structure, this will resolve and save the virtual addresses
/// // These calls may cause an Error if the function cannot be found; .add returns Result<(), ExportError>
/// let _ = exports.add("ntdll.dll", "NtOpenProcess");
/// let _ = exports.add("ntdll.dll", "NtQuerySystemTime");
///
/// // Attempt to get the virtual address; returns returns Result<(), ExportError> - an error will be returned where
/// // the input function name cannot be found in the vector of resolved functions (i.e. if the above step failed)
/// // or you have a typo.
/// let _nt = match exports.get_function_address("NtOpenProcess") {
/// Ok(v) => println!("NT: {:x}", v),
/// Err(e) => println!("Eeee {}", e),
/// };
/// }
/// ```
pub struct ExportList<'a> {
list: Vec>,
}
impl fmt::Debug for ExportResolver<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ExportResolver {{ module: \"{}\", function: \"{}\", address: {:#x} }}",
self.module, self.function, self.address
)
}
}
#[derive(Debug)]
pub enum ExportError {
FunctionNotFound { module: String, function: String },
FunctionNotInList { function: String },
}
impl fmt::Display for ExportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExportError::FunctionNotFound { module, function } => {
write!(f, "Failed to get function address for {} in {}", function, module)
},
ExportError::FunctionNotInList { function } => {
write!(f, "Function {} could not be found in the list of resolved functions, are you sure it's there?.", function)
}
}
}
}
impl std::error::Error for ExportError {}
impl<'a> ExportList<'a> {
/// Instantiate a new instance of the export list
pub fn new() -> ExportList<'a> {
ExportList {
list: Vec::new(),
}
}
/// Add a new function to the list which will use the module name and function name to
/// find the memory address of hte function within the export address table.
///
/// # Returns
///
/// The function operates on its own instance, but will return a result of the unit type, or an ExportError.
///
///
/// This will only work for x64, and there is no guarantee the addresses will always be valid.
///
pub fn add(&mut self, module: &'a str, function: &'a str) -> Result<(), ExportError> {
for item in &self.list {
// if we have already resolved the module, then skip straight to it
if item.module == module {
let result = get_function_from_exports(module, function, Some(item.base_address))
.ok_or_else(|| ExportError::FunctionNotFound {
module: module.to_string(),
function: function.to_string(),
})?;
self.list.push(result);
return Ok(());
}
}
// if we made it this far, there was no match in the current exports
let result = get_function_from_exports(module, function, None)
.ok_or_else(|| ExportError::FunctionNotFound {
module: module.to_string(),
function: function.to_string(),
})?;
self.list.push(result);
Ok(())
}
/// Get the function address of a function you have added to the vector of exports as a usize. This
/// may be converted to a *const c_void if required to be used as a raw pointer.
///
///
/// This will only work for x64, and there is no guarantee the addresses will always be valid.
///
pub fn get_function_address(&self, function_name: &str) -> Result {
self.list
.iter()
.find(|f| f.function == function_name)
.map(|f| f.address)
.ok_or_else(|| ExportError::FunctionNotInList {
function: function_name.to_string(),
})
}
}
/// Get the base address of a specified module. Obtains the base address by reading from the TEB -> PEB ->
/// PEB_LDR_DATA -> InMemoryOrderModuleList -> InMemoryOrderLinks -> DllBase
///
/// Returns the DLL base address as a Option
#[allow(unused_variables)]
#[allow(unused_assignments)]
fn get_module_base(module_name: &str) -> Option {
let mut peb: usize;
let mut ldr: usize;
let mut in_memory_order_module_list: usize;
let mut current_entry: usize;
unsafe {
// get the peb and module list
asm!(
"mov {peb}, gs:[0x60]",
"mov {ldr}, [{peb} + 0x18]",
"mov {in_memory_order_module_list}, [{ldr} + 0x10]", // points to the Flink
peb = out(reg) peb,
ldr = out(reg) ldr,
in_memory_order_module_list = out(reg) in_memory_order_module_list,
);
// set the current entry to the head of the list
current_entry = in_memory_order_module_list;
// iterate the modules searching for
loop {
// get the attributes we are after of the current entry
let dll_base = *(current_entry.add(0x30) as *const usize);
let module_name_address = *(current_entry.add(0x60) as *const usize);
let module_length = *(current_entry.add(0x58) as *const u16);
// check if the module name address is valid and not zero
if module_name_address != 0 && module_length > 0 {
// read the module name from memory
let dll_name_slice = from_raw_parts(module_name_address as *const u16, (module_length / 2) as usize);
let dll_name = OsString::from_wide(dll_name_slice);
// do we have a match on the module name?
if dll_name.to_string_lossy().eq_ignore_ascii_case(module_name) {
return Some(dll_base);
}
} else {
return None;
}
// dereference current_entry which contains the value of the next LDR_DATA_TABLE_ENTRY (specifically a pointer to LIST_ENTRY
// within the next LDR_DATA_TABLE_ENTRY)
current_entry = *(current_entry as *const usize);
// If we have looped back to the start, break
if current_entry == in_memory_order_module_list {
return None;
}
}
}
}
/// Get the function address of a function in a specified DLL from the DLL Base.
///
/// # Parameters
/// * dll_name -> the name of the DLL / module you are wanting to query
/// * needle -> the function name (case sensitive) of the function you are looking for
///
/// # Returns
/// Option<*const c_void> -> the function address as a pointer
fn get_function_from_exports<'a>(dll_name: &'a str, needle: &'a str, dll_base: Option) -> Option> {
// if the dll_base was already found from a previous search then use that
// otherwise, if it was None, make a call to get_module_base
let dll_base: *mut c_void = match dll_base {
Some(base) => base as *mut c_void,
None => {
match get_module_base(dll_name) {
Some(a) => a as *mut c_void,
None => {
return None;
},
}
},
};
// check we match the DOS header, cast as pointer to tell the compiler to treat the memory
// address as if it were a IMAGE_DOS_HEADER structure
let dos_header: IMAGE_DOS_HEADER = unsafe { read_memory(dll_base as *const IMAGE_DOS_HEADER) };
if dos_header.e_magic != IMAGE_DOS_SIGNATURE {
return None;
}
// check the NT headers
let nt_headers = unsafe { read_memory(dll_base.offset(dos_header.e_lfanew as isize) as *const IMAGE_NT_HEADERS64) };
if nt_headers.Signature != IMAGE_NT_SIGNATURE {
return None;
}
// get the export directory
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_directory
// found from first item in the DataDirectory; then we take the structure in memory at dll_base + RVA
let export_dir_rva = nt_headers.OptionalHeader.DataDirectory[0].VirtualAddress;
let export_offset = unsafe {dll_base.add(export_dir_rva as usize) };
let export_dir: IMAGE_EXPORT_DIRECTORY = unsafe { read_memory(export_offset as *const IMAGE_EXPORT_DIRECTORY) };
// get the addresses we need
let address_of_functions_rva = export_dir.AddressOfFunctions as usize;
let address_of_names_rva = export_dir.AddressOfNames as usize;
let ordinals_rva = export_dir.AddressOfNameOrdinals as usize;
let functions = unsafe { dll_base.add(address_of_functions_rva as usize) } as *const u32;
let names = unsafe { dll_base.add(address_of_names_rva as usize) } as *const u32;
let ordinals = unsafe { dll_base.add(ordinals_rva as usize) } as *const u16;
// get the amount of names to iterate over
let number_of_names = export_dir.NumberOfNames;
for i in 0..number_of_names {
// calculate the RVA of the function name
let name_rva = unsafe { *names.offset(i.try_into().unwrap()) as usize };
// actual memory address of the function name
let name_addr = unsafe { dll_base.add(name_rva) };
// read the function name
let function_name = unsafe {
let char = name_addr as *const u8;
let mut len = 0;
// iterate over the memory until a null terminator is found
while *char.add(len) != 0 {
len += 1;
}
std::slice::from_raw_parts(char, len)
};
let function_name = std::str::from_utf8(function_name).unwrap_or("Invalid UTF-8");
if function_name.eq("Invalid UTF-8") {
return None;
}
// if we have a match on our function name
if function_name.eq(needle) {
// calculate the RVA of the function address
let ordinal = unsafe { *ordinals.offset(i.try_into().unwrap()) as usize };
let fn_rva = unsafe { *functions.add(ordinal) as usize };
// actual memory address of the function address
let fn_addr = unsafe { dll_base.add(fn_rva) } as *const c_void;
let result = ExportResolver {
module: dll_name,
base_address: dll_base as usize,
function: needle,
address: fn_addr as usize,
};
return Some(result);
}
}
None
}
/// Read memory of any type
unsafe fn read_memory(address: *const T) -> T {
std::ptr::read(address)
}