use alloc::{
boxed::Box,
collections::BTreeMap,
ffi::CString,
format,
string::{String, ToString},
vec::Vec,
vec,
};
use core::intrinsics::{
volatile_copy_nonoverlapping_memory,
volatile_set_memory
};
use core::{
ffi::c_void,
mem::transmute,
ptr::{
null_mut,
read_unaligned,
write_unaligned
},
};
use log::{debug, info, warn};
use obfstr::{obfstr as obf, obfstring as s};
use dinvk::{dinvoke, helper::PE, types::NTSTATUS};
use dinvk::module::{
get_proc_address,
get_module_address,
get_ntdll_address
};
use dinvk::winapis::{
NT_SUCCESS, NtProtectVirtualMemory,
NtAllocateVirtualMemory, NtCurrentProcess,
LoadLibraryA,
};
use windows_sys::Win32::{
Foundation::*,
Storage::FileSystem::*,
System::{
Memory::*,
SystemServices::*,
Diagnostics::Debug::*,
LibraryLoader::DONT_RESOLVE_DLL_REFERENCES,
},
};
use crate::error::{CoffError, CoffeeLdrError, Result};
use crate::coff::{Coff, CoffMachine, CoffSource};
use crate::coff::{IMAGE_RELOCATION, IMAGE_SYMBOL};
use crate::beacon::{get_function_internal_address, get_output_data};
type CoffMain = extern "C" fn(*mut u8, usize);
#[derive(Default)]
pub struct CoffeeLdr<'a> {
coff: Coff<'a>,
section_map: Vec<SectionMap>,
symbols: CoffSymbol,
module: &'a str,
}
impl<'a> CoffeeLdr<'a> {
pub fn new<T: Into<CoffSource<'a>>>(source: T) -> Result<Self> {
let coff = match source.into() {
CoffSource::File(path) => {
info!("Try to read the file: {path}");
let buffer = read_file(path)
.map_err(|_| CoffError::FileReadError(path.to_string()))?;
Coff::parse(Box::leak(buffer.into_boxed_slice()))?
}
CoffSource::Buffer(buffer) => Coff::parse(buffer)?,
};
Ok(Self {
coff,
section_map: Vec::new(),
symbols: CoffSymbol::default(),
..Default::default()
})
}
#[must_use]
pub fn with_module_stomping(mut self, module: &'a str) -> Self {
self.module = module;
self
}
pub fn run(
&mut self,
entry: &str,
args: Option<*mut u8>,
argc: Option<usize>,
) -> Result<String> {
info!("Preparing environment for COFF execution.");
self.prepare()?;
for symbol in &self.coff.symbols {
let name = self.coff.get_symbol_name(symbol);
if name == entry && Coff::is_fcn(symbol.Type) {
info!("Running COFF file: entry point = {}, args = {:?}, argc = {:?}", name, args, argc);
let section_addr = self.section_map[(symbol.SectionNumber - 1) as usize].base;
let entrypoint = unsafe { section_addr.offset(symbol.Value as isize) };
let coff_main: CoffMain = unsafe { transmute(entrypoint) };
coff_main(args.unwrap_or(null_mut()), argc.unwrap_or(0));
break;
}
}
Ok(get_output_data()
.filter(|o| !o.buffer.is_empty())
.map(|o| o.to_string())
.unwrap_or_default())
}
fn prepare(&mut self) -> Result<()> {
self.coff.arch.check_architecture()?;
let mem = CoffMemory::new(&self.coff, self.module);
let (sections, sec_base) = mem.alloc()?;
self.section_map = sections;
let (functions, symbols) = CoffSymbol::new(&self.coff, self.module, sec_base)?;
self.symbols = symbols;
let reloc = CoffRelocation::new(&self.coff, &self.section_map);
reloc.apply_relocations(&functions, &self.symbols)?;
self.section_map
.iter_mut()
.filter(|section| section.size > 0)
.try_for_each(|section| section.adjust_permissions())?;
Ok(())
}
}
impl Drop for CoffeeLdr<'_> {
fn drop(&mut self) {
if !self.module.is_empty() {
return;
}
let mut size = 0;
for section in self.section_map.iter_mut() {
if !section.base.is_null() {
NtFreeVirtualMemory(
NtCurrentProcess(),
&mut section.base,
&mut size,
MEM_RELEASE
);
}
}
if !self.symbols.address.is_null() {
NtFreeVirtualMemory(
NtCurrentProcess(),
unsafe { &mut *self.symbols.address },
&mut size,
MEM_RELEASE
);
}
}
}
struct CoffMemory<'a> {
coff: &'a Coff<'a>,
module: &'a str,
}
impl<'a> CoffMemory<'a> {
pub fn new(coff: &'a Coff<'a>, module: &'a str) -> Self {
Self {
coff,
module,
}
}
pub fn alloc(&self) -> Result<(Vec<SectionMap>, Option<*mut c_void>)> {
if !self.module.is_empty() {
self.alloc_with_stomping()
} else {
self.alloc_bof_memory()
}
}
fn alloc_bof_memory(&self) -> Result<(Vec<SectionMap>, Option<*mut c_void>)> {
let mut size = self.coff.size();
let mut addr = null_mut();
let status = NtAllocateVirtualMemory(
NtCurrentProcess(),
&mut addr,
0,
&mut size,
MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN,
PAGE_READWRITE
);
if status != STATUS_SUCCESS {
return Err(CoffeeLdrError::MemoryAllocationError(unsafe { GetLastError() }));
}
debug!("Memory successfully allocated for BOF at address: {:?}", addr);
let (sections, _) = SectionMap::copy_sections(addr, self.coff);
Ok((sections, None))
}
fn alloc_with_stomping(&self) -> Result<(Vec<SectionMap>, Option<*mut c_void>)> {
let (mut text_address, mut size) = self.get_text_module()
.ok_or(CoffeeLdrError::StompingTextSectionNotFound)?;
if self.coff.size() > size {
return Err(CoffeeLdrError::StompingSizeOverflow);
}
let mut old = 0;
if !NT_SUCCESS(NtProtectVirtualMemory(
NtCurrentProcess(),
&mut text_address,
&mut size,
PAGE_READWRITE,
&mut old
)) {
return Err(CoffeeLdrError::MemoryProtectionError(unsafe { GetLastError() }));
}
debug!(
"Memory successfully allocated for BOF at address (Module Stomping): {:?}",
text_address
);
let (sections, sec_base) = SectionMap::copy_sections(text_address, self.coff);
Ok((sections, Some(sec_base)))
}
fn get_text_module(&self) -> Option<(*mut c_void, usize)> {
let target = format!("{}\0", self.module);
let h_module = {
let handle = get_module_address(self.module, None);
if handle.is_null() {
LoadLibraryExA(
target.as_ptr(),
null_mut(),
DONT_RESOLVE_DLL_REFERENCES
)?
} else {
handle
}
};
if h_module.is_null() {
return None;
}
let pe = PE::parse(h_module);
let section = pe.section_by_name(obf!(".text"))?;
let ptr = (h_module as usize + section.VirtualAddress as usize) as *mut c_void;
let size = section.SizeOfRawData as usize;
Some((ptr, size))
}
}
const MAX_SYMBOLS: usize = 600;
#[derive(Debug, Clone, Copy)]
struct CoffSymbol {
address: *mut *mut c_void,
}
impl CoffSymbol {
pub fn new(
coff: &Coff,
module: &str,
base_addr: Option<*mut c_void>,
) -> Result<(BTreeMap<String, usize>, Self)> {
let symbols = Self::process_symbols(coff)?;
let address = if !module.is_empty() {
let addr = base_addr.ok_or(CoffeeLdrError::MissingStompingBaseAddress)?;
addr as *mut *mut c_void
} else {
let mut size = MAX_SYMBOLS * size_of::<*mut c_void>();
let mut addr = null_mut();
let status = NtAllocateVirtualMemory(
NtCurrentProcess(),
&mut addr,
0,
&mut size,
MEM_COMMIT | MEM_RESERVE | MEM_TOP_DOWN,
PAGE_READWRITE,
);
if addr.is_null() || status != STATUS_SUCCESS {
return Err(CoffeeLdrError::MemoryAllocationError(unsafe { GetLastError() }));
}
addr as *mut *mut c_void
};
Ok((symbols, Self { address }))
}
fn process_symbols(coff: &Coff) -> Result<BTreeMap<String, usize>> {
let mut functions = BTreeMap::new();
for symbol in &coff.symbols {
if functions.len() >= MAX_SYMBOLS {
return Err(CoffeeLdrError::TooManySymbols(functions.len()));
}
if symbol.StorageClass == IMAGE_SYM_CLASS_EXTERNAL as u8 && symbol.SectionNumber == 0 {
let name = coff.get_symbol_name(symbol);
let address = Self::resolve_symbol_address(&name, coff)?;
functions.insert(name, address);
}
}
Ok(functions)
}
fn resolve_symbol_address(name: &str, coff: &Coff) -> Result<usize> {
debug!("Attempting to resolve address for symbol: {}", name);
let prefix = match coff.arch {
CoffMachine::X64 => "__imp_",
CoffMachine::X32 => "__imp__",
};
let symbol_name = name
.strip_prefix(prefix)
.map_or_else(|| Err(CoffeeLdrError::SymbolIgnored), Ok)?;
if symbol_name.starts_with(obf!("Beacon")) || symbol_name.starts_with(obf!("toWideChar")) {
debug!("Resolving Beacon: {}", symbol_name);
return get_function_internal_address(symbol_name);
}
let (dll, mut function) = symbol_name
.split_once('$')
.ok_or_else(|| CoffeeLdrError::ParseError(symbol_name.to_string()))?;
if let CoffMachine::X32 = coff.arch {
function = function.split('@').next().unwrap_or(function);
}
debug!("Resolving Module {} and Function {}", dll, function);
let module = {
let mut handle = get_module_address(dll.to_string(), None);
if handle.is_null() {
handle = LoadLibraryA(dll);
if handle.is_null() {
return Err(CoffeeLdrError::ModuleNotFound(dll.to_string()));
}
handle
} else {
handle
}
};
let addr = get_proc_address(module, function, None);
if addr.is_null() {
Err(CoffeeLdrError::FunctionNotFound(symbol_name.to_string()))
} else {
Ok(addr as usize)
}
}
}
impl Default for CoffSymbol {
fn default() -> Self {
Self { address: null_mut() }
}
}
#[derive(Debug, Clone)]
struct SectionMap {
base: *mut c_void,
size: usize,
characteristics: u32,
name: String,
}
impl SectionMap {
fn copy_sections(virt_addr: *mut c_void, coff: &Coff) -> (Vec<SectionMap>, *mut c_void) {
unsafe {
let sections = &coff.sections;
let mut base = virt_addr;
let sections = sections
.iter()
.map(|section| {
let size = section.SizeOfRawData as usize;
let name = Coff::get_section_name(section);
let address = coff.buffer.as_ptr().add(section.PointerToRawData as usize);
if section.PointerToRawData != 0 {
debug!("Copying section: {}", name);
volatile_copy_nonoverlapping_memory(base as *mut u8, address.cast_mut(), size);
} else {
volatile_set_memory(address.cast_mut(), 0, size);
}
let section_map = SectionMap {
base,
size,
characteristics: section.Characteristics,
name,
};
base = Coff::page_align((base as usize) + size) as *mut c_void;
section_map
})
.collect();
(sections, base)
}
}
fn adjust_permissions(&mut self) -> Result<()> {
info!(
"Adjusting memory permissions for section: Name = {}, Address = {:?}, Size = {}, Characteristics = 0x{:X}",
self.name, self.base, self.size, self.characteristics
);
let bitmask = self.characteristics & (IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE);
let mut protection = if bitmask == 0 {
PAGE_NOACCESS
} else if bitmask == IMAGE_SCN_MEM_EXECUTE {
PAGE_EXECUTE
} else if bitmask == IMAGE_SCN_MEM_READ {
PAGE_READONLY
} else if bitmask == (IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE) {
PAGE_EXECUTE_READ
} else if bitmask == IMAGE_SCN_MEM_WRITE {
PAGE_WRITECOPY
} else if bitmask == (IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE) {
PAGE_EXECUTE_WRITECOPY
} else if bitmask == (IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE) {
PAGE_READWRITE
} else if bitmask == (IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE) {
PAGE_EXECUTE_READWRITE
} else {
warn!("Unknown protection, using PAGE_EXECUTE_READWRITE");
PAGE_EXECUTE_READWRITE
};
if (protection & IMAGE_SCN_MEM_NOT_CACHED) == IMAGE_SCN_MEM_NOT_CACHED {
protection |= PAGE_NOCACHE;
}
let mut old = 0;
if !NT_SUCCESS(NtProtectVirtualMemory(
NtCurrentProcess(),
&mut self.base,
&mut self.size,
protection,
&mut old
)) {
return Err(CoffeeLdrError::MemoryProtectionError(unsafe { GetLastError() }));
}
Ok(())
}
}
struct CoffRelocation<'a> {
coff: &'a Coff<'a>,
section_map: &'a [SectionMap],
}
impl<'a> CoffRelocation<'a> {
pub fn new(coff: &'a Coff, section_map: &'a [SectionMap]) -> Self {
Self { coff, section_map }
}
pub fn apply_relocations(
&self,
functions: &BTreeMap<String, usize>,
symbols: &CoffSymbol
) -> Result<()> {
let mut index = 0;
for (i, section) in self.coff.sections.iter().enumerate() {
let relocations = self.coff.get_relocations(section);
for relocation in relocations.iter() {
let symbol = &self.coff.symbols[relocation.SymbolTableIndex as usize];
let symbol_reloc_addr = (self.section_map[i].base as usize
+ unsafe { relocation.Anonymous.VirtualAddress } as usize) as *mut c_void;
let name = self.coff.get_symbol_name(symbol);
if let Some(function_address) = functions.get(&name).map(|&addr| addr as *mut c_void) {
unsafe {
symbols
.address
.add(index)
.write_volatile(function_address);
self.process_relocations(
symbol_reloc_addr,
function_address,
symbols.address.add(index),
relocation,
symbol
)?;
};
index += 1;
} else {
self.process_relocations(
symbol_reloc_addr,
null_mut(),
null_mut(),
relocation,
symbol
)?;
}
}
}
Ok(())
}
fn process_relocations(
&self,
reloc_addr: *mut c_void,
function_address: *mut c_void,
symbols: *mut *mut c_void,
relocation: &IMAGE_RELOCATION,
symbol: &IMAGE_SYMBOL
) -> Result<()> {
debug!(
"Processing relocation: Type = {}, Symbol Type = {}, StorageClass = {}, Section Number: {}",
relocation.Type, symbol.Type, symbol.StorageClass, symbol.SectionNumber
);
unsafe {
if symbol.StorageClass == IMAGE_SYM_CLASS_EXTERNAL as u8 && symbol.SectionNumber == 0 {
match self.coff.arch {
CoffMachine::X64 => {
if relocation.Type as u32 == IMAGE_REL_AMD64_REL32 && !function_address.is_null() {
let relative_address = (symbols as usize)
.wrapping_sub(reloc_addr as usize)
.wrapping_sub(size_of::<u32>());
write_unaligned(reloc_addr as *mut u32, relative_address as u32);
return Ok(())
}
},
CoffMachine::X32 => {
if relocation.Type as u32 == IMAGE_REL_I386_DIR32 && !function_address.is_null() {
write_unaligned(reloc_addr as *mut u32, symbols as u32);
return Ok(())
}
}
}
}
let section_addr = self.section_map[(symbol.SectionNumber - 1) as usize].base;
match self.coff.arch {
CoffMachine::X64 => {
match relocation.Type as u32 {
IMAGE_REL_AMD64_ADDR32NB if function_address.is_null() => {
write_unaligned(
reloc_addr as *mut u32,
read_unaligned(reloc_addr as *mut u32)
.wrapping_add((section_addr as usize)
.wrapping_sub(reloc_addr as usize)
.wrapping_sub(size_of::<u32>()) as u32
),
);
},
IMAGE_REL_AMD64_ADDR64 if function_address.is_null() => {
write_unaligned(
reloc_addr as *mut u64,
read_unaligned(reloc_addr as *mut u64)
.wrapping_add(section_addr as u64),
);
},
r @ IMAGE_REL_AMD64_REL32..=IMAGE_REL_AMD64_REL32_5 => {
write_unaligned(
reloc_addr as *mut u32,
read_unaligned(reloc_addr as *mut u32)
.wrapping_add((section_addr as usize)
.wrapping_sub(reloc_addr as usize)
.wrapping_sub(size_of::<u32>())
.wrapping_sub((r - 4) as usize) as u32
),
);
},
_ => return Err(CoffeeLdrError::InvalidRelocationType(relocation.Type))
}
},
CoffMachine::X32 => {
match relocation.Type as u32 {
IMAGE_REL_I386_REL32 if function_address.is_null() => {
write_unaligned(
reloc_addr as *mut u32,
read_unaligned(reloc_addr as *mut u32)
.wrapping_add((section_addr as usize)
.wrapping_sub(reloc_addr as usize)
.wrapping_sub(size_of::<u32>()) as u32
)
);
},
IMAGE_REL_I386_DIR32 if function_address.is_null() => {
write_unaligned(
reloc_addr as *mut u32,
read_unaligned(reloc_addr as *mut u32)
.wrapping_add(section_addr as u32)
);
},
_ => return Err(CoffeeLdrError::InvalidRelocationType(relocation.Type))
}
}
}
}
Ok(())
}
}
fn read_file(name: &str) -> Result<Vec<u8>> {
let file_name = CString::new(name)
.map_err(|_| CoffeeLdrError::Msg(s!("invalid cstring")))?;
let h_file = unsafe {
CreateFileA(
file_name.as_ptr().cast(),
GENERIC_READ,
FILE_SHARE_READ,
null_mut(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
null_mut(),
)
};
if h_file == INVALID_HANDLE_VALUE {
return Err(CoffeeLdrError::Msg(s!("failed to open file")));
}
let size = unsafe { GetFileSize(h_file, null_mut()) };
if size == INVALID_FILE_SIZE {
return Err(CoffeeLdrError::Msg(s!("invalid file size")));
}
let mut out = vec![0u8; size as usize];
let mut bytes = 0;
unsafe {
ReadFile(
h_file,
out.as_mut_ptr(),
out.len() as u32,
&mut bytes,
null_mut(),
);
}
Ok(out)
}
#[inline]
fn NtFreeVirtualMemory(
process_handle: *mut c_void,
base_address: *mut *mut c_void,
region_size: *mut usize,
free_type: u32
) {
dinvoke!(
get_ntdll_address(),
s!("NtFreeVirtualMemory"),
unsafe extern "system" fn(
process_handle: *mut c_void,
base_address: *mut *mut c_void,
region_size: *mut usize,
free_type: u32,
) -> NTSTATUS,
process_handle,
base_address,
region_size,
free_type
);
}
#[inline]
fn LoadLibraryExA(
lp_lib_file_name: *const u8,
h_file: *mut c_void,
dw_flags: u32
) -> Option<*mut c_void> {
let kernel32 = get_module_address(2808682670u32, Some(dinvk::hash::murmur3));
dinvoke!(
kernel32,
s!("LoadLibraryExA"),
unsafe extern "system" fn(
lp_lib_file_name: *const u8,
h_file: *mut c_void,
dw_flags: u32,
) -> *mut c_void,
lp_lib_file_name,
h_file,
dw_flags
)
}
#[cfg(test)]
mod tests {
use crate::{*, error::Result};
#[test]
fn test_whoami() -> Result<()> {
let mut coffee = CoffeeLdr::new("bofs/whoami.x64.o")?;
let output = coffee.run("go", None, None)?;
assert!(
output.contains("\\")
|| output.contains("User")
|| output.contains("Account")
|| output.contains("Authority"),
"whoami output does not look valid: {output}"
);
Ok(())
}
#[test]
fn test_stomping() -> Result<()> {
let mut coffee = CoffeeLdr::new("bofs/whoami.x64.o")?.with_module_stomping("amsi.dll");
let output = coffee.run("go", None, None)?;
assert!(
output.contains("\\")
|| output.contains("User")
|| output.contains("Account"),
"whoami output (with stomping) looks invalid: {output}"
);
Ok(())
}
#[test]
fn test_dir() -> Result<()> {
let mut pack = BeaconPack::default();
pack.addstr("C:\\Windows")?;
let args = pack.get_buffer_hex()?;
let mut coffee = CoffeeLdr::new("bofs/dir.x64.o")?;
let output = coffee.run("go", Some(args.as_ptr() as _), Some(args.len()))?;
assert!(
output.contains("Directory of")
|| output.contains("File(s)")
|| output.contains("Dir(s)")
|| output.contains("bytes"),
"dir output does not look valid: {output}"
);
Ok(())
}
#[test]
fn test_buffer_memory() -> Result<()> {
let buffer = include_bytes!("../bofs/whoami.x64.o");
let mut coffee = CoffeeLdr::new(buffer)?;
let output = coffee.run("go", None, None)?;
assert!(
output.contains("\\")
|| output.contains("User")
|| output.contains("Account"),
"whoami buffer-loaded output does not look valid: {output}"
);
Ok(())
}
}