mmap_inject 0.1.1

Manual-map DLL injection for Windows x64 — inject DLLs without LoadLibrary
Documentation

mmap_inject

Rust License: MIT crates.io docs.rs

A minimal, robust manual-map DLL injection library for Windows x64, written in Rust.

Features

  • Manual Mapping — injects DLLs without LoadLibrary, evading basic user-mode hooks
  • Randomized Base Address — ASLR-style allocation in 64-bit user space (3 retries + OS fallback)
  • SEH Support — calls RtlAddFunctionTable for x64 structured exception handling
  • PE Header Wiping — overwrites PE headers immediately after injection
  • Import Resolution — resolves normal import tables (IAT) and delayed imports
  • TLS Callbacks — executes DLL_PROCESS_ATTACH TLS callbacks
  • Base Relocations — applies IMAGE_REL_BASED_DIR64 relocations when the DLL isn't loaded at its preferred base
  • Detailed error diagnostics — unique error codes identify exactly which step failed
  • Zero-cost FFI — single dependency on windows-sys, no heavy runtime

Quick Start

# Cargo.toml
[dependencies]
mmap_inject = "0.1"
use mmap_inject::inject_dll;
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_ALL_ACCESS};

let dll_bytes = std::fs::read("my_dll.dll")?;
let h_proc = unsafe { OpenProcess(PROCESS_ALL_ACCESS, 0, target_pid) };

unsafe {
    match inject_dll(h_proc, &dll_bytes) {
        Ok(()) => println!("Injected!"),
        Err(e) => eprintln!("Injection failed: {e}"),
    }
}

Architecture

┌──────────────────────────────────────────────────────┐
│                   Injector Process                    │
│                                                      │
│  injector.rs                                         │
│  ├─ validate PE                                      │
│  ├─ VirtualAllocEx (random base)                     │
│  ├─ write headers + sections                         │
│  ├─ apply base relocations (injector-side)           │
│  ├─ write MappingCtx + shellcode                     │
│  └─ CreateRemoteThread → WaitForSingleObject         │
│                                                      │
│                ▼                                     │
│  ┌──────────────────────────────────────┐            │
│  │         Target Process               │            │
│  │                                      │            │
│  │  shellcode.rs (minimal, ~2 KB)       │            │
│  │  ├─ resolve imports (IAT)            │            │
│  │  ├─ resolve delayed imports          │            │
│  │  ├─ execute TLS callbacks            │            │
│  │  ├─ RtlAddFunctionTable (SEH)        │            │
│  │  └─ DllMain(DLL_PROCESS_ATTACH)      │            │
│  └──────────────────────────────────────┘            │
└──────────────────────────────────────────────────────┘

API

inject_dll

pub unsafe fn inject_dll(process: HANDLE, dll_bytes: &[u8]) -> Result<()>

Manually maps the raw DLL bytes into the target process. PE headers are wiped immediately; shellcode and context memory are freed. The DLL itself stays loaded in the target until the process exits.

The process handle needs at minimum:

  • PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION
  • PROCESS_CREATE_THREAD

Error

Variant Meaning
InvalidPe Not a valid x64 PE file
Win32(u32) A Win32 API returned an error (GetLastError)
InvalidArgument Null handle, empty payload, etc.
ShellcodeFailed(usize) Import resolution failed; code indicates which step
SehRegistrationFailed DLL loaded but RtlAddFunctionTable failed
ShellcodeCrashed(u32, usize) Remote thread crashed; code = NTSTATUS, step = last marker

Building

Requires Rust 1.85+ (edition 2024). Release mode is required — debug builds are not supported because the compiler emits function calls (stack probes, non-inlined helpers) that fall outside the shellcode section.

cargo build --release

Static CRT

The .cargo/config.toml enables +crt-static (equivalent to MSVC /MT) so injected DLLs don't depend on VCRUNTIME140.dll. It also enables /DYNAMICBASE for full .reloc section generation.

Notes for injected DLLs

  • Rust heap allocation (format!, Box, String, Vec) works fine in manually-mapped DLLs — the default Windows allocator wraps HeapAlloc and needs no CRT initialization.
  • Statically link the CRT (+crt-static) so your DLL has no external CRT dependency.
  • Enable /DYNAMICBASE so the DLL has a .reloc section (the injector applies relocations when loading at a different base).

Testing

All build/run commands require --release — the shellcode lives in a custom linker section (.sc) and debug builds emit stack probes + non-inlined helpers that break the section layout.

# Unit tests — PE structs, RVA conversion, shellcode layout
cargo test --release

# Build examples (test_dll.dll + test_injector.exe)
cargo build --examples --release

# Self-injection test — shows a MessageBox
cargo run --example test_injector --release

# Inject into a specific process by PID
cargo run --example test_injector --release -- 12345

Project Structure

Path Description
src/ The mmap_inject library — manual-map DLL injection
examples/test_dll.rs Example DLL — MessageBox("Hello from test_dll")
examples/test_injector.rs Injector — self-inject or test_injector <PID>

License

MIT — see LICENSE.