Shellcode Reflective DLL Injection (sRDI): Converting DLLs into Position-Independent Shellcode

This blog series is the written companion to our Malware Development: Red Team Tradecraft YouTube playlist, where we walk through live demos of the evasion techniques discussed here, including payload staging, AV/EDR bypass, and in-memory, on disk, and network evasion. We recommend following the playlist along with these posts. 

In Part 1, we covered the foundational concepts of processes, threads, and memory. In Part 2, we explored Windows APIs, the PE format, and injection techniques; in Part 3, we covered Shellcode Fundamentals, Cryptography in Malware, Shellcode Encryption & Obfuscation, Evasion Techniques, etc.

Disclaimer

This blog series is designed for cybersecurity professionals, red teamers, and security analysts who want to understand malware development from both offensive and defensive perspectives. The knowledge shared here is meant to help you better create, detect, analyze, and defend against malicious software. Always use this information ethically and legally within authorized security assessments and controlled environments.

Introduction

Regular Reflective DLL Injection solved the disk artifact problem. It did not solve the wire problem. When you inject a DLL using Fewer’s original technique, a recognizable PE structure travels into the target process: MZ header, section table, import directory, all of it intact and readable by anything scanning incoming allocations. The payload announces its identity before a single instruction executes.

Shellcode Reflective DLL Injection, developed by Nick Landers (monoxgas), addresses that gap at the source. Before the payload ever reaches a target, a conversion utility strips the PE structure and rebuilds it as flat, position-independent shellcode. What arrives in the target process is a blob with no magic bytes, no section alignment, and no recognizable binary format. The loader logic, the DLL data, and the optional export call are all present, but encoded as raw executable bytes rather than a Windows image. From the perspective of any injection primitive, it is indistinguishable from arbitrary shellcode.

The operational consequence is significant. Because the output of sRDI is plain shellcode, it decouples the payload entirely from the delivery mechanism. The same converted blob can be injected via CreateRemoteThreadAPC injection, thread hijacking, process hollowing, or Early Bird, with no payload modification. The choice of injection primitive becomes an independent variable, tuned to the defensive environment rather than constrained by the payload format.

This blog covers the full technical picture: the offline conversion process, the internal structure of the shellcode blob, how the bootstrap loader reconstructs and executes the embedded DLL within the target process, the optional export-call mechanism, and where the detection surface actually sits once the PE has been mapped into memory.

What is sRDI?

Shellcode Reflective DLL Injection, known as sRDI, is an evolution of Stephen Fewer’s original Reflective DLL Injection technique. Developed by Nick Landers (monoxgas), sRDI solves a fundamental limitation of regular RDI: the payload must be a PE file. With sRDI, a DLL is converted into fully position-independent shellcode before it ever reaches the target process, meaning the payload arrives as a flat blob of code with no PE header, no section table, and no easily recognizable structure. This makes it significantly harder to detect and fingerprint than its predecessor.

How sRDI Differs from Regular RDI

To understand what sRDI adds, it helps to understand where regular RDI stops.

In regular RDI, the entire DLL, including its PE headers, section table, and all metadata, is written into the target process as-is. A ReflectiveLoader function embedded in the DLL then bootstraps the loading process from within the target. The DLL travels through the injection pipeline as a recognizable PE structure, and once loaded, the PE headers remain visible in the mapped memory of the target process.

In sRDI, a conversion step happens offline before any injection takes place. A Python script or equivalent conversion utility takes the DLL as input and produces a flat, self-contained shellcode blob as output. That blob is what gets injected. It contains everything needed to load and execute the DLL entirely in shellcode, with the original DLL embedded as raw data. From the perspective of any injection primitive, the payload is indistinguishable from ordinary shellcode.

The result is a cleaner injection profile: no PE header arrives in the target process, and the payload can be delivered through any shellcode injection technique without modification.

How sRDI Works

sRDI has two distinct phases: conversion and execution.

Phase 1: Conversion (Offline, Before Injection)

The conversion step takes place on the attacker’s machine before any interaction with the target. The input is a standard Windows DLL, typically one that exports a specific function to be called after loading. The converter produces a single shellcode blob structured as follows:

 
[ Bootstrap shellcode ] [ Embedded DLL data ] [ Function hash (optional) ] [ User data (optional) ]

The bootstrap shellcode is a small, hand-written, position-independent stub that knows how to find and process the embedded DLL data that follows it. The DLL itself is stored as raw bytes immediately after the bootstrap. An optional function hash tells the loader which exported function to call after DllMain, and optional user data can be passed through to that function.

Importantly, the conversion process strips or flattens the PE structure so that the blob does not present a recognizable PE signature to scanners that look for the MZ magic bytes or the PE header at the start of an allocation.

sRDI Conversion Diagram: The conversion pipeline diagram showing Input DLL to sRDI Converter to Shellcode Blob, with the blob anatomy breakdown below it (Bootstrap shellcode, Embedded DLL data, Function hash, User data), followed by the injection primitive arrow.

Additional Technical Notes:
The original sRDI project supports several loader flags (SRDI_CLEARHEADER, SRDI_CLEARMEMORY, SRDI_OBFUSCATEIMPORTS, etc.) that allow further artifact reduction and cleanup. The loader also handles TLS callbacks and proper section permissions. Note that sRDI payloads are larger than raw shellcode and may face compatibility issues with highly complex DLLs.

# Example: Converting a DLL to position-independent shellcode using sRDI
# Full implementation available in the official monoxgas/sRDI repository

from ShellcodeRDI import *

# Load the compiled DLL (must be the architecture matching your target process)
dll_bytes = open("your_payload.dll", 'rb').read()

# Perform the conversion
shellcode = ConvertToShellcode(
    dll_bytes,
    
    # Optional: Name of the exported function to call after DllMain
    # If omitted, only DllMain(DLL_PROCESS_ATTACH) is executed
    hashFunction="YourExportedFunction",   # e.g., "ReflectiveLoader" or your custom entrypoint
    
    # Loader flags - these significantly reduce artifacts in memory
    flags=SRDI_CLEARHEADER |      # Wipe PE headers after mapping
          SRDI_CLEARMEMORY |      # Clean up unused memory regions
          SRDI_OBFUSCATEIMPORTS   # Optional: further obfuscate import resolution
)

# Save the resulting flat shellcode blob
with open("payload.bin", "wb") as f:
    f.write(shellcode)

"[+] Successfully converted DLL to shellcode")
"    Original DLL size : {len(dll_bytes):,} bytes")
"    Shellcode size    : {len(shellcode):,} bytes")
"    Output saved to   : payload.bin")

Full implementation and CLI wrapper available in the official sRDI repository. The flags above help reduce memory artifacts.

Phase 2: Execution (Inside the Target Process)

Once the shellcode blob is injected and executed, the bootstrap shellcode takes over. The execution flow mirrors regular RDI in its logic but operates entirely without relying on any existing PE loader.

sRDI Execution Diagram: The execution flow diagram showing all 8 steps from injection through optional export call to return, color-coded by phase.

The steps are:

1. Self-location via PIC: The bootstrap shellcode must first figure out where it is loaded in memory. Since it is position-independent, it uses a classic technique (usually call followed by pop) to get its own memory address. This allows it to calculate the exact location of the embedded DLL data that follows the bootstrap code.

2. PEB walk & API resolution: The bootstrap walks the Process Environment Block (PEB), specifically the InMemoryOrderModuleList, to locate kernel32.dll without using any Windows API calls. It then manually parses kernel32.dll’s export table to resolve essential functions: VirtualAlloc, LoadLibraryA, GetProcAddress, and FlushInstructionCache.

3. Memory allocation and PE mapping: Using the resolved VirtualAlloc, the bootstrap allocates a new executable memory region. It then parses the embedded raw DLL bytes and maps each section (.text, .data, etc.) into this region, setting the correct memory permissions (read, write, execute) just like the Windows PE loader would.

4. Import resolution: The loader walks the DLL’s Import Address Table (IAT), loads required DLLs using LoadLibraryA, and resolves individual function addresses with GetProcAddress. These addresses are written into the mapped DLL so it can call external functions correctly.

5. Relocations: Most DLLs contain absolute addresses that assume a specific base address. The loader processes the relocation table and adjusts these addresses based on the actual memory location where the DLL was loaded.

6. Instruction cache flush and DllMain: FlushInstructionCache is called to ensure the CPU’s instruction cache is updated with the newly written code. The bootstrap then calls the DLL’s DllMain entry point with DLL_PROCESS_ATTACH, allowing the DLL to initialize itself.

7. Optional export call: If a function name/hash was provided during the conversion step, the loader walks the DLL’s export table, finds the matching function by comparing hashes, and calls it. Optional user data (pointer + size) can be passed to this function for runtime parameters.

8. Return to caller: Once everything is complete, execution returns from the shellcode back to the original injection primitive (e.g., the thread that called it). The original DLL is now fully loaded and running inside the target process with no disk artifacts.

Why the Conversion Step Matters

The offline conversion is what makes sRDI meaningfully different in practice, not just in theory.

Regular RDI payloads are PE files. Security tooling, memory scanners, and EDR products can scan incoming allocations for PE magic bytes, recognize section layouts, or inspect export tables to identify a DLL injection. The PE structure is a rich source of indicators.

With sRDI, the injected payload is a flat blob. There is no MZ header. There is no section table. There is no export table. The blob pattern is unique to each converted DLL and is not tied to any known binary format. Scanners that rely on PE-based heuristics see arbitrary bytes.

At the execution stage, once the embedded DLL has been mapped into memory, the mapped image does have PE structure internally, since the loader has rebuilt it. This is where detection is still possible, specifically through scanning for unbacked executable memory regions with PE headers, or through behavioral indicators like the VirtualAlloc plus WriteProcessMemory call chain. But the initial injection profile is substantially cleaner than regular RDI.

Injection Delivery Flexibility

Because the sRDI payload is plain shellcode, it can be delivered via any shellcode injection primitive. This is one of its most useful properties for red team operations. Common delivery options include:

  • VirtualAlloc + WriteProcessMemory + CreateRemoteThread (the classic shellcode injection path)
  • APC (Asynchronous Procedure Call) injection
  • Thread hijacking via SetThreadContext and ResumeThread
  • Process hollowing
  • Early Bird injection
  • Any other technique that results in shellcode execution in a target process

This flexibility means sRDI can be paired with whatever process injection technique best suits the operational environment, without changing the payload itself.

Calling a Specific Export

One feature sRDI adds over regular RDI is the ability to specify a target exported function at conversion time. When the conversion utility is invoked, an optional function name can be supplied. The converter hashes the function name and embeds that hash in the shellcode blob. After calling DllMain, the loader resolves the matching export by walking the DLL’s export table and comparing hashed names, then calls that function.

This means the DLL does not need to put all of its logic inside DllMain. Functionality can be cleanly separated into named exports, and the caller specifies which one to invoke at runtime. Optional user data (a pointer and length) can also be passed through to the called function, allowing parameters to be supplied to the payload at injection time.

Detection Considerations

sRDI is not undetectable, but it shifts the detection surface compared to regular RDI.

What becomes harder to detect:
The initial payload lacks a PE header, making PE-based scanning less effective during injection. The payload can be delivered via whichever injection primitive has the lowest detection footprint in a given environment.

What remains detectable:
Once the embedded DLL is mapped in memory, the mapped image has an internal PE structure. EDR solutions with memory scanning capabilities that look for PE headers in unbacked allocations can still find it. The VirtualAlloc call sequence and the behavioral pattern of writing shellcode to a remote process remain visible to kernel-level hooks and behavioral analytics. Hollowing or overwriting the PE header after mapping (a common additional step using flags like SRDI_CLEARHEADER) partially mitigates this but does not eliminate it entirely.

How to Hunt and Detect sRDI in Your Environment:

  • Use memory scanners such as Moneta, PE-sieve, or EDR-native tools to hunt for PE headers/signatures and module-like structures in unbacked executable memory.
  • Monitor for suspicious API call chains (PEB walking, VirtualAlloc/VirtualProtect, bulk GetProcAddress usage, etc.).
  • Deploy YARA rules targeting the sRDI bootstrap stub or hashed export resolution patterns.
  • Leverage Sigma rules for process injection behaviors and memory forensics tools (e.g., Volatility plugins) to identify reconstructed modules.
  • Test different injection primitives in purple team exercises, sRDI makes it easy to isolate the payload from the delivery method.

For purple team assessments, testing whether your EDR detects sRDI payloads delivered via different injection primitives is a high-value exercise. The detection gap between injection techniques is often significant.

GhostHook (a.k.a Pekenux): Offensive API Hooking with sRDI in Practice

The three techniques covered in this blog are not theoretical. To demonstrate the practical application of trampoline hooks in the offensive space, we built GhostHook (also known as Pekenux): a fileless infostealer that flips the EDR playbook. The same E9 JMP trampoline that antivirus and EDR products use to watch process behavior can be turned offensively to extract plaintext credentials from a running process before they are ever hashed or stored.

Target and Objective

The target is Password Safe (pwsafe.exe), a widely used credential manager. When a user opens their vault, the application presents a master password dialog. At the point where the user submits that dialog, the plaintext password exists in memory for a brief window before it is processed, hashed, and used to decrypt the vault. GhostHook intercepts at exactly that point.

Mechanism

GhostHook uses Microsoft Detours to install a trampoline hook on the target function handling master password submission inside pwsafe.exe. The hook is installed in-process after the implant is loaded via reflective injection, meaning no DLL is written to disk at any stage.

The Detours installation follows the standard trampoline pattern. The first bytes of the target function are overwritten with a JMP to GhostHook’s credential capture function. The original bytes are saved into a trampoline stub with a JMP back to the remainder of the target function. From the process’s perspective, execution proceeds normally. The user sees no error, the vault opens, and Password Safe behaves as expected.

Inside the hook function, the plaintext password is read directly from memory before the original function is called through the trampoline. The credential is captured in a local buffer, the trampoline is invoked to allow Password Safe to continue normally, and the captured string is then passed to the C2 beacon.

C2 Exfiltration

Credential exfiltration uses WinHTTP directly. WinHttpOpen, WinHttpConnect, WinHttpOpenRequest, and WinHttpSendRequest are called in sequence to POST the captured plaintext to the operator’s C2 infrastructure. No third-party networking library is used. The entire exfiltration path stays within Windows native API calls, keeping the network activity consistent with legitimate application traffic patterns.

Why This Works Against Defensive Tooling

GhostHook operates entirely in user space inside a legitimate, already-running process. It does not spawn a child process, write to disk, or load a module that would appear in the target process’s module list. The hook itself is installed using the same Detours pattern that many AV/EDR user-mode agents use internally, which creates a detection challenge: a hook that looks structurally identical to a legitimate security product’s own instrumentation.

Detection requires the defensive stack to go beyond module enumeration and look at unbacked executable memory regions, unexpected function prologue modifications in pwsafe.exe‘s mapped image, or anomalous outbound WinHTTP traffic from a credential manager process. Each of these is detectable with the right telemetry, but none is caught by file-based scanning or standard module integrity checks.

This is precisely the gap that purple team assessments are designed to expose: the distance between what your AV/EDR theoretically detects and what it actually catches in your environment, when evaluated against real tooling.

Summary: RDI vs sRDI

 RDIsRDI
Payload formatPE file (DLL with embedded ReflectiveLoader)Flat shellcode blob
PE header visible on injectionYesNo
Delivery methodCreateRemoteThread or bootstrap shellcodeAny shellcode injection primitive
Offline conversion requiredNoYes
Calls a specific exportNo (DllMain only)Yes (by function hash)
PE header visible after mappingYesPartially (internal to mapped region)
Attribution to a known techniqueEasier (Fewer’s ReflectiveLoader pattern)Harder (no recognizable PE on wire)

Work With US:

Want to see what this looks like deployed against your AV/EDR and infrastructure? We build and operate the same tooling in authorized engagements. book a session or get in touch

Resources

Training

  • Sektor7 Malware Development Courses (institute.sektor7.net): Covers position-independent code, custom shellcode loaders, and evasion techniques that are directly applicable to understanding both the sRDI conversion process and its bootstrap shellcode.

Official & Technical References

Defensive & Detection Resources

```yara
rule sRDI_Bootstrap_Indicators {
meta:
description = "Detects sRDI bootstrap patterns and common reflective loader strings"
author = "RBT Security"
strings:
$s1 = "ReflectiveLoader" ascii
$s2 = "Reflective" ascii
$s3 = { 48 8B 05 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? } // Common PEB walk
$s4 = "kernel32.dll" ascii
$s5 = "GetProcAddress" ascii
$s6 = "VirtualAlloc" ascii
condition:
3 of them
}

Share the Post:

Table of Contents

Subscribe To Our Blog

Light